// The app wired the way apps/web and apps/worker wire the account module -- the registry from the // manifests, a memory data source, the tenancy hooks, the account ports and the opt-in modules' // recording platforms -- plus `flaky`, a module whose share of an account can be made to fail, so // the partial-failure and resume paths are proven whichever opt-in modules this app kept. import { createHash } from "node:crypto" import { sessions } from "@conusai-kit/auth" import { type Actor, AppError, buildRegistry, Container, contribute, createCaller, defineModule, type ModuleManifest, type Registry, type TransportCaller, } from "@conusai-kit/core" import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA, toInstant } from "@conusai-kit/data" import { GUIDES_PLATFORM, GUIDES_REGISTRY } from "@conusai-kit/guides" // kit:guides import { recordingGuidesPlatform } from "@conusai-kit/guides/testing" // kit:guides import { SUPPORT_PLATFORM } from "@conusai-kit/support" // kit:support import { recordingSupportPlatform } from "@conusai-kit/support/testing" // kit:support import { accountData, createTenancyHooks, requireAppUser, TENANCY } from "@conusai-kit/tenancy" import { appModules } from "../../index.js" import { ACCOUNT_CREDENTIAL, ACCOUNT_IDENTITY_PROVIDER, ACCOUNT_PRIVACY, ACCOUNT_REGISTRY, type AccountCredential, type AccountIdentityProvider, type AccountPrivacy, } from "../index.js" export const ISSUER = "https://id.example.test" /** A stand-in for the hash of a person's session handle, distinct per person as the real one is. */ export const handleHashOf = (email: string): string => createHash("sha256").update(email).digest("hex") /** * The app's modules without `removed`, as `kit template:prune --without` leaves them: the module, and * every other module's contribution to an extension point the removed module declared. */ export function modulesWithout(...removed: string[]): ModuleManifest[] { return appModules .filter((module) => !removed.includes(module.id)) .map((module) => ({ ...module, extends: (module.extends ?? []).filter( (contribution) => !removed.includes(contribution.point.id.split(".")[0] ?? ""), ), })) } /** What `flaky` does when asked, and what it saw. */ export interface FlakyState { exportFails: boolean eraseFails: boolean readonly exported: string[] readonly erased: string[] /** Called inside each erasure, before it answers: a test looks at the stored rows at that moment. */ onErase?: (subject: string) => Promise } const unavailable = () => new AppError({ type: "/problems/flaky_unavailable", title: "Unavailable", status: 503, code: "flaky_unavailable" }) export function flakyModule(state: FlakyState): ModuleManifest { return defineModule({ id: "flaky", version: "0.1.0", kit: "^0.1.0", requires: ["tenancy"], extends: [ contribute(accountData, [ { key: "flaky", async export({ subject }) { if (state.exportFails) throw unavailable() state.exported.push(subject) return { subject, items: 1 } }, async erase({ subject }) { await state.onErase?.(subject) if (state.eraseFails) throw unavailable() state.erased.push(subject) }, }, ]), ], }) } /** * The memory data source, with the session table's listing made to fail while `failing.sessions` is * set: the first step of a deletion (revoking the person's live sessions) then throws, as a database * outage would, after the ledger row was written. */ export function failingSessions(data: MemoryData): { data: MemoryData; failing: { sessions: boolean } } { const failing = { sessions: false } const outage = () => { throw new AppError({ type: "/problems/unavailable", title: "Unavailable", status: 503, code: "unavailable" }) } const wrapped = new Proxy(data, { get(target, property, receiver) { const value = Reflect.get(target, property, receiver) if (property !== "global") return typeof value === "function" ? value.bind(target) : value return (entity: Parameters[0]) => { const repository = target.global(entity) if ((entity as unknown) !== sessions) return repository return new Proxy(repository, { get(repo, name, r) { const method = Reflect.get(repo, name, r) if (typeof method !== "function") return method return (...args: unknown[]) => (failing.sessions && name === "list" ? outage() : method.apply(repo, args)) }, }) } }, }) return { data: wrapped, failing } } export const newFlaky = (): FlakyState => ({ exportFails: false, eraseFails: false, exported: [], erased: [] }) export type MemoryData = ReturnType export interface Person { readonly actor: Actor & { readonly userId: string } readonly email: string readonly subject: string readonly sessionId: string /** The person's caller, as the web binds it for a browser session (or the credential given). */ call(credential?: AccountCredential): TransportCaller } export function makeApp( options: { modules?: readonly ModuleManifest[] data?: MemoryData /** Bound in the process container: every request and the worker's sweep have it. */ identityProvider?: AccountIdentityProvider /** Bound in each request's container only: a sweep in this app runs without it. */ requestIdentityProvider?: AccountIdentityProvider /** The platform's privacy service (ADR-049), bound in the process container. */ privacy?: AccountPrivacy } = {}, ) { const registry: Registry = buildRegistry([...(options.modules ?? appModules)]) const data = options.data ?? createMemoryDataSource({ entities: entitiesOf(registry, "postgres") }) const root = new Container() root.value(DATA_SOURCE, data) root.value(SYSTEM_DATA, data) root.value(ACCOUNT_REGISTRY, registry) if (options.identityProvider) root.value(ACCOUNT_IDENTITY_PROVIDER, options.identityProvider) if (options.privacy) root.value(ACCOUNT_PRIVACY, options.privacy) const guides = recordingGuidesPlatform() // kit:guides root.value(GUIDES_PLATFORM, guides) // kit:guides root.value(GUIDES_REGISTRY, registry) // kit:guides const support = recordingSupportPlatform() // kit:support root.value(SUPPORT_PLATFORM, support) // kit:support const callerFor = (actor: Actor, credential: AccountCredential): TransportCaller => { const tenancy = createTenancyHooks({ data, registry }) const container = root.child() container.value(TENANCY, tenancy) container.value(ACCOUNT_CREDENTIAL, credential) if (options.requestIdentityProvider) container.value(ACCOUNT_IDENTITY_PROVIDER, options.requestIdentityProvider) return createCaller({ registry, container, tenancy }, { actor }) } /** A first sign-in, as kit-auth's callback makes it: the user, the identity mapping, a session row. */ const signUp = async (email: string): Promise => { const subject = `sub-${email}` const user = await requireAppUser(data, { issuer: ISSUER, subject, email, displayName: email.split("@")[0] ?? email, locale: "en", }) const now = Date.now() const session = await data.global(sessions).insert({ userId: user.id, issuer: ISSUER, subject, handleHash: handleHashOf(email), tokens: "sealed-tokens-of-the-test", accessTokenExpiresAt: toInstant(now + 3600_000), lastSeenAt: toInstant(now), absoluteExpiresAt: toInstant(now + 14 * 86400_000), revokedAt: null, }) const actor = { userId: user.id } const browser: AccountCredential = { interactive: true, sessionId: session.id } return { actor, email, subject, sessionId: session.id, call: (credential = browser) => callerFor(actor, credential), } } /** The worker's run of the sweep job: a scopeless job, in a container with no credential. */ const sweep = async (): Promise => { const job = registry.jobs["account.deletion.sweep"] if (!job) throw new Error("the account module declares no sweep job") await job.def.handler(null, { container: root.child(), correlationId: "test-sweep", attempt: 1 }) } return { registry, data, root, signUp, sweep, guides, // kit:guides support, // kit:support } }