feat: add browser auth shell
CI / verify (pull_request) Successful in 38s
CI / infrastructure-smoke (pull_request) Successful in 12s
CI / migration-smoke (pull_request) Successful in 1m0s

This commit is contained in:
ekklesia
2026-08-23 22:01:26 -05:00
parent 91aa9c2930
commit a8c7c50b4d
26 changed files with 2702 additions and 194 deletions
+12 -2
View File
@@ -220,6 +220,7 @@ function createModuleRealtimeClient(
function createModuleApiClient(
config: EkklesiaHostConfig,
descriptor: HostModuleUiDescriptor,
onAuthorizationResponse?: (status: 401 | 403) => void,
): ModuleUiApiClient {
const performRequest = config.fetch ?? fetch;
const moduleRoot = new URL(resolveApiUrl(config, descriptor.apiBaseUrl));
@@ -244,7 +245,13 @@ function createModuleApiClient(
// The module's own `init` is forwarded whole — method, headers, body, signal — and only
// `credentials` is imposed, because session handling is the host's job and a module UI never
// sees or supplies a token (ADR 0022). A module cannot opt out of it.
return performRequest(target.href, { ...init, credentials: "include" });
const response = await performRequest(target.href, { ...init, credentials: "include" });
if (response.status === 401 || response.status === 403) {
// This callback stays entirely on the host side. The module still receives its original
// response, and the public ModuleUiContext gains no authentication control or event.
onAuthorizationResponse?.(response.status);
}
return response;
},
});
}
@@ -253,6 +260,9 @@ export interface CreateModuleUiContextOptions {
readonly config: EkklesiaHostConfig;
readonly descriptor: HostModuleUiDescriptor;
readonly principal: ModuleUiPrincipal;
/** Host-only signal used by the Dashboard to revalidate an apparently expired session. It is not
* included in the returned ModuleUiHostContext and is intentionally absent from the UI SDK. */
readonly onAuthorizationResponse?: (status: 401 | 403) => void;
}
/**
@@ -271,7 +281,7 @@ export interface CreateModuleUiContextOptions {
export function createModuleUiContext(options: CreateModuleUiContextOptions): ModuleUiHostContext {
const granted = new Set(options.descriptor.permissions);
return Object.freeze({
api: createModuleApiClient(options.config, options.descriptor),
api: createModuleApiClient(options.config, options.descriptor, options.onAuthorizationResponse),
moduleId: options.descriptor.id,
permissions: Object.freeze({ has: (permissionId: string) => granted.has(permissionId) }),
principal: options.principal,