@odla-ai/chapter 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -28
- package/dist/index.cjs +169 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -8
- package/dist/index.d.ts +136 -8
- package/dist/index.js +169 -14
- package/dist/index.js.map +1 -1
- package/dist/ui/index.d.ts +54 -3
- package/dist/ui/index.js +188 -93
- package/dist/ui/index.js.map +1 -1
- package/dist/worker/index.cjs +344 -17
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +60 -6
- package/dist/worker/index.d.ts +60 -6
- package/dist/worker/index.js +344 -17
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/worker.ts","../../src/worker-context.ts","../../src/auth.ts","../../src/worker-routes.ts","../../src/member.ts","../../src/network.ts","../../src/scheduling.ts","../../src/session.ts","../../src/worker-routes-schedule.ts","../../src/payments.ts","../../src/payments-stripe.ts","../../src/worker-routes-payments.ts"],"sourcesContent":["// chapterWorker — the Cloudflare Worker for a chapter/hub site. This entry\n// (@odla-ai/chapter/worker) is separate from the core so the CLI can load\n// odla.config.mjs without pulling in worker-runtime deps.\n//\n// The worker is the ONLY thing that talks to odla-db, using the app key\n// (ODLA_API_KEY), which bypasses the deny-all rules. Browsers never receive a\n// db credential. Access is admin-only: a request is authorized when its Clerk\n// session JWT verifies AND the user's role (a JWT claim, or a row in the\n// Studio-seeded `admins` allowlist) is an admin rung.\n//\n// The env typing + auth plumbing live in ./worker-context; the route handlers in\n// ./worker-routes. This entry only builds the context and runs the routes in\n// order — first match wins, else the static site (ASSETS).\n//\n// hub mode routes: GET /api/config, GET /api/me, /api/crm/*, POST\n// /api/network/shared, else ASSETS. chapter mode adds the public member surface:\n// GET /api/join-config and POST /api/applications (idempotent).\nimport { createWorkerContext } from \"./worker-context\";\nimport type { ChapterEnv, ChapterWorkerOptions } from \"./worker-context\";\nimport { handleConfig, handleCrm, handleMe, handleMember, handleNetworkShared } from \"./worker-routes\";\nimport { handleSchedule } from \"./worker-routes-schedule\";\nimport { handlePayments } from \"./worker-routes-payments\";\nimport type { Route } from \"./worker-routes\";\n\nexport type { ChapterEnv, ChapterWorkerOptions } from \"./worker-context\";\n\n// First-match-wins order (preserved from the original single-file handler;\n// scheduling + payments appended after the member surface, all chapter-mode only).\nconst ROUTES: Route[] = [handleConfig, handleMe, handleCrm, handleNetworkShared, handleMember, handleSchedule, handlePayments];\n\n/**\n * Build the Cloudflare `ExportedHandler` for a chapter/hub site: Clerk-JWT\n * verification, the odla-db admins-allowlist gate, the mounted @odla-ai/crm\n * routes, the hub→chapter network projection, and the static-asset fallback. In\n * hub mode it serves /api/config, /api/me, /api/crm/*, /api/network/shared;\n * chapter mode adds the public member surface (join/apply; Stripe + booking\n * land in the member-experience release).\n *\n * Observability is a host concern, not a chapter dependency. To trace, wrap the\n * result in your worker entry — `export default withObservability(chapterWorker(\n * { chapter }))` — with `withObservability` from `@odla-ai/o11y`. Sites that\n * don't run o11y bundle `@odla-ai/chapter/worker` without installing it.\n */\nexport function chapterWorker(options: ChapterWorkerOptions) {\n const ctx = createWorkerContext(options);\n return {\n async fetch(req: Request, env: ChapterEnv): Promise<Response> {\n const url = new URL(req.url);\n for (const route of ROUTES) {\n const res = await route(req, url, env, ctx);\n if (res) return res;\n }\n // Everything else is the static site.\n return env.ASSETS.fetch(req);\n },\n };\n}\n","// Shared context for the chapter Worker: env typing, the odla-db admin client,\n// Clerk-JWT verification, and the source-aware auth helpers. Split out of\n// worker.ts so the route modules share ONE construction of the caches (public\n// config + JWKS) per site, and the worker entry stays a thin composer under the\n// per-file LOC cap. Nothing here is re-exported from the worker entry, so the\n// public `@odla-ai/chapter/worker` surface is unchanged by the split.\nimport { initAdmin } from \"@odla-ai/db\";\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport { isAdminRole, roleFromClaim } from \"./auth\";\nimport type { Chapter } from \"./types\";\n\n/** The Cloudflare SEND_EMAIL binding payload. */\nexport interface EmailPayload {\n from: string;\n to: string[];\n subject: string;\n text?: string;\n html?: string;\n replyTo?: string;\n headers?: Record<string, string>;\n}\n\n/** The Worker env a chapter site provides (wrangler vars + the ODLA_API_KEY\n * secret pushed by provision). */\nexport interface ChapterEnv {\n ASSETS: { fetch(req: Request): Promise<Response> };\n ODLA_ENDPOINT: string;\n ODLA_TENANT: string;\n ODLA_PLATFORM: string;\n ODLA_APP_ID: string;\n ODLA_ENV: string;\n ODLA_API_KEY: string;\n SEND_EMAIL?: { send(payload: EmailPayload): Promise<{ messageId: string }> };\n EMAIL_FROM?: string;\n}\n\n/** Options for `chapterWorker`. */\nexport interface ChapterWorkerOptions {\n chapter: Chapter;\n /** CRM mount point. Default \"/api/crm\". */\n crmBasePath?: string;\n}\n\n/** The registry public-config a site reads to boot Clerk sign-in. */\nexport type PublicConfig = { env?: string; clerkPublishableKey?: string | null; issuer?: string | null };\n/** The odla-db admin client type. */\nexport type Db = ReturnType<typeof initAdmin>;\n\n/** A verified session: the Clerk `sub`, optional email, and the raw JWT payload\n * (so the role claim can be read for auth source \"claim\"). */\nexport interface Verified {\n userId: string;\n email?: string;\n payload: Record<string, unknown>;\n}\n\n/** JSON response helper. */\nexport const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), { status, headers: { \"content-type\": \"application/json\" } });\n\n/**\n * Build the per-site Worker context: env-independent helpers closing over the\n * public-config and JWKS caches, plus the source-aware auth gate (JWT claim or\n * the odla-db `admins` allowlist). Constructed once per `chapterWorker` and\n * shared by every route module.\n */\nexport function createWorkerContext(options: ChapterWorkerOptions) {\n const { chapter } = options;\n const auth = chapter.auth;\n const crmBase = options.crmBasePath ?? \"/api/crm\";\n\n let publicConfigCache: { value: PublicConfig; at: number } | null = null;\n const jwksByIssuer = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\n async function getPublicConfig(env: ChapterEnv): Promise<PublicConfig> {\n if (publicConfigCache && Date.now() - publicConfigCache.at < 5 * 60_000) return publicConfigCache.value;\n const res = await fetch(`${env.ODLA_PLATFORM}/registry/apps/${env.ODLA_APP_ID}/public-config?env=${env.ODLA_ENV}`);\n if (!res.ok) throw new Error(`public-config fetch failed: ${res.status}`);\n const value = (await res.json()) as PublicConfig;\n publicConfigCache = { value, at: Date.now() };\n return value;\n }\n\n async function verifyUser(req: Request, env: ChapterEnv): Promise<Verified | null> {\n const header = req.headers.get(\"authorization\") ?? \"\";\n if (!header.startsWith(\"Bearer \")) return null;\n const token = header.slice(7);\n const { issuer } = await getPublicConfig(env);\n if (!issuer) return null;\n let jwks = jwksByIssuer.get(issuer);\n if (!jwks) {\n jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));\n jwksByIssuer.set(issuer, jwks);\n }\n try {\n const { payload } = await jwtVerify(token, jwks, { issuer });\n if (!payload.sub) return null;\n return {\n userId: payload.sub,\n email: typeof payload.email === \"string\" ? payload.email : undefined,\n payload: payload as Record<string, unknown>,\n };\n } catch {\n return null;\n }\n }\n\n function makeDb(env: ChapterEnv): Db {\n return initAdmin({ appId: env.ODLA_TENANT, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_ENDPOINT });\n }\n\n // The `admins` allowlist gate (auth source \"table\"): no route ever writes it, so\n // membership can only be granted by a human in odla Studio.\n async function isAdminEmail(db: Db, email: string | undefined): Promise<boolean> {\n if (!email) return false;\n const { admins } = await db.query({ admins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });\n return Array.isArray(admins) && admins.length > 0;\n }\n\n // The read-only `superAdmins` tier — queried, never written (Studio-only).\n async function isSuperAdminEmail(db: Db, email: string | undefined): Promise<boolean> {\n if (!auth.superAdmins || !email) return false;\n const { superAdmins } = await db.query({ superAdmins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });\n return Array.isArray(superAdmins) && superAdmins.length > 0;\n }\n\n // The user's role, resolved per the auth source: a JWT claim, or the `admins`\n // allowlist synthesized to the admin rung / the lowest one.\n async function roleFor(db: Db, u: Verified): Promise<string> {\n if (auth.source === \"claim\") return roleFromClaim(u.payload, auth);\n return (await isAdminEmail(db, u.email)) ? auth.adminRole : (auth.ladder[0] as string);\n }\n\n // Admin authorization — the boolean gate used by /api/me and the CRM surface.\n async function isAdmin(db: Db, u: Verified): Promise<boolean> {\n if (auth.source === \"claim\") return isAdminRole(roleFromClaim(u.payload, auth), auth);\n return isAdminEmail(db, u.email);\n }\n\n function crmSender(env: ChapterEnv) {\n if (!env.SEND_EMAIL || !env.EMAIL_FROM) return undefined;\n const binding = env.SEND_EMAIL;\n return {\n async send(payload: EmailPayload): Promise<{ messageId: string }> {\n return binding.send(payload);\n },\n };\n }\n\n return { chapter, auth, crmBase, getPublicConfig, verifyUser, makeDb, isAdminEmail, isSuperAdminEmail, roleFor, isAdmin, crmSender };\n}\n\n/** The value returned by {@link createWorkerContext}, threaded to every route. */\nexport type WorkerContext = ReturnType<typeof createWorkerContext>;\n","// Identity + authorization for a chapter/hub site — the pieces every membership\n// site needs and none should re-derive: a resolved role policy, role resolution\n// from a JWT claim, the privilege-escalation guard, and a tenant-vault read.\n// Everything here is pure or structural (no runtime @odla-ai/db import), so it is\n// trivially testable and the worker stays the only thing that talks to odla-db.\nimport type { ChapterAuth, ChapterMode, ResolvedAuth } from \"./types\";\n\n/**\n * Apply defaults + validate the auth config into a {@link ResolvedAuth}. Defaults\n * by mode: `chapter` → the `provisional/member/admin` claim ladder with the\n * `superAdmins` tier (Silver & Salt); `hub` → the `admins` allowlist table, no\n * super tier (Built Not Found). Throws at import on a bad policy.\n */\nexport function resolveAuth(mode: ChapterMode, auth: ChapterAuth | undefined): ResolvedAuth {\n const a = auth ?? {};\n const source = a.source ?? (mode === \"hub\" ? \"table\" : \"claim\");\n if (source !== \"claim\" && source !== \"table\") {\n throw new Error(`defineChapter.auth.source: must be \"claim\" or \"table\" — got ${JSON.stringify(a.source)}`);\n }\n const claim = a.claim ?? \"role\";\n if (typeof claim !== \"string\" || claim === \"\") {\n throw new Error(\"defineChapter.auth.claim: must be a non-empty string\");\n }\n const ladder = a.ladder ?? [\"provisional\", \"member\", \"admin\"];\n if (!Array.isArray(ladder) || ladder.length === 0 || !ladder.every((r) => typeof r === \"string\" && r !== \"\")) {\n throw new Error(\"defineChapter.auth.ladder: must be a non-empty array of role strings\");\n }\n const adminRole = ladder[ladder.length - 1] as string;\n const superAdmins = a.superAdmins ?? source === \"claim\";\n return { source, claim, ladder, adminRole, superAdmins };\n}\n\n/** The role from a verified JWT payload, per the resolved policy. An unknown or\n * missing claim falls back to the lowest ladder rung (fail safe, never admin). */\nexport function roleFromClaim(payload: Record<string, unknown>, auth: ResolvedAuth): string {\n const raw = payload[auth.claim];\n return typeof raw === \"string\" && auth.ladder.includes(raw) ? raw : (auth.ladder[0] as string);\n}\n\n/** Does a role meet the admin bar (the highest ladder rung)? */\nexport function isAdminRole(role: string, auth: ResolvedAuth): boolean {\n return role === auth.adminRole;\n}\n\n/** Inputs to the role-change guard — resolved by the caller (route) from the\n * identity provider + the read-only `superAdmins` table. */\nexport interface RoleChangeContext {\n actorId: string;\n actorIsSuper: boolean;\n targetId: string;\n targetCurrentRole: string;\n targetIsSuper: boolean;\n newRole: string;\n auth: ResolvedAuth;\n}\n\n/** The result of {@link canChangeRole}: allow, or deny with the HTTP status +\n * message the route should return. */\nexport type GuardResult = { ok: true } | { ok: false; status: number; error: string };\n\n/**\n * The privilege-escalation guard — package-enforced so every site gets it and\n * none re-derives it. Denies: an out-of-ladder role; changing your own role;\n * touching a super-admin unless you are one; and (when a `superAdmins` tier\n * exists) creating or altering an admin unless you are a super-admin. Note the\n * super-admin tier itself is never writable here — it lives in the read-only\n * `superAdmins` table, set only in odla Studio.\n */\nexport function canChangeRole(ctx: RoleChangeContext): GuardResult {\n const { auth } = ctx;\n if (!auth.ladder.includes(ctx.newRole)) {\n return { ok: false, status: 400, error: `role must be one of: ${auth.ladder.join(\", \")}` };\n }\n if (ctx.actorId === ctx.targetId) {\n return { ok: false, status: 400, error: \"you cannot change your own role\" };\n }\n if (ctx.targetIsSuper && !ctx.actorIsSuper) {\n return { ok: false, status: 403, error: \"this person is a super-admin; their access is managed in odla Studio\" };\n }\n const touchesAdmin = ctx.newRole === auth.adminRole || ctx.targetCurrentRole === auth.adminRole;\n if (auth.superAdmins && touchesAdmin && !ctx.actorIsSuper) {\n return { ok: false, status: 403, error: `only super-admins can create or change an ${auth.adminRole}` };\n }\n return { ok: true };\n}\n\n/** Structural view of odla-db's tenant-vault read, so chapter takes no runtime\n * dependency on @odla-ai/db. The worker's admin client satisfies this. */\nexport interface SecretStore {\n secrets: { get(name: string): Promise<string> };\n}\n\n/**\n * Read a tenant-vault secret by name; `undefined` when it is absent or the vault\n * errors, so callers degrade gracefully (e.g. `paymentsReady: false`) rather than\n * throwing. Never logs the value.\n */\nexport async function getVaultSecret(db: SecretStore, name: string): Promise<string | undefined> {\n try {\n const value = await db.secrets.get(name);\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n } catch {\n return undefined;\n }\n}\n","// Route handlers for the chapter Worker. Each returns a Response when it owns the\n// request, or null to fall through to the next handler (finally ASSETS). Split\n// out of worker.ts so the entry stays a thin composer under the per-file LOC cap;\n// behaviour and route order are unchanged from the original single-file handler.\nimport { createCrmRoutes } from \"@odla-ai/crm\";\nimport { getVaultSecret, isAdminRole } from \"./auth\";\nimport { joinConfig, submitApplication } from \"./member\";\nimport { projectSharedRecord } from \"./network\";\nimport { resolveScheduling } from \"./scheduling\";\nimport { memberApplication } from \"./session\";\nimport type { ApplicationRecord, MeetingRecord, MemberApplication } from \"./session\";\nimport { json } from \"./worker-context\";\nimport type { ChapterEnv, WorkerContext } from \"./worker-context\";\nimport type { ChapterDb, ChapterScheduling } from \"./types\";\n\n// The signed-in member's own application (chapter mode): the latest by email,\n// folded with its latest scheduled meeting and rendered in the group's timezone.\n// The meetings row is canonical, so no live-calendar reconcile is needed here.\nasync function memberSessionApplication(db: ChapterDb, chapterId: string, email: string): Promise<MemberApplication | null> {\n const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: \"desc\" }, limit: 1 } } })).applications;\n const app = Array.isArray(apps) ? apps[0] : undefined;\n if (!app) return null;\n const meetings = (\n await db.query({ meetings: { $: { where: { applicationId: app.id, status: \"scheduled\" }, order: { createdAt: \"desc\" }, limit: 1 } } })\n ).meetings;\n const meeting = Array.isArray(meetings) ? meetings[0] : undefined;\n const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;\n const group = Array.isArray(groups) ? groups[0] : undefined;\n const timezone = resolveScheduling(group?.schedulingJson as ChapterScheduling | undefined).timezone;\n return memberApplication(app as unknown as ApplicationRecord, meeting as unknown as MeetingRecord | undefined, timezone);\n}\n\n/** One route handler: owns the request (Response) or falls through (null). */\nexport type Route = (req: Request, url: URL, env: ChapterEnv, ctx: WorkerContext) => Promise<Response | null>;\n\n/** GET /api/config — the public Clerk publishable key, so the SPA can boot sign-in. */\nexport const handleConfig: Route = async (_req, url, env, ctx) => {\n if (url.pathname !== \"/api/config\") return null;\n try {\n const { clerkPublishableKey } = await ctx.getPublicConfig(env);\n return json({ clerkPublishableKey: clerkPublishableKey ?? null, env: env.ODLA_ENV });\n } catch {\n return json({ clerkPublishableKey: null, env: env.ODLA_ENV });\n }\n};\n\n/** GET /api/me — the signed-in user's role, admin authorization, and super-admin\n * tier. In chapter mode it also carries the member's own `application` (their\n * status + booked call), so the member area renders from one call. */\nexport const handleMe: Route = async (req, url, env, ctx) => {\n if (url.pathname !== \"/api/me\") return null;\n const u = await ctx.verifyUser(req, env);\n if (!u) return json({ authorized: false }, 401);\n const db = ctx.makeDb(env);\n const role = await ctx.roleFor(db, u);\n const superAdmin = await ctx.isSuperAdminEmail(db, u.email);\n const base = { authorized: isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };\n if (ctx.chapter.mode !== \"chapter\" || !u.email) return json(base);\n const application = await memberSessionApplication(db as unknown as ChapterDb, ctx.chapter.id, u.email);\n return json({ ...base, application });\n};\n\n/** /api/crm/* — the CRM admin surface (mounted at ctx.crmBase). */\nexport const handleCrm: Route = async (req, url, env, ctx) => {\n const crmBase = ctx.crmBase;\n if (url.pathname !== crmBase && !url.pathname.startsWith(crmBase + \"/\")) return null;\n const db = ctx.makeDb(env);\n const routes = createCrmRoutes({\n crm: ctx.chapter.crm,\n db: db as never,\n authorize: async (r: Request) => {\n const u = await ctx.verifyUser(r, env);\n if (!u || !(await ctx.isAdmin(db, u))) return null;\n return u.email ? { userId: u.userId, email: u.email } : { userId: u.userId };\n },\n sender: ctx.crmSender(env),\n from: env.EMAIL_FROM,\n envName: env.ODLA_ENV,\n baseUrl: url.origin,\n basePath: crmBase,\n });\n const res = await routes(req);\n if (res) return res;\n return json({ error: \"not found\" }, 404);\n};\n\n/** POST /api/network/shared — the hub push projection into this site's crm_record\n * (vault-secret gated, works in both modes). */\nexport const handleNetworkShared: Route = async (req, url, env, ctx) => {\n if (req.method !== \"POST\" || url.pathname !== \"/api/network/shared\") return null;\n const db = ctx.makeDb(env);\n const secret = await getVaultSecret(db as unknown as ChapterDb, \"network_share_secret\");\n const provided = (req.headers.get(\"authorization\") ?? \"\").replace(/^Bearer /, \"\");\n if (!secret || provided.length !== secret.length || provided !== secret) {\n return json({ error: \"unauthorized\" }, 401);\n }\n let person: Record<string, unknown>;\n try {\n person = JSON.parse(await req.text()) as Record<string, unknown>;\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n if (typeof person.email !== \"string\" || typeof person.hubRecordId !== \"string\") {\n return json({ error: \"email and hubRecordId are required\" }, 400);\n }\n const { recordId } = await projectSharedRecord(\n { crm: ctx.chapter.crm, db: db as unknown as ChapterDb, now: () => Date.now(), newId: () => crypto.randomUUID() },\n person as never,\n );\n return json({ recordId });\n};\n\n/** Chapter-mode public member surface: GET /api/join-config, POST /api/applications.\n * Returns null in hub mode so those paths fall through to ASSETS. */\nexport const handleMember: Route = async (req, url, env, ctx) => {\n const chapter = ctx.chapter;\n if (chapter.mode !== \"chapter\") return null;\n\n // Public join config: prices + policy copy + payment readiness (B1/C2).\n if (req.method === \"GET\" && url.pathname === \"/api/join-config\") {\n const db = ctx.makeDb(env);\n const groupId = url.searchParams.get(\"group\") ?? chapter.id;\n const { groups } = await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } });\n const group = Array.isArray(groups) ? groups[0] : undefined;\n if (!group) return json({ error: \"not found\" }, 404);\n const stripeKey = await getVaultSecret(db as unknown as ChapterDb, \"stripe_secret_key\");\n const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);\n return json(joinConfig(group as never, paymentsReady));\n }\n\n // Public application submit — validated, body-capped, idempotent (B2/B3).\n if (req.method === \"POST\" && url.pathname === \"/api/applications\") {\n const raw = await req.text();\n if (raw.length > chapter.application.bodyCap) return json({ error: \"request body too large\" }, 413);\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n const submissionId = typeof parsed.submissionId === \"string\" ? parsed.submissionId : undefined;\n const result = await submitApplication(ctx.makeDb(env) as unknown as ChapterDb, chapter, parsed, {\n submissionId,\n groupId: chapter.id,\n now: Date.now(),\n newId: () => crypto.randomUUID(),\n });\n if (!result.ok) return json({ error: result.error }, 400);\n return json({ id: result.id, duplicate: result.duplicate, status: result.status });\n }\n\n return null;\n};\n","// The public member surface logic: the join config a site's join page reads (B1)\n// and the idempotent application submit (B2 validation + B3 exactly-once). Both\n// take the structural ChapterDb, so they're tested against an in-memory fake and\n// carry no runtime @odla-ai/db import. The worker builds the real db client, does\n// Clerk verification, enforces the body cap, and mounts these on chapter routes.\nimport type { Chapter, ChapterApplication, ChapterDb, ResolvedApplication } from \"./types\";\n\n// Silver & Salt's join form. `focus` (a json field) is always accepted.\nconst DEFAULT_REQUIRED = [\"firstName\", \"lastName\", \"email\", \"referral\", \"whoYouAre\", \"message\"];\nconst DEFAULT_OPTIONAL = [\"referralName\", \"linkedin\", \"phone\", \"state\"];\n\n/** Apply defaults + validate the application config. Throws at import on bad shape. */\nexport function resolveApplication(a: ChapterApplication | undefined): ResolvedApplication {\n const required = a?.required ?? DEFAULT_REQUIRED;\n const optional = a?.optional ?? DEFAULT_OPTIONAL;\n for (const [name, arr] of [[\"required\", required], [\"optional\", optional]] as const) {\n if (!Array.isArray(arr) || !arr.every((f) => typeof f === \"string\" && f !== \"\")) {\n throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);\n }\n }\n return {\n required,\n optional,\n maxLen: a?.maxLen ?? {},\n defaultMaxLen: a?.defaultMaxLen ?? 2000,\n bodyCap: a?.bodyCap ?? 32768,\n };\n}\n\n/** A validated submission, or a 400-worthy validation error the route returns. */\nexport type SubmitResult =\n | { ok: true; id: string; duplicate: boolean; status: string }\n | { ok: false; error: string };\n\n/**\n * Submit a membership application (B2 + B3). Validates the configured required\n * fields + max lengths, writes the `applications` row at the pipeline's initial\n * status, and — when the client supplies a `submissionId` — stamps it as the\n * transaction's mutationId (`join:${submissionId}`) so a double-tap can never\n * create two applications (the second returns `duplicate: true`). Idempotency is\n * package-enforced. `now`/`newId` are injected (deterministic in tests).\n */\nexport async function submitApplication(\n db: ChapterDb,\n chapter: Chapter,\n fields: Record<string, unknown>,\n opts: { submissionId?: string; groupId?: string; now: number; newId: () => string },\n): Promise<SubmitResult> {\n const app = chapter.application;\n for (const f of app.required) {\n const v = fields[f];\n if (typeof v !== \"string\" || v.trim() === \"\") return { ok: false, error: `${f} is required` };\n }\n for (const f of [...app.required, ...app.optional]) {\n const v = fields[f];\n const cap = app.maxLen[f] ?? app.defaultMaxLen;\n if (typeof v === \"string\" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };\n }\n\n const id = opts.newId();\n const row: Record<string, unknown> = { id, status: chapter.pipeline.initial, createdAt: opts.now };\n for (const f of [...app.required, ...app.optional]) {\n if (typeof fields[f] === \"string\") row[f] = (fields[f] as string).trim();\n }\n if (fields.focus !== undefined) row.focus = fields.focus;\n if (opts.groupId) row.groupId = opts.groupId;\n\n const { duplicate } = await db.transact(\n [{ t: \"update\", ns: \"applications\", id, attrs: row }],\n opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : undefined,\n );\n return { ok: true, id, duplicate, status: chapter.pipeline.initial };\n}\n\n/** The `groups`-row fields the join config exposes. */\nexport interface JoinConfigGroup {\n id: string;\n name: string;\n standardPriceCents?: number;\n foundingDiscountCents?: number;\n disclaimerText?: string;\n refundPolicyText?: string;\n trustCopy?: string;\n commitmentText?: string;\n normsText?: string;\n}\n\n/**\n * The public join config (B1) a site's join page reads: copy + prices from the\n * group row plus `paymentsReady`. When payments aren't wired the join flow drops\n * the payment step (C2) — the worker computes `paymentsReady` from the group's\n * Stripe keys + vault secret. Pure.\n */\nexport function joinConfig(group: JoinConfigGroup, paymentsReady: boolean): Record<string, unknown> {\n return {\n id: group.id,\n name: group.name,\n standardPriceCents: group.standardPriceCents ?? 0,\n foundingDiscountCents: group.foundingDiscountCents ?? 0,\n disclaimerText: group.disclaimerText ?? \"\",\n refundPolicyText: group.refundPolicyText ?? \"\",\n trustCopy: group.trustCopy ?? \"\",\n commitmentText: group.commitmentText ?? \"\",\n normsText: group.normsText ?? \"\",\n paymentsReady,\n };\n}\n","// The hub → chapter people projection (push model). The network hub curates\n// prospects and pushes a person's contact data into THIS chapter's own\n// crm_record, so a chapter admin sees network prospects beside their applicants.\n//\n// Invariants (package-enforced so no site re-derives them):\n// - One-way: the chapter never writes back to the hub through this path.\n// - Idempotent: keyed by the hub's record id (a re-share updates, never\n// duplicates) AND unified by primaryEmail — a shared prospect who later\n// submits an application lands on the SAME crm_record, so the two projections\n// compose instead of forking the person.\n// - A person may be shared with many chapters; that fan-out is hub-side, so each\n// chapter's projection here is independent.\n//\n// Reuses @odla-ai/crm's record ops (full validation via crm.prepare), driven by\n// the resolved chapter CRM engine + the structural ChapterDb.\nimport { createRecord, updateRecord } from \"@odla-ai/crm\";\nimport type { Crm } from \"@odla-ai/crm\";\nimport type { ChapterDb } from \"./types\";\n\n/** The contact data the hub shares for a prospect. `hubRecordId` is the stable\n * idempotency key (the hub's crm_record id). */\nexport interface SharedPerson {\n email: string;\n name?: string;\n firstName?: string;\n lastName?: string;\n phone?: string;\n linkedin?: string;\n hubRecordId: string;\n}\n\n/** Map a shared prospect to a crm `person` input (only the fields the default\n * person type accepts). Name falls back to first+last, then the email. */\nexport function sharedPersonInput(person: SharedPerson): Record<string, unknown> {\n const email = person.email.toLowerCase();\n const fullName = [person.firstName, person.lastName].filter(Boolean).join(\" \").trim();\n const input: Record<string, unknown> = { name: person.name ?? fullName ?? email, email };\n if (input.name === \"\") input.name = email;\n if (person.firstName) input.firstName = person.firstName;\n if (person.lastName) input.lastName = person.lastName;\n if (person.phone) input.phone = person.phone;\n if (person.linkedin) input.linkedin = person.linkedin;\n return input;\n}\n\n/** Deps for the projection — the resolved CRM engine, the structural db, and\n * injected clock/id (deterministic in tests). */\nexport interface ProjectionDeps {\n crm: Crm;\n db: ChapterDb;\n now: () => number;\n newId: () => string;\n}\n\n/**\n * Upsert a hub-shared prospect into this chapter's `crm_record` (push\n * projection). Resolves an existing person by lowercased `primaryEmail` and\n * updates it, else creates one with a `share:${hubRecordId}` mutationId. Returns\n * the chapter-side record id. Callers wrap this in `.catch` so a projection\n * failure never fails the hub's share request.\n */\nexport async function projectSharedRecord(deps: ProjectionDeps, person: SharedPerson): Promise<{ recordId: string }> {\n const email = person.email.toLowerCase();\n const input = sharedPersonInput(person);\n const crmDeps = { crm: deps.crm, db: deps.db as never, now: deps.now, newId: deps.newId };\n const { crm_record } = await deps.db.query({\n crm_record: { $: { where: { type: \"person\", primaryEmail: email }, limit: 1 } },\n });\n const existing = crm_record?.[0];\n if (existing && typeof existing.id === \"string\") {\n await updateRecord(crmDeps, { id: existing.id, input });\n return { recordId: existing.id };\n }\n const created = await createRecord(crmDeps, { type: \"person\", input, mutationId: `share:${person.hubRecordId}` });\n return { recordId: created.id };\n}\n","// Scheduling core — config resolution + the booking invariants, ported from the\n// proven Silver & Salt worker. Everything here is PURE (no @odla-ai/calendar, no\n// db), so the correctness properties are unit-testable and package-enforced; the\n// worker route owns only the I/O (FreeBusy, computeBookableSlots, calendar\n// create/reschedule, the db writes) and calls these.\n//\n// Package-enforced invariants:\n// - the `meetings` row is canonical; applications.meetingAt and the calendar\n// event are projections written from it.\n// - one intro event per application, forever: a rebooking RESCHEDULES the\n// existing event (preserving its Meet link + invite thread), never creates a\n// second — see {@link bookingDecision} + {@link introIdempotencyKey}.\n// - status never moves backward on booking; you can only book from an early\n// stage — see {@link canBookFrom} + {@link applicationBookingUpdate}.\n// - endAt is always derived server-side, never client-supplied — see\n// {@link endForSlot}.\nimport type { ChapterScheduling } from \"./types\";\n\n/** A fully-resolved scheduling config (every field present). */\nexport interface ResolvedScheduling {\n slotMinutes: number;\n days: readonly number[];\n startHour: number;\n endHour: number;\n timezone: string;\n minNoticeHours: number;\n windowDays: number;\n summaryTemplate: string;\n}\n\n/** The S&S-proven defaults: 45-minute weekday slots, 9–5 Pacific, 24h notice,\n * a 14-day window. The summary is generic (a chapter's group seed supplies a\n * name-branded one). */\nexport const SCHEDULING_DEFAULTS: ResolvedScheduling = {\n slotMinutes: 45,\n days: [1, 2, 3, 4, 5],\n startHour: 9,\n endHour: 17,\n timezone: \"America/Los_Angeles\",\n minNoticeHours: 24,\n windowDays: 14,\n summaryTemplate: \"Introduction call with {{firstName}} {{lastName}}\",\n};\n\nfunction isValidTimeZone(tz: string): boolean {\n try {\n new Intl.DateTimeFormat(undefined, { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve a group's scheduling config against the defaults, validating every\n * bound the way S&S does at config-write time (throws on a bad config, so a\n * misconfiguration surfaces immediately rather than yielding empty slots).\n * `windowDays` is capped at 62 because Google FreeBusy is.\n */\nexport function resolveScheduling(config?: ChapterScheduling): ResolvedScheduling {\n const d = config ?? {};\n const c: ResolvedScheduling = {\n slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,\n days: d.days ?? SCHEDULING_DEFAULTS.days,\n startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,\n endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,\n timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,\n minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,\n windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,\n summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate,\n };\n const fail = (msg: string): never => {\n throw new Error(`scheduling: ${msg}`);\n };\n if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) fail(\"slotMinutes must be 15–240\");\n if (!(c.windowDays >= 1 && c.windowDays <= 62)) fail(\"windowDays must be 1–62 (FreeBusy caps at 62)\");\n if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) fail(\"minNoticeHours must be 0–336\");\n if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) fail(\"require 0 ≤ startHour < endHour ≤ 24\");\n const days = [...c.days];\n if (!days.length || !days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {\n fail(\"days must be a non-empty list of weekday integers 0–6\");\n }\n if (typeof c.timezone !== \"string\" || !isValidTimeZone(c.timezone)) fail(`invalid IANA timezone \"${c.timezone}\"`);\n if (typeof c.summaryTemplate !== \"string\") fail(\"summaryTemplate must be a string\");\n return { ...c, days };\n}\n\n/** Statuses a member may book/reschedule from (early pipeline only). */\nexport const BOOKABLE_STATUSES: readonly string[] = [\"submitted\", \"paid_pending_vetting\", \"call_scheduled\"];\n\n/** Whether an application at `status` may book a call. */\nexport function canBookFrom(status: string): boolean {\n return BOOKABLE_STATUSES.includes(status);\n}\n\n/** The availability window: `[now, now + windowDays]` in epoch ms. */\nexport function slotWindow(now: number, windowDays: number): { from: number; to: number } {\n return { from: now, to: now + windowDays * 86_400_000 };\n}\n\n/** The slot's end instant, always derived from its start (never client-supplied). */\nexport function endForSlot(startAt: number, slotMinutes: number): number {\n return startAt + slotMinutes * 60_000;\n}\n\n/** Double-book pre-check: the requested start must land exactly on a currently\n * bookable slot boundary. */\nexport function isSlotAvailable(slots: readonly { startAt: number }[], startAt: number): boolean {\n return slots.some((s) => s.startAt === startAt);\n}\n\n/** Render a meeting summary from its template (`{{firstName}}`/`{{lastName}}`). */\nexport function renderSummary(template: string, app: { firstName?: string | null; lastName?: string | null }): string {\n return template.replace(\"{{firstName}}\", app.firstName ?? \"\").replace(\"{{lastName}}\", app.lastName ?? \"\");\n}\n\n/** The prior scheduled meeting for an application, as far as booking cares. */\nexport interface ExistingMeeting {\n id: string;\n googleEventId?: string | null;\n meetUrl?: string | null;\n htmlLink?: string | null;\n}\n\n/** Decide reschedule-vs-create: reschedule iff there's an existing event to move,\n * so the Meet link + invite thread survive and no second event is minted. */\nexport function bookingDecision(existing: ExistingMeeting | null | undefined): { reschedule: boolean; eventId: string | null } {\n const eventId = existing?.googleEventId ?? null;\n return { reschedule: Boolean(eventId), eventId };\n}\n\n/** The create idempotency key — one intro event per application, forever, so a\n * retried create returns the same booking rather than a duplicate. */\nexport function introIdempotencyKey(applicationId: string): string {\n return `application:${applicationId}:intro`;\n}\n\n/** The canonical `meetings` row for a first booking. A `type` (not `interface`)\n * so it stays assignable to a db op's `attrs` (Record<string, unknown>). */\nexport type NewMeetingRow = {\n id: string;\n applicationId: string;\n groupId: string;\n startAt: number;\n endAt: number;\n timezone: string;\n status: \"scheduled\";\n googleEventId: string;\n meetUrl?: string;\n htmlLink?: string;\n drift: \"none\";\n createdAt: number;\n};\n\n/** Build the new `meetings` row after the calendar created the event. Optional\n * scalars are omitted (never null), per the odla-db porting rule. */\nexport function meetingCreateRow(i: {\n meetingId: string;\n applicationId: string;\n groupId: string;\n startAt: number;\n endAt: number;\n timezone: string;\n googleEventId: string;\n meetUrl?: string | null;\n htmlLink?: string | null;\n createdAt: number;\n}): NewMeetingRow {\n return {\n id: i.meetingId,\n applicationId: i.applicationId,\n groupId: i.groupId,\n startAt: i.startAt,\n endAt: i.endAt,\n timezone: i.timezone,\n status: \"scheduled\",\n googleEventId: i.googleEventId,\n ...(i.meetUrl ? { meetUrl: i.meetUrl } : {}),\n ...(i.htmlLink ? { htmlLink: i.htmlLink } : {}),\n drift: \"none\",\n createdAt: i.createdAt,\n };\n}\n\n/** The `meetings`-row patch for a reschedule (same row id, moved in place). */\nexport type MeetingReschedulePatch = {\n startAt: number;\n endAt: number;\n drift: \"none\";\n};\n\n/** Patch to move an existing meeting to a new window. */\nexport function meetingRescheduleUpdate(startAt: number, endAt: number): MeetingReschedulePatch {\n return { startAt, endAt, drift: \"none\" };\n}\n\n/** The `applications`-row patch after a booking. */\nexport type ApplicationBookingPatch = {\n meetingAt: number;\n meetingLink?: string;\n status?: \"call_scheduled\";\n};\n\n/** Project the booking onto the application row: cache the time, adopt the\n * calendar link if any, and advance the status to `call_scheduled` unless it is\n * already there (never backward). */\nexport function applicationBookingUpdate(\n currentStatus: string,\n startAt: number,\n htmlLink?: string | null,\n): ApplicationBookingPatch {\n return {\n meetingAt: startAt,\n ...(htmlLink ? { meetingLink: htmlLink } : {}),\n ...(currentStatus !== \"call_scheduled\" ? { status: \"call_scheduled\" as const } : {}),\n };\n}\n","// The member session — what GET /api/me returns to a signed-in applicant/member.\n// Ported from Silver & Salt's applicationSummary + /api/me reconciliation. The\n// SHAPING is pure and package-enforced so no site re-derives it; the worker owns\n// only the I/O around it (locating the application by email, reconciling the\n// meeting against the calendar) and hands the resolved rows here.\n//\n// Two invariants live here, not in a site:\n// - `paid` is DERIVED, never a stored flag: a subscription exists and the\n// application wasn't refunded. Sites can't drift a stale boolean out of sync\n// with Stripe.\n// - the live meeting row wins: its startAt/meetUrl/timezone override whatever\n// the application row cached, and a non-scheduled meeting (a cancellation\n// adopted from the calendar) forces meetingAt back to null.\n\n/** An application row, as far as the session cares about it. */\nexport interface ApplicationRecord {\n id: string;\n firstName?: string | null;\n lastName?: string | null;\n email?: string | null;\n status: string;\n meetingAt?: number | null;\n meetingLink?: string | null;\n createdAt?: number | null;\n stripeSubscriptionId?: string | null;\n renewalAt?: number | null;\n canceled?: boolean;\n}\n\n/** The reconciled meeting row (already adopted against the calendar), or null. */\nexport interface MeetingRecord {\n status: string;\n startAt?: number | null;\n meetUrl?: string | null;\n timezone?: string | null;\n}\n\n/** The stable, non-meeting fields of an application (safe to expose to its own\n * owner). */\nexport interface ApplicationSummary {\n id: string;\n firstName: string | null;\n lastName: string | null;\n email: string | null;\n status: string;\n createdAt: number | null;\n meetingLink: string | null;\n paid: boolean;\n renewalAt: number | null;\n canceled: boolean;\n}\n\n/** A summary plus the reconciled meeting fields — the `application` the member\n * area renders. */\nexport interface MemberApplication extends ApplicationSummary {\n meetingAt: number | null;\n meetUrl: string | null;\n timezone: string;\n}\n\n/** The full GET /api/me payload for a signed-in user. */\nexport interface MemberSession {\n userId: string;\n email: string | null;\n role: string;\n superAdmin: boolean;\n application: MemberApplication | null;\n}\n\n/** Derive the summary fields from an application row. `paid` is computed, not\n * read, so it can never contradict Stripe. */\nexport function applicationSummary(app: ApplicationRecord): ApplicationSummary {\n return {\n id: app.id,\n firstName: app.firstName ?? null,\n lastName: app.lastName ?? null,\n email: app.email ?? null,\n status: app.status,\n createdAt: app.createdAt ?? null,\n meetingLink: app.meetingLink ?? null,\n paid: Boolean(app.stripeSubscriptionId) && app.status !== \"refunded\",\n renewalAt: app.renewalAt ?? null,\n canceled: app.canceled === true,\n };\n}\n\n/** Fold a (possibly absent, already-reconciled) meeting into the application the\n * member area renders. The live meeting overrides the application's cached\n * meeting fields; a non-`scheduled` meeting clears the booking. */\nexport function memberApplication(\n app: ApplicationRecord,\n meeting: MeetingRecord | null | undefined,\n defaultTimezone: string,\n): MemberApplication {\n const summary = applicationSummary(app);\n let meetingAt = app.meetingAt ?? null;\n let meetUrl: string | null = null;\n let timezone = defaultTimezone;\n if (meeting) {\n timezone = meeting.timezone ?? timezone;\n if (meeting.status === \"scheduled\") {\n meetingAt = meeting.startAt ?? null;\n meetUrl = meeting.meetUrl ?? null;\n } else {\n meetingAt = null;\n }\n }\n return { ...summary, meetingAt, meetUrl, timezone };\n}\n\n/** Identity of the signed-in user, from the verified session. */\nexport interface SessionUser {\n userId: string;\n email?: string | null;\n role: string;\n}\n\n/** Assemble the GET /api/me payload. `application` is null when the user has no\n * application on file (an admin who never applied, or a brand-new account). */\nexport function memberSession(\n user: SessionUser,\n opts: { application: MemberApplication | null; superAdmin: boolean },\n): MemberSession {\n return {\n userId: user.userId,\n email: user.email ?? null,\n role: user.role,\n superAdmin: opts.superAdmin,\n application: opts.application,\n };\n}\n","// Scheduling routes: GET /api/schedule/slots + POST /api/schedule/book. This\n// file owns the I/O — Google FreeBusy, the pure computeBookableSlots, the\n// calendar create/reschedule, and the db writes — and delegates every\n// correctness rule to ./scheduling. Calendar is RUNTIME-optional: a chapter with\n// no connected calendar has freeBusy throw, so /slots answers\n// { schedulingReady: false } (200) and the join flow degrades to \"we'll reach\n// out by email\" rather than erroring.\nimport { computeBookableSlots, initCalendar } from \"@odla-ai/calendar\";\nimport { json } from \"./worker-context\";\nimport type { ChapterEnv, WorkerContext } from \"./worker-context\";\nimport type { Route } from \"./worker-routes\";\nimport type { ChapterDb, ChapterScheduling, DbOp } from \"./types\";\nimport {\n applicationBookingUpdate,\n bookingDecision,\n canBookFrom,\n endForSlot,\n introIdempotencyKey,\n isSlotAvailable,\n meetingCreateRow,\n meetingRescheduleUpdate,\n renderSummary,\n resolveScheduling,\n slotWindow,\n} from \"./scheduling\";\nimport type { ExistingMeeting, ResolvedScheduling } from \"./scheduling\";\n\ntype Cal = ReturnType<typeof initCalendar>;\ntype Row = Record<string, unknown>;\n\n/** The stable `.code` off an OdlaError/provider error, else a safe default. */\nfunction errCode(err: unknown): string {\n if (err && typeof err === \"object\") {\n const code = (err as { code?: unknown }).code;\n if (typeof code === \"string\") return code;\n }\n return \"calendar_unavailable\";\n}\n\nfunction makeCalendar(env: ChapterEnv): Cal {\n // Calendar uses ODLA_APP_ID + ODLA_PLATFORM — distinct from the db client's\n // ODLA_TENANT + ODLA_ENDPOINT.\n return initCalendar({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });\n}\n\nasync function firstRow(db: ChapterDb, ns: string, q: Record<string, unknown>): Promise<Row | undefined> {\n const res = await db.query({ [ns]: { $: q } });\n const rows = res[ns];\n return Array.isArray(rows) ? rows[0] : undefined;\n}\n\nasync function computeSlots(cal: Cal, cfg: ResolvedScheduling): Promise<Array<{ startAt: number; endAt: number }>> {\n const { from, to } = slotWindow(Date.now(), cfg.windowDays);\n const fb = await cal.availability.freeBusy({ timeMin: from, timeMax: to });\n return computeBookableSlots(fb.busy, {\n from: fb.timeMin,\n to: fb.timeMax,\n timezone: cfg.timezone,\n slotMinutes: cfg.slotMinutes,\n businessHours: { days: [...cfg.days], startHour: cfg.startHour, endHour: cfg.endHour },\n minNoticeMs: cfg.minNoticeHours * 3_600_000,\n });\n}\n\nasync function bookSlot(req: Request, env: ChapterEnv, ctx: WorkerContext): Promise<Response> {\n let body: Row;\n try {\n body = JSON.parse(await req.text()) as Row;\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n const applicationId = typeof body.applicationId === \"string\" ? body.applicationId : \"\";\n const startAt = Number(body.startAt);\n if (!applicationId || !Number.isFinite(startAt)) return json({ error: \"applicationId and startAt required\" }, 400);\n\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const app = await firstRow(db, \"applications\", { where: { id: applicationId }, limit: 1 });\n if (!app) return json({ error: \"not found\" }, 404);\n const status = String(app.status ?? \"\");\n if (!canBookFrom(status)) return json({ error: `cannot book from status \"${status}\"` }, 409);\n\n const group = await firstRow(db, \"groups\", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });\n if (!group) return json({ error: \"group not found\" }, 500);\n const cfg = resolveScheduling(group.schedulingJson as ChapterScheduling | undefined);\n const endAt = endForSlot(startAt, cfg.slotMinutes);\n const cal = makeCalendar(env);\n\n // Double-book guard, layer 1: the requested start must still be a live slot.\n let slots: Array<{ startAt: number }>;\n try {\n slots = await computeSlots(cal, cfg);\n } catch (err) {\n return json({ error: \"scheduling unavailable\", code: errCode(err) }, 503);\n }\n if (!isSlotAvailable(slots, startAt)) return json({ error: \"slot no longer available\", code: \"calendar_slot_unavailable\" }, 409);\n\n const summary = renderSummary(cfg.summaryTemplate, { firstName: app.firstName as string, lastName: app.lastName as string });\n const existing = (await firstRow(db, \"meetings\", {\n where: { applicationId, status: \"scheduled\" },\n order: { createdAt: \"desc\" },\n limit: 1,\n })) as ExistingMeeting | undefined;\n const decision = bookingDecision(existing);\n\n let meetUrl: string | null = null;\n let htmlLink: string | null = null;\n let meetingOp: DbOp;\n try {\n if (decision.reschedule && decision.eventId) {\n // Reschedule the SAME event — the Meet link + invite thread survive.\n await cal.actions.reschedule(decision.eventId, { startAt, endAt });\n meetUrl = (existing?.meetUrl as string | undefined) ?? null;\n htmlLink = (existing?.htmlLink as string | undefined) ?? null;\n meetingOp = { t: \"update\", ns: \"meetings\", id: String(existing?.id), attrs: meetingRescheduleUpdate(startAt, endAt) };\n } else {\n const { booking } = await cal.actions.create(\n { summary, startAt, endAt, attendees: [String(app.email)], timezone: cfg.timezone, meet: true },\n { idempotencyKey: introIdempotencyKey(applicationId) },\n );\n meetUrl = booking.meetUrl ?? null;\n htmlLink = booking.htmlLink ?? null;\n const meetingId = crypto.randomUUID();\n meetingOp = {\n t: \"update\",\n ns: \"meetings\",\n id: meetingId,\n attrs: meetingCreateRow({\n meetingId,\n applicationId,\n groupId: String(group.id),\n startAt,\n endAt,\n timezone: cfg.timezone,\n googleEventId: booking.eventId,\n meetUrl: booking.meetUrl,\n htmlLink: booking.htmlLink,\n createdAt: Date.now(),\n }),\n };\n }\n } catch (err) {\n const code = errCode(err);\n // Double-book guard, layer 2 (authoritative): the provider rejected under lease.\n if (code === \"calendar_slot_unavailable\") return json({ error: \"slot no longer available\", code }, 409);\n return json({ error: \"booking failed\", code }, 502);\n }\n\n // meetings row is canonical; the application row is the projection.\n const appOp: DbOp = { t: \"update\", ns: \"applications\", id: applicationId, attrs: applicationBookingUpdate(status, startAt, htmlLink) };\n await db.transact([meetingOp, appOp]);\n\n return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });\n}\n\n/** GET /api/schedule/slots + POST /api/schedule/book (chapter mode only). */\nexport const handleSchedule: Route = async (req, url, env, ctx) => {\n if (ctx.chapter.mode !== \"chapter\") return null;\n\n if (req.method === \"GET\" && url.pathname === \"/api/schedule/slots\") {\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const group = await firstRow(db, \"groups\", { where: { id: url.searchParams.get(\"group\") ?? ctx.chapter.id }, limit: 1 });\n if (!group) return json({ error: \"not found\" }, 404);\n const cfg = resolveScheduling(group.schedulingJson as ChapterScheduling | undefined);\n try {\n const slots = await computeSlots(makeCalendar(env), cfg);\n return json({ schedulingReady: true, timezone: cfg.timezone, slotMinutes: cfg.slotMinutes, slots });\n } catch (err) {\n return json({ schedulingReady: false, code: errCode(err) });\n }\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/schedule/book\") {\n return bookSlot(req, env, ctx);\n }\n\n return null;\n};\n","// Payments primitives. The webhook-integrity check below is security-critical and\n// easy to get wrong, so it is pure and tested here; the worker wires a payments\n// provider (Stripe first — subscription create, webhook ingest, refund) around\n// it, and every resulting db write carries an event-derived mutationId for\n// exactly-once. Sites that don't charge omit payments entirely (paymentsReady:\n// false), so nothing here is imported unless a chapter runs the payment flow.\n\n/** Parse a Stripe-style `Stripe-Signature` header (`t=<unix>,v1=<hex>`). */\nfunction parseSigHeader(header: string): { t?: string; v1?: string } {\n const parts: Record<string, string> = {};\n for (const p of header.split(\",\")) {\n const [k, v] = p.split(\"=\", 2);\n if (k && v !== undefined) parts[k] = v;\n }\n return { t: parts.t, v1: parts.v1 };\n}\n\nfunction toHex(buf: ArrayBuffer): string {\n return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** Constant-time compare of two equal-length hex strings. */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\n/**\n * Verify a Stripe webhook signature (C3): HMAC-SHA256 over `` `${t}.${payload}` ``\n * with the endpoint signing secret, a replay window (default 5 minutes), and a\n * constant-time compare. Package-enforced — never left to a site. Returns `false`\n * (never throws) on a malformed header, a non-numeric or stale timestamp, or a\n * signature mismatch. `now`/`toleranceSec` are injectable for tests.\n */\nexport async function verifyStripeSignature(\n payload: string,\n header: string,\n secret: string,\n opts: { now?: number; toleranceSec?: number } = {},\n): Promise<boolean> {\n const { t, v1 } = parseSigHeader(header);\n if (!t || !v1) return false;\n const ts = Number(t);\n if (!Number.isFinite(ts)) return false;\n const nowSec = (opts.now ?? Date.now()) / 1000;\n const tolerance = opts.toleranceSec ?? 300;\n if (Math.abs(nowSec - ts) > tolerance) return false;\n\n const enc = new TextEncoder();\n const key = await crypto.subtle.importKey(\"raw\", enc.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n const mac = await crypto.subtle.sign(\"HMAC\", key, enc.encode(`${t}.${payload}`));\n return timingSafeEqual(toHex(mac), v1);\n}\n\n// ── readiness + Stripe wire helpers ──\n\n/** A group row's payment configuration, as far as readiness cares. */\nexport interface PaymentsGroup {\n stripePublishableKey?: string | null;\n stripePriceId?: string | null;\n}\n\n/** Whether a group can take payment: a publishable key + a price id (both on the\n * group row) AND a secret key (the vault). Anything missing drops the join\n * flow's payment step (paymentsReady:false) rather than half-charging. */\nexport function paymentsReady(group: PaymentsGroup, hasSecretKey: boolean): boolean {\n return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);\n}\n\n/** Form-encode params for Stripe's x-www-form-urlencoded API, expanding one level\n * of nested objects into bracket syntax (`metadata[applicationId]=...`). */\nexport function stripeForm(params: Record<string, unknown>): string {\n const out = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null) continue;\n if (typeof v === \"object\") {\n for (const [k2, v2] of Object.entries(v as Record<string, unknown>)) {\n if (v2 !== undefined && v2 !== null) out.append(`${k}[${k2}]`, String(v2));\n }\n } else {\n out.append(k, String(v));\n }\n }\n return out.toString();\n}\n\n/** The Stripe idempotency key for creating an application's subscription — one\n * per application, so a client retry can't orphan a second subscription (S&S\n * lacked this; the package enforces it). */\nexport function subscriptionIdempotencyKey(applicationId: string): string {\n return `sub:${applicationId}`;\n}\n\n/** The db mutationId for a webhook-driven write — exactly-once per Stripe event,\n * so replays are deduped at the db layer. */\nexport function webhookMutationId(eventId: string): string {\n return `stripe:${eventId}`;\n}\n\n// ── webhook normalization (pure; the worker owns the db lookup + writes) ──\n\n/** A raw Stripe event, as far as normalization cares. */\nexport interface StripeEvent {\n id: string;\n type: string;\n data?: { object?: Record<string, unknown> };\n}\n\n/** A normalized, provider-agnostic webhook event. `kind` drives the db write;\n * the application is resolved from `applicationId` (metadata) or `customerId`. */\nexport type WebhookEvent =\n | { kind: \"first_payment\"; applicationId?: string; customerId?: string; renewalAt?: number }\n | { kind: \"renewal\"; applicationId?: string; customerId?: string; renewalAt?: number }\n | { kind: \"refunded\"; applicationId?: string; customerId?: string }\n | { kind: \"canceled\"; applicationId?: string; customerId?: string }\n | { kind: \"ignored\"; type: string };\n\n/** Resolve the application reference on a Stripe object: `applicationId` from\n * metadata (direct, then subscription_details, then nested\n * parent.subscription_details), plus the customer id for the db fallback. */\nexport function findApplicationRef(obj: Record<string, unknown>): { applicationId?: string; customerId?: string } {\n const metaOf = (v: unknown): Record<string, unknown> =>\n v && typeof v === \"object\" ? ((v as Record<string, unknown>).metadata as Record<string, unknown>) ?? {} : {};\n const pick = (m: Record<string, unknown>): string | undefined =>\n typeof m.applicationId === \"string\" ? m.applicationId : undefined;\n const applicationId =\n pick(metaOf(obj)) ?? pick(metaOf(obj.subscription_details)) ?? pick(metaOf((obj.parent as Record<string, unknown> | undefined)?.subscription_details));\n const customerId = typeof obj.customer === \"string\" ? obj.customer : undefined;\n return { ...(applicationId ? { applicationId } : {}), ...(customerId ? { customerId } : {}) };\n}\n\n/** Normalize a verified Stripe event into a {@link WebhookEvent}. `invoice.paid`\n * splits into first_payment vs renewal by `billing_reason`; refunds and\n * cancellations map directly; everything else is ignored (acked, not retried). */\nexport function normalizeWebhookEvent(event: StripeEvent): WebhookEvent {\n const obj = event.data?.object ?? {};\n const ref = findApplicationRef(obj);\n switch (event.type) {\n case \"invoice.paid\": {\n const lines = ((obj.lines as Record<string, unknown> | undefined)?.data as Array<Record<string, unknown>> | undefined) ?? [];\n const periodEnd = (lines[0]?.period as Record<string, unknown> | undefined)?.end;\n const renewalAt = typeof periodEnd === \"number\" ? periodEnd * 1000 : undefined;\n const kind = obj.billing_reason === \"subscription_create\" ? \"first_payment\" : \"renewal\";\n return { kind, ...ref, ...(renewalAt !== undefined ? { renewalAt } : {}) };\n }\n case \"charge.refunded\":\n return { kind: \"refunded\", ...ref };\n case \"customer.subscription.deleted\":\n return { kind: \"canceled\", ...ref };\n default:\n return { kind: \"ignored\", type: event.type };\n }\n}\n\n// ── webhook write-set builders (the authoritative writers of paid/refunded) ──\n\n/** First-payment patch: advance submitted→paid_pending_vetting (never any other\n * transition) and record the renewal date. Empty when nothing changed, so the\n * caller can skip the write. */\nexport function firstPaymentPatch(currentStatus: string, renewalAt?: number): { status?: \"paid_pending_vetting\"; renewalAt?: number } {\n return {\n ...(currentStatus === \"submitted\" ? { status: \"paid_pending_vetting\" as const } : {}),\n ...(renewalAt !== undefined ? { renewalAt } : {}),\n };\n}\n\n/** Renewal-invoice patch: just the new renewal date. */\nexport function renewalPatch(renewalAt: number): { renewalAt: number } {\n return { renewalAt };\n}\n\n/** Refund patch — the SOLE writer of status \"refunded\" (the admin refund route\n * issues the Stripe refund but never sets this; the webhook does). */\nexport function refundedPatch(): { status: \"refunded\" } {\n return { status: \"refunded\" };\n}\n\n/** Subscription-cancellation patch. */\nexport function canceledPatch(): { canceled: true } {\n return { canceled: true };\n}\n","// The Stripe payments provider — the only I/O half of payments. Talks to the\n// Stripe REST API over fetch (no SDK), and maps 1:1 onto the pure helpers in\n// ./payments (form encoding, signature verify, event normalization). The worker\n// resolves secrets from the vault per request and constructs this; the routes own\n// every db write so idempotency stays at the db layer.\nimport { normalizeWebhookEvent, stripeForm, verifyStripeSignature } from \"./payments\";\nimport type { StripeEvent, WebhookEvent } from \"./payments\";\n\n/** Inputs to create an application's founding-member subscription. */\nexport interface CreateSubscriptionInput {\n applicationId: string;\n groupId: string;\n email: string;\n name: string;\n priceId: string;\n existingCustomerId?: string;\n}\n\n/** The client secret to confirm card entry, plus the ids to persist. */\nexport interface CreateSubscriptionResult {\n customerId: string;\n subscriptionId: string;\n clientSecret: string;\n}\n\n/** The outcome of a full refund. */\nexport interface RefundResult {\n refundedCents: number | null;\n subscriptionCanceled: boolean;\n}\n\n/** The capabilities the payment routes depend on — Stripe is one impl. */\nexport interface PaymentsProvider {\n createSubscription(input: CreateSubscriptionInput): Promise<CreateSubscriptionResult>;\n ingestWebhook(\n rawBody: string,\n sigHeader: string,\n ): Promise<{ ok: true; eventId: string; event: WebhookEvent } | { ok: false; reason: \"bad_signature\" }>;\n refund(input: { customerId: string; subscriptionId: string }): Promise<RefundResult>;\n}\n\ntype StripeResult = { ok: boolean; status: number; body: Record<string, unknown> };\n\nasync function stripeCall(\n sk: string,\n method: \"GET\" | \"POST\" | \"DELETE\",\n path: string,\n params?: Record<string, unknown>,\n idempotencyKey?: string,\n): Promise<StripeResult> {\n const qs = method === \"GET\" && params ? `?${stripeForm(params)}` : \"\";\n const headers: Record<string, string> = { authorization: `Bearer ${sk}` };\n if (idempotencyKey) headers[\"idempotency-key\"] = idempotencyKey;\n const init: RequestInit = { method, headers };\n if (method === \"POST\" && params) {\n headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n init.body = stripeForm(params);\n }\n const res = await fetch(`https://api.stripe.com${path}${qs}`, init);\n const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;\n return { ok: res.ok, status: res.status, body };\n}\n\nfunction fail(op: string, r: StripeResult): never {\n const err = new Error(`stripe ${op} failed: ${r.status}`) as Error & { code: string };\n err.code = \"stripe_error\";\n throw err;\n}\n\nfunction clientSecretOf(sub: Record<string, unknown>): string | undefined {\n const inv = sub.latest_invoice as Record<string, unknown> | undefined;\n const confirmation = inv?.confirmation_secret as Record<string, unknown> | undefined;\n const intent = inv?.payment_intent as Record<string, unknown> | undefined;\n const secret = confirmation?.client_secret ?? intent?.client_secret;\n return typeof secret === \"string\" ? secret : undefined;\n}\n\nfunction requireSecret(secretKey: string | undefined): string {\n if (!secretKey) {\n const err = new Error(\"stripe secret key missing\") as Error & { code: string };\n err.code = \"not_configured\";\n throw err;\n }\n return secretKey;\n}\n\n/** Build the Stripe provider. `secretKey` powers charging/refunds; `webhookSecret`\n * powers webhook ingest. Each is resolved from the vault per request, so the\n * webhook route can construct an ingest-only provider without the secret key. */\nexport function createStripeProvider(config: { secretKey?: string; webhookSecret?: string }): PaymentsProvider {\n const { secretKey, webhookSecret } = config;\n return {\n async createSubscription(input) {\n const sk = requireSecret(secretKey);\n const meta = { applicationId: input.applicationId, groupId: input.groupId, email: input.email };\n let customerId = input.existingCustomerId;\n if (!customerId) {\n const cust = await stripeCall(\n sk,\n \"POST\",\n \"/v1/customers\",\n { email: input.email, name: input.name, metadata: meta },\n `cus:${input.applicationId}`,\n );\n if (!cust.ok) fail(\"customer create\", cust);\n customerId = String(cust.body.id);\n }\n const sub = await stripeCall(\n sk,\n \"POST\",\n \"/v1/subscriptions\",\n {\n customer: customerId,\n \"items[0][price]\": input.priceId,\n payment_behavior: \"default_incomplete\",\n \"payment_settings[save_default_payment_method]\": \"on_subscription\",\n \"payment_settings[payment_method_types][0]\": \"card\",\n \"expand[0]\": \"latest_invoice.confirmation_secret\",\n metadata: meta,\n },\n // The hardening S&S lacked: one subscription per application, so a client\n // retry between create and the db write can't orphan a second one.\n `sub:${input.applicationId}`,\n );\n if (!sub.ok) fail(\"subscription create\", sub);\n const clientSecret = clientSecretOf(sub.body);\n if (!clientSecret) fail(\"subscription confirmation-secret missing\", sub);\n return { customerId, subscriptionId: String(sub.body.id), clientSecret };\n },\n\n async ingestWebhook(rawBody, sigHeader) {\n if (!webhookSecret || !(await verifyStripeSignature(rawBody, sigHeader, webhookSecret))) {\n return { ok: false, reason: \"bad_signature\" };\n }\n const event = JSON.parse(rawBody) as StripeEvent;\n return { ok: true, eventId: event.id, event: normalizeWebhookEvent(event) };\n },\n\n async refund(input) {\n const sk = requireSecret(secretKey);\n const charges = await stripeCall(sk, \"GET\", \"/v1/charges\", { customer: input.customerId, limit: 100 });\n if (!charges.ok) fail(\"charges list\", charges);\n const rows = (charges.body.data as Array<Record<string, unknown>> | undefined) ?? [];\n const paid = rows.filter((c) => c.status === \"succeeded\" && c.refunded !== true);\n const charge = paid[paid.length - 1]; // Stripe returns newest-first; refund the earliest.\n if (!charge) {\n const err = new Error(\"no paid charge to refund\") as Error & { code: string };\n err.code = \"no_charge\";\n throw err;\n }\n const refund = await stripeCall(sk, \"POST\", \"/v1/refunds\", { charge: String(charge.id) });\n if (!refund.ok) fail(\"refund\", refund);\n const cancel = await stripeCall(sk, \"DELETE\", `/v1/subscriptions/${input.subscriptionId}`);\n const amount = refund.body.amount;\n return { refundedCents: typeof amount === \"number\" ? amount : null, subscriptionCanceled: cancel.ok };\n },\n };\n}\n","// Payment routes: POST /api/payments/subscription (start a founding-member\n// subscription), POST /api/webhooks/stripe (the AUTHORITATIVE writer of\n// paid/refunded/canceled, exactly-once per event id), and POST\n// /api/admin/applications/:id/refund (admin-gated; issues the refund but never\n// writes status — the charge.refunded webhook does). I/O only; the correctness\n// rules live in ./payments and the provider in ./payments-stripe.\nimport { getVaultSecret } from \"./auth\";\nimport { canceledPatch, firstPaymentPatch, refundedPatch, renewalPatch, webhookMutationId } from \"./payments\";\nimport type { WebhookEvent } from \"./payments\";\nimport { createStripeProvider } from \"./payments-stripe\";\nimport { json } from \"./worker-context\";\nimport type { ChapterEnv, WorkerContext } from \"./worker-context\";\nimport type { Route } from \"./worker-routes\";\nimport type { ChapterDb } from \"./types\";\n\ntype Row = Record<string, unknown>;\n\nconst codeOf = (err: unknown): string =>\n err && typeof err === \"object\" && typeof (err as { code?: unknown }).code === \"string\" ? (err as { code: string }).code : \"unknown\";\n\nasync function firstRow(db: ChapterDb, ns: string, q: Record<string, unknown>): Promise<Row | undefined> {\n const res = await db.query({ [ns]: { $: q } });\n const rows = res[ns];\n return Array.isArray(rows) ? rows[0] : undefined;\n}\n\nfunction lineItems(group: Row): { standardCents: number; discountCents: number; dueTodayCents: number } {\n const standard = Number(group.standardPriceCents ?? 0);\n const discount = Number(group.foundingDiscountCents ?? 0);\n return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };\n}\n\n// Resolve the application an event targets: by metadata applicationId, else the\n// newest row for the Stripe customer.\nasync function findApplication(db: ChapterDb, event: WebhookEvent): Promise<Row | undefined> {\n if (\"applicationId\" in event && event.applicationId) {\n return firstRow(db, \"applications\", { where: { id: event.applicationId }, limit: 1 });\n }\n if (\"customerId\" in event && event.customerId) {\n return firstRow(db, \"applications\", { where: { stripeCustomerId: event.customerId }, order: { createdAt: \"desc\" }, limit: 1 });\n }\n return undefined;\n}\n\n// The application-row patch for a resolved event (empty = skip the write).\nfunction webhookPatch(event: WebhookEvent, status: string): Record<string, unknown> {\n switch (event.kind) {\n case \"first_payment\":\n return firstPaymentPatch(status, event.renewalAt);\n case \"renewal\":\n return event.renewalAt !== undefined ? renewalPatch(event.renewalAt) : {};\n case \"refunded\":\n return refundedPatch();\n case \"canceled\":\n return canceledPatch();\n default:\n return {};\n }\n}\n\nasync function startSubscription(req: Request, env: ChapterEnv, ctx: WorkerContext): Promise<Response> {\n let body: Row;\n try {\n body = JSON.parse(await req.text()) as Row;\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n const applicationId = typeof body.applicationId === \"string\" ? body.applicationId : \"\";\n if (!applicationId) return json({ error: \"applicationId required\" }, 400);\n if (body.refundPolicyAck !== true) return json({ error: \"refundPolicyAck required\" }, 400);\n\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const app = await firstRow(db, \"applications\", { where: { id: applicationId }, limit: 1 });\n if (!app) return json({ error: \"not found\" }, 404);\n // Single-charge guard: only from the initial state, so a re-post can't double-subscribe.\n if (app.status !== \"submitted\") return json({ error: \"already processed\" }, 409);\n\n const group = await firstRow(db, \"groups\", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });\n const priceId = group?.stripePriceId;\n const secretKey = await getVaultSecret(db, \"stripe_secret_key\");\n if (!group || !priceId || !secretKey) return json({ error: \"payments not configured\" }, 503);\n\n const provider = createStripeProvider({ secretKey });\n let result;\n try {\n result = await provider.createSubscription({\n applicationId,\n groupId: String(group.id),\n email: String(app.email ?? \"\"),\n name: `${app.firstName ?? \"\"} ${app.lastName ?? \"\"}`.trim(),\n priceId: String(priceId),\n existingCustomerId: typeof app.stripeCustomerId === \"string\" ? app.stripeCustomerId : undefined,\n });\n } catch (err) {\n return json({ error: \"payment setup failed\", code: codeOf(err) }, 502);\n }\n\n await db.transact([\n {\n t: \"update\",\n ns: \"applications\",\n id: applicationId,\n attrs: { stripeCustomerId: result.customerId, stripeSubscriptionId: result.subscriptionId, refundPolicyAckAt: Date.now() },\n },\n ]);\n return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: lineItems(group) });\n}\n\nasync function ingestWebhook(req: Request, env: ChapterEnv, ctx: WorkerContext): Promise<Response> {\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const webhookSecret = await getVaultSecret(db, \"stripe_webhook_secret\");\n if (!webhookSecret) return json({ error: \"webhook not configured\" }, 503);\n\n const rawBody = await req.text();\n const ingest = await createStripeProvider({ webhookSecret }).ingestWebhook(rawBody, req.headers.get(\"stripe-signature\") ?? \"\");\n if (!ingest.ok) return json({ error: \"invalid signature\" }, 400);\n const { eventId, event } = ingest;\n if (event.kind === \"ignored\") return json({ ok: true, ignored: event.type });\n\n const app = await findApplication(db, event);\n if (!app) return json({ ok: true, matched: false });\n\n const patch = webhookPatch(event, String(app.status ?? \"\"));\n if (Object.keys(patch).length) {\n await db.transact([{ t: \"update\", ns: \"applications\", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });\n }\n return json({ ok: true });\n}\n\nasync function refundApplication(req: Request, url: URL, env: ChapterEnv, ctx: WorkerContext): Promise<Response> {\n const rawDb = ctx.makeDb(env);\n const u = await ctx.verifyUser(req, env);\n if (!u || !(await ctx.isAdmin(rawDb, u))) return json({ error: \"forbidden\" }, 403);\n\n const id = url.pathname.split(\"/\")[4] ?? \"\";\n const db = rawDb as unknown as ChapterDb;\n const app = await firstRow(db, \"applications\", { where: { id }, limit: 1 });\n if (!app) return json({ error: \"not found\" }, 404);\n if (app.status === \"refunded\") return json({ error: \"already refunded\" }, 409);\n if (app.status === \"approved\") return json({ error: \"approved memberships are non-refundable\" }, 409);\n if (!app.stripeSubscriptionId) return json({ error: \"no subscription on file\" }, 409);\n if (!app.stripeCustomerId) return json({ error: \"no customer on file\" }, 409);\n\n const secretKey = await getVaultSecret(db, \"stripe_secret_key\");\n if (!secretKey) return json({ error: \"payments not configured\" }, 503);\n\n try {\n const result = await createStripeProvider({ secretKey }).refund({\n customerId: String(app.stripeCustomerId),\n subscriptionId: String(app.stripeSubscriptionId),\n });\n // Note: status \"refunded\" is NOT written here — the charge.refunded webhook is\n // the single writer, keeping Stripe the source of truth.\n return json({ ok: true, refundedCents: result.refundedCents, subscriptionCanceled: result.subscriptionCanceled });\n } catch (err) {\n const code = codeOf(err);\n return json({ error: \"refund failed\", code }, code === \"no_charge\" ? 409 : 502);\n }\n}\n\nconst REFUND_PATH = /^\\/api\\/admin\\/applications\\/[^/]+\\/refund$/;\n\n/** The payment routes (chapter mode). Webhook + subscription are public\n * (capability-guarded by the unguessable application id / the signed payload);\n * refund is admin-gated. */\nexport const handlePayments: Route = async (req, url, env, ctx) => {\n if (ctx.chapter.mode !== \"chapter\") return null;\n if (req.method === \"POST\" && url.pathname === \"/api/payments/subscription\") return startSubscription(req, env, ctx);\n if (req.method === \"POST\" && url.pathname === \"/api/webhooks/stripe\") return ingestWebhook(req, env, ctx);\n if (req.method === \"POST\" && REFUND_PATH.test(url.pathname)) return refundApplication(req, url, env, ctx);\n return null;\n};\n"],"mappings":"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","names":["import_crm","fail","firstRow"]}
|
|
1
|
+
{"version":3,"sources":["../../src/worker.ts","../../src/worker-context.ts","../../src/auth.ts","../../src/worker-routes.ts","../../src/clerk.ts","../../src/member.ts","../../src/network.ts","../../src/scheduling.ts","../../src/session.ts","../../src/email.ts","../../src/notify.ts","../../src/worker-routes-schedule.ts","../../src/pipeline.ts","../../src/payments.ts","../../src/payments-stripe.ts","../../src/worker-routes-payments.ts","../../src/worker-routes-admin.ts","../../src/reconcile.ts"],"sourcesContent":["// chapterWorker — the Cloudflare Worker for a chapter/hub site. This entry\n// (@odla-ai/chapter/worker) is separate from the core so the CLI can load\n// odla.config.mjs without pulling in worker-runtime deps.\n//\n// The worker is the ONLY thing that talks to odla-db, using the app key\n// (ODLA_API_KEY), which bypasses the deny-all rules. Browsers never receive a\n// db credential. Access is admin-only: a request is authorized when its Clerk\n// session JWT verifies AND the user's role (a JWT claim, or a row in the\n// Studio-seeded `admins` allowlist) is an admin rung.\n//\n// The env typing + auth plumbing live in ./worker-context; the route handlers in\n// ./worker-routes. This entry only builds the context and runs the routes in\n// order — first match wins, else the static site (ASSETS).\n//\n// hub mode routes: GET /api/config, GET /api/me, /api/crm/*, POST\n// /api/network/shared, else ASSETS. chapter mode adds the public member surface:\n// GET /api/join-config and POST /api/applications (idempotent).\nimport { createWorkerContext } from \"./worker-context\";\nimport type { ChapterEnv, ChapterWorkerOptions, Route } from \"./worker-context\";\nimport { handleConfig, handleCrm, handleHealth, handleMe, handleMember, handleNetworkShared } from \"./worker-routes\";\nimport { handleSchedule } from \"./worker-routes-schedule\";\nimport { handlePayments } from \"./worker-routes-payments\";\nimport { handleAdminMeetings, handleAdminScheduling } from \"./worker-routes-admin\";\n\n// The route seam: a wrapping worker can build its own routes + reuse chapter's\n// auth by composing against these.\nexport { createWorkerContext } from \"./worker-context\";\nexport type { ChapterEnv, ChapterWorkerOptions, WorkerContext, Route } from \"./worker-context\";\n\n// First-match-wins order. Host routes (options.routes) run BEFORE these.\nconst BUILTIN_ROUTES: Route[] = [\n handleHealth,\n handleConfig,\n handleMe,\n handleCrm,\n handleNetworkShared,\n handleMember,\n handleSchedule,\n handlePayments,\n handleAdminMeetings,\n handleAdminScheduling,\n];\n\n/**\n * Build the Cloudflare `ExportedHandler` for a chapter/hub site: Clerk-JWT\n * verification, the source-aware admin gate, the mounted @odla-ai/crm routes, the\n * hub→chapter network projection, and the static-asset fallback. Hub mode serves\n * /api/health, /api/config, /api/me, /api/crm/*, /api/network/shared; chapter mode\n * adds the public member surface (join/apply/pay/book) and the admin surface\n * (/api/admin/*).\n *\n * A wrapping site adds its own routes via `options.routes` — each receives the\n * same {@link WorkerContext} the built-ins get (so it reuses chapter's JWT\n * verify, db client, and role resolution instead of duplicating them), and runs\n * BEFORE the built-ins so it can override or alias a path.\n *\n * Observability is a host concern, not a chapter dependency. To trace, wrap the\n * result in your worker entry — `export default withObservability(chapterWorker(\n * { chapter }))` — with `withObservability` from `@odla-ai/o11y`. Sites that\n * don't run o11y bundle `@odla-ai/chapter/worker` without installing it.\n */\nexport function chapterWorker(options: ChapterWorkerOptions) {\n const ctx = createWorkerContext(options);\n const routes: Route[] = [...(options.routes ?? []), ...BUILTIN_ROUTES];\n return {\n async fetch(req: Request, env: ChapterEnv): Promise<Response> {\n const url = new URL(req.url);\n for (const route of routes) {\n const res = await route(req, url, env, ctx);\n if (res) return res;\n }\n // Everything else is the static site.\n return env.ASSETS.fetch(req);\n },\n };\n}\n","// Shared context for the chapter Worker: env typing, the odla-db admin client,\n// Clerk-JWT verification, and the source-aware auth helpers. Split out of\n// worker.ts so the route modules share ONE construction of the caches (public\n// config + JWKS) per site, and the worker entry stays a thin composer under the\n// per-file LOC cap. Nothing here is re-exported from the worker entry, so the\n// public `@odla-ai/chapter/worker` surface is unchanged by the split.\nimport { initAdmin } from \"@odla-ai/db\";\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport { isAdminRole, roleFromClaim } from \"./auth\";\nimport type { Chapter } from \"./types\";\n\n/** The Cloudflare SEND_EMAIL binding payload. */\nexport interface EmailPayload {\n from: string;\n to: string[];\n subject: string;\n text?: string;\n html?: string;\n replyTo?: string;\n headers?: Record<string, string>;\n}\n\n/** The Worker env a chapter site provides (wrangler vars + the ODLA_API_KEY\n * secret pushed by provision). */\nexport interface ChapterEnv {\n ASSETS: { fetch(req: Request): Promise<Response> };\n ODLA_ENDPOINT: string;\n ODLA_TENANT: string;\n ODLA_PLATFORM: string;\n ODLA_APP_ID: string;\n ODLA_ENV: string;\n ODLA_API_KEY: string;\n SEND_EMAIL?: { send(payload: EmailPayload): Promise<{ messageId: string }> };\n EMAIL_FROM?: string;\n}\n\n/** Options for `chapterWorker`. */\nexport interface ChapterWorkerOptions {\n chapter: Chapter;\n /** CRM mount point. Default \"/api/crm\". */\n crmBasePath?: string;\n /** Host routes, tried BEFORE the built-ins — so a wrapping site can add its own\n * routes (or override/alias a built-in path) and reuse chapter's auth via the\n * shared {@link WorkerContext}, instead of re-verifying JWTs itself. */\n routes?: Route[];\n}\n\n/** The registry public-config a site reads to boot Clerk sign-in. */\nexport type PublicConfig = { env?: string; clerkPublishableKey?: string | null; issuer?: string | null };\n/** The odla-db admin client type. */\nexport type Db = ReturnType<typeof initAdmin>;\n\n/** A verified session: the Clerk `sub`, optional email, and the raw JWT payload\n * (so the role claim can be read for auth source \"claim\"). */\nexport interface Verified {\n userId: string;\n email?: string;\n payload: Record<string, unknown>;\n}\n\n/** JSON response helper. */\nexport const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), { status, headers: { \"content-type\": \"application/json\" } });\n\n/**\n * Build the per-site Worker context: env-independent helpers closing over the\n * public-config and JWKS caches, plus the source-aware auth gate (JWT claim or\n * the odla-db `admins` allowlist). Constructed once per `chapterWorker` and\n * shared by every route module.\n */\nexport function createWorkerContext(options: ChapterWorkerOptions) {\n const { chapter } = options;\n const auth = chapter.auth;\n const crmBase = options.crmBasePath ?? \"/api/crm\";\n\n let publicConfigCache: { value: PublicConfig; at: number } | null = null;\n const jwksByIssuer = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\n async function getPublicConfig(env: ChapterEnv): Promise<PublicConfig> {\n if (publicConfigCache && Date.now() - publicConfigCache.at < 5 * 60_000) return publicConfigCache.value;\n const res = await fetch(`${env.ODLA_PLATFORM}/registry/apps/${env.ODLA_APP_ID}/public-config?env=${env.ODLA_ENV}`);\n if (!res.ok) throw new Error(`public-config fetch failed: ${res.status}`);\n const value = (await res.json()) as PublicConfig;\n publicConfigCache = { value, at: Date.now() };\n return value;\n }\n\n async function verifyUser(req: Request, env: ChapterEnv): Promise<Verified | null> {\n const header = req.headers.get(\"authorization\") ?? \"\";\n if (!header.startsWith(\"Bearer \")) return null;\n const token = header.slice(7);\n const { issuer } = await getPublicConfig(env);\n if (!issuer) return null;\n let jwks = jwksByIssuer.get(issuer);\n if (!jwks) {\n jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));\n jwksByIssuer.set(issuer, jwks);\n }\n try {\n const { payload } = await jwtVerify(token, jwks, { issuer });\n if (!payload.sub) return null;\n return {\n userId: payload.sub,\n email: typeof payload.email === \"string\" ? payload.email : undefined,\n payload: payload as Record<string, unknown>,\n };\n } catch {\n return null;\n }\n }\n\n function makeDb(env: ChapterEnv): Db {\n return initAdmin({ appId: env.ODLA_TENANT, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_ENDPOINT });\n }\n\n // The `admins` allowlist gate (auth source \"table\"): no route ever writes it, so\n // membership can only be granted by a human in odla Studio.\n async function isAdminEmail(db: Db, email: string | undefined): Promise<boolean> {\n if (!email) return false;\n const { admins } = await db.query({ admins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });\n return Array.isArray(admins) && admins.length > 0;\n }\n\n // The read-only `superAdmins` tier — queried, never written (Studio-only).\n async function isSuperAdminEmail(db: Db, email: string | undefined): Promise<boolean> {\n if (!auth.superAdmins || !email) return false;\n const { superAdmins } = await db.query({ superAdmins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });\n return Array.isArray(superAdmins) && superAdmins.length > 0;\n }\n\n // The user's role, resolved per the auth source: a JWT claim, or the `admins`\n // allowlist synthesized to the admin rung / the lowest one.\n async function roleFor(db: Db, u: Verified): Promise<string> {\n if (auth.source === \"claim\") return roleFromClaim(u.payload, auth);\n return (await isAdminEmail(db, u.email)) ? auth.adminRole : (auth.ladder[0] as string);\n }\n\n // Admin authorization — the boolean gate used by /api/me and the CRM surface.\n async function isAdmin(db: Db, u: Verified): Promise<boolean> {\n if (auth.source === \"claim\") return isAdminRole(roleFromClaim(u.payload, auth), auth);\n return isAdminEmail(db, u.email);\n }\n\n function crmSender(env: ChapterEnv) {\n if (!env.SEND_EMAIL || !env.EMAIL_FROM) return undefined;\n const binding = env.SEND_EMAIL;\n return {\n async send(payload: EmailPayload): Promise<{ messageId: string }> {\n return binding.send(payload);\n },\n };\n }\n\n return { chapter, auth, crmBase, getPublicConfig, verifyUser, makeDb, isAdminEmail, isSuperAdminEmail, roleFor, isAdmin, crmSender };\n}\n\n/** The value returned by {@link createWorkerContext}, threaded to every route. */\nexport type WorkerContext = ReturnType<typeof createWorkerContext>;\n\n/** A worker route handler: owns the request (returns a Response) or falls through\n * (returns null). Host routes passed to `chapterWorker` compose against the same\n * {@link WorkerContext} the built-ins receive. */\nexport type Route = (req: Request, url: URL, env: ChapterEnv, ctx: WorkerContext) => Promise<Response | null>;\n","// Identity + authorization for a chapter/hub site — the pieces every membership\n// site needs and none should re-derive: a resolved role policy, role resolution\n// from a JWT claim, the privilege-escalation guard, and a tenant-vault read.\n// Everything here is pure or structural (no runtime @odla-ai/db import), so it is\n// trivially testable and the worker stays the only thing that talks to odla-db.\nimport type { ChapterAuth, ChapterMode, ResolvedAuth } from \"./types\";\n\n/**\n * Apply defaults + validate the auth config into a {@link ResolvedAuth}. Defaults\n * by mode: `chapter` → the `provisional/member/admin` claim ladder with the\n * `superAdmins` tier (Silver & Salt); `hub` → the `admins` allowlist table, no\n * super tier (Built Not Found). Throws at import on a bad policy.\n */\nexport function resolveAuth(mode: ChapterMode, auth: ChapterAuth | undefined): ResolvedAuth {\n const a = auth ?? {};\n const source = a.source ?? (mode === \"hub\" ? \"table\" : \"claim\");\n if (source !== \"claim\" && source !== \"table\") {\n throw new Error(`defineChapter.auth.source: must be \"claim\" or \"table\" — got ${JSON.stringify(a.source)}`);\n }\n const claim = a.claim ?? \"role\";\n if (typeof claim !== \"string\" || claim === \"\") {\n throw new Error(\"defineChapter.auth.claim: must be a non-empty string\");\n }\n const ladder = a.ladder ?? [\"provisional\", \"member\", \"admin\"];\n if (!Array.isArray(ladder) || ladder.length === 0 || !ladder.every((r) => typeof r === \"string\" && r !== \"\")) {\n throw new Error(\"defineChapter.auth.ladder: must be a non-empty array of role strings\");\n }\n const adminRole = ladder[ladder.length - 1] as string;\n const superAdmins = a.superAdmins ?? source === \"claim\";\n return { source, claim, ladder, adminRole, superAdmins };\n}\n\n/** The role from a verified JWT payload, per the resolved policy. An unknown or\n * missing claim falls back to the lowest ladder rung (fail safe, never admin). */\nexport function roleFromClaim(payload: Record<string, unknown>, auth: ResolvedAuth): string {\n const raw = payload[auth.claim];\n return typeof raw === \"string\" && auth.ladder.includes(raw) ? raw : (auth.ladder[0] as string);\n}\n\n/** Does a role meet the admin bar (the highest ladder rung)? */\nexport function isAdminRole(role: string, auth: ResolvedAuth): boolean {\n return role === auth.adminRole;\n}\n\n/** Inputs to the role-change guard — resolved by the caller (route) from the\n * identity provider + the read-only `superAdmins` table. */\nexport interface RoleChangeContext {\n actorId: string;\n actorIsSuper: boolean;\n targetId: string;\n targetCurrentRole: string;\n targetIsSuper: boolean;\n newRole: string;\n auth: ResolvedAuth;\n}\n\n/** The result of {@link canChangeRole}: allow, or deny with the HTTP status +\n * message the route should return. */\nexport type GuardResult = { ok: true } | { ok: false; status: number; error: string };\n\n/**\n * The privilege-escalation guard — package-enforced so every site gets it and\n * none re-derives it. Denies: an out-of-ladder role; changing your own role;\n * touching a super-admin unless you are one; and (when a `superAdmins` tier\n * exists) creating or altering an admin unless you are a super-admin. Note the\n * super-admin tier itself is never writable here — it lives in the read-only\n * `superAdmins` table, set only in odla Studio.\n */\nexport function canChangeRole(ctx: RoleChangeContext): GuardResult {\n const { auth } = ctx;\n if (!auth.ladder.includes(ctx.newRole)) {\n return { ok: false, status: 400, error: `role must be one of: ${auth.ladder.join(\", \")}` };\n }\n if (ctx.actorId === ctx.targetId) {\n return { ok: false, status: 400, error: \"you cannot change your own role\" };\n }\n if (ctx.targetIsSuper && !ctx.actorIsSuper) {\n return { ok: false, status: 403, error: \"this person is a super-admin; their access is managed in odla Studio\" };\n }\n const touchesAdmin = ctx.newRole === auth.adminRole || ctx.targetCurrentRole === auth.adminRole;\n if (auth.superAdmins && touchesAdmin && !ctx.actorIsSuper) {\n return { ok: false, status: 403, error: `only super-admins can create or change an ${auth.adminRole}` };\n }\n return { ok: true };\n}\n\n/** Structural view of odla-db's tenant-vault read, so chapter takes no runtime\n * dependency on @odla-ai/db. The worker's admin client satisfies this. */\nexport interface SecretStore {\n secrets: { get(name: string): Promise<string> };\n}\n\n/**\n * Read a tenant-vault secret by name; `undefined` when it is absent or the vault\n * errors, so callers degrade gracefully (e.g. `paymentsReady: false`) rather than\n * throwing. Never logs the value.\n */\nexport async function getVaultSecret(db: SecretStore, name: string): Promise<string | undefined> {\n try {\n const value = await db.secrets.get(name);\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n } catch {\n return undefined;\n }\n}\n","// Route handlers for the chapter Worker. Each returns a Response when it owns the\n// request, or null to fall through to the next handler (finally ASSETS). Split\n// out of worker.ts so the entry stays a thin composer under the per-file LOC cap;\n// behaviour and route order are unchanged from the original single-file handler.\nimport { createCrmRoutes } from \"@odla-ai/crm\";\nimport { getVaultSecret, isAdminRole } from \"./auth\";\nimport { createClerkInvitation } from \"./clerk\";\nimport { joinConfig, submitApplication } from \"./member\";\nimport { projectApplicant, projectSharedRecord } from \"./network\";\nimport { resolveScheduling } from \"./scheduling\";\nimport { memberApplication } from \"./session\";\nimport type { ApplicationRecord, MeetingRecord, MemberApplication } from \"./session\";\nimport { emailGroupFrom, sendTemplated } from \"./notify\";\nimport { json } from \"./worker-context\";\nimport type { ChapterEnv, Route } from \"./worker-context\";\nimport type { Chapter, ChapterDb, ChapterScheduling } from \"./types\";\n\nexport type { Route };\n\n// Apply-time provisioning (both best-effort — never fail the application):\n// project the applicant into crm_record, and mint a Clerk invitation so they get\n// a path into their member area (only when a clerk_secret_key is in the vault).\nasync function provisionApplicant(\n db: ChapterDb,\n chapter: Chapter,\n applicationId: string,\n fields: Record<string, unknown>,\n): Promise<void> {\n const email = typeof fields.email === \"string\" ? fields.email : \"\";\n if (!email) return;\n const s = (v: unknown): string | undefined => (typeof v === \"string\" ? v : undefined);\n try {\n await projectApplicant(\n { crm: chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },\n { applicationId, email, firstName: s(fields.firstName), lastName: s(fields.lastName), phone: s(fields.phone), linkedin: s(fields.linkedin) },\n );\n } catch {\n // CRM projection is best-effort\n }\n try {\n const secret = await getVaultSecret(db, \"clerk_secret_key\");\n if (secret) await createClerkInvitation(secret, { email });\n } catch {\n // account provisioning is best-effort\n }\n}\n\n// Best-effort admin notification when an application arrives, so a human sees it\n// even before payment. Exactly-once per application via the dedupeKey.\nasync function notifyAdminOfApplication(\n db: ChapterDb,\n env: ChapterEnv,\n chapterId: string,\n applicationId: string,\n fields: Record<string, unknown>,\n): Promise<void> {\n try {\n const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;\n const group = Array.isArray(groups) ? groups[0] : undefined;\n if (!group || typeof group.notificationEmail !== \"string\" || !group.notificationEmail) return;\n const s = (v: unknown): string => (typeof v === \"string\" ? v : \"\");\n await sendTemplated(\n { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },\n {\n group: emailGroupFrom(group),\n template: \"adminNotification\",\n to: group.notificationEmail,\n vars: { firstName: s(fields.firstName), lastName: s(fields.lastName), email: s(fields.email), phone: s(fields.phone), state: s(fields.state) },\n dedupeKey: `apply:${applicationId}:admin`,\n applicationId,\n },\n );\n } catch {\n // never fail the application on a notification error\n }\n}\n\n// The signed-in member's own application (chapter mode): the latest by email,\n// folded with its latest scheduled meeting and rendered in the group's timezone.\n// The meetings row is canonical, so no live-calendar reconcile is needed here.\nasync function memberSessionApplication(db: ChapterDb, chapterId: string, email: string): Promise<MemberApplication | null> {\n const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: \"desc\" }, limit: 1 } } })).applications;\n const app = Array.isArray(apps) ? apps[0] : undefined;\n if (!app) return null;\n const meetings = (\n await db.query({ meetings: { $: { where: { applicationId: app.id, status: \"scheduled\" }, order: { createdAt: \"desc\" }, limit: 1 } } })\n ).meetings;\n const meeting = Array.isArray(meetings) ? meetings[0] : undefined;\n const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;\n const group = Array.isArray(groups) ? groups[0] : undefined;\n const timezone = resolveScheduling(group?.schedulingJson as ChapterScheduling | undefined).timezone;\n return memberApplication(app as unknown as ApplicationRecord, meeting as unknown as MeetingRecord | undefined, timezone);\n}\n\n/** GET /api/health — a public liveness probe (what deploy checks hit). */\nexport const handleHealth: Route = async (_req, url) => (url.pathname === \"/api/health\" ? json({ ok: true }) : null);\n\n/** GET /api/config — the public Clerk publishable key, so the SPA can boot sign-in. */\nexport const handleConfig: Route = async (_req, url, env, ctx) => {\n if (url.pathname !== \"/api/config\") return null;\n try {\n const { clerkPublishableKey } = await ctx.getPublicConfig(env);\n return json({ clerkPublishableKey: clerkPublishableKey ?? null, env: env.ODLA_ENV });\n } catch {\n return json({ clerkPublishableKey: null, env: env.ODLA_ENV });\n }\n};\n\n/** GET /api/me — the signed-in user's role, admin authorization, and super-admin\n * tier. In chapter mode it also carries the member's own `application` (their\n * status + booked call), so the member area renders from one call. */\nexport const handleMe: Route = async (req, url, env, ctx) => {\n if (url.pathname !== \"/api/me\") return null;\n const u = await ctx.verifyUser(req, env);\n if (!u) return json({ authorized: false }, 401);\n const db = ctx.makeDb(env);\n const role = await ctx.roleFor(db, u);\n const superAdmin = await ctx.isSuperAdminEmail(db, u.email);\n const base = { authorized: isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };\n if (ctx.chapter.mode !== \"chapter\" || !u.email) return json(base);\n const application = await memberSessionApplication(db as unknown as ChapterDb, ctx.chapter.id, u.email);\n return json({ ...base, application });\n};\n\n/** /api/crm/* — the CRM admin surface (mounted at ctx.crmBase). */\nexport const handleCrm: Route = async (req, url, env, ctx) => {\n const crmBase = ctx.crmBase;\n if (url.pathname !== crmBase && !url.pathname.startsWith(crmBase + \"/\")) return null;\n const db = ctx.makeDb(env);\n const routes = createCrmRoutes({\n crm: ctx.chapter.crm,\n db: db as never,\n authorize: async (r: Request) => {\n const u = await ctx.verifyUser(r, env);\n if (!u || !(await ctx.isAdmin(db, u))) return null;\n return u.email ? { userId: u.userId, email: u.email } : { userId: u.userId };\n },\n sender: ctx.crmSender(env),\n from: env.EMAIL_FROM,\n envName: env.ODLA_ENV,\n baseUrl: url.origin,\n basePath: crmBase,\n });\n const res = await routes(req);\n if (res) return res;\n return json({ error: \"not found\" }, 404);\n};\n\n/** POST /api/network/shared — the hub push projection into this site's crm_record\n * (vault-secret gated, works in both modes). */\nexport const handleNetworkShared: Route = async (req, url, env, ctx) => {\n if (req.method !== \"POST\" || url.pathname !== \"/api/network/shared\") return null;\n const db = ctx.makeDb(env);\n const secret = await getVaultSecret(db as unknown as ChapterDb, \"network_share_secret\");\n const provided = (req.headers.get(\"authorization\") ?? \"\").replace(/^Bearer /, \"\");\n if (!secret || provided.length !== secret.length || provided !== secret) {\n return json({ error: \"unauthorized\" }, 401);\n }\n let person: Record<string, unknown>;\n try {\n person = JSON.parse(await req.text()) as Record<string, unknown>;\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n if (typeof person.email !== \"string\" || typeof person.hubRecordId !== \"string\") {\n return json({ error: \"email and hubRecordId are required\" }, 400);\n }\n const { recordId } = await projectSharedRecord(\n { crm: ctx.chapter.crm, db: db as unknown as ChapterDb, now: () => Date.now(), newId: () => crypto.randomUUID() },\n person as never,\n );\n return json({ recordId });\n};\n\n/** Chapter-mode public member surface: GET /api/join-config, POST /api/applications.\n * Returns null in hub mode so those paths fall through to ASSETS. */\nexport const handleMember: Route = async (req, url, env, ctx) => {\n const chapter = ctx.chapter;\n if (chapter.mode !== \"chapter\") return null;\n\n // Public join config: prices + policy copy + payment readiness (B1/C2).\n if (req.method === \"GET\" && url.pathname === \"/api/join-config\") {\n const db = ctx.makeDb(env);\n const groupId = url.searchParams.get(\"group\") ?? chapter.id;\n const { groups } = await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } });\n const group = Array.isArray(groups) ? groups[0] : undefined;\n if (!group) return json({ error: \"not found\" }, 404);\n const stripeKey = await getVaultSecret(db as unknown as ChapterDb, \"stripe_secret_key\");\n const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);\n return json(joinConfig(group as never, paymentsReady));\n }\n\n // Public application submit — validated, body-capped, idempotent (B2/B3).\n if (req.method === \"POST\" && url.pathname === \"/api/applications\") {\n const raw = await req.text();\n if (raw.length > chapter.application.bodyCap) return json({ error: \"request body too large\" }, 413);\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n const submissionId = typeof parsed.submissionId === \"string\" ? parsed.submissionId : undefined;\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const result = await submitApplication(db, chapter, parsed, {\n submissionId,\n groupId: chapter.id,\n now: Date.now(),\n newId: () => crypto.randomUUID(),\n });\n if (!result.ok) return json({ error: result.error }, 400);\n if (!result.duplicate) {\n await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);\n await provisionApplicant(db, chapter, result.id, parsed);\n }\n return json({ id: result.id, duplicate: result.duplicate, status: result.status });\n }\n\n return null;\n};\n","// Apply-time account provisioning. When an application arrives, the worker mints\n// a Clerk invitation so the applicant gets a path into their member area — the\n// server-side account step S&S does by hand, generalized. Chapter calls the Clerk\n// Backend API directly over fetch (vault clerk_secret_key), mirroring\n// @odla-ai/auth-clerk's createInvitation but self-contained, so it never pulls the\n// auth-clerk UI package into the worker bundle. Best-effort at the call site: a\n// provisioning failure never fails the application.\n\n/** Inputs for a Clerk invitation. */\nexport interface ClerkInviteInput {\n email: string;\n /** Where the accept-invitation link lands (usually the member area). */\n redirectUrl?: string;\n}\n\n/** Build the Clerk Backend API invitation request (path + JSON body). Pure, so\n * the wire shape is testable without a network call. */\nexport function clerkInviteRequest(input: ClerkInviteInput): { path: string; body: Record<string, unknown> } {\n return {\n path: \"/v1/invitations\",\n body: {\n email_address: input.email,\n notify: true,\n ...(input.redirectUrl ? { redirect_url: input.redirectUrl } : {}),\n },\n };\n}\n\n/** POST the invitation to the Clerk Backend API. `ignore_existing` isn't set, so\n * a repeat invite for an already-invited/known email returns non-ok — the caller\n * swallows that (idempotent-enough for a best-effort apply-time invite). */\nexport async function createClerkInvitation(\n secretKey: string,\n input: ClerkInviteInput,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number }> {\n const { path, body } = clerkInviteRequest(input);\n const res = await fetchImpl(`https://api.clerk.com${path}`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${secretKey}`, \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n return { ok: res.ok, status: res.status };\n}\n","// The public member surface logic: the join config a site's join page reads (B1)\n// and the idempotent application submit (B2 validation + B3 exactly-once). Both\n// take the structural ChapterDb, so they're tested against an in-memory fake and\n// carry no runtime @odla-ai/db import. The worker builds the real db client, does\n// Clerk verification, enforces the body cap, and mounts these on chapter routes.\nimport type { Chapter, ChapterApplication, ChapterDb, ResolvedApplication } from \"./types\";\n\n// Silver & Salt's join form. `focus` (a json field) is always accepted.\nconst DEFAULT_REQUIRED = [\"firstName\", \"lastName\", \"email\", \"referral\", \"whoYouAre\", \"message\"];\nconst DEFAULT_OPTIONAL = [\"referralName\", \"linkedin\", \"phone\", \"state\"];\n\n/** Apply defaults + validate the application config. Throws at import on bad shape. */\nexport function resolveApplication(a: ChapterApplication | undefined): ResolvedApplication {\n const required = a?.required ?? DEFAULT_REQUIRED;\n const optional = a?.optional ?? DEFAULT_OPTIONAL;\n for (const [name, arr] of [[\"required\", required], [\"optional\", optional]] as const) {\n if (!Array.isArray(arr) || !arr.every((f) => typeof f === \"string\" && f !== \"\")) {\n throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);\n }\n }\n return {\n required,\n optional,\n maxLen: a?.maxLen ?? {},\n defaultMaxLen: a?.defaultMaxLen ?? 2000,\n bodyCap: a?.bodyCap ?? 32768,\n };\n}\n\n/** A validated submission, or a 400-worthy validation error the route returns. */\nexport type SubmitResult =\n | { ok: true; id: string; duplicate: boolean; status: string }\n | { ok: false; error: string };\n\n/**\n * Submit a membership application (B2 + B3). Validates the configured required\n * fields + max lengths, writes the `applications` row at the pipeline's initial\n * status, and — when the client supplies a `submissionId` — stamps it as the\n * transaction's mutationId (`join:${submissionId}`) so a double-tap can never\n * create two applications (the second returns `duplicate: true`). Idempotency is\n * package-enforced. `now`/`newId` are injected (deterministic in tests).\n */\nexport async function submitApplication(\n db: ChapterDb,\n chapter: Chapter,\n fields: Record<string, unknown>,\n opts: { submissionId?: string; groupId?: string; now: number; newId: () => string },\n): Promise<SubmitResult> {\n const app = chapter.application;\n for (const f of app.required) {\n const v = fields[f];\n if (typeof v !== \"string\" || v.trim() === \"\") return { ok: false, error: `${f} is required` };\n }\n for (const f of [...app.required, ...app.optional]) {\n const v = fields[f];\n const cap = app.maxLen[f] ?? app.defaultMaxLen;\n if (typeof v === \"string\" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };\n }\n\n const id = opts.newId();\n const row: Record<string, unknown> = { id, status: chapter.pipeline.initial, createdAt: opts.now };\n for (const f of [...app.required, ...app.optional]) {\n if (typeof fields[f] === \"string\") row[f] = (fields[f] as string).trim();\n }\n if (fields.focus !== undefined) row.focus = fields.focus;\n if (opts.groupId) row.groupId = opts.groupId;\n\n const { duplicate } = await db.transact(\n [{ t: \"update\", ns: \"applications\", id, attrs: row }],\n opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : undefined,\n );\n return { ok: true, id, duplicate, status: chapter.pipeline.initial };\n}\n\n/** The `groups`-row fields the join config exposes. */\nexport interface JoinConfigGroup {\n id: string;\n name: string;\n standardPriceCents?: number;\n foundingDiscountCents?: number;\n disclaimerText?: string;\n refundPolicyText?: string;\n trustCopy?: string;\n commitmentText?: string;\n normsText?: string;\n}\n\n/**\n * The public join config (B1) a site's join page reads: copy + prices from the\n * group row plus `paymentsReady`. When payments aren't wired the join flow drops\n * the payment step (C2) — the worker computes `paymentsReady` from the group's\n * Stripe keys + vault secret. Pure.\n */\nexport function joinConfig(group: JoinConfigGroup, paymentsReady: boolean): Record<string, unknown> {\n return {\n id: group.id,\n name: group.name,\n standardPriceCents: group.standardPriceCents ?? 0,\n foundingDiscountCents: group.foundingDiscountCents ?? 0,\n disclaimerText: group.disclaimerText ?? \"\",\n refundPolicyText: group.refundPolicyText ?? \"\",\n trustCopy: group.trustCopy ?? \"\",\n commitmentText: group.commitmentText ?? \"\",\n normsText: group.normsText ?? \"\",\n paymentsReady,\n };\n}\n","// The hub → chapter people projection (push model). The network hub curates\n// prospects and pushes a person's contact data into THIS chapter's own\n// crm_record, so a chapter admin sees network prospects beside their applicants.\n//\n// Invariants (package-enforced so no site re-derives them):\n// - One-way: the chapter never writes back to the hub through this path.\n// - Idempotent: keyed by the hub's record id (a re-share updates, never\n// duplicates) AND unified by primaryEmail — a shared prospect who later\n// submits an application lands on the SAME crm_record, so the two projections\n// compose instead of forking the person.\n// - A person may be shared with many chapters; that fan-out is hub-side, so each\n// chapter's projection here is independent.\n//\n// Reuses @odla-ai/crm's record ops (full validation via crm.prepare), driven by\n// the resolved chapter CRM engine + the structural ChapterDb.\nimport { createRecord, updateRecord } from \"@odla-ai/crm\";\nimport type { Crm } from \"@odla-ai/crm\";\nimport type { ChapterDb } from \"./types\";\n\n/** The contact data the hub shares for a prospect. `hubRecordId` is the stable\n * idempotency key (the hub's crm_record id). */\nexport interface SharedPerson {\n email: string;\n name?: string;\n firstName?: string;\n lastName?: string;\n phone?: string;\n linkedin?: string;\n hubRecordId: string;\n}\n\n/** Map a shared prospect to a crm `person` input (only the fields the default\n * person type accepts). Name falls back to first+last, then the email. */\nexport function sharedPersonInput(person: SharedPerson): Record<string, unknown> {\n const email = person.email.toLowerCase();\n const fullName = [person.firstName, person.lastName].filter(Boolean).join(\" \").trim();\n const input: Record<string, unknown> = { name: person.name ?? fullName ?? email, email };\n if (input.name === \"\") input.name = email;\n if (person.firstName) input.firstName = person.firstName;\n if (person.lastName) input.lastName = person.lastName;\n if (person.phone) input.phone = person.phone;\n if (person.linkedin) input.linkedin = person.linkedin;\n return input;\n}\n\n/** Deps for the projection — the resolved CRM engine, the structural db, and\n * injected clock/id (deterministic in tests). */\nexport interface ProjectionDeps {\n crm: Crm;\n db: ChapterDb;\n now: () => number;\n newId: () => string;\n}\n\n// Resolve an existing crm_record person by lowercased primaryEmail and update it,\n// else create one under the given mutationId. The shared core of every one-way\n// person projection (hub share, arriving applicant).\nasync function upsertPerson(\n deps: ProjectionDeps,\n opts: { email: string; input: Record<string, unknown>; mutationId: string },\n): Promise<{ recordId: string }> {\n const email = opts.email.toLowerCase();\n const crmDeps = { crm: deps.crm, db: deps.db as never, now: deps.now, newId: deps.newId };\n const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: \"person\", primaryEmail: email }, limit: 1 } } });\n const existing = crm_record?.[0];\n if (existing && typeof existing.id === \"string\") {\n await updateRecord(crmDeps, { id: existing.id, input: opts.input });\n return { recordId: existing.id };\n }\n const created = await createRecord(crmDeps, { type: \"person\", input: opts.input, mutationId: opts.mutationId });\n return { recordId: created.id };\n}\n\n/**\n * Upsert a hub-shared prospect into this chapter's `crm_record` (push\n * projection), idempotent by the hub record id and unified by email. Callers wrap\n * this in `.catch` so a projection failure never fails the hub's share request.\n */\nexport async function projectSharedRecord(deps: ProjectionDeps, person: SharedPerson): Promise<{ recordId: string }> {\n return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });\n}\n\n/** An arriving applicant, as far as the CRM projection cares. */\nexport interface Applicant {\n applicationId: string;\n email: string;\n firstName?: string;\n lastName?: string;\n phone?: string;\n linkedin?: string;\n}\n\n/**\n * Project an arriving applicant into this chapter's `crm_record`, so a new\n * application shows up in the CRM immediately, unified by email with any prior\n * record. Idempotent per application (`apply:${applicationId}`). Best-effort at\n * the call site — a projection failure never fails the application.\n */\nexport async function projectApplicant(deps: ProjectionDeps, applicant: Applicant): Promise<{ recordId: string }> {\n const input = sharedPersonInput({\n email: applicant.email,\n firstName: applicant.firstName,\n lastName: applicant.lastName,\n phone: applicant.phone,\n linkedin: applicant.linkedin,\n hubRecordId: applicant.applicationId,\n });\n return upsertPerson(deps, { email: applicant.email, input, mutationId: `apply:${applicant.applicationId}` });\n}\n","// Scheduling core — config resolution + the booking invariants, ported from the\n// proven Silver & Salt worker. Everything here is PURE (no @odla-ai/calendar, no\n// db), so the correctness properties are unit-testable and package-enforced; the\n// worker route owns only the I/O (FreeBusy, computeBookableSlots, calendar\n// create/reschedule, the db writes) and calls these.\n//\n// Package-enforced invariants:\n// - the `meetings` row is canonical; applications.meetingAt and the calendar\n// event are projections written from it.\n// - one intro event per application, forever: a rebooking RESCHEDULES the\n// existing event (preserving its Meet link + invite thread), never creates a\n// second — see {@link bookingDecision} + {@link introIdempotencyKey}.\n// - status never moves backward on booking; you can only book from an early\n// stage — see {@link canBookFrom} + {@link applicationBookingUpdate}.\n// - endAt is always derived server-side, never client-supplied — see\n// {@link endForSlot}.\nimport type { ChapterScheduling } from \"./types\";\n\n/** A fully-resolved scheduling config (every field present). */\nexport interface ResolvedScheduling {\n slotMinutes: number;\n days: readonly number[];\n startHour: number;\n endHour: number;\n timezone: string;\n minNoticeHours: number;\n windowDays: number;\n summaryTemplate: string;\n}\n\n/** The S&S-proven defaults: 45-minute weekday slots, 9–5 Pacific, 24h notice,\n * a 14-day window. The summary is generic (a chapter's group seed supplies a\n * name-branded one). */\nexport const SCHEDULING_DEFAULTS: ResolvedScheduling = {\n slotMinutes: 45,\n days: [1, 2, 3, 4, 5],\n startHour: 9,\n endHour: 17,\n timezone: \"America/Los_Angeles\",\n minNoticeHours: 24,\n windowDays: 14,\n summaryTemplate: \"Introduction call with {{firstName}} {{lastName}}\",\n};\n\nfunction isValidTimeZone(tz: string): boolean {\n try {\n new Intl.DateTimeFormat(undefined, { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve a group's scheduling config against the defaults, validating every\n * bound the way S&S does at config-write time (throws on a bad config, so a\n * misconfiguration surfaces immediately rather than yielding empty slots).\n * `windowDays` is capped at 62 because Google FreeBusy is.\n */\nexport function resolveScheduling(config?: ChapterScheduling): ResolvedScheduling {\n const d = config ?? {};\n const c: ResolvedScheduling = {\n slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,\n days: d.days ?? SCHEDULING_DEFAULTS.days,\n startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,\n endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,\n timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,\n minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,\n windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,\n summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate,\n };\n const fail = (msg: string): never => {\n throw new Error(`scheduling: ${msg}`);\n };\n if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) fail(\"slotMinutes must be 15–240\");\n if (!(c.windowDays >= 1 && c.windowDays <= 62)) fail(\"windowDays must be 1–62 (FreeBusy caps at 62)\");\n if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) fail(\"minNoticeHours must be 0–336\");\n if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) fail(\"require 0 ≤ startHour < endHour ≤ 24\");\n const days = [...c.days];\n if (!days.length || !days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {\n fail(\"days must be a non-empty list of weekday integers 0–6\");\n }\n if (typeof c.timezone !== \"string\" || !isValidTimeZone(c.timezone)) fail(`invalid IANA timezone \"${c.timezone}\"`);\n if (typeof c.summaryTemplate !== \"string\") fail(\"summaryTemplate must be a string\");\n return { ...c, days };\n}\n\n/** Statuses a member may book/reschedule from (early pipeline only). */\n// Booking eligibility is answered by the configured pipeline — canBook(status,\n// pipeline) in ./pipeline — not a hardcoded status list, so a chapter that\n// customizes its stages gets one consistent answer.\n\n/** The availability window: `[now, now + windowDays]` in epoch ms. */\nexport function slotWindow(now: number, windowDays: number): { from: number; to: number } {\n return { from: now, to: now + windowDays * 86_400_000 };\n}\n\n/** The slot's end instant, always derived from its start (never client-supplied). */\nexport function endForSlot(startAt: number, slotMinutes: number): number {\n return startAt + slotMinutes * 60_000;\n}\n\n/** Double-book pre-check: the requested start must land exactly on a currently\n * bookable slot boundary. */\nexport function isSlotAvailable(slots: readonly { startAt: number }[], startAt: number): boolean {\n return slots.some((s) => s.startAt === startAt);\n}\n\n/** Render a meeting summary from its template (`{{firstName}}`/`{{lastName}}`). */\nexport function renderSummary(template: string, app: { firstName?: string | null; lastName?: string | null }): string {\n return template.replace(\"{{firstName}}\", app.firstName ?? \"\").replace(\"{{lastName}}\", app.lastName ?? \"\");\n}\n\n/** The prior scheduled meeting for an application, as far as booking cares. */\nexport interface ExistingMeeting {\n id: string;\n googleEventId?: string | null;\n meetUrl?: string | null;\n htmlLink?: string | null;\n}\n\n/** Decide reschedule-vs-create: reschedule iff there's an existing event to move,\n * so the Meet link + invite thread survive and no second event is minted. */\nexport function bookingDecision(existing: ExistingMeeting | null | undefined): { reschedule: boolean; eventId: string | null } {\n const eventId = existing?.googleEventId ?? null;\n return { reschedule: Boolean(eventId), eventId };\n}\n\n/** The create idempotency key — one intro event per application, forever, so a\n * retried create returns the same booking rather than a duplicate. */\nexport function introIdempotencyKey(applicationId: string): string {\n return `application:${applicationId}:intro`;\n}\n\n/** The canonical `meetings` row for a first booking. A `type` (not `interface`)\n * so it stays assignable to a db op's `attrs` (Record<string, unknown>). */\nexport type NewMeetingRow = {\n id: string;\n applicationId: string;\n groupId: string;\n startAt: number;\n endAt: number;\n timezone: string;\n status: \"scheduled\";\n googleEventId: string;\n meetUrl?: string;\n htmlLink?: string;\n drift: \"none\";\n createdAt: number;\n};\n\n/** Build the new `meetings` row after the calendar created the event. Optional\n * scalars are omitted (never null), per the odla-db porting rule. */\nexport function meetingCreateRow(i: {\n meetingId: string;\n applicationId: string;\n groupId: string;\n startAt: number;\n endAt: number;\n timezone: string;\n googleEventId: string;\n meetUrl?: string | null;\n htmlLink?: string | null;\n createdAt: number;\n}): NewMeetingRow {\n return {\n id: i.meetingId,\n applicationId: i.applicationId,\n groupId: i.groupId,\n startAt: i.startAt,\n endAt: i.endAt,\n timezone: i.timezone,\n status: \"scheduled\",\n googleEventId: i.googleEventId,\n ...(i.meetUrl ? { meetUrl: i.meetUrl } : {}),\n ...(i.htmlLink ? { htmlLink: i.htmlLink } : {}),\n drift: \"none\",\n createdAt: i.createdAt,\n };\n}\n\n/** The `meetings`-row patch for a reschedule (same row id, moved in place). */\nexport type MeetingReschedulePatch = {\n startAt: number;\n endAt: number;\n drift: \"none\";\n};\n\n/** Patch to move an existing meeting to a new window. */\nexport function meetingRescheduleUpdate(startAt: number, endAt: number): MeetingReschedulePatch {\n return { startAt, endAt, drift: \"none\" };\n}\n\n/** The `applications`-row patch after a booking. */\nexport type ApplicationBookingPatch = {\n meetingAt: number;\n meetingLink?: string;\n status?: \"call_scheduled\";\n};\n\n/** Project the booking onto the application row: cache the time, adopt the\n * calendar link if any, and advance the status to `call_scheduled` unless it is\n * already there (never backward). */\nexport function applicationBookingUpdate(\n currentStatus: string,\n startAt: number,\n htmlLink?: string | null,\n): ApplicationBookingPatch {\n return {\n meetingAt: startAt,\n ...(htmlLink ? { meetingLink: htmlLink } : {}),\n ...(currentStatus !== \"call_scheduled\" ? { status: \"call_scheduled\" as const } : {}),\n };\n}\n","// The member session — what GET /api/me returns to a signed-in applicant/member.\n// Ported from Silver & Salt's applicationSummary + /api/me reconciliation. The\n// SHAPING is pure and package-enforced so no site re-derives it; the worker owns\n// only the I/O around it (locating the application by email, reconciling the\n// meeting against the calendar) and hands the resolved rows here.\n//\n// Two invariants live here, not in a site:\n// - `paid` is DERIVED, never a stored flag: a subscription exists and the\n// application wasn't refunded. Sites can't drift a stale boolean out of sync\n// with Stripe.\n// - the live meeting row wins: its startAt/meetUrl/timezone override whatever\n// the application row cached, and a non-scheduled meeting (a cancellation\n// adopted from the calendar) forces meetingAt back to null.\n\n/** An application row, as far as the session cares about it. */\nexport interface ApplicationRecord {\n id: string;\n firstName?: string | null;\n lastName?: string | null;\n email?: string | null;\n status: string;\n meetingAt?: number | null;\n meetingLink?: string | null;\n createdAt?: number | null;\n stripeSubscriptionId?: string | null;\n renewalAt?: number | null;\n canceled?: boolean;\n}\n\n/** The reconciled meeting row (already adopted against the calendar), or null. */\nexport interface MeetingRecord {\n status: string;\n startAt?: number | null;\n meetUrl?: string | null;\n timezone?: string | null;\n}\n\n/** The stable, non-meeting fields of an application (safe to expose to its own\n * owner). */\nexport interface ApplicationSummary {\n id: string;\n firstName: string | null;\n lastName: string | null;\n email: string | null;\n status: string;\n createdAt: number | null;\n meetingLink: string | null;\n paid: boolean;\n renewalAt: number | null;\n canceled: boolean;\n}\n\n/** A summary plus the reconciled meeting fields — the `application` the member\n * area renders. */\nexport interface MemberApplication extends ApplicationSummary {\n meetingAt: number | null;\n meetUrl: string | null;\n timezone: string;\n}\n\n/** The full GET /api/me payload for a signed-in user. */\nexport interface MemberSession {\n userId: string;\n email: string | null;\n role: string;\n superAdmin: boolean;\n application: MemberApplication | null;\n}\n\n/** Derive the summary fields from an application row. `paid` is computed, not\n * read, so it can never contradict Stripe. */\nexport function applicationSummary(app: ApplicationRecord): ApplicationSummary {\n return {\n id: app.id,\n firstName: app.firstName ?? null,\n lastName: app.lastName ?? null,\n email: app.email ?? null,\n status: app.status,\n createdAt: app.createdAt ?? null,\n meetingLink: app.meetingLink ?? null,\n paid: Boolean(app.stripeSubscriptionId) && app.status !== \"refunded\",\n renewalAt: app.renewalAt ?? null,\n canceled: app.canceled === true,\n };\n}\n\n/** Fold a (possibly absent, already-reconciled) meeting into the application the\n * member area renders. The live meeting overrides the application's cached\n * meeting fields; a non-`scheduled` meeting clears the booking. */\nexport function memberApplication(\n app: ApplicationRecord,\n meeting: MeetingRecord | null | undefined,\n defaultTimezone: string,\n): MemberApplication {\n const summary = applicationSummary(app);\n let meetingAt = app.meetingAt ?? null;\n let meetUrl: string | null = null;\n let timezone = defaultTimezone;\n if (meeting) {\n timezone = meeting.timezone ?? timezone;\n if (meeting.status === \"scheduled\") {\n meetingAt = meeting.startAt ?? null;\n meetUrl = meeting.meetUrl ?? null;\n } else {\n meetingAt = null;\n }\n }\n return { ...summary, meetingAt, meetUrl, timezone };\n}\n\n/** Identity of the signed-in user, from the verified session. */\nexport interface SessionUser {\n userId: string;\n email?: string | null;\n role: string;\n}\n\n/** Assemble the GET /api/me payload. `application` is null when the user has no\n * application on file (an admin who never applied, or a brand-new account). */\nexport function memberSession(\n user: SessionUser,\n opts: { application: MemberApplication | null; superAdmin: boolean },\n): MemberSession {\n return {\n userId: user.userId,\n email: user.email ?? null,\n role: user.role,\n superAdmin: opts.superAdmin,\n application: opts.application,\n };\n}\n","// The chapter email pipeline: exactly-once delivery, a non-production fail-safe,\n// and template rendering. Every property here is easy to get wrong and expensive\n// to get wrong, so the correctness-critical decisions — the dedupe check (E1),\n// the dev-redirect / log-only fail-safe (E2), and template rendering (E4) — are\n// PURE and fully tested in this module. The worker supplies the transport +\n// odla-db and performs the actual send + emailLog write around these decisions.\n//\n// Chapter's operational templates ({ subject, text, enabled? }) are\n// transactional lifecycle mail by construction — a site owner edits the copy in\n// Settings but cannot reclassify one as marketing. Consent-gated marketing blasts\n// go through @odla-ai/crm, which owns the transactional-vs-marketing template\n// class as code (E3), so relabeling copy can never bypass the consent gate.\n\n/** One owner-editable template row on the group. `enabled` absent = enabled. */\nexport interface EmailTemplateRow {\n subject: string;\n text: string;\n enabled?: boolean;\n}\n\n/** The `groups`-row fields the email pipeline reads. */\nexport interface EmailGroup {\n id: string;\n name: string;\n replyTo: string;\n /** Non-prod debug inbox: all mail redirects here outside prod (E2). */\n debugEmail?: string;\n refundPolicyText?: string;\n commitmentText?: string;\n normsText?: string;\n emailTemplates: Record<string, EmailTemplateRow>;\n}\n\n/** `{{placeholder}}` substitution; unknown placeholders render empty. */\nexport function render(template: string, vars: Record<string, string>): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => vars[key] ?? \"\");\n}\n\n/** Group-level vars every template receives, under the caller's vars. */\nfunction groupVars(group: EmailGroup, vars: Record<string, string>): Record<string, string> {\n return {\n ...vars,\n refundPolicyText: group.refundPolicyText ?? \"\",\n commitmentText: group.commitmentText ?? \"\",\n normsText: group.normsText ?? \"\",\n };\n}\n\n/**\n * Re-render a template's body for history/preview (E4): the CRM comms history\n * reads back emails whose body predates `emailLog.body` by rendering the current\n * template with the recipient's vars. Same substitution + group vars as the send\n * path. `null` for an unknown template. Reflects the copy as it reads today, not\n * necessarily the exact bytes originally sent (only `emailLog.body` is byte-exact).\n */\nexport function renderTemplateBody(group: EmailGroup, template: string, vars: Record<string, string>): string | null {\n const tpl = group.emailTemplates?.[template];\n if (!tpl) return null;\n return render(tpl.text, groupVars(group, vars));\n}\n\n/**\n * E1 (exactly-once): given the prior `emailLog` rows for a `dedupeKey`, has the\n * mail already been delivered? A prior row with **no error** means yes — the\n * caller short-circuits the resend. Failure rows (which carry an `error` and are\n * written without the dedupe mutationId) do not count, so a retry after a failure\n * can still succeed.\n */\nexport function isAlreadySent(priorRows: ReadonlyArray<{ error?: unknown }>): boolean {\n return priorRows.some((row) => !row.error);\n}\n\n/** The pure delivery decision produced by {@link planDelivery}. */\nexport type DeliveryDecision =\n | { deliver: false; reason: \"template-missing\" | \"disabled\" }\n | {\n deliver: true;\n /** Which transport to use — `log-only` records the send but delivers nothing. */\n transport: \"cloudflare\" | \"log-only\";\n to: string;\n subject: string;\n text: string;\n /** True when redirected to the non-prod debug inbox. */\n redirected: boolean;\n };\n\n/**\n * The pure delivery decision (E2 fail-safe + E3 enabled). Given the env, group,\n * template, recipient, and whether a real Cloudflare transport is wired:\n * - missing template → not delivered (`template-missing`);\n * - disabled template and not forced → not delivered (`disabled`);\n * - **non-prod with a debug inbox** → REDIRECT to it, `\"[dev] \"` subject prefix,\n * a dev-redirect note in the body, so test applicants never receive real mail;\n * - **non-prod with NO debug inbox** → force `log-only` (deliver nothing) — the\n * fail-safe that protects every site's test data;\n * - prod → deliver via the real transport (`cloudflare` if wired, else `log-only`).\n */\nexport function planDelivery(input: {\n envName: string;\n group: EmailGroup;\n template: string;\n to: string;\n vars: Record<string, string>;\n /** Whether a Cloudflare Email Service transport (binding + verified from) is wired. */\n cloudflareReady: boolean;\n /** The admin test route may send a disabled template. */\n force?: boolean;\n}): DeliveryDecision {\n const tpl = input.group.emailTemplates?.[input.template];\n if (!tpl) return { deliver: false, reason: \"template-missing\" };\n if (tpl.enabled === false && !input.force) return { deliver: false, reason: \"disabled\" };\n\n const vars = groupVars(input.group, input.vars);\n const isProd = input.envName === \"prod\";\n const redirect = !isProd && !!input.group.debugEmail;\n const transport: \"cloudflare\" | \"log-only\" =\n !isProd && !redirect ? \"log-only\" : input.cloudflareReady ? \"cloudflare\" : \"log-only\";\n const to = redirect ? (input.group.debugEmail as string) : input.to;\n const subject = (redirect ? \"[dev] \" : \"\") + render(tpl.subject, vars);\n const text = redirect\n ? `(dev redirect; original recipient: ${input.to})\\n\\n` + render(tpl.text, vars)\n : render(tpl.text, vars);\n return { deliver: true, transport, to, subject, text, redirected: redirect };\n}\n","// sendTemplated — the operational-email orchestration that wires the pure email\n// decisions (./email: isAlreadySent E1, planDelivery E2/E3) to odla-db + a\n// transport. Structural (takes a ChapterDb + a sender), so it's FakeDb-testable\n// like submitApplication/projectSharedRecord. The worker's lifecycle routes call\n// it to send the prep / payment-confirmation / admin-alert mail.\n//\n// Exactly-once is enforced twice: a prior successful emailLog row short-circuits\n// the resend (query + isAlreadySent), AND the success row is written under the\n// `email:${dedupeKey}` mutationId so a racing double-send dedupes at the db layer.\n// Failure rows omit the mutationId, so a retry after a transient failure can send.\nimport { isAlreadySent, planDelivery } from \"./email\";\nimport type { EmailGroup } from \"./email\";\nimport type { ChapterDb } from \"./types\";\n\n/** The mail transport (the worker's Cloudflare SEND_EMAIL binding). */\nexport interface MailSender {\n send(payload: { from: string; to: string[]; subject: string; text?: string; replyTo?: string }): Promise<{ messageId: string }>;\n}\n\n/** Deps for {@link sendTemplated} — the db, the env name (drives the fail-safe),\n * the transport + from (absent ⇒ log-only), and injected clock/id. */\nexport interface NotifyDeps {\n db: ChapterDb;\n envName: string;\n sender?: MailSender;\n from?: string;\n now: () => number;\n newId: () => string;\n}\n\n/** One templated send. `dedupeKey` is the exactly-once key. */\nexport interface NotifyInput {\n group: EmailGroup;\n template: string;\n to: string;\n vars: Record<string, string>;\n dedupeKey: string;\n applicationId?: string;\n /** The admin test route may send a disabled template. */\n force?: boolean;\n}\n\n/** The outcome. `sent:true` includes the already-sent short-circuit. */\nexport interface NotifyResult {\n sent: boolean;\n reason?: string;\n}\n\n/**\n * Send a templated lifecycle email exactly once. Short-circuits on a prior\n * successful send; otherwise plans delivery (dev fail-safe applies), sends via the\n * transport when one is wired and the plan calls for it, and records an emailLog\n * row either way (success keyed for exactly-once, failure unkeyed for retry).\n */\nexport async function sendTemplated(deps: NotifyDeps, input: NotifyInput): Promise<NotifyResult> {\n const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });\n const prior = (Array.isArray(emailLog) ? emailLog : []) as Array<{ error?: unknown }>;\n if (isAlreadySent(prior)) return { sent: true, reason: \"already-sent\" };\n\n const cloudflareReady = Boolean(deps.sender && deps.from);\n const decision = planDelivery({\n envName: deps.envName,\n group: input.group,\n template: input.template,\n to: input.to,\n vars: input.vars,\n cloudflareReady,\n force: input.force,\n });\n if (!decision.deliver) return { sent: false, reason: decision.reason };\n\n let error: string | undefined;\n let messageId: string | undefined;\n if (decision.transport === \"cloudflare\" && deps.sender && deps.from) {\n try {\n const res = await deps.sender.send({\n from: deps.from,\n to: [decision.to],\n subject: decision.subject,\n text: decision.text,\n replyTo: input.group.replyTo,\n });\n messageId = res.messageId;\n } catch (e) {\n error = e instanceof Error ? e.message : String(e);\n }\n }\n\n const id = deps.newId();\n const row: Record<string, unknown> = {\n id,\n groupId: input.group.id,\n to: decision.to,\n template: input.template,\n subject: decision.subject,\n body: decision.text,\n transport: decision.transport,\n redirected: decision.redirected,\n dedupeKey: input.dedupeKey,\n sentAt: deps.now(),\n ...(input.applicationId ? { applicationId: input.applicationId } : {}),\n ...(messageId ? { messageId } : {}),\n ...(error ? { error } : {}),\n };\n await deps.db.transact([{ t: \"update\", ns: \"emailLog\", id, attrs: row }], error ? undefined : { mutationId: `email:${input.dedupeKey}` });\n return error ? { sent: false, reason: error } : { sent: true };\n}\n\n/** Project a `groups` row into the {@link EmailGroup} the email pipeline reads. */\nexport function emailGroupFrom(row: Record<string, unknown>): EmailGroup {\n const str = (v: unknown): string | undefined => (typeof v === \"string\" ? v : undefined);\n const templates = row.emailTemplates && typeof row.emailTemplates === \"object\" ? row.emailTemplates : {};\n return {\n id: String(row.id),\n name: String(row.name ?? \"\"),\n replyTo: str(row.replyTo) ?? \"\",\n debugEmail: str(row.debugEmail),\n refundPolicyText: str(row.refundPolicyText),\n commitmentText: str(row.commitmentText),\n normsText: str(row.normsText),\n emailTemplates: templates as EmailGroup[\"emailTemplates\"],\n };\n}\n","// Scheduling routes: GET /api/schedule/slots + POST /api/schedule/book. This\n// file owns the I/O — Google FreeBusy, the pure computeBookableSlots, the\n// calendar create/reschedule, and the db writes — and delegates every\n// correctness rule to ./scheduling. Calendar is RUNTIME-optional: a chapter with\n// no connected calendar has freeBusy throw, so /slots answers\n// { schedulingReady: false } (200) and the join flow degrades to \"we'll reach\n// out by email\" rather than erroring.\nimport { computeBookableSlots, initCalendar } from \"@odla-ai/calendar\";\nimport { emailGroupFrom, sendTemplated } from \"./notify\";\nimport { json } from \"./worker-context\";\nimport type { ChapterEnv, WorkerContext } from \"./worker-context\";\nimport type { Route } from \"./worker-routes\";\nimport type { ChapterDb, ChapterScheduling, DbOp } from \"./types\";\nimport { canBook } from \"./pipeline\";\nimport {\n applicationBookingUpdate,\n bookingDecision,\n endForSlot,\n introIdempotencyKey,\n isSlotAvailable,\n meetingCreateRow,\n meetingRescheduleUpdate,\n renderSummary,\n resolveScheduling,\n slotWindow,\n} from \"./scheduling\";\nimport type { ExistingMeeting, ResolvedScheduling } from \"./scheduling\";\n\ntype Cal = ReturnType<typeof initCalendar>;\ntype Row = Record<string, unknown>;\n\n/** The stable `.code` off an OdlaError/provider error, else a safe default. */\nfunction errCode(err: unknown): string {\n if (err && typeof err === \"object\") {\n const code = (err as { code?: unknown }).code;\n if (typeof code === \"string\") return code;\n }\n return \"calendar_unavailable\";\n}\n\nfunction makeCalendar(env: ChapterEnv): Cal {\n // Calendar uses ODLA_APP_ID + ODLA_PLATFORM — distinct from the db client's\n // ODLA_TENANT + ODLA_ENDPOINT.\n return initCalendar({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });\n}\n\nasync function firstRow(db: ChapterDb, ns: string, q: Record<string, unknown>): Promise<Row | undefined> {\n const res = await db.query({ [ns]: { $: q } });\n const rows = res[ns];\n return Array.isArray(rows) ? rows[0] : undefined;\n}\n\nasync function computeSlots(cal: Cal, cfg: ResolvedScheduling): Promise<Array<{ startAt: number; endAt: number }>> {\n const { from, to } = slotWindow(Date.now(), cfg.windowDays);\n const fb = await cal.availability.freeBusy({ timeMin: from, timeMax: to });\n return computeBookableSlots(fb.busy, {\n from: fb.timeMin,\n to: fb.timeMax,\n timezone: cfg.timezone,\n slotMinutes: cfg.slotMinutes,\n businessHours: { days: [...cfg.days], startHour: cfg.startHour, endHour: cfg.endHour },\n minNoticeMs: cfg.minNoticeHours * 3_600_000,\n });\n}\n\nasync function bookSlot(req: Request, env: ChapterEnv, ctx: WorkerContext): Promise<Response> {\n let body: Row;\n try {\n body = JSON.parse(await req.text()) as Row;\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n const applicationId = typeof body.applicationId === \"string\" ? body.applicationId : \"\";\n const startAt = Number(body.startAt);\n if (!applicationId || !Number.isFinite(startAt)) return json({ error: \"applicationId and startAt required\" }, 400);\n\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const app = await firstRow(db, \"applications\", { where: { id: applicationId }, limit: 1 });\n if (!app) return json({ error: \"not found\" }, 404);\n const status = String(app.status ?? \"\");\n if (!canBook(status, ctx.chapter.pipeline)) return json({ error: `cannot book from status \"${status}\"` }, 409);\n\n const group = await firstRow(db, \"groups\", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });\n if (!group) return json({ error: \"group not found\" }, 500);\n const cfg = resolveScheduling(group.schedulingJson as ChapterScheduling | undefined);\n const endAt = endForSlot(startAt, cfg.slotMinutes);\n const cal = makeCalendar(env);\n\n // Double-book guard, layer 1: the requested start must still be a live slot.\n let slots: Array<{ startAt: number }>;\n try {\n slots = await computeSlots(cal, cfg);\n } catch (err) {\n return json({ error: \"scheduling unavailable\", code: errCode(err) }, 503);\n }\n if (!isSlotAvailable(slots, startAt)) return json({ error: \"slot no longer available\", code: \"calendar_slot_unavailable\" }, 409);\n\n const summary = renderSummary(cfg.summaryTemplate, { firstName: app.firstName as string, lastName: app.lastName as string });\n const existing = (await firstRow(db, \"meetings\", {\n where: { applicationId, status: \"scheduled\" },\n order: { createdAt: \"desc\" },\n limit: 1,\n })) as ExistingMeeting | undefined;\n const decision = bookingDecision(existing);\n\n let meetUrl: string | null = null;\n let htmlLink: string | null = null;\n let meetingOp: DbOp;\n try {\n if (decision.reschedule && decision.eventId) {\n // Reschedule the SAME event — the Meet link + invite thread survive.\n await cal.actions.reschedule(decision.eventId, { startAt, endAt });\n meetUrl = (existing?.meetUrl as string | undefined) ?? null;\n htmlLink = (existing?.htmlLink as string | undefined) ?? null;\n meetingOp = { t: \"update\", ns: \"meetings\", id: String(existing?.id), attrs: meetingRescheduleUpdate(startAt, endAt) };\n } else {\n const { booking } = await cal.actions.create(\n { summary, startAt, endAt, attendees: [String(app.email)], timezone: cfg.timezone, meet: true },\n { idempotencyKey: introIdempotencyKey(applicationId) },\n );\n meetUrl = booking.meetUrl ?? null;\n htmlLink = booking.htmlLink ?? null;\n const meetingId = crypto.randomUUID();\n meetingOp = {\n t: \"update\",\n ns: \"meetings\",\n id: meetingId,\n attrs: meetingCreateRow({\n meetingId,\n applicationId,\n groupId: String(group.id),\n startAt,\n endAt,\n timezone: cfg.timezone,\n googleEventId: booking.eventId,\n meetUrl: booking.meetUrl,\n htmlLink: booking.htmlLink,\n createdAt: Date.now(),\n }),\n };\n }\n } catch (err) {\n const code = errCode(err);\n // Double-book guard, layer 2 (authoritative): the provider rejected under lease.\n if (code === \"calendar_slot_unavailable\") return json({ error: \"slot no longer available\", code }, 409);\n return json({ error: \"booking failed\", code }, 502);\n }\n\n // meetings row is canonical; the application row is the projection.\n const appOp: DbOp = { t: \"update\", ns: \"applications\", id: applicationId, attrs: applicationBookingUpdate(status, startAt, htmlLink) };\n await db.transact([meetingOp, appOp]);\n\n // Prep email — best-effort, exactly-once per application (never fails the booking).\n if (typeof app.email === \"string\" && app.email) {\n await sendTemplated(\n { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },\n { group: emailGroupFrom(group), template: \"prepEmail\", to: app.email, vars: { firstName: String(app.firstName ?? \"\") }, dedupeKey: `prep:${applicationId}`, applicationId },\n ).catch(() => undefined);\n }\n\n return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });\n}\n\n/** GET /api/schedule/slots + POST /api/schedule/book (chapter mode only). */\nexport const handleSchedule: Route = async (req, url, env, ctx) => {\n if (ctx.chapter.mode !== \"chapter\") return null;\n\n if (req.method === \"GET\" && url.pathname === \"/api/schedule/slots\") {\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const group = await firstRow(db, \"groups\", { where: { id: url.searchParams.get(\"group\") ?? ctx.chapter.id }, limit: 1 });\n if (!group) return json({ error: \"not found\" }, 404);\n const cfg = resolveScheduling(group.schedulingJson as ChapterScheduling | undefined);\n try {\n const slots = await computeSlots(makeCalendar(env), cfg);\n return json({ schedulingReady: true, timezone: cfg.timezone, slotMinutes: cfg.slotMinutes, slots });\n } catch (err) {\n return json({ schedulingReady: false, code: errCode(err) });\n }\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/schedule/book\") {\n return bookSlot(req, env, ctx);\n }\n\n return null;\n};\n","// The application status pipeline — config, not code. Which statuses exist, which\n// a member can book an intro call from, and which an admin can approve from\n// differ per site; the one invariant every site wants is that status never moves\n// backwards. All of this is pure + tested here; the worker enforces it on every\n// status write, and the CRM record.stage mirrors application.status (never the\n// reverse). Defaults reproduce Silver & Salt's pipeline exactly.\nimport type { ChapterPipeline, ResolvedPipeline } from \"./types\";\n\nconst DEFAULT_STAGES = [\n \"submitted\",\n \"paid_pending_vetting\",\n \"call_scheduled\",\n \"interviewed\",\n \"approved\",\n \"declined\",\n \"refunded\",\n] as const;\nconst DEFAULT_BOOKABLE = [\"submitted\", \"paid_pending_vetting\", \"call_scheduled\"] as const;\nconst DEFAULT_APPROVABLE = [\"paid_pending_vetting\", \"call_scheduled\", \"interviewed\"] as const;\n\n/**\n * Apply defaults + validate the pipeline config. With no config, the full Silver\n * & Salt pipeline. With `stages` given but the subsets omitted, the subsets\n * default to empty (a site opts in to bookable/approvable states explicitly).\n * Throws at import on a bad pipeline (empty/duplicate stages, an initial or a\n * subset entry not on the ladder).\n */\nexport function resolvePipeline(p: ChapterPipeline | undefined): ResolvedPipeline {\n const usingDefaults = !p?.stages;\n const stages = p?.stages ?? [...DEFAULT_STAGES];\n if (!Array.isArray(stages) || stages.length === 0 || !stages.every((s) => typeof s === \"string\" && s !== \"\")) {\n throw new Error(\"defineChapter.pipeline.stages: must be a non-empty array of status strings\");\n }\n if (new Set(stages).size !== stages.length) {\n throw new Error(\"defineChapter.pipeline.stages: statuses must be unique\");\n }\n const initial = p?.initial ?? (stages[0] as string);\n if (!stages.includes(initial)) {\n throw new Error(`defineChapter.pipeline.initial: \"${initial}\" is not one of the stages`);\n }\n const bookableFrom = p?.bookableFrom ?? (usingDefaults ? [...DEFAULT_BOOKABLE] : []);\n const approvableFrom = p?.approvableFrom ?? (usingDefaults ? [...DEFAULT_APPROVABLE] : []);\n for (const [name, subset] of [\n [\"bookableFrom\", bookableFrom],\n [\"approvableFrom\", approvableFrom],\n ] as const) {\n for (const s of subset) {\n if (!stages.includes(s)) throw new Error(`defineChapter.pipeline.${name}: \"${s}\" is not one of the stages`);\n }\n }\n return { stages, bookableFrom, approvableFrom, initial };\n}\n\n/** The ordinal of a status in the ladder, or -1 if unknown. */\nexport function stageIndex(status: string, p: ResolvedPipeline): number {\n return p.stages.indexOf(status);\n}\n\n/**\n * The status-never-moves-backwards invariant: a transition is allowed only when\n * both statuses are on the ladder and `to` is at or ahead of `from`. The worker\n * calls this before every status write; a violation is a 409, never a silent\n * downgrade.\n */\nexport function canTransition(from: string, to: string, p: ResolvedPipeline): boolean {\n const fi = p.stages.indexOf(from);\n const ti = p.stages.indexOf(to);\n return fi >= 0 && ti >= 0 && ti >= fi;\n}\n\n/** May an intro call be booked from this status? */\nexport function canBook(status: string, p: ResolvedPipeline): boolean {\n return p.bookableFrom.includes(status);\n}\n\n/** May an application be approved (→ member) from this status? */\nexport function canApprove(status: string, p: ResolvedPipeline): boolean {\n return p.approvableFrom.includes(status);\n}\n","// Payments primitives. The webhook-integrity check below is security-critical and\n// easy to get wrong, so it is pure and tested here; the worker wires a payments\n// provider (Stripe first — subscription create, webhook ingest, refund) around\n// it, and every resulting db write carries an event-derived mutationId for\n// exactly-once. Sites that don't charge omit payments entirely (paymentsReady:\n// false), so nothing here is imported unless a chapter runs the payment flow.\n\n/** Parse a Stripe-style `Stripe-Signature` header (`t=<unix>,v1=<hex>`). */\nfunction parseSigHeader(header: string): { t?: string; v1?: string } {\n const parts: Record<string, string> = {};\n for (const p of header.split(\",\")) {\n const [k, v] = p.split(\"=\", 2);\n if (k && v !== undefined) parts[k] = v;\n }\n return { t: parts.t, v1: parts.v1 };\n}\n\nfunction toHex(buf: ArrayBuffer): string {\n return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** Constant-time compare of two equal-length hex strings. */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\n/**\n * Verify a Stripe webhook signature (C3): HMAC-SHA256 over `` `${t}.${payload}` ``\n * with the endpoint signing secret, a replay window (default 5 minutes), and a\n * constant-time compare. Package-enforced — never left to a site. Returns `false`\n * (never throws) on a malformed header, a non-numeric or stale timestamp, or a\n * signature mismatch. `now`/`toleranceSec` are injectable for tests.\n */\nexport async function verifyStripeSignature(\n payload: string,\n header: string,\n secret: string,\n opts: { now?: number; toleranceSec?: number } = {},\n): Promise<boolean> {\n const { t, v1 } = parseSigHeader(header);\n if (!t || !v1) return false;\n const ts = Number(t);\n if (!Number.isFinite(ts)) return false;\n const nowSec = (opts.now ?? Date.now()) / 1000;\n const tolerance = opts.toleranceSec ?? 300;\n if (Math.abs(nowSec - ts) > tolerance) return false;\n\n const enc = new TextEncoder();\n const key = await crypto.subtle.importKey(\"raw\", enc.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n const mac = await crypto.subtle.sign(\"HMAC\", key, enc.encode(`${t}.${payload}`));\n return timingSafeEqual(toHex(mac), v1);\n}\n\n// ── readiness + Stripe wire helpers ──\n\n/** A group row's payment configuration, as far as readiness cares. */\nexport interface PaymentsGroup {\n stripePublishableKey?: string | null;\n stripePriceId?: string | null;\n}\n\n/** Whether a group can take payment: a publishable key + a price id (both on the\n * group row) AND a secret key (the vault). Anything missing drops the join\n * flow's payment step (paymentsReady:false) rather than half-charging. */\nexport function paymentsReady(group: PaymentsGroup, hasSecretKey: boolean): boolean {\n return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);\n}\n\n/** Form-encode params for Stripe's x-www-form-urlencoded API, expanding one level\n * of nested objects into bracket syntax (`metadata[applicationId]=...`). */\nexport function stripeForm(params: Record<string, unknown>): string {\n const out = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null) continue;\n if (typeof v === \"object\") {\n for (const [k2, v2] of Object.entries(v as Record<string, unknown>)) {\n if (v2 !== undefined && v2 !== null) out.append(`${k}[${k2}]`, String(v2));\n }\n } else {\n out.append(k, String(v));\n }\n }\n return out.toString();\n}\n\n/** The Stripe idempotency key for creating an application's subscription — one\n * per application, so a client retry can't orphan a second subscription (S&S\n * lacked this; the package enforces it). */\nexport function subscriptionIdempotencyKey(applicationId: string): string {\n return `sub:${applicationId}`;\n}\n\n/** The db mutationId for a webhook-driven write — exactly-once per Stripe event,\n * so replays are deduped at the db layer. */\nexport function webhookMutationId(eventId: string): string {\n return `stripe:${eventId}`;\n}\n\n// ── webhook normalization (pure; the worker owns the db lookup + writes) ──\n\n/** A raw Stripe event, as far as normalization cares. */\nexport interface StripeEvent {\n id: string;\n type: string;\n data?: { object?: Record<string, unknown> };\n}\n\n/** A normalized, provider-agnostic webhook event. `kind` drives the db write;\n * the application is resolved from `applicationId` (metadata) or `customerId`. */\nexport type WebhookEvent =\n | { kind: \"first_payment\"; applicationId?: string; customerId?: string; renewalAt?: number }\n | { kind: \"renewal\"; applicationId?: string; customerId?: string; renewalAt?: number }\n | { kind: \"refunded\"; applicationId?: string; customerId?: string }\n | { kind: \"canceled\"; applicationId?: string; customerId?: string }\n | { kind: \"ignored\"; type: string };\n\n/** Resolve the application reference on a Stripe object: `applicationId` from\n * metadata (direct, then subscription_details, then nested\n * parent.subscription_details), plus the customer id for the db fallback. */\nexport function findApplicationRef(obj: Record<string, unknown>): { applicationId?: string; customerId?: string } {\n const metaOf = (v: unknown): Record<string, unknown> =>\n v && typeof v === \"object\" ? ((v as Record<string, unknown>).metadata as Record<string, unknown>) ?? {} : {};\n const pick = (m: Record<string, unknown>): string | undefined =>\n typeof m.applicationId === \"string\" ? m.applicationId : undefined;\n const applicationId =\n pick(metaOf(obj)) ?? pick(metaOf(obj.subscription_details)) ?? pick(metaOf((obj.parent as Record<string, unknown> | undefined)?.subscription_details));\n const customerId = typeof obj.customer === \"string\" ? obj.customer : undefined;\n return { ...(applicationId ? { applicationId } : {}), ...(customerId ? { customerId } : {}) };\n}\n\n/** Normalize a verified Stripe event into a {@link WebhookEvent}. `invoice.paid`\n * splits into first_payment vs renewal by `billing_reason`; refunds and\n * cancellations map directly; everything else is ignored (acked, not retried). */\nexport function normalizeWebhookEvent(event: StripeEvent): WebhookEvent {\n const obj = event.data?.object ?? {};\n const ref = findApplicationRef(obj);\n switch (event.type) {\n case \"invoice.paid\": {\n const lines = ((obj.lines as Record<string, unknown> | undefined)?.data as Array<Record<string, unknown>> | undefined) ?? [];\n const periodEnd = (lines[0]?.period as Record<string, unknown> | undefined)?.end;\n const renewalAt = typeof periodEnd === \"number\" ? periodEnd * 1000 : undefined;\n const kind = obj.billing_reason === \"subscription_create\" ? \"first_payment\" : \"renewal\";\n return { kind, ...ref, ...(renewalAt !== undefined ? { renewalAt } : {}) };\n }\n case \"charge.refunded\":\n return { kind: \"refunded\", ...ref };\n case \"customer.subscription.deleted\":\n return { kind: \"canceled\", ...ref };\n default:\n return { kind: \"ignored\", type: event.type };\n }\n}\n\n// ── webhook write-set builders (the authoritative writers of paid/refunded) ──\n\n/** First-payment patch: advance submitted→paid_pending_vetting (never any other\n * transition) and record the renewal date. Empty when nothing changed, so the\n * caller can skip the write. */\nexport function firstPaymentPatch(currentStatus: string, renewalAt?: number): { status?: \"paid_pending_vetting\"; renewalAt?: number } {\n return {\n ...(currentStatus === \"submitted\" ? { status: \"paid_pending_vetting\" as const } : {}),\n ...(renewalAt !== undefined ? { renewalAt } : {}),\n };\n}\n\n/** Renewal-invoice patch: just the new renewal date. */\nexport function renewalPatch(renewalAt: number): { renewalAt: number } {\n return { renewalAt };\n}\n\n/** Refund patch — the SOLE writer of status \"refunded\" (the admin refund route\n * issues the Stripe refund but never sets this; the webhook does). */\nexport function refundedPatch(): { status: \"refunded\" } {\n return { status: \"refunded\" };\n}\n\n/** Subscription-cancellation patch. */\nexport function canceledPatch(): { canceled: true } {\n return { canceled: true };\n}\n","// The Stripe payments provider — the only I/O half of payments. Talks to the\n// Stripe REST API over fetch (no SDK), and maps 1:1 onto the pure helpers in\n// ./payments (form encoding, signature verify, event normalization). The worker\n// resolves secrets from the vault per request and constructs this; the routes own\n// every db write so idempotency stays at the db layer.\nimport { normalizeWebhookEvent, stripeForm, verifyStripeSignature } from \"./payments\";\nimport type { StripeEvent, WebhookEvent } from \"./payments\";\n\n/** Inputs to create an application's founding-member subscription. */\nexport interface CreateSubscriptionInput {\n applicationId: string;\n groupId: string;\n email: string;\n name: string;\n priceId: string;\n existingCustomerId?: string;\n}\n\n/** The client secret to confirm card entry, plus the ids to persist. */\nexport interface CreateSubscriptionResult {\n customerId: string;\n subscriptionId: string;\n clientSecret: string;\n}\n\n/** The outcome of a full refund. */\nexport interface RefundResult {\n refundedCents: number | null;\n subscriptionCanceled: boolean;\n}\n\n/** The capabilities the payment routes depend on — Stripe is one impl. */\nexport interface PaymentsProvider {\n createSubscription(input: CreateSubscriptionInput): Promise<CreateSubscriptionResult>;\n ingestWebhook(\n rawBody: string,\n sigHeader: string,\n ): Promise<{ ok: true; eventId: string; event: WebhookEvent } | { ok: false; reason: \"bad_signature\" }>;\n refund(input: { customerId: string; subscriptionId: string }): Promise<RefundResult>;\n}\n\ntype StripeResult = { ok: boolean; status: number; body: Record<string, unknown> };\n\nasync function stripeCall(\n sk: string,\n method: \"GET\" | \"POST\" | \"DELETE\",\n path: string,\n params?: Record<string, unknown>,\n idempotencyKey?: string,\n): Promise<StripeResult> {\n const qs = method === \"GET\" && params ? `?${stripeForm(params)}` : \"\";\n const headers: Record<string, string> = { authorization: `Bearer ${sk}` };\n if (idempotencyKey) headers[\"idempotency-key\"] = idempotencyKey;\n const init: RequestInit = { method, headers };\n if (method === \"POST\" && params) {\n headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n init.body = stripeForm(params);\n }\n const res = await fetch(`https://api.stripe.com${path}${qs}`, init);\n const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;\n return { ok: res.ok, status: res.status, body };\n}\n\nfunction fail(op: string, r: StripeResult): never {\n const err = new Error(`stripe ${op} failed: ${r.status}`) as Error & { code: string };\n err.code = \"stripe_error\";\n throw err;\n}\n\nfunction clientSecretOf(sub: Record<string, unknown>): string | undefined {\n const inv = sub.latest_invoice as Record<string, unknown> | undefined;\n const confirmation = inv?.confirmation_secret as Record<string, unknown> | undefined;\n const intent = inv?.payment_intent as Record<string, unknown> | undefined;\n const secret = confirmation?.client_secret ?? intent?.client_secret;\n return typeof secret === \"string\" ? secret : undefined;\n}\n\nfunction requireSecret(secretKey: string | undefined): string {\n if (!secretKey) {\n const err = new Error(\"stripe secret key missing\") as Error & { code: string };\n err.code = \"not_configured\";\n throw err;\n }\n return secretKey;\n}\n\n/** Build the Stripe provider. `secretKey` powers charging/refunds; `webhookSecret`\n * powers webhook ingest. Each is resolved from the vault per request, so the\n * webhook route can construct an ingest-only provider without the secret key. */\nexport function createStripeProvider(config: { secretKey?: string; webhookSecret?: string }): PaymentsProvider {\n const { secretKey, webhookSecret } = config;\n return {\n async createSubscription(input) {\n const sk = requireSecret(secretKey);\n const meta = { applicationId: input.applicationId, groupId: input.groupId, email: input.email };\n let customerId = input.existingCustomerId;\n if (!customerId) {\n const cust = await stripeCall(\n sk,\n \"POST\",\n \"/v1/customers\",\n { email: input.email, name: input.name, metadata: meta },\n `cus:${input.applicationId}`,\n );\n if (!cust.ok) fail(\"customer create\", cust);\n customerId = String(cust.body.id);\n }\n const sub = await stripeCall(\n sk,\n \"POST\",\n \"/v1/subscriptions\",\n {\n customer: customerId,\n \"items[0][price]\": input.priceId,\n payment_behavior: \"default_incomplete\",\n \"payment_settings[save_default_payment_method]\": \"on_subscription\",\n \"payment_settings[payment_method_types][0]\": \"card\",\n \"expand[0]\": \"latest_invoice.confirmation_secret\",\n metadata: meta,\n },\n // The hardening S&S lacked: one subscription per application, so a client\n // retry between create and the db write can't orphan a second one.\n `sub:${input.applicationId}`,\n );\n if (!sub.ok) fail(\"subscription create\", sub);\n const clientSecret = clientSecretOf(sub.body);\n if (!clientSecret) fail(\"subscription confirmation-secret missing\", sub);\n return { customerId, subscriptionId: String(sub.body.id), clientSecret };\n },\n\n async ingestWebhook(rawBody, sigHeader) {\n if (!webhookSecret || !(await verifyStripeSignature(rawBody, sigHeader, webhookSecret))) {\n return { ok: false, reason: \"bad_signature\" };\n }\n const event = JSON.parse(rawBody) as StripeEvent;\n return { ok: true, eventId: event.id, event: normalizeWebhookEvent(event) };\n },\n\n async refund(input) {\n const sk = requireSecret(secretKey);\n const charges = await stripeCall(sk, \"GET\", \"/v1/charges\", { customer: input.customerId, limit: 100 });\n if (!charges.ok) fail(\"charges list\", charges);\n const rows = (charges.body.data as Array<Record<string, unknown>> | undefined) ?? [];\n const paid = rows.filter((c) => c.status === \"succeeded\" && c.refunded !== true);\n const charge = paid[paid.length - 1]; // Stripe returns newest-first; refund the earliest.\n if (!charge) {\n const err = new Error(\"no paid charge to refund\") as Error & { code: string };\n err.code = \"no_charge\";\n throw err;\n }\n const refund = await stripeCall(sk, \"POST\", \"/v1/refunds\", { charge: String(charge.id) });\n if (!refund.ok) fail(\"refund\", refund);\n const cancel = await stripeCall(sk, \"DELETE\", `/v1/subscriptions/${input.subscriptionId}`);\n const amount = refund.body.amount;\n return { refundedCents: typeof amount === \"number\" ? amount : null, subscriptionCanceled: cancel.ok };\n },\n };\n}\n","// Payment routes: POST /api/payments/subscription (start a founding-member\n// subscription), POST /api/webhooks/stripe (the AUTHORITATIVE writer of\n// paid/refunded/canceled, exactly-once per event id), and POST\n// /api/admin/applications/:id/refund (admin-gated; issues the refund but never\n// writes status — the charge.refunded webhook does). I/O only; the correctness\n// rules live in ./payments and the provider in ./payments-stripe.\nimport { getVaultSecret } from \"./auth\";\nimport { emailGroupFrom, sendTemplated } from \"./notify\";\nimport { canceledPatch, firstPaymentPatch, refundedPatch, renewalPatch, webhookMutationId } from \"./payments\";\nimport type { WebhookEvent } from \"./payments\";\nimport { createStripeProvider } from \"./payments-stripe\";\nimport { json } from \"./worker-context\";\nimport type { ChapterEnv, WorkerContext } from \"./worker-context\";\nimport type { Route } from \"./worker-routes\";\nimport type { ChapterDb } from \"./types\";\n\ntype Row = Record<string, unknown>;\n\nconst codeOf = (err: unknown): string =>\n err && typeof err === \"object\" && typeof (err as { code?: unknown }).code === \"string\" ? (err as { code: string }).code : \"unknown\";\n\nasync function firstRow(db: ChapterDb, ns: string, q: Record<string, unknown>): Promise<Row | undefined> {\n const res = await db.query({ [ns]: { $: q } });\n const rows = res[ns];\n return Array.isArray(rows) ? rows[0] : undefined;\n}\n\nfunction lineItems(group: Row): { standardCents: number; discountCents: number; dueTodayCents: number } {\n const standard = Number(group.standardPriceCents ?? 0);\n const discount = Number(group.foundingDiscountCents ?? 0);\n return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };\n}\n\n// Resolve the application an event targets: by metadata applicationId, else the\n// newest row for the Stripe customer.\nasync function findApplication(db: ChapterDb, event: WebhookEvent): Promise<Row | undefined> {\n if (\"applicationId\" in event && event.applicationId) {\n return firstRow(db, \"applications\", { where: { id: event.applicationId }, limit: 1 });\n }\n if (\"customerId\" in event && event.customerId) {\n return firstRow(db, \"applications\", { where: { stripeCustomerId: event.customerId }, order: { createdAt: \"desc\" }, limit: 1 });\n }\n return undefined;\n}\n\n// Best-effort payment-confirmation to the applicant on the first successful\n// invoice. Exactly-once per Stripe event via the dedupeKey.\nasync function notifyPaymentConfirmed(db: ChapterDb, env: ChapterEnv, eventId: string, app: Row): Promise<void> {\n try {\n if (typeof app.email !== \"string\" || !app.email) return;\n const group = await firstRow(db, \"groups\", { where: { id: String(app.groupId ?? \"\") }, limit: 1 });\n if (!group) return;\n await sendTemplated(\n { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },\n {\n group: emailGroupFrom(group),\n template: \"paymentConfirmation\",\n to: app.email,\n vars: { firstName: typeof app.firstName === \"string\" ? app.firstName : \"\" },\n dedupeKey: `${eventId}:confirm`,\n applicationId: String(app.id),\n },\n );\n } catch {\n // never let a confirmation-email failure affect the webhook 200\n }\n}\n\n// The application-row patch for a resolved event (empty = skip the write).\nfunction webhookPatch(event: WebhookEvent, status: string): Record<string, unknown> {\n switch (event.kind) {\n case \"first_payment\":\n return firstPaymentPatch(status, event.renewalAt);\n case \"renewal\":\n return event.renewalAt !== undefined ? renewalPatch(event.renewalAt) : {};\n case \"refunded\":\n return refundedPatch();\n case \"canceled\":\n return canceledPatch();\n default:\n return {};\n }\n}\n\nasync function startSubscription(req: Request, env: ChapterEnv, ctx: WorkerContext): Promise<Response> {\n let body: Row;\n try {\n body = JSON.parse(await req.text()) as Row;\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n const applicationId = typeof body.applicationId === \"string\" ? body.applicationId : \"\";\n if (!applicationId) return json({ error: \"applicationId required\" }, 400);\n if (body.refundPolicyAck !== true) return json({ error: \"refundPolicyAck required\" }, 400);\n\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const app = await firstRow(db, \"applications\", { where: { id: applicationId }, limit: 1 });\n if (!app) return json({ error: \"not found\" }, 404);\n // Single-charge guard: only from the initial state, so a re-post can't double-subscribe.\n if (app.status !== \"submitted\") return json({ error: \"already processed\" }, 409);\n\n const group = await firstRow(db, \"groups\", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });\n const priceId = group?.stripePriceId;\n const secretKey = await getVaultSecret(db, \"stripe_secret_key\");\n if (!group || !priceId || !secretKey) return json({ error: \"payments not configured\" }, 503);\n\n const provider = createStripeProvider({ secretKey });\n let result;\n try {\n result = await provider.createSubscription({\n applicationId,\n groupId: String(group.id),\n email: String(app.email ?? \"\"),\n name: `${app.firstName ?? \"\"} ${app.lastName ?? \"\"}`.trim(),\n priceId: String(priceId),\n existingCustomerId: typeof app.stripeCustomerId === \"string\" ? app.stripeCustomerId : undefined,\n });\n } catch (err) {\n return json({ error: \"payment setup failed\", code: codeOf(err) }, 502);\n }\n\n await db.transact([\n {\n t: \"update\",\n ns: \"applications\",\n id: applicationId,\n attrs: { stripeCustomerId: result.customerId, stripeSubscriptionId: result.subscriptionId, refundPolicyAckAt: Date.now() },\n },\n ]);\n return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: lineItems(group) });\n}\n\nasync function ingestWebhook(req: Request, env: ChapterEnv, ctx: WorkerContext): Promise<Response> {\n const db = ctx.makeDb(env) as unknown as ChapterDb;\n const webhookSecret = await getVaultSecret(db, \"stripe_webhook_secret\");\n if (!webhookSecret) return json({ error: \"webhook not configured\" }, 503);\n\n const rawBody = await req.text();\n const ingest = await createStripeProvider({ webhookSecret }).ingestWebhook(rawBody, req.headers.get(\"stripe-signature\") ?? \"\");\n if (!ingest.ok) return json({ error: \"invalid signature\" }, 400);\n const { eventId, event } = ingest;\n if (event.kind === \"ignored\") return json({ ok: true, ignored: event.type });\n\n const app = await findApplication(db, event);\n if (!app) return json({ ok: true, matched: false });\n\n const patch = webhookPatch(event, String(app.status ?? \"\"));\n if (Object.keys(patch).length) {\n await db.transact([{ t: \"update\", ns: \"applications\", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });\n }\n if (event.kind === \"first_payment\") await notifyPaymentConfirmed(db, env, eventId, app);\n return json({ ok: true });\n}\n\nasync function refundApplication(req: Request, url: URL, env: ChapterEnv, ctx: WorkerContext): Promise<Response> {\n const rawDb = ctx.makeDb(env);\n const u = await ctx.verifyUser(req, env);\n if (!u || !(await ctx.isAdmin(rawDb, u))) return json({ error: \"forbidden\" }, 403);\n\n const id = url.pathname.split(\"/\")[4] ?? \"\";\n const db = rawDb as unknown as ChapterDb;\n const app = await firstRow(db, \"applications\", { where: { id }, limit: 1 });\n if (!app) return json({ error: \"not found\" }, 404);\n if (app.status === \"refunded\") return json({ error: \"already refunded\" }, 409);\n if (app.status === \"approved\") return json({ error: \"approved memberships are non-refundable\" }, 409);\n if (!app.stripeSubscriptionId) return json({ error: \"no subscription on file\" }, 409);\n if (!app.stripeCustomerId) return json({ error: \"no customer on file\" }, 409);\n\n const secretKey = await getVaultSecret(db, \"stripe_secret_key\");\n if (!secretKey) return json({ error: \"payments not configured\" }, 503);\n\n try {\n const result = await createStripeProvider({ secretKey }).refund({\n customerId: String(app.stripeCustomerId),\n subscriptionId: String(app.stripeSubscriptionId),\n });\n // Note: status \"refunded\" is NOT written here — the charge.refunded webhook is\n // the single writer, keeping Stripe the source of truth.\n return json({ ok: true, refundedCents: result.refundedCents, subscriptionCanceled: result.subscriptionCanceled });\n } catch (err) {\n const code = codeOf(err);\n return json({ error: \"refund failed\", code }, code === \"no_charge\" ? 409 : 502);\n }\n}\n\nconst REFUND_PATH = /^\\/api\\/admin\\/applications\\/[^/]+\\/refund$/;\n\n/** The payment routes (chapter mode). Webhook + subscription are public\n * (capability-guarded by the unguessable application id / the signed payload);\n * refund is admin-gated. */\nexport const handlePayments: Route = async (req, url, env, ctx) => {\n if (ctx.chapter.mode !== \"chapter\") return null;\n if (req.method === \"POST\" && url.pathname === \"/api/payments/subscription\") return startSubscription(req, env, ctx);\n if (req.method === \"POST\" && url.pathname === \"/api/webhooks/stripe\") return ingestWebhook(req, env, ctx);\n if (req.method === \"POST\" && REFUND_PATH.test(url.pathname)) return refundApplication(req, url, env, ctx);\n return null;\n};\n","// Admin operational routes. GET /api/admin/meetings is the admin agenda,\n// reconciled against the live platform calendar: it reads upcoming events through\n// @odla-ai/calendar (chapter never calls Google directly) and adopts any\n// owner-side move/cancel onto the canonical meetings row + the application\n// projection, via the pure ./reconcile logic. Admin-gated; the read still\n// succeeds (serving canonical rows) if the calendar is unavailable.\nimport { initCalendar } from \"@odla-ai/calendar\";\nimport { reconcileMeetings } from \"./reconcile\";\nimport type { LiveEvent, MeetingForReconcile, ReconcileDecision } from \"./reconcile\";\nimport { resolveScheduling } from \"./scheduling\";\nimport { json } from \"./worker-context\";\nimport type { ChapterEnv, WorkerContext } from \"./worker-context\";\nimport type { Route } from \"./worker-routes\";\nimport type { ChapterDb, ChapterScheduling, DbOp } from \"./types\";\n\n// Load the admin's target group (query param `group`, else the chapter), gated on\n// an admin session. Returns [db, group] or a Response to short-circuit.\nasync function adminGroup(\n req: Request,\n env: ChapterEnv,\n ctx: WorkerContext,\n url: URL,\n): Promise<{ db: ChapterDb; group: Record<string, unknown> } | Response> {\n const rawDb = ctx.makeDb(env);\n const u = await ctx.verifyUser(req, env);\n if (!u || !(await ctx.isAdmin(rawDb, u))) return json({ error: \"forbidden\" }, 403);\n const db = rawDb as unknown as ChapterDb;\n const groupId = url.searchParams.get(\"group\") ?? ctx.chapter.id;\n const group = (await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } })).groups?.[0];\n if (!group) return json({ error: \"not found\" }, 404);\n return { db, group };\n}\n\n/** GET/PUT /api/admin/scheduling — read the group's booking rules, or replace\n * them (validated via resolveScheduling; a bad config is a 400, never persisted).\n * The backend for the availability-editor section. */\nexport const handleAdminScheduling: Route = async (req, url, env, ctx) => {\n if (url.pathname !== \"/api/admin/scheduling\" || (req.method !== \"GET\" && req.method !== \"PUT\")) return null;\n const got = await adminGroup(req, env, ctx, url);\n if (got instanceof Response) return got;\n const { db, group } = got;\n\n if (req.method === \"GET\") {\n return json({ scheduling: resolveScheduling(group.schedulingJson as ChapterScheduling | undefined) });\n }\n let body: unknown;\n try {\n body = JSON.parse(await req.text());\n } catch {\n return json({ error: \"invalid JSON body\" }, 400);\n }\n let resolved;\n try {\n resolved = resolveScheduling(body as ChapterScheduling);\n } catch (e) {\n return json({ error: e instanceof Error ? e.message : \"invalid scheduling config\" }, 400);\n }\n await db.transact([{ t: \"update\", ns: \"groups\", id: String(group.id), attrs: { schedulingJson: resolved } }]);\n return json({ scheduling: resolved });\n};\n\nasync function upcomingEvents(env: ChapterEnv): Promise<LiveEvent[]> {\n const cal = initCalendar({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });\n const res = await cal.availability.upcoming();\n return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));\n}\n\nfunction toReconcile(rows: Array<Record<string, unknown>>): MeetingForReconcile[] {\n return rows.map((m) => ({\n id: String(m.id),\n applicationId: String(m.applicationId),\n googleEventId: typeof m.googleEventId === \"string\" ? m.googleEventId : null,\n status: String(m.status ?? \"\"),\n startAt: typeof m.startAt === \"number\" ? m.startAt : null,\n endAt: typeof m.endAt === \"number\" ? m.endAt : null,\n }));\n}\n\n/** GET /api/admin/meetings — the scheduled agenda, reconciled against the live\n * calendar (adopts owner moves/cancellations onto the canonical rows). */\nexport const handleAdminMeetings: Route = async (req, url, env, ctx) => {\n if (req.method !== \"GET\" || url.pathname !== \"/api/admin/meetings\") return null;\n const rawDb = ctx.makeDb(env);\n const u = await ctx.verifyUser(req, env);\n if (!u || !(await ctx.isAdmin(rawDb, u))) return json({ error: \"forbidden\" }, 403);\n const db = rawDb as unknown as ChapterDb;\n\n const query = await db.query({ meetings: { $: { where: { status: \"scheduled\" }, order: { startAt: \"asc\" }, limit: 500 } } });\n const rows = (Array.isArray(query.meetings) ? query.meetings : []) as Array<Record<string, unknown>>;\n\n let decisions: ReconcileDecision[] = [];\n try {\n decisions = reconcileMeetings(toReconcile(rows), await upcomingEvents(env), Date.now());\n } catch {\n // Calendar unavailable — serve the canonical rows without adopting edits.\n }\n\n // Apply adopt decisions (best-effort; the read still succeeds if a write fails).\n const patched = new Map<string, Record<string, unknown>>();\n for (const d of decisions) {\n const ops: DbOp[] = [\n { t: \"update\", ns: \"meetings\", id: d.meetingId, attrs: d.meetingPatch },\n { t: \"update\", ns: \"applications\", id: d.applicationId, attrs: d.applicationPatch },\n ];\n try {\n await db.transact(ops);\n patched.set(d.meetingId, d.meetingPatch);\n } catch {\n // leave the row as-is if the write fails\n }\n }\n\n // Reflect adopted patches into the response; drop meetings adopted as cancelled.\n const meetings = rows\n .map((m) => ({ ...m, ...(patched.get(String(m.id)) ?? {}) }))\n .filter((m) => m.status === \"scheduled\");\n return json({ meetings, adopted: decisions.length });\n};\n","// The Google → chapter half of the bidirectional calendar sync. Booking writes\n// events TO the platform calendar (0.4.0's create/reschedule); this ADOPTS edits\n// FROM it: when the owner moves or cancels the intro call directly in Google,\n// reconcile mirrors that onto our canonical `meetings` row and the `application`\n// projection. Chapter never calls Google — the worker reads live events through\n// @odla-ai/calendar's upcoming() (platform-brokered) and applies the ops this\n// returns. Pure + testable; adoption policy (owner edits win) matches the shipped\n// Silver & Salt worker.\n\n/** A `meetings` row, as far as reconciliation cares. */\nexport interface MeetingForReconcile {\n id: string;\n applicationId: string;\n googleEventId?: string | null;\n status: string;\n startAt?: number | null;\n endAt?: number | null;\n}\n\n/** One live calendar event (a subset of @odla-ai/calendar's Booking). */\nexport interface LiveEvent {\n eventId: string;\n status?: string;\n startAt?: number;\n endAt?: number;\n}\n\n/** An adopt decision: mirror a Google move/cancel onto our rows. */\nexport interface ReconcileDecision {\n meetingId: string;\n applicationId: string;\n kind: \"cancelled\" | \"moved\";\n /** Attrs to write onto the `meetings` row. */\n meetingPatch: Record<string, unknown>;\n /** Attrs to write onto the `applications` row (the projection). */\n applicationPatch: Record<string, unknown>;\n}\n\n/** Meetings still worth reconciling: a scheduled booking with a Google event that\n * starts in the future (or within the last hour, to catch a just-passed edit). */\nexport function isReconcilable(meeting: MeetingForReconcile, now: number): boolean {\n return meeting.status === \"scheduled\" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 3_600_000;\n}\n\n/**\n * Diff canonical `meetings` against the live calendar events and return the adopt\n * decisions — only for meetings that actually changed (a still-matching meeting\n * is omitted). A meeting whose event vanished or is `cancelled` in Google is\n * adopted as cancelled (application `meetingAt` zeroed — 0 means \"was booked,\n * then cancelled\"); a meeting whose event moved adopts the new window (duration\n * preserved when the event omits `endAt`).\n */\nexport function reconcileMeetings(\n meetings: readonly MeetingForReconcile[],\n events: readonly LiveEvent[],\n now: number,\n): ReconcileDecision[] {\n const byEvent = new Map(events.map((e) => [e.eventId, e]));\n const decisions: ReconcileDecision[] = [];\n for (const m of meetings) {\n if (!isReconcilable(m, now) || !m.googleEventId) continue;\n const g = byEvent.get(m.googleEventId);\n if (!g || g.status === \"cancelled\") {\n decisions.push({\n meetingId: m.id,\n applicationId: m.applicationId,\n kind: \"cancelled\",\n meetingPatch: { status: \"cancelled\", drift: \"none\", adoptedFromGoogleAt: now },\n applicationPatch: { meetingAt: 0, meetingLink: \"\" },\n });\n } else if (g.startAt !== undefined && g.startAt !== m.startAt) {\n const duration = (m.endAt ?? 0) - (m.startAt ?? 0);\n decisions.push({\n meetingId: m.id,\n applicationId: m.applicationId,\n kind: \"moved\",\n meetingPatch: { startAt: g.startAt, endAt: g.endAt ?? g.startAt + duration, drift: \"none\", adoptedFromGoogleAt: now },\n applicationPatch: { meetingAt: g.startAt },\n });\n }\n }\n return decisions;\n}\n"],"mappings":"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","names":["import_crm","fail","firstRow","import_calendar"]}
|