feat: add authorization bootstrap and HTTP enforcement
Wires Core's identity and authorization domains into apps/api over one shared, validated PostgreSQL pool; adds an explicit, single-use, concurrency-safe platform-owner bootstrap CLI; adds a smallest-possible immutable permission registry with one Core-owned restricted permission (core.authorization.grants.manage); and adds reusable Hono authorization enforcement plus a narrowly scoped /api/v1/grants HTTP API for managing explicit grants only, gated by an exact trusted Origin on every mutation.
This commit is contained in:
+27
-2
@@ -1,13 +1,27 @@
|
||||
import { isAuthenticatedPrincipal } from "@ekklesia/contracts";
|
||||
import {
|
||||
type AuthorizationEvaluator,
|
||||
type AuthorizationStore,
|
||||
CORE_PERMISSION_REGISTRY,
|
||||
} from "@ekklesia/core/authorization";
|
||||
import { type EkklesiaIdentityRuntime, IdentityRuntimeError } from "@ekklesia/core/identity";
|
||||
import { Hono } from "hono";
|
||||
import type { Context } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { createGrantsRoutes } from "./grants-routes.js";
|
||||
|
||||
export interface CreateApiAppAuthorizationOptions {
|
||||
readonly evaluator: AuthorizationEvaluator;
|
||||
readonly store: AuthorizationStore;
|
||||
}
|
||||
|
||||
export interface CreateApiAppOptions {
|
||||
readonly authorization: CreateApiAppAuthorizationOptions;
|
||||
readonly runtime: EkklesiaIdentityRuntime;
|
||||
/** Exact allowed browser origins. Never a wildcard; combined with `credentials: true`, only an
|
||||
* exact, reflected match is ever returned in `Access-Control-Allow-Origin`. */
|
||||
* exact, reflected match is ever returned in `Access-Control-Allow-Origin`. Also the trusted set
|
||||
* `requireTrustedOrigin` checks for grant-mutation routes — there is no second, competing origin
|
||||
* configuration. */
|
||||
readonly trustedOrigins: readonly string[];
|
||||
}
|
||||
|
||||
@@ -20,7 +34,7 @@ function sanitizedErrorResponse(c: Context, error: unknown, status: 500 | 503) {
|
||||
/**
|
||||
* Pure Hono application factory: no I/O, no environment reads, no process side effects. Suitable
|
||||
* for unit and integration tests via `app.fetch` directly, with a fully injected identity runtime
|
||||
* standing in for real Better Auth/PostgreSQL where appropriate.
|
||||
* and authorization evaluator/store standing in for real Better Auth/PostgreSQL where appropriate.
|
||||
*/
|
||||
export function createApiApp(options: CreateApiAppOptions): Hono {
|
||||
const { runtime } = options;
|
||||
@@ -62,6 +76,17 @@ export function createApiApp(options: CreateApiAppOptions): Hono {
|
||||
}
|
||||
});
|
||||
|
||||
app.route(
|
||||
"/",
|
||||
createGrantsRoutes({
|
||||
evaluator: options.authorization.evaluator,
|
||||
registry: CORE_PERMISSION_REGISTRY,
|
||||
runtime,
|
||||
store: options.authorization.store,
|
||||
trustedOrigins: allowedOrigins,
|
||||
}),
|
||||
);
|
||||
|
||||
app.notFound((c) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
return c.json({ code: "NOT_FOUND" }, 404);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { EkklesiaPrincipal, PermissionId } from "@ekklesia/contracts";
|
||||
import type { AuthorizationEvaluator, PermissionRegistry } from "@ekklesia/core/authorization";
|
||||
import type { EkklesiaIdentityRuntime } from "@ekklesia/core/identity";
|
||||
import type { Context, MiddlewareHandler, Next } from "hono";
|
||||
|
||||
const PRINCIPAL_CONTEXT_KEY = "ekklesiaPrincipal";
|
||||
|
||||
export interface AuthorizationMiddlewareOptions {
|
||||
readonly evaluator: AuthorizationEvaluator;
|
||||
readonly registry: PermissionRegistry;
|
||||
readonly runtime: EkklesiaIdentityRuntime;
|
||||
}
|
||||
|
||||
function sanitizedDenial(c: Context, status: 403 | 503, code: string): Response {
|
||||
c.header("Cache-Control", "no-store");
|
||||
return c.json({ code }, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Hono middleware that enforces `permissionId`, resolved exclusively from Core's own
|
||||
* trusted, server-side permission registry — a route never supplies its own baseline, and no
|
||||
* request input can influence which baseline is checked. The principal is resolved exclusively
|
||||
* through the injected identity runtime (`resolvePrincipal`), the same one `/api/v1/principal`
|
||||
* already uses; nothing here ever trusts a client-supplied claim about identity, ownership, or
|
||||
* grants. A permission id that is not registered fails closed (403) exactly like a denied
|
||||
* decision — in normal operation this route only ever hard-codes an already-registered constant,
|
||||
* but the check stays live per-request as defense in depth rather than being trusted once at
|
||||
* composition time.
|
||||
*
|
||||
* On success, the resolved `EkklesiaPrincipal` — Core's own narrow `{kind, userId}` contract, not
|
||||
* Better Auth's richer session/user object — is stored on the Hono context so the route handler
|
||||
* can build an actor without resolving the principal a second time. This is deliberately narrower
|
||||
* than the "no generalized session/user object in Hono context" concern ADR 0011 raised about
|
||||
* Better Auth's own documented example: only Core's already-public-safe principal shape is ever
|
||||
* set, never a session token, raw user row, or Better Auth type.
|
||||
*
|
||||
* A genuine infrastructure failure — principal resolution, evaluation, or the underlying
|
||||
* database — is never folded into a 403; it returns a sanitized 503, mirroring
|
||||
* `/api/v1/principal`'s existing behavior for the identical reason: a caller must never see "no
|
||||
* permission" when the true answer is "could not be determined."
|
||||
*/
|
||||
export function requirePermission(
|
||||
options: AuthorizationMiddlewareOptions,
|
||||
permissionId: PermissionId,
|
||||
): MiddlewareHandler {
|
||||
return async (c: Context, next: Next) => {
|
||||
const declaration = options.registry.get(permissionId);
|
||||
if (!declaration) {
|
||||
return sanitizedDenial(c, 403, "AUTHORIZATION_DENIED");
|
||||
}
|
||||
|
||||
let principal: EkklesiaPrincipal;
|
||||
try {
|
||||
principal = await options.runtime.resolvePrincipal(c.req.raw.headers);
|
||||
} catch {
|
||||
return sanitizedDenial(c, 503, "PRINCIPAL_RESOLUTION_FAILED");
|
||||
}
|
||||
|
||||
let decision: { allowed: boolean };
|
||||
try {
|
||||
decision = await options.evaluator.authorize({
|
||||
baseline: declaration.baseline,
|
||||
permission: declaration.id,
|
||||
principal,
|
||||
});
|
||||
} catch {
|
||||
return sanitizedDenial(c, 503, "AUTHORIZATION_EVALUATION_FAILED");
|
||||
}
|
||||
|
||||
if (!decision.allowed) {
|
||||
return sanitizedDenial(c, 403, "AUTHORIZATION_DENIED");
|
||||
}
|
||||
|
||||
c.set(PRINCIPAL_CONTEXT_KEY, principal);
|
||||
await next();
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
/** Reads the principal a preceding `requirePermission` middleware already resolved and verified.
|
||||
* Throws (a route-wiring bug, not a runtime condition) if called on a route this middleware never
|
||||
* protected. */
|
||||
export function getAuthorizedPrincipal(c: Context): EkklesiaPrincipal {
|
||||
const principal = c.get(PRINCIPAL_CONTEXT_KEY) as EkklesiaPrincipal | undefined;
|
||||
if (!principal) {
|
||||
throw new Error("getAuthorizedPrincipal called outside a requirePermission-protected route.");
|
||||
}
|
||||
return principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires an exact trusted `Origin` header for a cookie-authenticated, state-changing route.
|
||||
* CORS response headers alone are not CSRF protection — they only stop a browser from *reading* a
|
||||
* cross-origin response, not from *sending* the state-changing request in the first place — so
|
||||
* mutation routes need this explicit, server-side check in addition to (not instead of) the
|
||||
* existing CORS policy. A missing or untrusted origin is rejected before the route (or the
|
||||
* permission check) ever runs. Non-browser, non-cookie automation (service accounts/API tokens)
|
||||
* is deliberately out of scope for this slice and has no way to satisfy this check yet.
|
||||
*/
|
||||
export function requireTrustedOrigin(trustedOrigins: ReadonlySet<string>): MiddlewareHandler {
|
||||
return async (c: Context, next: Next) => {
|
||||
const origin = c.req.header("origin");
|
||||
if (!origin || !trustedOrigins.has(origin)) {
|
||||
return sanitizedDenial(c, 403, "UNTRUSTED_ORIGIN");
|
||||
}
|
||||
await next();
|
||||
return;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
type AuthorizationDrizzleDatabase,
|
||||
createAuthorizationDrizzleView,
|
||||
} from "@ekklesia/core/authorization";
|
||||
import { createValidatedPostgresPool, type PostgresPoolOptions } from "@ekklesia/core/database";
|
||||
import { createIdentityDrizzleView, type IdentityDrizzleDatabase } from "@ekklesia/core/identity";
|
||||
|
||||
export interface ApiDatabaseHandle {
|
||||
readonly authorization: AuthorizationDrizzleDatabase;
|
||||
readonly identity: IdentityDrizzleDatabase;
|
||||
/** Closes the one shared pool exactly once, regardless of how many typed views were created
|
||||
* over it. Safe to call from both a startup-failure cleanup path and normal shutdown. */
|
||||
readonly close: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes the one physical PostgreSQL pool this process uses for both Core's identity and
|
||||
* authorization schemas: one validated, timeout-bounded `pg.Pool` (`createValidatedPostgresPool`,
|
||||
* `@ekklesia/core/database`), wrapped by two independent typed Drizzle views
|
||||
* (`createIdentityDrizzleView`, `createAuthorizationDrizzleView`). Neither Core domain owns or
|
||||
* closes the pool in this composition — `close()` here is the only thing that ever calls
|
||||
* `pool.end()`. `createIdentityDrizzleDatabase`/`createAuthorizationDrizzleDatabase` remain
|
||||
* available, unaffected, and independently testable for callers that only need one domain.
|
||||
*/
|
||||
export function createApiDatabase(
|
||||
databaseUrl: string,
|
||||
options: PostgresPoolOptions = {},
|
||||
): ApiDatabaseHandle {
|
||||
const { close, pool } = createValidatedPostgresPool(databaseUrl, options);
|
||||
|
||||
return Object.freeze({
|
||||
authorization: createAuthorizationDrizzleView(pool),
|
||||
close,
|
||||
identity: createIdentityDrizzleView(pool),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import type { AuthorizationScope, EkklesiaUserId, PermissionId } from "@ekklesia/contracts";
|
||||
import {
|
||||
createUserActor,
|
||||
isAuthenticatedPrincipal,
|
||||
safeParseAuthorizationScope,
|
||||
safeParseEkklesiaUserId,
|
||||
safeParseGrantId,
|
||||
safeParsePermissionId,
|
||||
} from "@ekklesia/contracts";
|
||||
import {
|
||||
type AuthorizationStore,
|
||||
AuthorizationStoreError,
|
||||
CORE_AUTHORIZATION_GRANTS_MANAGE,
|
||||
type GrantRecord,
|
||||
} from "@ekklesia/core/authorization";
|
||||
import { Hono } from "hono";
|
||||
import type { Context } from "hono";
|
||||
import {
|
||||
type AuthorizationMiddlewareOptions,
|
||||
getAuthorizedPrincipal,
|
||||
requirePermission,
|
||||
requireTrustedOrigin,
|
||||
} from "./authorization-middleware.js";
|
||||
|
||||
export interface CreateGrantsRoutesOptions extends AuthorizationMiddlewareOptions {
|
||||
readonly store: AuthorizationStore;
|
||||
readonly trustedOrigins: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
function sanitizedStoreErrorResponse(c: Context, error: unknown): Response {
|
||||
c.header("Cache-Control", "no-store");
|
||||
if (error instanceof AuthorizationStoreError) {
|
||||
const status = error.code === "AUTHORIZATION_GRANT_NOT_FOUND" ? 404 : 503;
|
||||
return c.json({ code: error.code }, status);
|
||||
}
|
||||
return c.json({ code: "INTERNAL_ERROR" }, 503);
|
||||
}
|
||||
|
||||
interface ParsedCreateGrantBody {
|
||||
readonly expiresAt?: Date;
|
||||
readonly permission: PermissionId;
|
||||
readonly scope?: AuthorizationScope;
|
||||
readonly subjectUserId: EkklesiaUserId;
|
||||
}
|
||||
|
||||
const CREATE_GRANT_ALLOWED_KEYS = new Set(["subjectUserId", "permission", "scope", "expiresAt"]);
|
||||
|
||||
/**
|
||||
* Accepts only `subjectUserId`, `permission`, an optional `scope`, and an optional `expiresAt`.
|
||||
* Any other key present — `actor`, `sourceKind`, `sourceRef`, any audit field, `createdAt`,
|
||||
* `revokedAt`, a grant id, or anything claiming owner/effective-permission state — makes the
|
||||
* whole body invalid rather than being silently dropped, so a caller cannot smuggle
|
||||
* server-derived fields through a request that otherwise looks well-formed.
|
||||
*/
|
||||
function parseCreateGrantBody(raw: unknown): ParsedCreateGrantBody | undefined {
|
||||
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const body = raw as Record<string, unknown>;
|
||||
if (Object.keys(body).some((key) => !CREATE_GRANT_ALLOWED_KEYS.has(key))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const subjectUserId = safeParseEkklesiaUserId(body.subjectUserId);
|
||||
if (!subjectUserId.success) {
|
||||
return undefined;
|
||||
}
|
||||
const permission = safeParsePermissionId(body.permission);
|
||||
if (!permission.success) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let scope: AuthorizationScope | undefined;
|
||||
if (body.scope !== undefined) {
|
||||
const parsedScope = safeParseAuthorizationScope(body.scope);
|
||||
if (!parsedScope.success) {
|
||||
return undefined;
|
||||
}
|
||||
scope = parsedScope.data;
|
||||
}
|
||||
|
||||
let expiresAt: Date | undefined;
|
||||
if (body.expiresAt !== undefined) {
|
||||
if (typeof body.expiresAt !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const parsedDate = new Date(body.expiresAt);
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
expiresAt = parsedDate;
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
...(expiresAt === undefined ? {} : { expiresAt }),
|
||||
permission: permission.data,
|
||||
...(scope === undefined ? {} : { scope }),
|
||||
subjectUserId: subjectUserId.data,
|
||||
});
|
||||
}
|
||||
|
||||
function serializeGrant(grant: GrantRecord) {
|
||||
return {
|
||||
createdAt: grant.createdAt.toISOString(),
|
||||
expiresAt: grant.expiresAt ? grant.expiresAt.toISOString() : null,
|
||||
id: grant.id,
|
||||
permission: grant.permission,
|
||||
revokedAt: grant.revokedAt ? grant.revokedAt.toISOString() : null,
|
||||
scope: grant.scope ?? null,
|
||||
sourceKind: grant.sourceKind,
|
||||
userId: grant.userId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The v1 explicit-grant management API: list, create, and revoke *explicit* grants only. Every
|
||||
* route requires `core.authorization.grants.manage`; a platform owner passes through the
|
||||
* evaluator's existing owner bypass without ever needing an explicit grant of this permission
|
||||
* themselves — that bypass is exactly how a platform owner deliberately delegates grant
|
||||
* administration to another account by granting them this one permission.
|
||||
*
|
||||
* Relationship-derived grants are never reachable through this API in either direction: creation
|
||||
* always hard-codes `sourceKind: "explicit"` server-side (the request body cannot supply a
|
||||
* `sourceKind` at all), and revocation is constrained to `sourceKind: "explicit"`, so attempting
|
||||
* to revoke a relationship-derived grant by id behaves exactly like the grant does not exist —
|
||||
* those grants remain owned by the module operation that established the relationship.
|
||||
*/
|
||||
export function createGrantsRoutes(options: CreateGrantsRoutesOptions): Hono {
|
||||
const routes = new Hono();
|
||||
const requireManagePermission = requirePermission(options, CORE_AUTHORIZATION_GRANTS_MANAGE);
|
||||
const requireOrigin = requireTrustedOrigin(options.trustedOrigins);
|
||||
|
||||
routes.get("/api/v1/grants", requireManagePermission, async (c) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
const subjectUserId = safeParseEkklesiaUserId(c.req.query("userId"));
|
||||
if (!subjectUserId.success) {
|
||||
return c.json({ code: "INVALID_REQUEST" }, 400);
|
||||
}
|
||||
try {
|
||||
const grants = await options.store.listGrantsForUser(subjectUserId.data, {
|
||||
sourceKind: "explicit",
|
||||
});
|
||||
return c.json({ grants: grants.map(serializeGrant) }, 200);
|
||||
} catch (error) {
|
||||
return sanitizedStoreErrorResponse(c, error);
|
||||
}
|
||||
});
|
||||
|
||||
routes.post("/api/v1/grants", requireOrigin, requireManagePermission, async (c) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
const principal = getAuthorizedPrincipal(c);
|
||||
if (!isAuthenticatedPrincipal(principal)) {
|
||||
return c.json({ code: "AUTHORIZATION_DENIED" }, 403);
|
||||
}
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await c.req.json();
|
||||
} catch {
|
||||
return c.json({ code: "INVALID_REQUEST" }, 400);
|
||||
}
|
||||
const parsed = parseCreateGrantBody(rawBody);
|
||||
if (!parsed) {
|
||||
return c.json({ code: "INVALID_REQUEST" }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const grant = await options.store.createGrant({
|
||||
actor: createUserActor(principal.userId),
|
||||
...(parsed.expiresAt === undefined ? {} : { expiresAt: parsed.expiresAt }),
|
||||
permission: parsed.permission,
|
||||
...(parsed.scope === undefined ? {} : { scope: parsed.scope }),
|
||||
sourceKind: "explicit",
|
||||
userId: parsed.subjectUserId,
|
||||
});
|
||||
return c.json({ grant: serializeGrant(grant) }, 201);
|
||||
} catch (error) {
|
||||
return sanitizedStoreErrorResponse(c, error);
|
||||
}
|
||||
});
|
||||
|
||||
routes.post(
|
||||
"/api/v1/grants/:grantId/revoke",
|
||||
requireOrigin,
|
||||
requireManagePermission,
|
||||
async (c) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
const principal = getAuthorizedPrincipal(c);
|
||||
if (!isAuthenticatedPrincipal(principal)) {
|
||||
return c.json({ code: "AUTHORIZATION_DENIED" }, 403);
|
||||
}
|
||||
|
||||
const grantId = safeParseGrantId(c.req.param("grantId"));
|
||||
if (!grantId.success) {
|
||||
return c.json({ code: "INVALID_REQUEST" }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
await options.store.revokeGrant({
|
||||
actor: createUserActor(principal.userId),
|
||||
grantId: grantId.data,
|
||||
sourceKind: "explicit",
|
||||
});
|
||||
return c.json({ success: true }, 200);
|
||||
} catch (error) {
|
||||
return sanitizedStoreErrorResponse(c, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return routes;
|
||||
}
|
||||
+27
-14
@@ -1,15 +1,15 @@
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
createIdentityDrizzleDatabase,
|
||||
createIdentityRuntime,
|
||||
type IdentityDatabaseHandle,
|
||||
type IdentityRateLimitOverride,
|
||||
} from "@ekklesia/core/identity";
|
||||
createAuthorizationEvaluator,
|
||||
createAuthorizationStore,
|
||||
} from "@ekklesia/core/authorization";
|
||||
import { createIdentityRuntime, type IdentityRateLimitOverride } from "@ekklesia/core/identity";
|
||||
import { serve, type ServerType } from "@hono/node-server";
|
||||
import type { Hono } from "hono";
|
||||
import { createApiApp } from "./app.js";
|
||||
import { type ApiRuntimeConfig, createApiRuntimeConfig } from "./config.js";
|
||||
import { type ApiDatabaseHandle, createApiDatabase } from "./database.js";
|
||||
|
||||
const connectionTimeoutMs = 10_000;
|
||||
const operationTimeoutMs = 10_000;
|
||||
@@ -26,7 +26,7 @@ export interface StartApiOptions {
|
||||
readonly rateLimitOverride?: IdentityRateLimitOverride;
|
||||
}
|
||||
|
||||
async function safeCloseDatabase(handle: IdentityDatabaseHandle): Promise<void> {
|
||||
async function safeCloseDatabase(handle: ApiDatabaseHandle): Promise<void> {
|
||||
try {
|
||||
await handle.close();
|
||||
} catch {
|
||||
@@ -54,34 +54,47 @@ function bindServer(app: Hono, host: string, port: number): Promise<ServerType>
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit runtime composition: constructs the PostgreSQL pool and Drizzle database, the Core
|
||||
* identity runtime, and the Hono application, then starts the Node HTTP server. Returns a handle
|
||||
* with an idempotent `shutdown()` so both the executable entry point and tests can stop the
|
||||
* server and close the database pool deterministically. Never applies migrations or generates
|
||||
* schema — the database must already be migrated through `pnpm migrations:apply`.
|
||||
* Explicit runtime composition: constructs the one shared PostgreSQL pool (identity and
|
||||
* authorization each get a typed Drizzle view over it — see `./database.js`), the Core identity
|
||||
* runtime, the Core authorization store and evaluator, and the Hono application, then starts the
|
||||
* Node HTTP server. Returns a handle with an idempotent `shutdown()` so both the executable entry
|
||||
* point and tests can stop the server and close the shared pool deterministically. Never applies
|
||||
* migrations or generates schema — the database must already be migrated through
|
||||
* `pnpm migrations:apply`.
|
||||
*/
|
||||
export async function startApi(
|
||||
config: ApiRuntimeConfig,
|
||||
options: StartApiOptions = {},
|
||||
): Promise<RunningApi> {
|
||||
const databaseHandle = createIdentityDrizzleDatabase(config.databaseUrl, {
|
||||
const databaseHandle = createApiDatabase(config.databaseUrl, {
|
||||
connectionTimeoutMs,
|
||||
operationTimeoutMs,
|
||||
});
|
||||
|
||||
let runtime: ReturnType<typeof createIdentityRuntime>;
|
||||
let authorizationStore: ReturnType<typeof createAuthorizationStore>;
|
||||
try {
|
||||
runtime = createIdentityRuntime({
|
||||
config: config.identity,
|
||||
db: databaseHandle.db,
|
||||
db: databaseHandle.identity,
|
||||
...(options.rateLimitOverride ? { rateLimitOverride: options.rateLimitOverride } : {}),
|
||||
});
|
||||
authorizationStore = createAuthorizationStore(databaseHandle.authorization);
|
||||
} catch (error) {
|
||||
await safeCloseDatabase(databaseHandle);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const app = createApiApp({ runtime, trustedOrigins: config.identity.trustedOrigins });
|
||||
const evaluator = createAuthorizationEvaluator({
|
||||
grants: authorizationStore,
|
||||
platformOwners: authorizationStore,
|
||||
});
|
||||
|
||||
const app = createApiApp({
|
||||
authorization: { evaluator, store: authorizationStore },
|
||||
runtime,
|
||||
trustedOrigins: config.identity.trustedOrigins,
|
||||
});
|
||||
|
||||
let server: ServerType;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user