@augmenting-integrations/auth 8.0.4 → 8.0.5
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/dist/server/createAuth.d.ts.map +1 -1
- package/dist/server/tenant.d.ts.map +1 -1
- package/dist/server.cjs +2 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -3
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createAuth.d.ts","sourceRoot":"","sources":["../../src/server/createAuth.ts"],"names":[],"mappings":"AAkBA,OAAiB,EACf,KAAK,cAAc,EAEnB,KAAK,OAAO,EACb,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAM7D,OAAO,QAAQ,WAAW,CAAC;IACzB,UAAU,OAAO;QACf,IAAI,EAAE;YACJ,MAAM,EAAE,MAAM,EAAE,CAAC;SAClB,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;KAC5B;IACD,UAAU,IAAI;QACZ,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB;CACF;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;OAIG;IACH,MAAM,EAAE,kBAAkB,CAAC;IAC3B,2DAA2D;IAC3D,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAIF,qBAAa,SAAU,SAAQ,KAAK;IACf,IAAI,EAAE,iBAAiB,GAAG,WAAW;gBAArC,IAAI,EAAE,iBAAiB,GAAG,WAAW;CAIzD;AAID,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAE3E;AAED,+CAA+C;AAC/C,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAInF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,EACnC,GAAG,KAAK,EAAE,MAAM,EAAE,GACjB,IAAI,CAKN;AA+BD,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"createAuth.d.ts","sourceRoot":"","sources":["../../src/server/createAuth.ts"],"names":[],"mappings":"AAkBA,OAAiB,EACf,KAAK,cAAc,EAEnB,KAAK,OAAO,EACb,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAM7D,OAAO,QAAQ,WAAW,CAAC;IACzB,UAAU,OAAO;QACf,IAAI,EAAE;YACJ,MAAM,EAAE,MAAM,EAAE,CAAC;SAClB,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;KAC5B;IACD,UAAU,IAAI;QACZ,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB;CACF;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;OAIG;IACH,MAAM,EAAE,kBAAkB,CAAC;IAC3B,2DAA2D;IAC3D,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAIF,qBAAa,SAAU,SAAQ,KAAK;IACf,IAAI,EAAE,iBAAiB,GAAG,WAAW;gBAArC,IAAI,EAAE,iBAAiB,GAAG,WAAW;CAIzD;AAID,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAE3E;AAED,+CAA+C;AAC/C,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAInF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,EACnC,GAAG,KAAK,EAAE,MAAM,EAAE,GACjB,IAAI,CAKN;AA+BD,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,sCAuIjD;AAED,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/server/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AACrB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAoB5B,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACzC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/server/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AACrB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAoB5B,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACzC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,kBAAkB,CAqEtE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CAU3E;AAeD,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,kBAAkB,CAAA;CAAE,wEAM1E;AAED,OAAO,EACL,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,UAAU,GAChB,MAAM,oBAAoB,CAAC"}
|
package/dist/server.cjs
CHANGED
|
@@ -94,7 +94,7 @@ function createAuth(opts) {
|
|
|
94
94
|
const { tenant, authSecret, cognitoClientSecret } = opts;
|
|
95
95
|
const isProd = opts.isProd ?? process.env.NODE_ENV === "production";
|
|
96
96
|
const cookieDomain = isProd ? tenant.cookieDomain : void 0;
|
|
97
|
-
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build"
|
|
97
|
+
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
|
|
98
98
|
if (tenant.role === "apex" && !isBuildPhase) {
|
|
99
99
|
if (!tenant.cognitoClientId || !tenant.cognitoIssuer) {
|
|
100
100
|
throw new Error(
|
|
@@ -394,8 +394,7 @@ function loadTenantConfig(opts) {
|
|
|
394
394
|
} else {
|
|
395
395
|
required.push({ key: "appSlug", env: "APP_SLUG" });
|
|
396
396
|
}
|
|
397
|
-
|
|
398
|
-
if (isBuildPhase) {
|
|
397
|
+
if (process.env.NEXT_PHASE === "phase-production-build") {
|
|
399
398
|
return draft;
|
|
400
399
|
}
|
|
401
400
|
const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);
|
package/dist/server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/server/createAuth.ts","../src/server/jit.ts","../src/server/impersonation.ts","../src/server/tenant.ts","../src/tenant-types.ts"],"sourcesContent":["export {\n createAuth,\n type CreateAuthOptions,\n type NextAuthConfig,\n AuthError,\n getUserGroups,\n hasGroup,\n requireGroup,\n} from \"./server/createAuth.js\";\nexport {\n createGetOrCreateAppUser,\n type BaseAppUser,\n type AppUserWithImpersonation,\n type CreateGetOrCreateAppUserOptions,\n type PrismaLikeClient,\n type PrismaLikeUserDelegate,\n type PrismaLikeInvitationDelegate,\n} from \"./server/jit.js\";\nexport {\n mintImpersonationToken,\n verifyImpersonationToken,\n IMPERSONATE_COOKIE_NAME,\n IMPERSONATE_TTL_SECONDS,\n type ImpersonationClaims,\n} from \"./server/impersonation.js\";\nexport {\n loadTenantConfig,\n publicSubset,\n TenantBootScript,\n TENANT_GLOBAL_KEY,\n type LoadOptions,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"./server/tenant.js\";\n","// Auth.js v5 (the package is still distributed as `next-auth`, but treat\n// these as Auth.js v5 internally — docs at https://authjs.dev, NOT\n// next-auth.js.org which is v4 and incompatible).\n//\n// Subdomain ecosystem model:\n// - One Cognito User Pool per tenant.\n// - One Cognito App Client with ONE callback URL at the apex\n// (https://<apex>/api/auth/callback/cognito).\n// - The apex app is the auth broker. Subdomain apps redirect through it.\n// - Session cookie scoped to Domain=.<apex> so every subdomain sees it.\n// - All apps use the same createAuth() invocation; the package derives\n// the right signInPage from appDomain + allowedParentDomain.\n//\n// Provider strategy:\n// - Production: Cognito OIDC. cognito:groups drives session.user.groups.\n// - Dev / preview: Credentials with a role picker, shaped to mirror\n// Cognito's claim payload (same groups, sub, email).\n\nimport NextAuth, {\n type DefaultSession,\n type NextAuthConfig,\n type Session,\n} from \"next-auth\";\nimport Credentials from \"next-auth/providers/credentials\";\nimport Cognito from \"next-auth/providers/cognito\";\nimport type { TenantServerConfig } from \"../tenant-types.js\";\n\n// Session augmentation: `groups` is the Cognito-groups array surfaced from\n// the JWT. `role` lives on the application User row (Prisma), exposed via\n// `useAppUser()` from `@augmenting-integrations/auth/client`. There is one\n// source of role-truth; do not add a session-level `role` field back here.\ndeclare module \"next-auth\" {\n interface Session {\n user: {\n groups: string[];\n } & DefaultSession[\"user\"];\n }\n interface User {\n groups?: string[];\n }\n}\n\nexport type CreateAuthOptions = {\n /**\n * Full tenant configuration. Provides apex/cookieDomain/parentDomain/\n * appDomain/role + cognito client id + issuer + allowed admin emails.\n * Load via `loadTenantConfig()` from `@augmenting-integrations/tenant/server`.\n */\n tenant: TenantServerConfig;\n /** Path prefixes that require an authenticated session. */\n authedRoutePrefixes: string[];\n /**\n * JWT signing secret. Caller fetches from Secrets Manager via\n * `getSecret(tenant.authSecretArn)` and passes the resolved value here.\n * The library doesn't bundle AWS SDK reads itself.\n */\n authSecret: string;\n /**\n * Cognito OAuth client secret. Apex apps only -- spokes never run the\n * OAuth dance. Caller fetches from Secrets Manager via\n * `getSecret(tenant.authCognitoSecretArn)`.\n */\n cognitoClientSecret?: string;\n /**\n * Override the auto-derived sign-in page (rarely needed). Default:\n * apex apps get `/login`; spoke apps get `https://<apex>/login`.\n */\n signInPage?: string;\n /** Override prod/dev detection. Default reads NODE_ENV. */\n isProd?: boolean;\n};\n\n// ----- AuthError used by requireGroup -----\n\nexport class AuthError extends Error {\n constructor(public code: \"unauthenticated\" | \"forbidden\") {\n super(code);\n this.name = \"AuthError\";\n }\n}\n\n// ----- Group/authorization helpers -----\n\n/** Returns the user's Cognito groups (always an array, possibly empty). */\nexport function getUserGroups(session: Session | null | undefined): string[] {\n return session?.user?.groups ?? [];\n}\n\n/** Case-insensitive group membership check. */\nexport function hasGroup(session: Session | null | undefined, name: string): boolean {\n if (!session) return false;\n const target = name.toLowerCase();\n return getUserGroups(session).some((g) => g.toLowerCase() === target);\n}\n\n/**\n * Throws AuthError if no session (`unauthenticated`) or if the user is in\n * none of the provided groups (`forbidden`). Pass multiple names to allow\n * any-of.\n */\nexport function requireGroup(\n session: Session | null | undefined,\n ...names: string[]\n): void {\n if (!session) throw new AuthError(\"unauthenticated\");\n if (names.length === 0) return;\n const ok = names.some((n) => hasGroup(session, n));\n if (!ok) throw new AuthError(\"forbidden\");\n}\n\n// ----- Redirect callback factory -----\n\nfunction buildRedirectCallback(parentDomain: string) {\n return ({ url, baseUrl }: { url: string; baseUrl: string }): string => {\n try {\n const target = new URL(url, baseUrl);\n const apex = parentDomain.replace(/^\\./, \"\").toLowerCase();\n const host = target.hostname.toLowerCase();\n const ok = host === apex || host.endsWith(`.${apex}`);\n return ok ? target.toString() : baseUrl;\n } catch {\n return baseUrl;\n }\n };\n}\n\n// ----- Sign-in page auto-derivation -----\n\nfunction deriveSignInPage(args: {\n signInPage: string | undefined;\n appDomain: string;\n apex: string;\n}): string {\n if (args.signInPage) return args.signInPage;\n return args.appDomain === args.apex ? \"/login\" : `https://${args.apex}/login`;\n}\n\n// ----- Main factory -----\n\nexport function createAuth(opts: CreateAuthOptions) {\n const { tenant, authSecret, cognitoClientSecret } = opts;\n const isProd = opts.isProd ?? process.env.NODE_ENV === \"production\";\n const cookieDomain = isProd ? tenant.cookieDomain : undefined;\n\n // Skip runtime assertions during next build (env-less, evaluates\n // module-init code but never serves requests).\n const isBuildPhase =\n process.env.NEXT_PHASE === \"phase-production-build\" ||\n (!process.env.AWS_LAMBDA_FUNCTION_NAME && process.env.NODE_ENV === \"production\");\n\n if (tenant.role === \"apex\" && !isBuildPhase) {\n if (!tenant.cognitoClientId || !tenant.cognitoIssuer) {\n throw new Error(\n \"createAuth: tenant.role='apex' requires cognitoClientId + cognitoIssuer. Check loadTenantConfig() inputs.\",\n );\n }\n if (isProd && !cognitoClientSecret) {\n throw new Error(\n \"createAuth: tenant.role='apex' in production requires cognitoClientSecret. Fetch via getSecret(tenant.authCognitoSecretArn) and pass it in.\",\n );\n }\n }\n\n const signInPage = deriveSignInPage({\n signInPage: opts.signInPage,\n appDomain: tenant.appDomain,\n apex: tenant.apex,\n });\n\n const config: NextAuthConfig = {\n secret: authSecret,\n cookies: cookieDomain\n ? {\n sessionToken: {\n name: \"authjs.session-token\",\n options: {\n domain: cookieDomain,\n sameSite: \"lax\",\n secure: true,\n httpOnly: true,\n path: \"/\",\n },\n },\n }\n : undefined,\n providers: isProd\n ? [\n Cognito({\n clientId: tenant.cognitoClientId,\n clientSecret: cognitoClientSecret,\n issuer: tenant.cognitoIssuer,\n }),\n ]\n : [\n Credentials({\n name: \"Mock role (dev only)\",\n credentials: {\n role: {\n label: \"Role\",\n type: \"text\",\n placeholder: \"any role string\",\n },\n },\n authorize: async (credentials) => {\n const role = credentials?.role as string | undefined;\n if (!role) return null;\n const display = role.charAt(0).toUpperCase() + role.slice(1);\n return {\n id: `mock-${role}`,\n name: `${display} (mock)`,\n email: `${role}@example.local`,\n role,\n groups: [role],\n };\n },\n }),\n ],\n session: { strategy: \"jwt\" },\n callbacks: {\n jwt: ({ token, user, profile }) => {\n if (user) {\n token.sub ??= user.id ?? undefined;\n token.email ??= user.email ?? undefined;\n if (!isProd) {\n const u = user as { groups?: string[]; role?: string };\n const groups = u.groups ?? (u.role ? [u.role] : []);\n if (groups.length > 0) {\n (token as Record<string, unknown>)[\"cognito:groups\"] = groups;\n }\n }\n }\n if (isProd && profile) {\n const groups = (profile as Record<string, unknown>)[\"cognito:groups\"];\n if (groups) {\n (token as Record<string, unknown>)[\"cognito:groups\"] = groups;\n }\n }\n return token;\n },\n session: ({ session, token }) => {\n const groups =\n ((token as Record<string, unknown>)[\"cognito:groups\"] as\n | string[]\n | undefined) ?? [];\n session.user.groups = groups;\n return session;\n },\n authorized: ({ auth: session, request: { nextUrl } }) => {\n const path = nextUrl.pathname;\n const isAuthedRoute = opts.authedRoutePrefixes.some(\n (prefix) => path === prefix || path.startsWith(`${prefix}/`),\n );\n if (!session && isAuthedRoute) {\n // For subdomain apps signInPage is an absolute URL on the apex\n // broker. Auth.js's default middleware redirect treats\n // pages.signIn as a relative path and prepends the current\n // host, producing malformed Location URLs like\n // https://sub.<apex>/https://<apex>/login. Returning an\n // explicit Response.redirect bypasses that path and sends the\n // user to the apex broker correctly.\n if (signInPage.startsWith(\"http\")) {\n const target = new URL(signInPage);\n target.searchParams.set(\"callbackUrl\", nextUrl.href);\n return Response.redirect(target.toString(), 302);\n }\n return false;\n }\n return true;\n },\n redirect: buildRedirectCallback(tenant.parentDomain),\n },\n pages: { signIn: signInPage },\n trustHost: true,\n };\n\n return NextAuth(config);\n}\n\nexport type { NextAuthConfig } from \"next-auth\";\n","import \"server-only\";\n\nimport { cookies } from \"next/headers\";\nimport type { Session } from \"next-auth\";\n\nimport { IMPERSONATE_COOKIE_NAME, verifyImpersonationToken } from \"./impersonation.js\";\n\n// =============================================================================\n// JIT user provisioning factory.\n//\n// Pattern: every authed request hands a session into getOrCreateAppUser() to\n// resolve the DB User row (creating one on first sign-in for that email).\n// The factory pattern lets each spoke configure:\n//\n// - `db`: how to reach Prisma (the library doesn't bundle the client)\n// - `defaultRole`: fallback when Cognito groups + ADMIN_EMAILS don't decide\n// - `computeCreditBalance(role)`: starting credit balance per role\n// - `adminEmails`: CSV of emails auto-promoted to admin on first sign-in\n// - `placeholderPasswordHash`: schema-inherited not-null constraint filler\n//\n// Impersonation short-circuit (runs BEFORE the session-driven lookup): if\n// `__impersonate` cookie is present and verifies against AUTH_SECRET, and the\n// underlying admin still exists with role==='admin', returns the *target* user\n// with `impersonatedBy` set to the admin's stringified id. Orphaned tokens\n// silently fall through to the session user.\n//\n// Invitation auto-accept: if a pending Invitation row exists for this email\n// (accepted_at IS NULL, expires_at > now), the new User inherits the\n// invitation's parent_id and intended_role and the invitation is marked\n// accepted in the same transaction.\n// =============================================================================\n\n/**\n * Minimum contract every spoke User row must satisfy. Spokes can widen this\n * with additional fields (credit_balance, must_change_password, etc.) and the\n * factory will preserve them through the returned `Promise<TUser>`.\n */\nexport type BaseAppUser = {\n id: bigint | string | number;\n email: string;\n name: string;\n role: string;\n parent_id: bigint | string | number | null;\n};\n\n/**\n * Loose typing for the Prisma delegates the factory touches. Each spoke has\n * its own generated client whose actual types are concrete; we use loose\n * shapes here so the factory works with any spoke's schema.\n */\nexport type PrismaLikeUserDelegate<TUser> = {\n findUnique: (args: {\n where: { id?: unknown; email?: string };\n }) => Promise<TUser | null>;\n create: (args: { data: unknown }) => Promise<TUser>;\n};\n\nexport type PrismaLikeInvitationDelegate = {\n findFirst: (args: {\n where: { email: string; accepted_at: null; expires_at: { gt: Date } };\n orderBy?: unknown;\n }) => Promise<{\n id: bigint | string | number;\n intended_role: string;\n parent_id: bigint | string | number | null;\n } | null>;\n update: (args: {\n where: { id: unknown };\n data: { accepted_at: Date; accepted_by_user_id: unknown };\n }) => Promise<unknown>;\n};\n\nexport type PrismaLikeClient<TUser> = {\n user: PrismaLikeUserDelegate<TUser>;\n invitation: PrismaLikeInvitationDelegate;\n $transaction: <T>(\n fn: (tx: {\n user: PrismaLikeUserDelegate<TUser>;\n invitation: PrismaLikeInvitationDelegate;\n }) => Promise<T>,\n ) => Promise<T>;\n};\n\nexport type CreateGetOrCreateAppUserOptions<TUser extends BaseAppUser> = {\n /** Returns the spoke's PrismaClient (lazily). */\n db: () => Promise<PrismaLikeClient<TUser>>;\n /** Fallback role when no admin email + no Cognito groups. */\n defaultRole: string;\n /** Starting credit balance per role. */\n computeCreditBalance: (role: string) => number;\n /** Emails auto-promoted to \"admin\" role on first sign-in (case-insensitive). */\n adminEmails?: string[];\n /**\n * Hash value written to User.password on creation. Schema-inherited\n * not-null constraint; never used to authenticate (Cognito does that).\n * Default: a recognizable placeholder string.\n */\n placeholderPasswordHash?: string;\n /**\n * Extra column values written on creation. Use this for spoke-specific\n * defaults (e.g. is_active: true, must_change_password: false).\n */\n extraCreateFields?: Record<string, unknown>;\n};\n\nexport type AppUserWithImpersonation<TUser extends BaseAppUser> = TUser & {\n /** Stringified admin id when this session is impersonated; absent otherwise. */\n impersonatedBy?: string;\n};\n\nconst DEFAULT_PLACEHOLDER_HASH =\n \"$2y$12$.cognito-managed.never.used-for-login.placeholder\";\n\n/**\n * Build a `getOrCreateAppUser(session)` function configured for this spoke.\n *\n * Returned function is idempotent: subsequent calls with the same email\n * return the existing row. First-time emails are created inside a transaction\n * that also auto-accepts a matching Invitation row if present.\n */\nexport function createGetOrCreateAppUser<TUser extends BaseAppUser>(\n opts: CreateGetOrCreateAppUserOptions<TUser>,\n): (session: Session) => Promise<AppUserWithImpersonation<TUser>> {\n const adminEmailsLower = (opts.adminEmails ?? []).map((s) => s.toLowerCase());\n const placeholder = opts.placeholderPasswordHash ?? DEFAULT_PLACEHOLDER_HASH;\n\n return async function getOrCreateAppUser(\n session: Session,\n ): Promise<AppUserWithImpersonation<TUser>> {\n const email = session.user?.email;\n if (!email) {\n throw new Error(\"getOrCreateAppUser called with a session that has no user.email\");\n }\n\n const db = await opts.db();\n\n // -- Impersonation short-circuit (before the session-driven lookup) --\n try {\n const cookieStore = await cookies();\n const cookie = cookieStore.get(IMPERSONATE_COOKIE_NAME);\n if (cookie?.value) {\n const claims = await verifyImpersonationToken(cookie.value);\n if (claims) {\n const [admin, target] = await Promise.all([\n db.user.findUnique({ where: { id: claims.impersonatedBy } }),\n db.user.findUnique({ where: { id: claims.sub } }),\n ]);\n if (admin && admin.role === \"admin\" && target) {\n return Object.assign(target, {\n impersonatedBy: claims.impersonatedBy,\n });\n }\n // Orphaned/expired admin or target -- fall through silently.\n }\n }\n } catch {\n // No cookie context (called from a non-request scope) -- ignore.\n }\n\n const existing = await db.user.findUnique({ where: { email } });\n if (existing) return existing;\n\n // -- New user provisioning --\n const groups = (session.user as { groups?: string[] }).groups ?? [];\n const fallbackRole = adminEmailsLower.includes(email.toLowerCase())\n ? \"admin\"\n : (groups[0] ?? opts.defaultRole);\n const name = (session.user as { name?: string | null }).name ?? email.split(\"@\")[0]!;\n\n return db.$transaction(async (tx) => {\n const pendingInvite = await tx.invitation.findFirst({\n where: {\n email,\n accepted_at: null,\n expires_at: { gt: new Date() },\n },\n orderBy: { created_at: \"desc\" },\n });\n\n const role = pendingInvite ? pendingInvite.intended_role : fallbackRole;\n const parent_id = pendingInvite ? pendingInvite.parent_id : null;\n\n const created = await tx.user.create({\n data: {\n email,\n name,\n role,\n parent_id,\n password: placeholder,\n credit_balance: opts.computeCreditBalance(role),\n ...(opts.extraCreateFields ?? {}),\n },\n });\n\n if (pendingInvite) {\n await tx.invitation.update({\n where: { id: pendingInvite.id },\n data: {\n accepted_at: new Date(),\n accepted_by_user_id: created.id,\n },\n });\n }\n\n return created;\n });\n };\n}\n","import \"server-only\";\n\nimport { encode, decode } from \"next-auth/jwt\";\nimport { getSecret } from \"@augmenting-integrations/aws/server\";\n\n// =============================================================================\n// Impersonation cookie + JWT helpers.\n//\n// Pattern: an admin issues POST /api/admin/users/:id/impersonate, which mints\n// a short-lived JWT and sets it as the `__impersonate` httpOnly cookie. On\n// every subsequent authed request, getOrCreateAppUser reads the cookie,\n// verifies the JWT against AUTH_SECRET, and -- if valid -- returns the\n// *target* user instead of the session user with `impersonatedBy` set.\n//\n// The cookie does NOT replace the next-auth session cookie. It is read\n// alongside the session. Invalid / expired tokens silently fall through.\n//\n// JWT library: next-auth re-exports @auth/core's `encode` / `decode` (JWE).\n// Salted differently from session tokens so they can't be cross-replayed.\n// =============================================================================\n\nexport const IMPERSONATE_COOKIE_NAME = \"__impersonate\";\nexport const IMPERSONATE_TTL_SECONDS = 3600;\nconst IMPERSONATE_JWT_SALT = \"impersonate.v1\";\n\nexport type ImpersonationClaims = {\n /** Admin user id who started the impersonation (stringified BigInt). */\n impersonatedBy: string;\n /** Target user id being impersonated (stringified BigInt). */\n sub: string;\n /** Issued-at (seconds since epoch). */\n iat: number;\n /** Expiry (seconds since epoch). */\n exp: number;\n};\n\nlet cachedSecret: string | null = null;\n\nasync function getAuthSecret(): Promise<string> {\n if (cachedSecret) return cachedSecret;\n const arn = process.env.AUTH_SECRET_ARN;\n const fromSm = arn ? await getSecret(arn) : null;\n const secret = fromSm ?? process.env.AUTH_SECRET;\n if (!secret) {\n throw new Error(\n \"AUTH_SECRET (or AUTH_SECRET_ARN) must be set to mint/verify impersonation tokens\",\n );\n }\n cachedSecret = secret;\n return secret;\n}\n\nexport async function mintImpersonationToken(args: {\n adminId: bigint | string;\n targetId: bigint | string;\n now?: Date;\n}): Promise<{ token: string; expiresAt: Date }> {\n const secret = await getAuthSecret();\n const nowSec = Math.floor((args.now?.getTime() ?? Date.now()) / 1000);\n const exp = nowSec + IMPERSONATE_TTL_SECONDS;\n const token = await encode({\n secret,\n salt: IMPERSONATE_JWT_SALT,\n maxAge: IMPERSONATE_TTL_SECONDS,\n token: {\n impersonatedBy: String(args.adminId),\n sub: String(args.targetId),\n iat: nowSec,\n exp,\n },\n });\n return { token, expiresAt: new Date(exp * 1000) };\n}\n\nexport async function verifyImpersonationToken(\n token: string,\n): Promise<ImpersonationClaims | null> {\n try {\n const secret = await getAuthSecret();\n const decoded = await decode({\n token,\n secret,\n salt: IMPERSONATE_JWT_SALT,\n });\n if (!decoded) return null;\n const impersonatedBy = decoded[\"impersonatedBy\"];\n const sub = decoded[\"sub\"];\n const iat = decoded[\"iat\"];\n const exp = decoded[\"exp\"];\n if (\n typeof impersonatedBy !== \"string\" ||\n typeof sub !== \"string\" ||\n typeof iat !== \"number\" ||\n typeof exp !== \"number\"\n ) {\n return null;\n }\n if (exp * 1000 < Date.now()) return null;\n return { impersonatedBy, sub, iat, exp };\n } catch {\n return null;\n }\n}\n","import \"server-only\";\nimport * as React from \"react\";\nimport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantRole,\n type TenantServerConfig,\n} from \"../tenant-types.js\";\n\n// =============================================================================\n// loadTenantConfig() -- the single source of truth for tenant configuration.\n//\n// Every required process.env read happens here. Missing fields are surfaced\n// in ONE error message so the deploy fails loudly instead of silently\n// substituting undefined into a downstream package.\n//\n// Apex apps call loadTenantConfig({ role: \"apex\" }). Spoke apps call\n// loadTenantConfig({ role: \"spoke\" }). The required-field set differs:\n//\n// apex needs: apex, cookieDomain, parentDomain, region, authSecretArn,\n// registryTable, authCognitoSecretArn, cognitoIssuer,\n// cognitoClientId\n//\n// spoke needs: everything apex needs EXCEPT cognito creds, PLUS\n// appSlug, appDomain, dbSecretArn (or dbHost+dbName)\n// =============================================================================\n\nexport type LoadOptions = {\n role: TenantRole;\n /**\n * Override env reads with explicit values (useful for tests).\n */\n overrides?: Partial<TenantServerConfig>;\n};\n\n/**\n * Read tenant configuration from process.env with optional overrides.\n * Throws a single Error listing every missing required field.\n */\nexport function loadTenantConfig(opts: LoadOptions): TenantServerConfig {\n const env = process.env;\n const o = opts.overrides ?? {};\n\n // Apex derives from the parent domain (strip leading dot) when not\n // explicitly set. This is the only env var we synthesize -- everything\n // else maps 1:1 to a process.env name.\n const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;\n const apexFallback = parentDomainRaw?.replace(/^\\./, \"\");\n\n const draft: Partial<TenantServerConfig> = {\n role: opts.role,\n apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,\n cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,\n parentDomain: parentDomainRaw,\n region: o.region ?? env.AWS_REGION ?? \"us-east-1\",\n appSlug: o.appSlug ?? env.APP_SLUG,\n appDomain: o.appDomain ?? env.APP_DOMAIN,\n authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,\n registryTable: o.registryTable ?? env.APP_REGISTRY_TABLE,\n authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,\n cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,\n cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,\n adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,\n dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,\n dbHost: o.dbHost ?? env.DB_HOST,\n dbName: o.dbName ?? env.DB_NAME,\n stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,\n stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN,\n };\n\n // Default appDomain to apex for apex apps.\n if (opts.role === \"apex\" && !draft.appDomain) {\n draft.appDomain = draft.apex;\n }\n\n const required: Array<{ key: keyof TenantServerConfig; env: string }> = [\n { key: \"apex\", env: \"APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)\" },\n { key: \"cookieDomain\", env: \"AUTH_COOKIE_DOMAIN\" },\n { key: \"parentDomain\", env: \"AUTH_ALLOWED_PARENT_DOMAIN\" },\n { key: \"authSecretArn\", env: \"AUTH_SECRET_ARN\" },\n { key: \"appDomain\", env: \"APP_DOMAIN\" },\n ];\n if (opts.role === \"apex\") {\n required.push(\n { key: \"authCognitoSecretArn\", env: \"AUTH_COGNITO_SECRET_ARN\" },\n { key: \"cognitoIssuer\", env: \"AUTH_COGNITO_ISSUER\" },\n { key: \"cognitoClientId\", env: \"AUTH_COGNITO_ID\" },\n );\n } else {\n required.push({ key: \"appSlug\", env: \"APP_SLUG\" });\n }\n\n // Skip validation during `next build`. The build phase has no real env\n // vars but evaluates module-init code (so we can't just lazy-throw).\n // At runtime in a Lambda the env is fully populated and validation runs\n // normally. NEXT_PHASE is set by next.js itself; the absence of\n // AWS_LAMBDA_FUNCTION_NAME means we're not actually in a request handler.\n const isBuildPhase =\n process.env.NEXT_PHASE === \"phase-production-build\" ||\n (!process.env.AWS_LAMBDA_FUNCTION_NAME && process.env.NODE_ENV === \"production\");\n if (isBuildPhase) {\n return draft as TenantServerConfig;\n }\n\n const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);\n if (missing.length > 0) {\n throw new Error(\n `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(\", \")}`,\n );\n }\n\n return draft as TenantServerConfig;\n}\n\n/**\n * Reduce a TenantServerConfig to the public-safe subset. Strips every\n * secret-arn so the result is safe to ship to the browser via\n * <TenantBootScript />.\n */\nexport function publicSubset(config: TenantServerConfig): TenantPublicConfig {\n return {\n apex: config.apex,\n cookieDomain: config.cookieDomain,\n parentDomain: config.parentDomain,\n region: config.region,\n appSlug: config.appSlug,\n appDomain: config.appDomain,\n role: config.role,\n };\n}\n\n// =============================================================================\n// <TenantBootScript /> -- server component that injects window.__TENANT__\n// before paint. Every client widget (ThemeProvider, RoleSwitcher, AppShell,\n// CartDrawer, etc.) reads from this global instead of receiving props\n// threaded through React context.\n//\n// The payload is JSON.stringify of a TYPED struct -- we control every field\n// shape. The </script> escape protects against rare-but-real \"config\n// contains </script>\" payloads.\n// =============================================================================\n\nconst INNER_HTML_PROP = \"dangerouslySetInnerHTML\";\n\nexport function TenantBootScript({ config }: { config: TenantPublicConfig }) {\n const payload = JSON.stringify(config).replace(/</g, \"\\\\u003c\");\n const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;\n const props: Record<string, unknown> = {};\n props[INNER_HTML_PROP] = { __html: body };\n return React.createElement(\"script\", props);\n}\n\nexport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"../tenant-types.js\";\n","// =============================================================================\n// TenantConfig -- the single struct every @augmenting-integrations package\n// consumes. Apex apps and spokes share the same type; spoke-only fields are\n// optional. The `role` discriminator tells loadTenantConfig() which fields\n// to demand.\n//\n// Public fields (apex + parent domain + slug) are safe to ship to the browser\n// via <TenantBootScript />. Secret-arn fields are server-only and never reach\n// the client bundle.\n// =============================================================================\n\nexport type TenantRole = \"apex\" | \"spoke\";\n\nexport type TenantPublicConfig = {\n /** The tenant apex FQDN, e.g. \"agency.aillc.link\". */\n apex: string;\n /**\n * Cookie Domain attribute. Always the apex (no leading dot needed -- the\n * browser implies it for shared cookies). Auth.js session cookie and the\n * theme x-theme/x-theme-variant cookies use this. Without it cookies are\n * host-only and the subdomain ecosystem breaks.\n */\n cookieDomain: string;\n /**\n * The registrable parent domain (e.g. \"aillc.link\"). Used by the auth\n * redirect callback to validate post-login callbacks back to any subdomain\n * of the tenant. Distinct from cookieDomain in two-level apex setups.\n */\n parentDomain: string;\n /** AWS region. Default: us-east-1. */\n region: string;\n /**\n * For spoke apps: this spoke's slug (matches app registry primary key).\n * For apex: undefined.\n */\n appSlug?: string;\n /**\n * For spoke apps: this spoke's FQDN (e.g. \"leads.agency.aillc.link\").\n * For apex: same as `apex`.\n */\n appDomain: string;\n /** \"apex\" or \"spoke\". Affects which secret-arn fields are required. */\n role: TenantRole;\n};\n\nexport type TenantServerConfig = TenantPublicConfig & {\n /** AUTH_SECRET ARN in Secrets Manager. Used by createAuth(). */\n authSecretArn: string;\n /** App registry DynamoDB table name. Apex owns the table; spokes read. */\n registryTable: string;\n /** Cognito client secret ARN. Apex only -- spokes don't run the OAuth dance. */\n authCognitoSecretArn?: string;\n /** Cognito issuer URL (apex only). */\n cognitoIssuer?: string;\n /** Cognito client ID (apex only). */\n cognitoClientId?: string;\n /** Comma-separated admin emails (auto-promoted on first sign-in). */\n adminEmails?: string;\n /** Aurora connection secret ARN (spoke only). */\n dbSecretArn?: string;\n /** Aurora endpoint host (spoke only). */\n dbHost?: string;\n /** Aurora database name (spoke only). */\n dbName?: string;\n /** Stripe credentials bundle ARN (spoke that does billing). */\n stripeSecretArn?: string;\n /** Stripe webhook signing secret ARN (spoke that does billing). */\n stripeWebhookSecretArn?: string;\n};\n\nexport const TENANT_GLOBAL_KEY = \"__TENANT__\" as const;\n\ndeclare global {\n interface Window {\n [TENANT_GLOBAL_KEY]?: TenantPublicConfig;\n }\n}\n"],"mappings":"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","names":["Cognito","Credentials","NextAuth","import_server_only","import_server_only"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/server/createAuth.ts","../src/server/jit.ts","../src/server/impersonation.ts","../src/server/tenant.ts","../src/tenant-types.ts"],"sourcesContent":["export {\n createAuth,\n type CreateAuthOptions,\n type NextAuthConfig,\n AuthError,\n getUserGroups,\n hasGroup,\n requireGroup,\n} from \"./server/createAuth.js\";\nexport {\n createGetOrCreateAppUser,\n type BaseAppUser,\n type AppUserWithImpersonation,\n type CreateGetOrCreateAppUserOptions,\n type PrismaLikeClient,\n type PrismaLikeUserDelegate,\n type PrismaLikeInvitationDelegate,\n} from \"./server/jit.js\";\nexport {\n mintImpersonationToken,\n verifyImpersonationToken,\n IMPERSONATE_COOKIE_NAME,\n IMPERSONATE_TTL_SECONDS,\n type ImpersonationClaims,\n} from \"./server/impersonation.js\";\nexport {\n loadTenantConfig,\n publicSubset,\n TenantBootScript,\n TENANT_GLOBAL_KEY,\n type LoadOptions,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"./server/tenant.js\";\n","// Auth.js v5 (the package is still distributed as `next-auth`, but treat\n// these as Auth.js v5 internally — docs at https://authjs.dev, NOT\n// next-auth.js.org which is v4 and incompatible).\n//\n// Subdomain ecosystem model:\n// - One Cognito User Pool per tenant.\n// - One Cognito App Client with ONE callback URL at the apex\n// (https://<apex>/api/auth/callback/cognito).\n// - The apex app is the auth broker. Subdomain apps redirect through it.\n// - Session cookie scoped to Domain=.<apex> so every subdomain sees it.\n// - All apps use the same createAuth() invocation; the package derives\n// the right signInPage from appDomain + allowedParentDomain.\n//\n// Provider strategy:\n// - Production: Cognito OIDC. cognito:groups drives session.user.groups.\n// - Dev / preview: Credentials with a role picker, shaped to mirror\n// Cognito's claim payload (same groups, sub, email).\n\nimport NextAuth, {\n type DefaultSession,\n type NextAuthConfig,\n type Session,\n} from \"next-auth\";\nimport Credentials from \"next-auth/providers/credentials\";\nimport Cognito from \"next-auth/providers/cognito\";\nimport type { TenantServerConfig } from \"../tenant-types.js\";\n\n// Session augmentation: `groups` is the Cognito-groups array surfaced from\n// the JWT. `role` lives on the application User row (Prisma), exposed via\n// `useAppUser()` from `@augmenting-integrations/auth/client`. There is one\n// source of role-truth; do not add a session-level `role` field back here.\ndeclare module \"next-auth\" {\n interface Session {\n user: {\n groups: string[];\n } & DefaultSession[\"user\"];\n }\n interface User {\n groups?: string[];\n }\n}\n\nexport type CreateAuthOptions = {\n /**\n * Full tenant configuration. Provides apex/cookieDomain/parentDomain/\n * appDomain/role + cognito client id + issuer + allowed admin emails.\n * Load via `loadTenantConfig()` from `@augmenting-integrations/tenant/server`.\n */\n tenant: TenantServerConfig;\n /** Path prefixes that require an authenticated session. */\n authedRoutePrefixes: string[];\n /**\n * JWT signing secret. Caller fetches from Secrets Manager via\n * `getSecret(tenant.authSecretArn)` and passes the resolved value here.\n * The library doesn't bundle AWS SDK reads itself.\n */\n authSecret: string;\n /**\n * Cognito OAuth client secret. Apex apps only -- spokes never run the\n * OAuth dance. Caller fetches from Secrets Manager via\n * `getSecret(tenant.authCognitoSecretArn)`.\n */\n cognitoClientSecret?: string;\n /**\n * Override the auto-derived sign-in page (rarely needed). Default:\n * apex apps get `/login`; spoke apps get `https://<apex>/login`.\n */\n signInPage?: string;\n /** Override prod/dev detection. Default reads NODE_ENV. */\n isProd?: boolean;\n};\n\n// ----- AuthError used by requireGroup -----\n\nexport class AuthError extends Error {\n constructor(public code: \"unauthenticated\" | \"forbidden\") {\n super(code);\n this.name = \"AuthError\";\n }\n}\n\n// ----- Group/authorization helpers -----\n\n/** Returns the user's Cognito groups (always an array, possibly empty). */\nexport function getUserGroups(session: Session | null | undefined): string[] {\n return session?.user?.groups ?? [];\n}\n\n/** Case-insensitive group membership check. */\nexport function hasGroup(session: Session | null | undefined, name: string): boolean {\n if (!session) return false;\n const target = name.toLowerCase();\n return getUserGroups(session).some((g) => g.toLowerCase() === target);\n}\n\n/**\n * Throws AuthError if no session (`unauthenticated`) or if the user is in\n * none of the provided groups (`forbidden`). Pass multiple names to allow\n * any-of.\n */\nexport function requireGroup(\n session: Session | null | undefined,\n ...names: string[]\n): void {\n if (!session) throw new AuthError(\"unauthenticated\");\n if (names.length === 0) return;\n const ok = names.some((n) => hasGroup(session, n));\n if (!ok) throw new AuthError(\"forbidden\");\n}\n\n// ----- Redirect callback factory -----\n\nfunction buildRedirectCallback(parentDomain: string) {\n return ({ url, baseUrl }: { url: string; baseUrl: string }): string => {\n try {\n const target = new URL(url, baseUrl);\n const apex = parentDomain.replace(/^\\./, \"\").toLowerCase();\n const host = target.hostname.toLowerCase();\n const ok = host === apex || host.endsWith(`.${apex}`);\n return ok ? target.toString() : baseUrl;\n } catch {\n return baseUrl;\n }\n };\n}\n\n// ----- Sign-in page auto-derivation -----\n\nfunction deriveSignInPage(args: {\n signInPage: string | undefined;\n appDomain: string;\n apex: string;\n}): string {\n if (args.signInPage) return args.signInPage;\n return args.appDomain === args.apex ? \"/login\" : `https://${args.apex}/login`;\n}\n\n// ----- Main factory -----\n\nexport function createAuth(opts: CreateAuthOptions) {\n const { tenant, authSecret, cognitoClientSecret } = opts;\n const isProd = opts.isProd ?? process.env.NODE_ENV === \"production\";\n const cookieDomain = isProd ? tenant.cookieDomain : undefined;\n\n // Skip runtime assertions during next build (env-less, evaluates\n // module-init code but never serves requests).\n const isBuildPhase = process.env.NEXT_PHASE === \"phase-production-build\";\n\n if (tenant.role === \"apex\" && !isBuildPhase) {\n if (!tenant.cognitoClientId || !tenant.cognitoIssuer) {\n throw new Error(\n \"createAuth: tenant.role='apex' requires cognitoClientId + cognitoIssuer. Check loadTenantConfig() inputs.\",\n );\n }\n if (isProd && !cognitoClientSecret) {\n throw new Error(\n \"createAuth: tenant.role='apex' in production requires cognitoClientSecret. Fetch via getSecret(tenant.authCognitoSecretArn) and pass it in.\",\n );\n }\n }\n\n const signInPage = deriveSignInPage({\n signInPage: opts.signInPage,\n appDomain: tenant.appDomain,\n apex: tenant.apex,\n });\n\n const config: NextAuthConfig = {\n secret: authSecret,\n cookies: cookieDomain\n ? {\n sessionToken: {\n name: \"authjs.session-token\",\n options: {\n domain: cookieDomain,\n sameSite: \"lax\",\n secure: true,\n httpOnly: true,\n path: \"/\",\n },\n },\n }\n : undefined,\n providers: isProd\n ? [\n Cognito({\n clientId: tenant.cognitoClientId,\n clientSecret: cognitoClientSecret,\n issuer: tenant.cognitoIssuer,\n }),\n ]\n : [\n Credentials({\n name: \"Mock role (dev only)\",\n credentials: {\n role: {\n label: \"Role\",\n type: \"text\",\n placeholder: \"any role string\",\n },\n },\n authorize: async (credentials) => {\n const role = credentials?.role as string | undefined;\n if (!role) return null;\n const display = role.charAt(0).toUpperCase() + role.slice(1);\n return {\n id: `mock-${role}`,\n name: `${display} (mock)`,\n email: `${role}@example.local`,\n role,\n groups: [role],\n };\n },\n }),\n ],\n session: { strategy: \"jwt\" },\n callbacks: {\n jwt: ({ token, user, profile }) => {\n if (user) {\n token.sub ??= user.id ?? undefined;\n token.email ??= user.email ?? undefined;\n if (!isProd) {\n const u = user as { groups?: string[]; role?: string };\n const groups = u.groups ?? (u.role ? [u.role] : []);\n if (groups.length > 0) {\n (token as Record<string, unknown>)[\"cognito:groups\"] = groups;\n }\n }\n }\n if (isProd && profile) {\n const groups = (profile as Record<string, unknown>)[\"cognito:groups\"];\n if (groups) {\n (token as Record<string, unknown>)[\"cognito:groups\"] = groups;\n }\n }\n return token;\n },\n session: ({ session, token }) => {\n const groups =\n ((token as Record<string, unknown>)[\"cognito:groups\"] as\n | string[]\n | undefined) ?? [];\n session.user.groups = groups;\n return session;\n },\n authorized: ({ auth: session, request: { nextUrl } }) => {\n const path = nextUrl.pathname;\n const isAuthedRoute = opts.authedRoutePrefixes.some(\n (prefix) => path === prefix || path.startsWith(`${prefix}/`),\n );\n if (!session && isAuthedRoute) {\n // For subdomain apps signInPage is an absolute URL on the apex\n // broker. Auth.js's default middleware redirect treats\n // pages.signIn as a relative path and prepends the current\n // host, producing malformed Location URLs like\n // https://sub.<apex>/https://<apex>/login. Returning an\n // explicit Response.redirect bypasses that path and sends the\n // user to the apex broker correctly.\n if (signInPage.startsWith(\"http\")) {\n const target = new URL(signInPage);\n target.searchParams.set(\"callbackUrl\", nextUrl.href);\n return Response.redirect(target.toString(), 302);\n }\n return false;\n }\n return true;\n },\n redirect: buildRedirectCallback(tenant.parentDomain),\n },\n pages: { signIn: signInPage },\n trustHost: true,\n };\n\n return NextAuth(config);\n}\n\nexport type { NextAuthConfig } from \"next-auth\";\n","import \"server-only\";\n\nimport { cookies } from \"next/headers\";\nimport type { Session } from \"next-auth\";\n\nimport { IMPERSONATE_COOKIE_NAME, verifyImpersonationToken } from \"./impersonation.js\";\n\n// =============================================================================\n// JIT user provisioning factory.\n//\n// Pattern: every authed request hands a session into getOrCreateAppUser() to\n// resolve the DB User row (creating one on first sign-in for that email).\n// The factory pattern lets each spoke configure:\n//\n// - `db`: how to reach Prisma (the library doesn't bundle the client)\n// - `defaultRole`: fallback when Cognito groups + ADMIN_EMAILS don't decide\n// - `computeCreditBalance(role)`: starting credit balance per role\n// - `adminEmails`: CSV of emails auto-promoted to admin on first sign-in\n// - `placeholderPasswordHash`: schema-inherited not-null constraint filler\n//\n// Impersonation short-circuit (runs BEFORE the session-driven lookup): if\n// `__impersonate` cookie is present and verifies against AUTH_SECRET, and the\n// underlying admin still exists with role==='admin', returns the *target* user\n// with `impersonatedBy` set to the admin's stringified id. Orphaned tokens\n// silently fall through to the session user.\n//\n// Invitation auto-accept: if a pending Invitation row exists for this email\n// (accepted_at IS NULL, expires_at > now), the new User inherits the\n// invitation's parent_id and intended_role and the invitation is marked\n// accepted in the same transaction.\n// =============================================================================\n\n/**\n * Minimum contract every spoke User row must satisfy. Spokes can widen this\n * with additional fields (credit_balance, must_change_password, etc.) and the\n * factory will preserve them through the returned `Promise<TUser>`.\n */\nexport type BaseAppUser = {\n id: bigint | string | number;\n email: string;\n name: string;\n role: string;\n parent_id: bigint | string | number | null;\n};\n\n/**\n * Loose typing for the Prisma delegates the factory touches. Each spoke has\n * its own generated client whose actual types are concrete; we use loose\n * shapes here so the factory works with any spoke's schema.\n */\nexport type PrismaLikeUserDelegate<TUser> = {\n findUnique: (args: {\n where: { id?: unknown; email?: string };\n }) => Promise<TUser | null>;\n create: (args: { data: unknown }) => Promise<TUser>;\n};\n\nexport type PrismaLikeInvitationDelegate = {\n findFirst: (args: {\n where: { email: string; accepted_at: null; expires_at: { gt: Date } };\n orderBy?: unknown;\n }) => Promise<{\n id: bigint | string | number;\n intended_role: string;\n parent_id: bigint | string | number | null;\n } | null>;\n update: (args: {\n where: { id: unknown };\n data: { accepted_at: Date; accepted_by_user_id: unknown };\n }) => Promise<unknown>;\n};\n\nexport type PrismaLikeClient<TUser> = {\n user: PrismaLikeUserDelegate<TUser>;\n invitation: PrismaLikeInvitationDelegate;\n $transaction: <T>(\n fn: (tx: {\n user: PrismaLikeUserDelegate<TUser>;\n invitation: PrismaLikeInvitationDelegate;\n }) => Promise<T>,\n ) => Promise<T>;\n};\n\nexport type CreateGetOrCreateAppUserOptions<TUser extends BaseAppUser> = {\n /** Returns the spoke's PrismaClient (lazily). */\n db: () => Promise<PrismaLikeClient<TUser>>;\n /** Fallback role when no admin email + no Cognito groups. */\n defaultRole: string;\n /** Starting credit balance per role. */\n computeCreditBalance: (role: string) => number;\n /** Emails auto-promoted to \"admin\" role on first sign-in (case-insensitive). */\n adminEmails?: string[];\n /**\n * Hash value written to User.password on creation. Schema-inherited\n * not-null constraint; never used to authenticate (Cognito does that).\n * Default: a recognizable placeholder string.\n */\n placeholderPasswordHash?: string;\n /**\n * Extra column values written on creation. Use this for spoke-specific\n * defaults (e.g. is_active: true, must_change_password: false).\n */\n extraCreateFields?: Record<string, unknown>;\n};\n\nexport type AppUserWithImpersonation<TUser extends BaseAppUser> = TUser & {\n /** Stringified admin id when this session is impersonated; absent otherwise. */\n impersonatedBy?: string;\n};\n\nconst DEFAULT_PLACEHOLDER_HASH =\n \"$2y$12$.cognito-managed.never.used-for-login.placeholder\";\n\n/**\n * Build a `getOrCreateAppUser(session)` function configured for this spoke.\n *\n * Returned function is idempotent: subsequent calls with the same email\n * return the existing row. First-time emails are created inside a transaction\n * that also auto-accepts a matching Invitation row if present.\n */\nexport function createGetOrCreateAppUser<TUser extends BaseAppUser>(\n opts: CreateGetOrCreateAppUserOptions<TUser>,\n): (session: Session) => Promise<AppUserWithImpersonation<TUser>> {\n const adminEmailsLower = (opts.adminEmails ?? []).map((s) => s.toLowerCase());\n const placeholder = opts.placeholderPasswordHash ?? DEFAULT_PLACEHOLDER_HASH;\n\n return async function getOrCreateAppUser(\n session: Session,\n ): Promise<AppUserWithImpersonation<TUser>> {\n const email = session.user?.email;\n if (!email) {\n throw new Error(\"getOrCreateAppUser called with a session that has no user.email\");\n }\n\n const db = await opts.db();\n\n // -- Impersonation short-circuit (before the session-driven lookup) --\n try {\n const cookieStore = await cookies();\n const cookie = cookieStore.get(IMPERSONATE_COOKIE_NAME);\n if (cookie?.value) {\n const claims = await verifyImpersonationToken(cookie.value);\n if (claims) {\n const [admin, target] = await Promise.all([\n db.user.findUnique({ where: { id: claims.impersonatedBy } }),\n db.user.findUnique({ where: { id: claims.sub } }),\n ]);\n if (admin && admin.role === \"admin\" && target) {\n return Object.assign(target, {\n impersonatedBy: claims.impersonatedBy,\n });\n }\n // Orphaned/expired admin or target -- fall through silently.\n }\n }\n } catch {\n // No cookie context (called from a non-request scope) -- ignore.\n }\n\n const existing = await db.user.findUnique({ where: { email } });\n if (existing) return existing;\n\n // -- New user provisioning --\n const groups = (session.user as { groups?: string[] }).groups ?? [];\n const fallbackRole = adminEmailsLower.includes(email.toLowerCase())\n ? \"admin\"\n : (groups[0] ?? opts.defaultRole);\n const name = (session.user as { name?: string | null }).name ?? email.split(\"@\")[0]!;\n\n return db.$transaction(async (tx) => {\n const pendingInvite = await tx.invitation.findFirst({\n where: {\n email,\n accepted_at: null,\n expires_at: { gt: new Date() },\n },\n orderBy: { created_at: \"desc\" },\n });\n\n const role = pendingInvite ? pendingInvite.intended_role : fallbackRole;\n const parent_id = pendingInvite ? pendingInvite.parent_id : null;\n\n const created = await tx.user.create({\n data: {\n email,\n name,\n role,\n parent_id,\n password: placeholder,\n credit_balance: opts.computeCreditBalance(role),\n ...(opts.extraCreateFields ?? {}),\n },\n });\n\n if (pendingInvite) {\n await tx.invitation.update({\n where: { id: pendingInvite.id },\n data: {\n accepted_at: new Date(),\n accepted_by_user_id: created.id,\n },\n });\n }\n\n return created;\n });\n };\n}\n","import \"server-only\";\n\nimport { encode, decode } from \"next-auth/jwt\";\nimport { getSecret } from \"@augmenting-integrations/aws/server\";\n\n// =============================================================================\n// Impersonation cookie + JWT helpers.\n//\n// Pattern: an admin issues POST /api/admin/users/:id/impersonate, which mints\n// a short-lived JWT and sets it as the `__impersonate` httpOnly cookie. On\n// every subsequent authed request, getOrCreateAppUser reads the cookie,\n// verifies the JWT against AUTH_SECRET, and -- if valid -- returns the\n// *target* user instead of the session user with `impersonatedBy` set.\n//\n// The cookie does NOT replace the next-auth session cookie. It is read\n// alongside the session. Invalid / expired tokens silently fall through.\n//\n// JWT library: next-auth re-exports @auth/core's `encode` / `decode` (JWE).\n// Salted differently from session tokens so they can't be cross-replayed.\n// =============================================================================\n\nexport const IMPERSONATE_COOKIE_NAME = \"__impersonate\";\nexport const IMPERSONATE_TTL_SECONDS = 3600;\nconst IMPERSONATE_JWT_SALT = \"impersonate.v1\";\n\nexport type ImpersonationClaims = {\n /** Admin user id who started the impersonation (stringified BigInt). */\n impersonatedBy: string;\n /** Target user id being impersonated (stringified BigInt). */\n sub: string;\n /** Issued-at (seconds since epoch). */\n iat: number;\n /** Expiry (seconds since epoch). */\n exp: number;\n};\n\nlet cachedSecret: string | null = null;\n\nasync function getAuthSecret(): Promise<string> {\n if (cachedSecret) return cachedSecret;\n const arn = process.env.AUTH_SECRET_ARN;\n const fromSm = arn ? await getSecret(arn) : null;\n const secret = fromSm ?? process.env.AUTH_SECRET;\n if (!secret) {\n throw new Error(\n \"AUTH_SECRET (or AUTH_SECRET_ARN) must be set to mint/verify impersonation tokens\",\n );\n }\n cachedSecret = secret;\n return secret;\n}\n\nexport async function mintImpersonationToken(args: {\n adminId: bigint | string;\n targetId: bigint | string;\n now?: Date;\n}): Promise<{ token: string; expiresAt: Date }> {\n const secret = await getAuthSecret();\n const nowSec = Math.floor((args.now?.getTime() ?? Date.now()) / 1000);\n const exp = nowSec + IMPERSONATE_TTL_SECONDS;\n const token = await encode({\n secret,\n salt: IMPERSONATE_JWT_SALT,\n maxAge: IMPERSONATE_TTL_SECONDS,\n token: {\n impersonatedBy: String(args.adminId),\n sub: String(args.targetId),\n iat: nowSec,\n exp,\n },\n });\n return { token, expiresAt: new Date(exp * 1000) };\n}\n\nexport async function verifyImpersonationToken(\n token: string,\n): Promise<ImpersonationClaims | null> {\n try {\n const secret = await getAuthSecret();\n const decoded = await decode({\n token,\n secret,\n salt: IMPERSONATE_JWT_SALT,\n });\n if (!decoded) return null;\n const impersonatedBy = decoded[\"impersonatedBy\"];\n const sub = decoded[\"sub\"];\n const iat = decoded[\"iat\"];\n const exp = decoded[\"exp\"];\n if (\n typeof impersonatedBy !== \"string\" ||\n typeof sub !== \"string\" ||\n typeof iat !== \"number\" ||\n typeof exp !== \"number\"\n ) {\n return null;\n }\n if (exp * 1000 < Date.now()) return null;\n return { impersonatedBy, sub, iat, exp };\n } catch {\n return null;\n }\n}\n","import \"server-only\";\nimport * as React from \"react\";\nimport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantRole,\n type TenantServerConfig,\n} from \"../tenant-types.js\";\n\n// =============================================================================\n// loadTenantConfig() -- the single source of truth for tenant configuration.\n//\n// Every required process.env read happens here. Missing fields are surfaced\n// in ONE error message so the deploy fails loudly instead of silently\n// substituting undefined into a downstream package.\n//\n// Apex apps call loadTenantConfig({ role: \"apex\" }). Spoke apps call\n// loadTenantConfig({ role: \"spoke\" }). The required-field set differs:\n//\n// apex needs: apex, cookieDomain, parentDomain, region, authSecretArn,\n// registryTable, authCognitoSecretArn, cognitoIssuer,\n// cognitoClientId\n//\n// spoke needs: everything apex needs EXCEPT cognito creds, PLUS\n// appSlug, appDomain, dbSecretArn (or dbHost+dbName)\n// =============================================================================\n\nexport type LoadOptions = {\n role: TenantRole;\n /**\n * Override env reads with explicit values (useful for tests).\n */\n overrides?: Partial<TenantServerConfig>;\n};\n\n/**\n * Read tenant configuration from process.env with optional overrides.\n * Throws a single Error listing every missing required field.\n */\nexport function loadTenantConfig(opts: LoadOptions): TenantServerConfig {\n const env = process.env;\n const o = opts.overrides ?? {};\n\n // Apex derives from the parent domain (strip leading dot) when not\n // explicitly set. This is the only env var we synthesize -- everything\n // else maps 1:1 to a process.env name.\n const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;\n const apexFallback = parentDomainRaw?.replace(/^\\./, \"\");\n\n const draft: Partial<TenantServerConfig> = {\n role: opts.role,\n apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,\n cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,\n parentDomain: parentDomainRaw,\n region: o.region ?? env.AWS_REGION ?? \"us-east-1\",\n appSlug: o.appSlug ?? env.APP_SLUG,\n appDomain: o.appDomain ?? env.APP_DOMAIN,\n authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,\n registryTable: o.registryTable ?? env.APP_REGISTRY_TABLE,\n authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,\n cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,\n cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,\n adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,\n dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,\n dbHost: o.dbHost ?? env.DB_HOST,\n dbName: o.dbName ?? env.DB_NAME,\n stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,\n stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN,\n };\n\n // Default appDomain to apex for apex apps.\n if (opts.role === \"apex\" && !draft.appDomain) {\n draft.appDomain = draft.apex;\n }\n\n const required: Array<{ key: keyof TenantServerConfig; env: string }> = [\n { key: \"apex\", env: \"APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)\" },\n { key: \"cookieDomain\", env: \"AUTH_COOKIE_DOMAIN\" },\n { key: \"parentDomain\", env: \"AUTH_ALLOWED_PARENT_DOMAIN\" },\n { key: \"authSecretArn\", env: \"AUTH_SECRET_ARN\" },\n { key: \"appDomain\", env: \"APP_DOMAIN\" },\n ];\n if (opts.role === \"apex\") {\n required.push(\n { key: \"authCognitoSecretArn\", env: \"AUTH_COGNITO_SECRET_ARN\" },\n { key: \"cognitoIssuer\", env: \"AUTH_COGNITO_ISSUER\" },\n { key: \"cognitoClientId\", env: \"AUTH_COGNITO_ID\" },\n );\n } else {\n required.push({ key: \"appSlug\", env: \"APP_SLUG\" });\n }\n\n // Skip validation during `next build`. NEXT_PHASE is set by next.js\n // itself during the production build step; module-init code evaluates\n // but no requests are served. ANY other phase (dev server, prod runtime,\n // localhost E2E) must enforce the required fields.\n if (process.env.NEXT_PHASE === \"phase-production-build\") {\n return draft as TenantServerConfig;\n }\n\n const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);\n if (missing.length > 0) {\n throw new Error(\n `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(\", \")}`,\n );\n }\n\n return draft as TenantServerConfig;\n}\n\n/**\n * Reduce a TenantServerConfig to the public-safe subset. Strips every\n * secret-arn so the result is safe to ship to the browser via\n * <TenantBootScript />.\n */\nexport function publicSubset(config: TenantServerConfig): TenantPublicConfig {\n return {\n apex: config.apex,\n cookieDomain: config.cookieDomain,\n parentDomain: config.parentDomain,\n region: config.region,\n appSlug: config.appSlug,\n appDomain: config.appDomain,\n role: config.role,\n };\n}\n\n// =============================================================================\n// <TenantBootScript /> -- server component that injects window.__TENANT__\n// before paint. Every client widget (ThemeProvider, RoleSwitcher, AppShell,\n// CartDrawer, etc.) reads from this global instead of receiving props\n// threaded through React context.\n//\n// The payload is JSON.stringify of a TYPED struct -- we control every field\n// shape. The </script> escape protects against rare-but-real \"config\n// contains </script>\" payloads.\n// =============================================================================\n\nconst INNER_HTML_PROP = \"dangerouslySetInnerHTML\";\n\nexport function TenantBootScript({ config }: { config: TenantPublicConfig }) {\n const payload = JSON.stringify(config).replace(/</g, \"\\\\u003c\");\n const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;\n const props: Record<string, unknown> = {};\n props[INNER_HTML_PROP] = { __html: body };\n return React.createElement(\"script\", props);\n}\n\nexport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"../tenant-types.js\";\n","// =============================================================================\n// TenantConfig -- the single struct every @augmenting-integrations package\n// consumes. Apex apps and spokes share the same type; spoke-only fields are\n// optional. The `role` discriminator tells loadTenantConfig() which fields\n// to demand.\n//\n// Public fields (apex + parent domain + slug) are safe to ship to the browser\n// via <TenantBootScript />. Secret-arn fields are server-only and never reach\n// the client bundle.\n// =============================================================================\n\nexport type TenantRole = \"apex\" | \"spoke\";\n\nexport type TenantPublicConfig = {\n /** The tenant apex FQDN, e.g. \"agency.aillc.link\". */\n apex: string;\n /**\n * Cookie Domain attribute. Always the apex (no leading dot needed -- the\n * browser implies it for shared cookies). Auth.js session cookie and the\n * theme x-theme/x-theme-variant cookies use this. Without it cookies are\n * host-only and the subdomain ecosystem breaks.\n */\n cookieDomain: string;\n /**\n * The registrable parent domain (e.g. \"aillc.link\"). Used by the auth\n * redirect callback to validate post-login callbacks back to any subdomain\n * of the tenant. Distinct from cookieDomain in two-level apex setups.\n */\n parentDomain: string;\n /** AWS region. Default: us-east-1. */\n region: string;\n /**\n * For spoke apps: this spoke's slug (matches app registry primary key).\n * For apex: undefined.\n */\n appSlug?: string;\n /**\n * For spoke apps: this spoke's FQDN (e.g. \"leads.agency.aillc.link\").\n * For apex: same as `apex`.\n */\n appDomain: string;\n /** \"apex\" or \"spoke\". Affects which secret-arn fields are required. */\n role: TenantRole;\n};\n\nexport type TenantServerConfig = TenantPublicConfig & {\n /** AUTH_SECRET ARN in Secrets Manager. Used by createAuth(). */\n authSecretArn: string;\n /** App registry DynamoDB table name. Apex owns the table; spokes read. */\n registryTable: string;\n /** Cognito client secret ARN. Apex only -- spokes don't run the OAuth dance. */\n authCognitoSecretArn?: string;\n /** Cognito issuer URL (apex only). */\n cognitoIssuer?: string;\n /** Cognito client ID (apex only). */\n cognitoClientId?: string;\n /** Comma-separated admin emails (auto-promoted on first sign-in). */\n adminEmails?: string;\n /** Aurora connection secret ARN (spoke only). */\n dbSecretArn?: string;\n /** Aurora endpoint host (spoke only). */\n dbHost?: string;\n /** Aurora database name (spoke only). */\n dbName?: string;\n /** Stripe credentials bundle ARN (spoke that does billing). */\n stripeSecretArn?: string;\n /** Stripe webhook signing secret ARN (spoke that does billing). */\n stripeWebhookSecretArn?: string;\n};\n\nexport const TENANT_GLOBAL_KEY = \"__TENANT__\" as const;\n\ndeclare global {\n interface Window {\n [TENANT_GLOBAL_KEY]?: TenantPublicConfig;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkBA,uBAIO;AACP,yBAAwB;AACxB,qBAAoB;AAkDb,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnC,YAAmB,MAAuC;AACxD,UAAM,IAAI;AADO;AAEjB,SAAK,OAAO;AAAA,EACd;AAAA,EAHmB;AAIrB;AAKO,SAAS,cAAc,SAA+C;AAC3E,SAAO,SAAS,MAAM,UAAU,CAAC;AACnC;AAGO,SAAS,SAAS,SAAqC,MAAuB;AACnF,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,SAAS,KAAK,YAAY;AAChC,SAAO,cAAc,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,MAAM;AACtE;AAOO,SAAS,aACd,YACG,OACG;AACN,MAAI,CAAC,QAAS,OAAM,IAAI,UAAU,iBAAiB;AACnD,MAAI,MAAM,WAAW,EAAG;AACxB,QAAM,KAAK,MAAM,KAAK,CAAC,MAAM,SAAS,SAAS,CAAC,CAAC;AACjD,MAAI,CAAC,GAAI,OAAM,IAAI,UAAU,WAAW;AAC1C;AAIA,SAAS,sBAAsB,cAAsB;AACnD,SAAO,CAAC,EAAE,KAAK,QAAQ,MAAgD;AACrE,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,KAAK,OAAO;AACnC,YAAM,OAAO,aAAa,QAAQ,OAAO,EAAE,EAAE,YAAY;AACzD,YAAM,OAAO,OAAO,SAAS,YAAY;AACzC,YAAM,KAAK,SAAS,QAAQ,KAAK,SAAS,IAAI,IAAI,EAAE;AACpD,aAAO,KAAK,OAAO,SAAS,IAAI;AAAA,IAClC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAIA,SAAS,iBAAiB,MAIf;AACT,MAAI,KAAK,WAAY,QAAO,KAAK;AACjC,SAAO,KAAK,cAAc,KAAK,OAAO,WAAW,WAAW,KAAK,IAAI;AACvE;AAIO,SAAS,WAAW,MAAyB;AAClD,QAAM,EAAE,QAAQ,YAAY,oBAAoB,IAAI;AACpD,QAAM,SAAS,KAAK,UAAU,QAAQ,IAAI,aAAa;AACvD,QAAM,eAAe,SAAS,OAAO,eAAe;AAIpD,QAAM,eAAe,QAAQ,IAAI,eAAe;AAEhD,MAAI,OAAO,SAAS,UAAU,CAAC,cAAc;AAC3C,QAAI,CAAC,OAAO,mBAAmB,CAAC,OAAO,eAAe;AACpD,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,UAAU,CAAC,qBAAqB;AAClC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAa,iBAAiB;AAAA,IAClC,YAAY,KAAK;AAAA,IACjB,WAAW,OAAO;AAAA,IAClB,MAAM,OAAO;AAAA,EACf,CAAC;AAED,QAAM,SAAyB;AAAA,IAC7B,QAAQ;AAAA,IACR,SAAS,eACL;AAAA,MACE,cAAc;AAAA,QACZ,MAAM;AAAA,QACN,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,MAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF,IACA;AAAA,IACJ,WAAW,SACP;AAAA,UACE,eAAAA,SAAQ;AAAA,QACN,UAAU,OAAO;AAAA,QACjB,cAAc;AAAA,QACd,QAAQ,OAAO;AAAA,MACjB,CAAC;AAAA,IACH,IACA;AAAA,UACE,mBAAAC,SAAY;AAAA,QACV,MAAM;AAAA,QACN,aAAa;AAAA,UACX,MAAM;AAAA,YACJ,OAAO;AAAA,YACP,MAAM;AAAA,YACN,aAAa;AAAA,UACf;AAAA,QACF;AAAA,QACA,WAAW,OAAO,gBAAgB;AAChC,gBAAM,OAAO,aAAa;AAC1B,cAAI,CAAC,KAAM,QAAO;AAClB,gBAAM,UAAU,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC;AAC3D,iBAAO;AAAA,YACL,IAAI,QAAQ,IAAI;AAAA,YAChB,MAAM,GAAG,OAAO;AAAA,YAChB,OAAO,GAAG,IAAI;AAAA,YACd;AAAA,YACA,QAAQ,CAAC,IAAI;AAAA,UACf;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACJ,SAAS,EAAE,UAAU,MAAM;AAAA,IAC3B,WAAW;AAAA,MACT,KAAK,CAAC,EAAE,OAAO,MAAM,QAAQ,MAAM;AACjC,YAAI,MAAM;AACR,gBAAM,QAAQ,KAAK,MAAM;AACzB,gBAAM,UAAU,KAAK,SAAS;AAC9B,cAAI,CAAC,QAAQ;AACX,kBAAM,IAAI;AACV,kBAAM,SAAS,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,IAAI,IAAI,CAAC;AACjD,gBAAI,OAAO,SAAS,GAAG;AACrB,cAAC,MAAkC,gBAAgB,IAAI;AAAA,YACzD;AAAA,UACF;AAAA,QACF;AACA,YAAI,UAAU,SAAS;AACrB,gBAAM,SAAU,QAAoC,gBAAgB;AACpE,cAAI,QAAQ;AACV,YAAC,MAAkC,gBAAgB,IAAI;AAAA,UACzD;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,MACA,SAAS,CAAC,EAAE,SAAS,MAAM,MAAM;AAC/B,cAAM,SACF,MAAkC,gBAAgB,KAElC,CAAC;AACrB,gBAAQ,KAAK,SAAS;AACtB,eAAO;AAAA,MACT;AAAA,MACA,YAAY,CAAC,EAAE,MAAM,SAAS,SAAS,EAAE,QAAQ,EAAE,MAAM;AACvD,cAAM,OAAO,QAAQ;AACrB,cAAM,gBAAgB,KAAK,oBAAoB;AAAA,UAC7C,CAAC,WAAW,SAAS,UAAU,KAAK,WAAW,GAAG,MAAM,GAAG;AAAA,QAC7D;AACA,YAAI,CAAC,WAAW,eAAe;AAQ7B,cAAI,WAAW,WAAW,MAAM,GAAG;AACjC,kBAAM,SAAS,IAAI,IAAI,UAAU;AACjC,mBAAO,aAAa,IAAI,eAAe,QAAQ,IAAI;AACnD,mBAAO,SAAS,SAAS,OAAO,SAAS,GAAG,GAAG;AAAA,UACjD;AACA,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAAA,MACA,UAAU,sBAAsB,OAAO,YAAY;AAAA,IACrD;AAAA,IACA,OAAO,EAAE,QAAQ,WAAW;AAAA,IAC5B,WAAW;AAAA,EACb;AAEA,aAAO,iBAAAC,SAAS,MAAM;AACxB;;;AClRA,IAAAC,sBAAO;AAEP,qBAAwB;;;ACFxB,yBAAO;AAEP,iBAA+B;AAC/B,oBAA0B;AAkBnB,IAAM,0BAA0B;AAChC,IAAM,0BAA0B;AACvC,IAAM,uBAAuB;AAa7B,IAAI,eAA8B;AAElC,eAAe,gBAAiC;AAC9C,MAAI,aAAc,QAAO;AACzB,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAAS,MAAM,UAAM,yBAAU,GAAG,IAAI;AAC5C,QAAM,SAAS,UAAU,QAAQ,IAAI;AACrC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,iBAAe;AACf,SAAO;AACT;AAEA,eAAsB,uBAAuB,MAIG;AAC9C,QAAM,SAAS,MAAM,cAAc;AACnC,QAAM,SAAS,KAAK,OAAO,KAAK,KAAK,QAAQ,KAAK,KAAK,IAAI,KAAK,GAAI;AACpE,QAAM,MAAM,SAAS;AACrB,QAAM,QAAQ,UAAM,mBAAO;AAAA,IACzB;AAAA,IACA,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,MACL,gBAAgB,OAAO,KAAK,OAAO;AAAA,MACnC,KAAK,OAAO,KAAK,QAAQ;AAAA,MACzB,KAAK;AAAA,MACL;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO,EAAE,OAAO,WAAW,IAAI,KAAK,MAAM,GAAI,EAAE;AAClD;AAEA,eAAsB,yBACpB,OACqC;AACrC,MAAI;AACF,UAAM,SAAS,MAAM,cAAc;AACnC,UAAM,UAAU,UAAM,mBAAO;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,iBAAiB,QAAQ,gBAAgB;AAC/C,UAAM,MAAM,QAAQ,KAAK;AACzB,UAAM,MAAM,QAAQ,KAAK;AACzB,UAAM,MAAM,QAAQ,KAAK;AACzB,QACE,OAAO,mBAAmB,YAC1B,OAAO,QAAQ,YACf,OAAO,QAAQ,YACf,OAAO,QAAQ,UACf;AACA,aAAO;AAAA,IACT;AACA,QAAI,MAAM,MAAO,KAAK,IAAI,EAAG,QAAO;AACpC,WAAO,EAAE,gBAAgB,KAAK,KAAK,IAAI;AAAA,EACzC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ADQA,IAAM,2BACJ;AASK,SAAS,yBACd,MACgE;AAChE,QAAM,oBAAoB,KAAK,eAAe,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AAC5E,QAAM,cAAc,KAAK,2BAA2B;AAEpD,SAAO,eAAe,mBACpB,SAC0C;AAC1C,UAAM,QAAQ,QAAQ,MAAM;AAC5B,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,iEAAiE;AAAA,IACnF;AAEA,UAAM,KAAK,MAAM,KAAK,GAAG;AAGzB,QAAI;AACF,YAAM,cAAc,UAAM,wBAAQ;AAClC,YAAM,SAAS,YAAY,IAAI,uBAAuB;AACtD,UAAI,QAAQ,OAAO;AACjB,cAAM,SAAS,MAAM,yBAAyB,OAAO,KAAK;AAC1D,YAAI,QAAQ;AACV,gBAAM,CAAC,OAAO,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,YACxC,GAAG,KAAK,WAAW,EAAE,OAAO,EAAE,IAAI,OAAO,eAAe,EAAE,CAAC;AAAA,YAC3D,GAAG,KAAK,WAAW,EAAE,OAAO,EAAE,IAAI,OAAO,IAAI,EAAE,CAAC;AAAA,UAClD,CAAC;AACD,cAAI,SAAS,MAAM,SAAS,WAAW,QAAQ;AAC7C,mBAAO,OAAO,OAAO,QAAQ;AAAA,cAC3B,gBAAgB,OAAO;AAAA,YACzB,CAAC;AAAA,UACH;AAAA,QAEF;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,UAAM,WAAW,MAAM,GAAG,KAAK,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC9D,QAAI,SAAU,QAAO;AAGrB,UAAM,SAAU,QAAQ,KAA+B,UAAU,CAAC;AAClE,UAAM,eAAe,iBAAiB,SAAS,MAAM,YAAY,CAAC,IAC9D,UACC,OAAO,CAAC,KAAK,KAAK;AACvB,UAAM,OAAQ,QAAQ,KAAkC,QAAQ,MAAM,MAAM,GAAG,EAAE,CAAC;AAElF,WAAO,GAAG,aAAa,OAAO,OAAO;AACnC,YAAM,gBAAgB,MAAM,GAAG,WAAW,UAAU;AAAA,QAClD,OAAO;AAAA,UACL;AAAA,UACA,aAAa;AAAA,UACb,YAAY,EAAE,IAAI,oBAAI,KAAK,EAAE;AAAA,QAC/B;AAAA,QACA,SAAS,EAAE,YAAY,OAAO;AAAA,MAChC,CAAC;AAED,YAAM,OAAO,gBAAgB,cAAc,gBAAgB;AAC3D,YAAM,YAAY,gBAAgB,cAAc,YAAY;AAE5D,YAAM,UAAU,MAAM,GAAG,KAAK,OAAO;AAAA,QACnC,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU;AAAA,UACV,gBAAgB,KAAK,qBAAqB,IAAI;AAAA,UAC9C,GAAI,KAAK,qBAAqB,CAAC;AAAA,QACjC;AAAA,MACF,CAAC;AAED,UAAI,eAAe;AACjB,cAAM,GAAG,WAAW,OAAO;AAAA,UACzB,OAAO,EAAE,IAAI,cAAc,GAAG;AAAA,UAC9B,MAAM;AAAA,YACJ,aAAa,oBAAI,KAAK;AAAA,YACtB,qBAAqB,QAAQ;AAAA,UAC/B;AAAA,QACF,CAAC;AAAA,MACH;AAEA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;;;AE/MA,IAAAC,sBAAO;AACP,YAAuB;;;ACqEhB,IAAM,oBAAoB;;;AD/B1B,SAAS,iBAAiB,MAAuC;AACtE,QAAM,MAAM,QAAQ;AACpB,QAAM,IAAI,KAAK,aAAa,CAAC;AAK7B,QAAM,kBAAkB,EAAE,gBAAgB,IAAI;AAC9C,QAAM,eAAe,iBAAiB,QAAQ,OAAO,EAAE;AAEvD,QAAM,QAAqC;AAAA,IACzC,MAAM,KAAK;AAAA,IACX,MAAM,EAAE,QAAQ,IAAI,eAAe;AAAA,IACnC,cAAc,EAAE,gBAAgB,IAAI;AAAA,IACpC,cAAc;AAAA,IACd,QAAQ,EAAE,UAAU,IAAI,cAAc;AAAA,IACtC,SAAS,EAAE,WAAW,IAAI;AAAA,IAC1B,WAAW,EAAE,aAAa,IAAI;AAAA,IAC9B,eAAe,EAAE,iBAAiB,IAAI;AAAA,IACtC,eAAe,EAAE,iBAAiB,IAAI;AAAA,IACtC,sBAAsB,EAAE,wBAAwB,IAAI;AAAA,IACpD,eAAe,EAAE,iBAAiB,IAAI;AAAA,IACtC,iBAAiB,EAAE,mBAAmB,IAAI;AAAA,IAC1C,aAAa,EAAE,eAAe,IAAI;AAAA,IAClC,aAAa,EAAE,eAAe,IAAI;AAAA,IAClC,QAAQ,EAAE,UAAU,IAAI;AAAA,IACxB,QAAQ,EAAE,UAAU,IAAI;AAAA,IACxB,iBAAiB,EAAE,mBAAmB,IAAI;AAAA,IAC1C,wBAAwB,EAAE,0BAA0B,IAAI;AAAA,EAC1D;AAGA,MAAI,KAAK,SAAS,UAAU,CAAC,MAAM,WAAW;AAC5C,UAAM,YAAY,MAAM;AAAA,EAC1B;AAEA,QAAM,WAAkE;AAAA,IACtE,EAAE,KAAK,QAAQ,KAAK,2DAA2D;AAAA,IAC/E,EAAE,KAAK,gBAAgB,KAAK,qBAAqB;AAAA,IACjD,EAAE,KAAK,gBAAgB,KAAK,6BAA6B;AAAA,IACzD,EAAE,KAAK,iBAAiB,KAAK,kBAAkB;AAAA,IAC/C,EAAE,KAAK,aAAa,KAAK,aAAa;AAAA,EACxC;AACA,MAAI,KAAK,SAAS,QAAQ;AACxB,aAAS;AAAA,MACP,EAAE,KAAK,wBAAwB,KAAK,0BAA0B;AAAA,MAC9D,EAAE,KAAK,iBAAiB,KAAK,sBAAsB;AAAA,MACnD,EAAE,KAAK,mBAAmB,KAAK,kBAAkB;AAAA,IACnD;AAAA,EACF,OAAO;AACL,aAAS,KAAK,EAAE,KAAK,WAAW,KAAK,WAAW,CAAC;AAAA,EACnD;AAMA,MAAI,QAAQ,IAAI,eAAe,0BAA0B;AACvD,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,SAAS,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AACtE,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,oBAAoB,KAAK,IAAI,iCAAiC,QAAQ,KAAK,IAAI,CAAC;AAAA,IAClF;AAAA,EACF;AAEA,SAAO;AACT;AAOO,SAAS,aAAa,QAAgD;AAC3E,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,cAAc,OAAO;AAAA,IACrB,cAAc,OAAO;AAAA,IACrB,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,IAChB,WAAW,OAAO;AAAA,IAClB,MAAM,OAAO;AAAA,EACf;AACF;AAaA,IAAM,kBAAkB;AAEjB,SAAS,iBAAiB,EAAE,OAAO,GAAmC;AAC3E,QAAM,UAAU,KAAK,UAAU,MAAM,EAAE,QAAQ,MAAM,SAAS;AAC9D,QAAM,OAAO,UAAU,iBAAiB,IAAI,OAAO;AACnD,QAAM,QAAiC,CAAC;AACxC,QAAM,eAAe,IAAI,EAAE,QAAQ,KAAK;AACxC,SAAa,oBAAc,UAAU,KAAK;AAC5C;","names":["Cognito","Credentials","NextAuth","import_server_only","import_server_only"]}
|
package/dist/server.js
CHANGED
|
@@ -45,7 +45,7 @@ function createAuth(opts) {
|
|
|
45
45
|
const { tenant, authSecret, cognitoClientSecret } = opts;
|
|
46
46
|
const isProd = opts.isProd ?? process.env.NODE_ENV === "production";
|
|
47
47
|
const cookieDomain = isProd ? tenant.cookieDomain : void 0;
|
|
48
|
-
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build"
|
|
48
|
+
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
|
|
49
49
|
if (tenant.role === "apex" && !isBuildPhase) {
|
|
50
50
|
if (!tenant.cognitoClientId || !tenant.cognitoIssuer) {
|
|
51
51
|
throw new Error(
|
|
@@ -345,8 +345,7 @@ function loadTenantConfig(opts) {
|
|
|
345
345
|
} else {
|
|
346
346
|
required.push({ key: "appSlug", env: "APP_SLUG" });
|
|
347
347
|
}
|
|
348
|
-
|
|
349
|
-
if (isBuildPhase) {
|
|
348
|
+
if (process.env.NEXT_PHASE === "phase-production-build") {
|
|
350
349
|
return draft;
|
|
351
350
|
}
|
|
352
351
|
const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/createAuth.ts","../src/server/jit.ts","../src/server/impersonation.ts","../src/server/tenant.ts","../src/tenant-types.ts"],"sourcesContent":["// Auth.js v5 (the package is still distributed as `next-auth`, but treat\n// these as Auth.js v5 internally — docs at https://authjs.dev, NOT\n// next-auth.js.org which is v4 and incompatible).\n//\n// Subdomain ecosystem model:\n// - One Cognito User Pool per tenant.\n// - One Cognito App Client with ONE callback URL at the apex\n// (https://<apex>/api/auth/callback/cognito).\n// - The apex app is the auth broker. Subdomain apps redirect through it.\n// - Session cookie scoped to Domain=.<apex> so every subdomain sees it.\n// - All apps use the same createAuth() invocation; the package derives\n// the right signInPage from appDomain + allowedParentDomain.\n//\n// Provider strategy:\n// - Production: Cognito OIDC. cognito:groups drives session.user.groups.\n// - Dev / preview: Credentials with a role picker, shaped to mirror\n// Cognito's claim payload (same groups, sub, email).\n\nimport NextAuth, {\n type DefaultSession,\n type NextAuthConfig,\n type Session,\n} from \"next-auth\";\nimport Credentials from \"next-auth/providers/credentials\";\nimport Cognito from \"next-auth/providers/cognito\";\nimport type { TenantServerConfig } from \"../tenant-types.js\";\n\n// Session augmentation: `groups` is the Cognito-groups array surfaced from\n// the JWT. `role` lives on the application User row (Prisma), exposed via\n// `useAppUser()` from `@augmenting-integrations/auth/client`. There is one\n// source of role-truth; do not add a session-level `role` field back here.\ndeclare module \"next-auth\" {\n interface Session {\n user: {\n groups: string[];\n } & DefaultSession[\"user\"];\n }\n interface User {\n groups?: string[];\n }\n}\n\nexport type CreateAuthOptions = {\n /**\n * Full tenant configuration. Provides apex/cookieDomain/parentDomain/\n * appDomain/role + cognito client id + issuer + allowed admin emails.\n * Load via `loadTenantConfig()` from `@augmenting-integrations/tenant/server`.\n */\n tenant: TenantServerConfig;\n /** Path prefixes that require an authenticated session. */\n authedRoutePrefixes: string[];\n /**\n * JWT signing secret. Caller fetches from Secrets Manager via\n * `getSecret(tenant.authSecretArn)` and passes the resolved value here.\n * The library doesn't bundle AWS SDK reads itself.\n */\n authSecret: string;\n /**\n * Cognito OAuth client secret. Apex apps only -- spokes never run the\n * OAuth dance. Caller fetches from Secrets Manager via\n * `getSecret(tenant.authCognitoSecretArn)`.\n */\n cognitoClientSecret?: string;\n /**\n * Override the auto-derived sign-in page (rarely needed). Default:\n * apex apps get `/login`; spoke apps get `https://<apex>/login`.\n */\n signInPage?: string;\n /** Override prod/dev detection. Default reads NODE_ENV. */\n isProd?: boolean;\n};\n\n// ----- AuthError used by requireGroup -----\n\nexport class AuthError extends Error {\n constructor(public code: \"unauthenticated\" | \"forbidden\") {\n super(code);\n this.name = \"AuthError\";\n }\n}\n\n// ----- Group/authorization helpers -----\n\n/** Returns the user's Cognito groups (always an array, possibly empty). */\nexport function getUserGroups(session: Session | null | undefined): string[] {\n return session?.user?.groups ?? [];\n}\n\n/** Case-insensitive group membership check. */\nexport function hasGroup(session: Session | null | undefined, name: string): boolean {\n if (!session) return false;\n const target = name.toLowerCase();\n return getUserGroups(session).some((g) => g.toLowerCase() === target);\n}\n\n/**\n * Throws AuthError if no session (`unauthenticated`) or if the user is in\n * none of the provided groups (`forbidden`). Pass multiple names to allow\n * any-of.\n */\nexport function requireGroup(\n session: Session | null | undefined,\n ...names: string[]\n): void {\n if (!session) throw new AuthError(\"unauthenticated\");\n if (names.length === 0) return;\n const ok = names.some((n) => hasGroup(session, n));\n if (!ok) throw new AuthError(\"forbidden\");\n}\n\n// ----- Redirect callback factory -----\n\nfunction buildRedirectCallback(parentDomain: string) {\n return ({ url, baseUrl }: { url: string; baseUrl: string }): string => {\n try {\n const target = new URL(url, baseUrl);\n const apex = parentDomain.replace(/^\\./, \"\").toLowerCase();\n const host = target.hostname.toLowerCase();\n const ok = host === apex || host.endsWith(`.${apex}`);\n return ok ? target.toString() : baseUrl;\n } catch {\n return baseUrl;\n }\n };\n}\n\n// ----- Sign-in page auto-derivation -----\n\nfunction deriveSignInPage(args: {\n signInPage: string | undefined;\n appDomain: string;\n apex: string;\n}): string {\n if (args.signInPage) return args.signInPage;\n return args.appDomain === args.apex ? \"/login\" : `https://${args.apex}/login`;\n}\n\n// ----- Main factory -----\n\nexport function createAuth(opts: CreateAuthOptions) {\n const { tenant, authSecret, cognitoClientSecret } = opts;\n const isProd = opts.isProd ?? process.env.NODE_ENV === \"production\";\n const cookieDomain = isProd ? tenant.cookieDomain : undefined;\n\n // Skip runtime assertions during next build (env-less, evaluates\n // module-init code but never serves requests).\n const isBuildPhase =\n process.env.NEXT_PHASE === \"phase-production-build\" ||\n (!process.env.AWS_LAMBDA_FUNCTION_NAME && process.env.NODE_ENV === \"production\");\n\n if (tenant.role === \"apex\" && !isBuildPhase) {\n if (!tenant.cognitoClientId || !tenant.cognitoIssuer) {\n throw new Error(\n \"createAuth: tenant.role='apex' requires cognitoClientId + cognitoIssuer. Check loadTenantConfig() inputs.\",\n );\n }\n if (isProd && !cognitoClientSecret) {\n throw new Error(\n \"createAuth: tenant.role='apex' in production requires cognitoClientSecret. Fetch via getSecret(tenant.authCognitoSecretArn) and pass it in.\",\n );\n }\n }\n\n const signInPage = deriveSignInPage({\n signInPage: opts.signInPage,\n appDomain: tenant.appDomain,\n apex: tenant.apex,\n });\n\n const config: NextAuthConfig = {\n secret: authSecret,\n cookies: cookieDomain\n ? {\n sessionToken: {\n name: \"authjs.session-token\",\n options: {\n domain: cookieDomain,\n sameSite: \"lax\",\n secure: true,\n httpOnly: true,\n path: \"/\",\n },\n },\n }\n : undefined,\n providers: isProd\n ? [\n Cognito({\n clientId: tenant.cognitoClientId,\n clientSecret: cognitoClientSecret,\n issuer: tenant.cognitoIssuer,\n }),\n ]\n : [\n Credentials({\n name: \"Mock role (dev only)\",\n credentials: {\n role: {\n label: \"Role\",\n type: \"text\",\n placeholder: \"any role string\",\n },\n },\n authorize: async (credentials) => {\n const role = credentials?.role as string | undefined;\n if (!role) return null;\n const display = role.charAt(0).toUpperCase() + role.slice(1);\n return {\n id: `mock-${role}`,\n name: `${display} (mock)`,\n email: `${role}@example.local`,\n role,\n groups: [role],\n };\n },\n }),\n ],\n session: { strategy: \"jwt\" },\n callbacks: {\n jwt: ({ token, user, profile }) => {\n if (user) {\n token.sub ??= user.id ?? undefined;\n token.email ??= user.email ?? undefined;\n if (!isProd) {\n const u = user as { groups?: string[]; role?: string };\n const groups = u.groups ?? (u.role ? [u.role] : []);\n if (groups.length > 0) {\n (token as Record<string, unknown>)[\"cognito:groups\"] = groups;\n }\n }\n }\n if (isProd && profile) {\n const groups = (profile as Record<string, unknown>)[\"cognito:groups\"];\n if (groups) {\n (token as Record<string, unknown>)[\"cognito:groups\"] = groups;\n }\n }\n return token;\n },\n session: ({ session, token }) => {\n const groups =\n ((token as Record<string, unknown>)[\"cognito:groups\"] as\n | string[]\n | undefined) ?? [];\n session.user.groups = groups;\n return session;\n },\n authorized: ({ auth: session, request: { nextUrl } }) => {\n const path = nextUrl.pathname;\n const isAuthedRoute = opts.authedRoutePrefixes.some(\n (prefix) => path === prefix || path.startsWith(`${prefix}/`),\n );\n if (!session && isAuthedRoute) {\n // For subdomain apps signInPage is an absolute URL on the apex\n // broker. Auth.js's default middleware redirect treats\n // pages.signIn as a relative path and prepends the current\n // host, producing malformed Location URLs like\n // https://sub.<apex>/https://<apex>/login. Returning an\n // explicit Response.redirect bypasses that path and sends the\n // user to the apex broker correctly.\n if (signInPage.startsWith(\"http\")) {\n const target = new URL(signInPage);\n target.searchParams.set(\"callbackUrl\", nextUrl.href);\n return Response.redirect(target.toString(), 302);\n }\n return false;\n }\n return true;\n },\n redirect: buildRedirectCallback(tenant.parentDomain),\n },\n pages: { signIn: signInPage },\n trustHost: true,\n };\n\n return NextAuth(config);\n}\n\nexport type { NextAuthConfig } from \"next-auth\";\n","import \"server-only\";\n\nimport { cookies } from \"next/headers\";\nimport type { Session } from \"next-auth\";\n\nimport { IMPERSONATE_COOKIE_NAME, verifyImpersonationToken } from \"./impersonation.js\";\n\n// =============================================================================\n// JIT user provisioning factory.\n//\n// Pattern: every authed request hands a session into getOrCreateAppUser() to\n// resolve the DB User row (creating one on first sign-in for that email).\n// The factory pattern lets each spoke configure:\n//\n// - `db`: how to reach Prisma (the library doesn't bundle the client)\n// - `defaultRole`: fallback when Cognito groups + ADMIN_EMAILS don't decide\n// - `computeCreditBalance(role)`: starting credit balance per role\n// - `adminEmails`: CSV of emails auto-promoted to admin on first sign-in\n// - `placeholderPasswordHash`: schema-inherited not-null constraint filler\n//\n// Impersonation short-circuit (runs BEFORE the session-driven lookup): if\n// `__impersonate` cookie is present and verifies against AUTH_SECRET, and the\n// underlying admin still exists with role==='admin', returns the *target* user\n// with `impersonatedBy` set to the admin's stringified id. Orphaned tokens\n// silently fall through to the session user.\n//\n// Invitation auto-accept: if a pending Invitation row exists for this email\n// (accepted_at IS NULL, expires_at > now), the new User inherits the\n// invitation's parent_id and intended_role and the invitation is marked\n// accepted in the same transaction.\n// =============================================================================\n\n/**\n * Minimum contract every spoke User row must satisfy. Spokes can widen this\n * with additional fields (credit_balance, must_change_password, etc.) and the\n * factory will preserve them through the returned `Promise<TUser>`.\n */\nexport type BaseAppUser = {\n id: bigint | string | number;\n email: string;\n name: string;\n role: string;\n parent_id: bigint | string | number | null;\n};\n\n/**\n * Loose typing for the Prisma delegates the factory touches. Each spoke has\n * its own generated client whose actual types are concrete; we use loose\n * shapes here so the factory works with any spoke's schema.\n */\nexport type PrismaLikeUserDelegate<TUser> = {\n findUnique: (args: {\n where: { id?: unknown; email?: string };\n }) => Promise<TUser | null>;\n create: (args: { data: unknown }) => Promise<TUser>;\n};\n\nexport type PrismaLikeInvitationDelegate = {\n findFirst: (args: {\n where: { email: string; accepted_at: null; expires_at: { gt: Date } };\n orderBy?: unknown;\n }) => Promise<{\n id: bigint | string | number;\n intended_role: string;\n parent_id: bigint | string | number | null;\n } | null>;\n update: (args: {\n where: { id: unknown };\n data: { accepted_at: Date; accepted_by_user_id: unknown };\n }) => Promise<unknown>;\n};\n\nexport type PrismaLikeClient<TUser> = {\n user: PrismaLikeUserDelegate<TUser>;\n invitation: PrismaLikeInvitationDelegate;\n $transaction: <T>(\n fn: (tx: {\n user: PrismaLikeUserDelegate<TUser>;\n invitation: PrismaLikeInvitationDelegate;\n }) => Promise<T>,\n ) => Promise<T>;\n};\n\nexport type CreateGetOrCreateAppUserOptions<TUser extends BaseAppUser> = {\n /** Returns the spoke's PrismaClient (lazily). */\n db: () => Promise<PrismaLikeClient<TUser>>;\n /** Fallback role when no admin email + no Cognito groups. */\n defaultRole: string;\n /** Starting credit balance per role. */\n computeCreditBalance: (role: string) => number;\n /** Emails auto-promoted to \"admin\" role on first sign-in (case-insensitive). */\n adminEmails?: string[];\n /**\n * Hash value written to User.password on creation. Schema-inherited\n * not-null constraint; never used to authenticate (Cognito does that).\n * Default: a recognizable placeholder string.\n */\n placeholderPasswordHash?: string;\n /**\n * Extra column values written on creation. Use this for spoke-specific\n * defaults (e.g. is_active: true, must_change_password: false).\n */\n extraCreateFields?: Record<string, unknown>;\n};\n\nexport type AppUserWithImpersonation<TUser extends BaseAppUser> = TUser & {\n /** Stringified admin id when this session is impersonated; absent otherwise. */\n impersonatedBy?: string;\n};\n\nconst DEFAULT_PLACEHOLDER_HASH =\n \"$2y$12$.cognito-managed.never.used-for-login.placeholder\";\n\n/**\n * Build a `getOrCreateAppUser(session)` function configured for this spoke.\n *\n * Returned function is idempotent: subsequent calls with the same email\n * return the existing row. First-time emails are created inside a transaction\n * that also auto-accepts a matching Invitation row if present.\n */\nexport function createGetOrCreateAppUser<TUser extends BaseAppUser>(\n opts: CreateGetOrCreateAppUserOptions<TUser>,\n): (session: Session) => Promise<AppUserWithImpersonation<TUser>> {\n const adminEmailsLower = (opts.adminEmails ?? []).map((s) => s.toLowerCase());\n const placeholder = opts.placeholderPasswordHash ?? DEFAULT_PLACEHOLDER_HASH;\n\n return async function getOrCreateAppUser(\n session: Session,\n ): Promise<AppUserWithImpersonation<TUser>> {\n const email = session.user?.email;\n if (!email) {\n throw new Error(\"getOrCreateAppUser called with a session that has no user.email\");\n }\n\n const db = await opts.db();\n\n // -- Impersonation short-circuit (before the session-driven lookup) --\n try {\n const cookieStore = await cookies();\n const cookie = cookieStore.get(IMPERSONATE_COOKIE_NAME);\n if (cookie?.value) {\n const claims = await verifyImpersonationToken(cookie.value);\n if (claims) {\n const [admin, target] = await Promise.all([\n db.user.findUnique({ where: { id: claims.impersonatedBy } }),\n db.user.findUnique({ where: { id: claims.sub } }),\n ]);\n if (admin && admin.role === \"admin\" && target) {\n return Object.assign(target, {\n impersonatedBy: claims.impersonatedBy,\n });\n }\n // Orphaned/expired admin or target -- fall through silently.\n }\n }\n } catch {\n // No cookie context (called from a non-request scope) -- ignore.\n }\n\n const existing = await db.user.findUnique({ where: { email } });\n if (existing) return existing;\n\n // -- New user provisioning --\n const groups = (session.user as { groups?: string[] }).groups ?? [];\n const fallbackRole = adminEmailsLower.includes(email.toLowerCase())\n ? \"admin\"\n : (groups[0] ?? opts.defaultRole);\n const name = (session.user as { name?: string | null }).name ?? email.split(\"@\")[0]!;\n\n return db.$transaction(async (tx) => {\n const pendingInvite = await tx.invitation.findFirst({\n where: {\n email,\n accepted_at: null,\n expires_at: { gt: new Date() },\n },\n orderBy: { created_at: \"desc\" },\n });\n\n const role = pendingInvite ? pendingInvite.intended_role : fallbackRole;\n const parent_id = pendingInvite ? pendingInvite.parent_id : null;\n\n const created = await tx.user.create({\n data: {\n email,\n name,\n role,\n parent_id,\n password: placeholder,\n credit_balance: opts.computeCreditBalance(role),\n ...(opts.extraCreateFields ?? {}),\n },\n });\n\n if (pendingInvite) {\n await tx.invitation.update({\n where: { id: pendingInvite.id },\n data: {\n accepted_at: new Date(),\n accepted_by_user_id: created.id,\n },\n });\n }\n\n return created;\n });\n };\n}\n","import \"server-only\";\n\nimport { encode, decode } from \"next-auth/jwt\";\nimport { getSecret } from \"@augmenting-integrations/aws/server\";\n\n// =============================================================================\n// Impersonation cookie + JWT helpers.\n//\n// Pattern: an admin issues POST /api/admin/users/:id/impersonate, which mints\n// a short-lived JWT and sets it as the `__impersonate` httpOnly cookie. On\n// every subsequent authed request, getOrCreateAppUser reads the cookie,\n// verifies the JWT against AUTH_SECRET, and -- if valid -- returns the\n// *target* user instead of the session user with `impersonatedBy` set.\n//\n// The cookie does NOT replace the next-auth session cookie. It is read\n// alongside the session. Invalid / expired tokens silently fall through.\n//\n// JWT library: next-auth re-exports @auth/core's `encode` / `decode` (JWE).\n// Salted differently from session tokens so they can't be cross-replayed.\n// =============================================================================\n\nexport const IMPERSONATE_COOKIE_NAME = \"__impersonate\";\nexport const IMPERSONATE_TTL_SECONDS = 3600;\nconst IMPERSONATE_JWT_SALT = \"impersonate.v1\";\n\nexport type ImpersonationClaims = {\n /** Admin user id who started the impersonation (stringified BigInt). */\n impersonatedBy: string;\n /** Target user id being impersonated (stringified BigInt). */\n sub: string;\n /** Issued-at (seconds since epoch). */\n iat: number;\n /** Expiry (seconds since epoch). */\n exp: number;\n};\n\nlet cachedSecret: string | null = null;\n\nasync function getAuthSecret(): Promise<string> {\n if (cachedSecret) return cachedSecret;\n const arn = process.env.AUTH_SECRET_ARN;\n const fromSm = arn ? await getSecret(arn) : null;\n const secret = fromSm ?? process.env.AUTH_SECRET;\n if (!secret) {\n throw new Error(\n \"AUTH_SECRET (or AUTH_SECRET_ARN) must be set to mint/verify impersonation tokens\",\n );\n }\n cachedSecret = secret;\n return secret;\n}\n\nexport async function mintImpersonationToken(args: {\n adminId: bigint | string;\n targetId: bigint | string;\n now?: Date;\n}): Promise<{ token: string; expiresAt: Date }> {\n const secret = await getAuthSecret();\n const nowSec = Math.floor((args.now?.getTime() ?? Date.now()) / 1000);\n const exp = nowSec + IMPERSONATE_TTL_SECONDS;\n const token = await encode({\n secret,\n salt: IMPERSONATE_JWT_SALT,\n maxAge: IMPERSONATE_TTL_SECONDS,\n token: {\n impersonatedBy: String(args.adminId),\n sub: String(args.targetId),\n iat: nowSec,\n exp,\n },\n });\n return { token, expiresAt: new Date(exp * 1000) };\n}\n\nexport async function verifyImpersonationToken(\n token: string,\n): Promise<ImpersonationClaims | null> {\n try {\n const secret = await getAuthSecret();\n const decoded = await decode({\n token,\n secret,\n salt: IMPERSONATE_JWT_SALT,\n });\n if (!decoded) return null;\n const impersonatedBy = decoded[\"impersonatedBy\"];\n const sub = decoded[\"sub\"];\n const iat = decoded[\"iat\"];\n const exp = decoded[\"exp\"];\n if (\n typeof impersonatedBy !== \"string\" ||\n typeof sub !== \"string\" ||\n typeof iat !== \"number\" ||\n typeof exp !== \"number\"\n ) {\n return null;\n }\n if (exp * 1000 < Date.now()) return null;\n return { impersonatedBy, sub, iat, exp };\n } catch {\n return null;\n }\n}\n","import \"server-only\";\nimport * as React from \"react\";\nimport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantRole,\n type TenantServerConfig,\n} from \"../tenant-types.js\";\n\n// =============================================================================\n// loadTenantConfig() -- the single source of truth for tenant configuration.\n//\n// Every required process.env read happens here. Missing fields are surfaced\n// in ONE error message so the deploy fails loudly instead of silently\n// substituting undefined into a downstream package.\n//\n// Apex apps call loadTenantConfig({ role: \"apex\" }). Spoke apps call\n// loadTenantConfig({ role: \"spoke\" }). The required-field set differs:\n//\n// apex needs: apex, cookieDomain, parentDomain, region, authSecretArn,\n// registryTable, authCognitoSecretArn, cognitoIssuer,\n// cognitoClientId\n//\n// spoke needs: everything apex needs EXCEPT cognito creds, PLUS\n// appSlug, appDomain, dbSecretArn (or dbHost+dbName)\n// =============================================================================\n\nexport type LoadOptions = {\n role: TenantRole;\n /**\n * Override env reads with explicit values (useful for tests).\n */\n overrides?: Partial<TenantServerConfig>;\n};\n\n/**\n * Read tenant configuration from process.env with optional overrides.\n * Throws a single Error listing every missing required field.\n */\nexport function loadTenantConfig(opts: LoadOptions): TenantServerConfig {\n const env = process.env;\n const o = opts.overrides ?? {};\n\n // Apex derives from the parent domain (strip leading dot) when not\n // explicitly set. This is the only env var we synthesize -- everything\n // else maps 1:1 to a process.env name.\n const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;\n const apexFallback = parentDomainRaw?.replace(/^\\./, \"\");\n\n const draft: Partial<TenantServerConfig> = {\n role: opts.role,\n apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,\n cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,\n parentDomain: parentDomainRaw,\n region: o.region ?? env.AWS_REGION ?? \"us-east-1\",\n appSlug: o.appSlug ?? env.APP_SLUG,\n appDomain: o.appDomain ?? env.APP_DOMAIN,\n authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,\n registryTable: o.registryTable ?? env.APP_REGISTRY_TABLE,\n authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,\n cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,\n cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,\n adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,\n dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,\n dbHost: o.dbHost ?? env.DB_HOST,\n dbName: o.dbName ?? env.DB_NAME,\n stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,\n stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN,\n };\n\n // Default appDomain to apex for apex apps.\n if (opts.role === \"apex\" && !draft.appDomain) {\n draft.appDomain = draft.apex;\n }\n\n const required: Array<{ key: keyof TenantServerConfig; env: string }> = [\n { key: \"apex\", env: \"APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)\" },\n { key: \"cookieDomain\", env: \"AUTH_COOKIE_DOMAIN\" },\n { key: \"parentDomain\", env: \"AUTH_ALLOWED_PARENT_DOMAIN\" },\n { key: \"authSecretArn\", env: \"AUTH_SECRET_ARN\" },\n { key: \"appDomain\", env: \"APP_DOMAIN\" },\n ];\n if (opts.role === \"apex\") {\n required.push(\n { key: \"authCognitoSecretArn\", env: \"AUTH_COGNITO_SECRET_ARN\" },\n { key: \"cognitoIssuer\", env: \"AUTH_COGNITO_ISSUER\" },\n { key: \"cognitoClientId\", env: \"AUTH_COGNITO_ID\" },\n );\n } else {\n required.push({ key: \"appSlug\", env: \"APP_SLUG\" });\n }\n\n // Skip validation during `next build`. The build phase has no real env\n // vars but evaluates module-init code (so we can't just lazy-throw).\n // At runtime in a Lambda the env is fully populated and validation runs\n // normally. NEXT_PHASE is set by next.js itself; the absence of\n // AWS_LAMBDA_FUNCTION_NAME means we're not actually in a request handler.\n const isBuildPhase =\n process.env.NEXT_PHASE === \"phase-production-build\" ||\n (!process.env.AWS_LAMBDA_FUNCTION_NAME && process.env.NODE_ENV === \"production\");\n if (isBuildPhase) {\n return draft as TenantServerConfig;\n }\n\n const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);\n if (missing.length > 0) {\n throw new Error(\n `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(\", \")}`,\n );\n }\n\n return draft as TenantServerConfig;\n}\n\n/**\n * Reduce a TenantServerConfig to the public-safe subset. Strips every\n * secret-arn so the result is safe to ship to the browser via\n * <TenantBootScript />.\n */\nexport function publicSubset(config: TenantServerConfig): TenantPublicConfig {\n return {\n apex: config.apex,\n cookieDomain: config.cookieDomain,\n parentDomain: config.parentDomain,\n region: config.region,\n appSlug: config.appSlug,\n appDomain: config.appDomain,\n role: config.role,\n };\n}\n\n// =============================================================================\n// <TenantBootScript /> -- server component that injects window.__TENANT__\n// before paint. Every client widget (ThemeProvider, RoleSwitcher, AppShell,\n// CartDrawer, etc.) reads from this global instead of receiving props\n// threaded through React context.\n//\n// The payload is JSON.stringify of a TYPED struct -- we control every field\n// shape. The </script> escape protects against rare-but-real \"config\n// contains </script>\" payloads.\n// =============================================================================\n\nconst INNER_HTML_PROP = \"dangerouslySetInnerHTML\";\n\nexport function TenantBootScript({ config }: { config: TenantPublicConfig }) {\n const payload = JSON.stringify(config).replace(/</g, \"\\\\u003c\");\n const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;\n const props: Record<string, unknown> = {};\n props[INNER_HTML_PROP] = { __html: body };\n return React.createElement(\"script\", props);\n}\n\nexport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"../tenant-types.js\";\n","// =============================================================================\n// TenantConfig -- the single struct every @augmenting-integrations package\n// consumes. Apex apps and spokes share the same type; spoke-only fields are\n// optional. The `role` discriminator tells loadTenantConfig() which fields\n// to demand.\n//\n// Public fields (apex + parent domain + slug) are safe to ship to the browser\n// via <TenantBootScript />. Secret-arn fields are server-only and never reach\n// the client bundle.\n// =============================================================================\n\nexport type TenantRole = \"apex\" | \"spoke\";\n\nexport type TenantPublicConfig = {\n /** The tenant apex FQDN, e.g. \"agency.aillc.link\". */\n apex: string;\n /**\n * Cookie Domain attribute. Always the apex (no leading dot needed -- the\n * browser implies it for shared cookies). Auth.js session cookie and the\n * theme x-theme/x-theme-variant cookies use this. Without it cookies are\n * host-only and the subdomain ecosystem breaks.\n */\n cookieDomain: string;\n /**\n * The registrable parent domain (e.g. \"aillc.link\"). Used by the auth\n * redirect callback to validate post-login callbacks back to any subdomain\n * of the tenant. Distinct from cookieDomain in two-level apex setups.\n */\n parentDomain: string;\n /** AWS region. Default: us-east-1. */\n region: string;\n /**\n * For spoke apps: this spoke's slug (matches app registry primary key).\n * For apex: undefined.\n */\n appSlug?: string;\n /**\n * For spoke apps: this spoke's FQDN (e.g. \"leads.agency.aillc.link\").\n * For apex: same as `apex`.\n */\n appDomain: string;\n /** \"apex\" or \"spoke\". Affects which secret-arn fields are required. */\n role: TenantRole;\n};\n\nexport type TenantServerConfig = TenantPublicConfig & {\n /** AUTH_SECRET ARN in Secrets Manager. Used by createAuth(). */\n authSecretArn: string;\n /** App registry DynamoDB table name. Apex owns the table; spokes read. */\n registryTable: string;\n /** Cognito client secret ARN. Apex only -- spokes don't run the OAuth dance. */\n authCognitoSecretArn?: string;\n /** Cognito issuer URL (apex only). */\n cognitoIssuer?: string;\n /** Cognito client ID (apex only). */\n cognitoClientId?: string;\n /** Comma-separated admin emails (auto-promoted on first sign-in). */\n adminEmails?: string;\n /** Aurora connection secret ARN (spoke only). */\n dbSecretArn?: string;\n /** Aurora endpoint host (spoke only). */\n dbHost?: string;\n /** Aurora database name (spoke only). */\n dbName?: string;\n /** Stripe credentials bundle ARN (spoke that does billing). */\n stripeSecretArn?: string;\n /** Stripe webhook signing secret ARN (spoke that does billing). */\n stripeWebhookSecretArn?: string;\n};\n\nexport const TENANT_GLOBAL_KEY = \"__TENANT__\" as const;\n\ndeclare global {\n interface Window {\n [TENANT_GLOBAL_KEY]?: TenantPublicConfig;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/server/createAuth.ts","../src/server/jit.ts","../src/server/impersonation.ts","../src/server/tenant.ts","../src/tenant-types.ts"],"sourcesContent":["// Auth.js v5 (the package is still distributed as `next-auth`, but treat\n// these as Auth.js v5 internally — docs at https://authjs.dev, NOT\n// next-auth.js.org which is v4 and incompatible).\n//\n// Subdomain ecosystem model:\n// - One Cognito User Pool per tenant.\n// - One Cognito App Client with ONE callback URL at the apex\n// (https://<apex>/api/auth/callback/cognito).\n// - The apex app is the auth broker. Subdomain apps redirect through it.\n// - Session cookie scoped to Domain=.<apex> so every subdomain sees it.\n// - All apps use the same createAuth() invocation; the package derives\n// the right signInPage from appDomain + allowedParentDomain.\n//\n// Provider strategy:\n// - Production: Cognito OIDC. cognito:groups drives session.user.groups.\n// - Dev / preview: Credentials with a role picker, shaped to mirror\n// Cognito's claim payload (same groups, sub, email).\n\nimport NextAuth, {\n type DefaultSession,\n type NextAuthConfig,\n type Session,\n} from \"next-auth\";\nimport Credentials from \"next-auth/providers/credentials\";\nimport Cognito from \"next-auth/providers/cognito\";\nimport type { TenantServerConfig } from \"../tenant-types.js\";\n\n// Session augmentation: `groups` is the Cognito-groups array surfaced from\n// the JWT. `role` lives on the application User row (Prisma), exposed via\n// `useAppUser()` from `@augmenting-integrations/auth/client`. There is one\n// source of role-truth; do not add a session-level `role` field back here.\ndeclare module \"next-auth\" {\n interface Session {\n user: {\n groups: string[];\n } & DefaultSession[\"user\"];\n }\n interface User {\n groups?: string[];\n }\n}\n\nexport type CreateAuthOptions = {\n /**\n * Full tenant configuration. Provides apex/cookieDomain/parentDomain/\n * appDomain/role + cognito client id + issuer + allowed admin emails.\n * Load via `loadTenantConfig()` from `@augmenting-integrations/tenant/server`.\n */\n tenant: TenantServerConfig;\n /** Path prefixes that require an authenticated session. */\n authedRoutePrefixes: string[];\n /**\n * JWT signing secret. Caller fetches from Secrets Manager via\n * `getSecret(tenant.authSecretArn)` and passes the resolved value here.\n * The library doesn't bundle AWS SDK reads itself.\n */\n authSecret: string;\n /**\n * Cognito OAuth client secret. Apex apps only -- spokes never run the\n * OAuth dance. Caller fetches from Secrets Manager via\n * `getSecret(tenant.authCognitoSecretArn)`.\n */\n cognitoClientSecret?: string;\n /**\n * Override the auto-derived sign-in page (rarely needed). Default:\n * apex apps get `/login`; spoke apps get `https://<apex>/login`.\n */\n signInPage?: string;\n /** Override prod/dev detection. Default reads NODE_ENV. */\n isProd?: boolean;\n};\n\n// ----- AuthError used by requireGroup -----\n\nexport class AuthError extends Error {\n constructor(public code: \"unauthenticated\" | \"forbidden\") {\n super(code);\n this.name = \"AuthError\";\n }\n}\n\n// ----- Group/authorization helpers -----\n\n/** Returns the user's Cognito groups (always an array, possibly empty). */\nexport function getUserGroups(session: Session | null | undefined): string[] {\n return session?.user?.groups ?? [];\n}\n\n/** Case-insensitive group membership check. */\nexport function hasGroup(session: Session | null | undefined, name: string): boolean {\n if (!session) return false;\n const target = name.toLowerCase();\n return getUserGroups(session).some((g) => g.toLowerCase() === target);\n}\n\n/**\n * Throws AuthError if no session (`unauthenticated`) or if the user is in\n * none of the provided groups (`forbidden`). Pass multiple names to allow\n * any-of.\n */\nexport function requireGroup(\n session: Session | null | undefined,\n ...names: string[]\n): void {\n if (!session) throw new AuthError(\"unauthenticated\");\n if (names.length === 0) return;\n const ok = names.some((n) => hasGroup(session, n));\n if (!ok) throw new AuthError(\"forbidden\");\n}\n\n// ----- Redirect callback factory -----\n\nfunction buildRedirectCallback(parentDomain: string) {\n return ({ url, baseUrl }: { url: string; baseUrl: string }): string => {\n try {\n const target = new URL(url, baseUrl);\n const apex = parentDomain.replace(/^\\./, \"\").toLowerCase();\n const host = target.hostname.toLowerCase();\n const ok = host === apex || host.endsWith(`.${apex}`);\n return ok ? target.toString() : baseUrl;\n } catch {\n return baseUrl;\n }\n };\n}\n\n// ----- Sign-in page auto-derivation -----\n\nfunction deriveSignInPage(args: {\n signInPage: string | undefined;\n appDomain: string;\n apex: string;\n}): string {\n if (args.signInPage) return args.signInPage;\n return args.appDomain === args.apex ? \"/login\" : `https://${args.apex}/login`;\n}\n\n// ----- Main factory -----\n\nexport function createAuth(opts: CreateAuthOptions) {\n const { tenant, authSecret, cognitoClientSecret } = opts;\n const isProd = opts.isProd ?? process.env.NODE_ENV === \"production\";\n const cookieDomain = isProd ? tenant.cookieDomain : undefined;\n\n // Skip runtime assertions during next build (env-less, evaluates\n // module-init code but never serves requests).\n const isBuildPhase = process.env.NEXT_PHASE === \"phase-production-build\";\n\n if (tenant.role === \"apex\" && !isBuildPhase) {\n if (!tenant.cognitoClientId || !tenant.cognitoIssuer) {\n throw new Error(\n \"createAuth: tenant.role='apex' requires cognitoClientId + cognitoIssuer. Check loadTenantConfig() inputs.\",\n );\n }\n if (isProd && !cognitoClientSecret) {\n throw new Error(\n \"createAuth: tenant.role='apex' in production requires cognitoClientSecret. Fetch via getSecret(tenant.authCognitoSecretArn) and pass it in.\",\n );\n }\n }\n\n const signInPage = deriveSignInPage({\n signInPage: opts.signInPage,\n appDomain: tenant.appDomain,\n apex: tenant.apex,\n });\n\n const config: NextAuthConfig = {\n secret: authSecret,\n cookies: cookieDomain\n ? {\n sessionToken: {\n name: \"authjs.session-token\",\n options: {\n domain: cookieDomain,\n sameSite: \"lax\",\n secure: true,\n httpOnly: true,\n path: \"/\",\n },\n },\n }\n : undefined,\n providers: isProd\n ? [\n Cognito({\n clientId: tenant.cognitoClientId,\n clientSecret: cognitoClientSecret,\n issuer: tenant.cognitoIssuer,\n }),\n ]\n : [\n Credentials({\n name: \"Mock role (dev only)\",\n credentials: {\n role: {\n label: \"Role\",\n type: \"text\",\n placeholder: \"any role string\",\n },\n },\n authorize: async (credentials) => {\n const role = credentials?.role as string | undefined;\n if (!role) return null;\n const display = role.charAt(0).toUpperCase() + role.slice(1);\n return {\n id: `mock-${role}`,\n name: `${display} (mock)`,\n email: `${role}@example.local`,\n role,\n groups: [role],\n };\n },\n }),\n ],\n session: { strategy: \"jwt\" },\n callbacks: {\n jwt: ({ token, user, profile }) => {\n if (user) {\n token.sub ??= user.id ?? undefined;\n token.email ??= user.email ?? undefined;\n if (!isProd) {\n const u = user as { groups?: string[]; role?: string };\n const groups = u.groups ?? (u.role ? [u.role] : []);\n if (groups.length > 0) {\n (token as Record<string, unknown>)[\"cognito:groups\"] = groups;\n }\n }\n }\n if (isProd && profile) {\n const groups = (profile as Record<string, unknown>)[\"cognito:groups\"];\n if (groups) {\n (token as Record<string, unknown>)[\"cognito:groups\"] = groups;\n }\n }\n return token;\n },\n session: ({ session, token }) => {\n const groups =\n ((token as Record<string, unknown>)[\"cognito:groups\"] as\n | string[]\n | undefined) ?? [];\n session.user.groups = groups;\n return session;\n },\n authorized: ({ auth: session, request: { nextUrl } }) => {\n const path = nextUrl.pathname;\n const isAuthedRoute = opts.authedRoutePrefixes.some(\n (prefix) => path === prefix || path.startsWith(`${prefix}/`),\n );\n if (!session && isAuthedRoute) {\n // For subdomain apps signInPage is an absolute URL on the apex\n // broker. Auth.js's default middleware redirect treats\n // pages.signIn as a relative path and prepends the current\n // host, producing malformed Location URLs like\n // https://sub.<apex>/https://<apex>/login. Returning an\n // explicit Response.redirect bypasses that path and sends the\n // user to the apex broker correctly.\n if (signInPage.startsWith(\"http\")) {\n const target = new URL(signInPage);\n target.searchParams.set(\"callbackUrl\", nextUrl.href);\n return Response.redirect(target.toString(), 302);\n }\n return false;\n }\n return true;\n },\n redirect: buildRedirectCallback(tenant.parentDomain),\n },\n pages: { signIn: signInPage },\n trustHost: true,\n };\n\n return NextAuth(config);\n}\n\nexport type { NextAuthConfig } from \"next-auth\";\n","import \"server-only\";\n\nimport { cookies } from \"next/headers\";\nimport type { Session } from \"next-auth\";\n\nimport { IMPERSONATE_COOKIE_NAME, verifyImpersonationToken } from \"./impersonation.js\";\n\n// =============================================================================\n// JIT user provisioning factory.\n//\n// Pattern: every authed request hands a session into getOrCreateAppUser() to\n// resolve the DB User row (creating one on first sign-in for that email).\n// The factory pattern lets each spoke configure:\n//\n// - `db`: how to reach Prisma (the library doesn't bundle the client)\n// - `defaultRole`: fallback when Cognito groups + ADMIN_EMAILS don't decide\n// - `computeCreditBalance(role)`: starting credit balance per role\n// - `adminEmails`: CSV of emails auto-promoted to admin on first sign-in\n// - `placeholderPasswordHash`: schema-inherited not-null constraint filler\n//\n// Impersonation short-circuit (runs BEFORE the session-driven lookup): if\n// `__impersonate` cookie is present and verifies against AUTH_SECRET, and the\n// underlying admin still exists with role==='admin', returns the *target* user\n// with `impersonatedBy` set to the admin's stringified id. Orphaned tokens\n// silently fall through to the session user.\n//\n// Invitation auto-accept: if a pending Invitation row exists for this email\n// (accepted_at IS NULL, expires_at > now), the new User inherits the\n// invitation's parent_id and intended_role and the invitation is marked\n// accepted in the same transaction.\n// =============================================================================\n\n/**\n * Minimum contract every spoke User row must satisfy. Spokes can widen this\n * with additional fields (credit_balance, must_change_password, etc.) and the\n * factory will preserve them through the returned `Promise<TUser>`.\n */\nexport type BaseAppUser = {\n id: bigint | string | number;\n email: string;\n name: string;\n role: string;\n parent_id: bigint | string | number | null;\n};\n\n/**\n * Loose typing for the Prisma delegates the factory touches. Each spoke has\n * its own generated client whose actual types are concrete; we use loose\n * shapes here so the factory works with any spoke's schema.\n */\nexport type PrismaLikeUserDelegate<TUser> = {\n findUnique: (args: {\n where: { id?: unknown; email?: string };\n }) => Promise<TUser | null>;\n create: (args: { data: unknown }) => Promise<TUser>;\n};\n\nexport type PrismaLikeInvitationDelegate = {\n findFirst: (args: {\n where: { email: string; accepted_at: null; expires_at: { gt: Date } };\n orderBy?: unknown;\n }) => Promise<{\n id: bigint | string | number;\n intended_role: string;\n parent_id: bigint | string | number | null;\n } | null>;\n update: (args: {\n where: { id: unknown };\n data: { accepted_at: Date; accepted_by_user_id: unknown };\n }) => Promise<unknown>;\n};\n\nexport type PrismaLikeClient<TUser> = {\n user: PrismaLikeUserDelegate<TUser>;\n invitation: PrismaLikeInvitationDelegate;\n $transaction: <T>(\n fn: (tx: {\n user: PrismaLikeUserDelegate<TUser>;\n invitation: PrismaLikeInvitationDelegate;\n }) => Promise<T>,\n ) => Promise<T>;\n};\n\nexport type CreateGetOrCreateAppUserOptions<TUser extends BaseAppUser> = {\n /** Returns the spoke's PrismaClient (lazily). */\n db: () => Promise<PrismaLikeClient<TUser>>;\n /** Fallback role when no admin email + no Cognito groups. */\n defaultRole: string;\n /** Starting credit balance per role. */\n computeCreditBalance: (role: string) => number;\n /** Emails auto-promoted to \"admin\" role on first sign-in (case-insensitive). */\n adminEmails?: string[];\n /**\n * Hash value written to User.password on creation. Schema-inherited\n * not-null constraint; never used to authenticate (Cognito does that).\n * Default: a recognizable placeholder string.\n */\n placeholderPasswordHash?: string;\n /**\n * Extra column values written on creation. Use this for spoke-specific\n * defaults (e.g. is_active: true, must_change_password: false).\n */\n extraCreateFields?: Record<string, unknown>;\n};\n\nexport type AppUserWithImpersonation<TUser extends BaseAppUser> = TUser & {\n /** Stringified admin id when this session is impersonated; absent otherwise. */\n impersonatedBy?: string;\n};\n\nconst DEFAULT_PLACEHOLDER_HASH =\n \"$2y$12$.cognito-managed.never.used-for-login.placeholder\";\n\n/**\n * Build a `getOrCreateAppUser(session)` function configured for this spoke.\n *\n * Returned function is idempotent: subsequent calls with the same email\n * return the existing row. First-time emails are created inside a transaction\n * that also auto-accepts a matching Invitation row if present.\n */\nexport function createGetOrCreateAppUser<TUser extends BaseAppUser>(\n opts: CreateGetOrCreateAppUserOptions<TUser>,\n): (session: Session) => Promise<AppUserWithImpersonation<TUser>> {\n const adminEmailsLower = (opts.adminEmails ?? []).map((s) => s.toLowerCase());\n const placeholder = opts.placeholderPasswordHash ?? DEFAULT_PLACEHOLDER_HASH;\n\n return async function getOrCreateAppUser(\n session: Session,\n ): Promise<AppUserWithImpersonation<TUser>> {\n const email = session.user?.email;\n if (!email) {\n throw new Error(\"getOrCreateAppUser called with a session that has no user.email\");\n }\n\n const db = await opts.db();\n\n // -- Impersonation short-circuit (before the session-driven lookup) --\n try {\n const cookieStore = await cookies();\n const cookie = cookieStore.get(IMPERSONATE_COOKIE_NAME);\n if (cookie?.value) {\n const claims = await verifyImpersonationToken(cookie.value);\n if (claims) {\n const [admin, target] = await Promise.all([\n db.user.findUnique({ where: { id: claims.impersonatedBy } }),\n db.user.findUnique({ where: { id: claims.sub } }),\n ]);\n if (admin && admin.role === \"admin\" && target) {\n return Object.assign(target, {\n impersonatedBy: claims.impersonatedBy,\n });\n }\n // Orphaned/expired admin or target -- fall through silently.\n }\n }\n } catch {\n // No cookie context (called from a non-request scope) -- ignore.\n }\n\n const existing = await db.user.findUnique({ where: { email } });\n if (existing) return existing;\n\n // -- New user provisioning --\n const groups = (session.user as { groups?: string[] }).groups ?? [];\n const fallbackRole = adminEmailsLower.includes(email.toLowerCase())\n ? \"admin\"\n : (groups[0] ?? opts.defaultRole);\n const name = (session.user as { name?: string | null }).name ?? email.split(\"@\")[0]!;\n\n return db.$transaction(async (tx) => {\n const pendingInvite = await tx.invitation.findFirst({\n where: {\n email,\n accepted_at: null,\n expires_at: { gt: new Date() },\n },\n orderBy: { created_at: \"desc\" },\n });\n\n const role = pendingInvite ? pendingInvite.intended_role : fallbackRole;\n const parent_id = pendingInvite ? pendingInvite.parent_id : null;\n\n const created = await tx.user.create({\n data: {\n email,\n name,\n role,\n parent_id,\n password: placeholder,\n credit_balance: opts.computeCreditBalance(role),\n ...(opts.extraCreateFields ?? {}),\n },\n });\n\n if (pendingInvite) {\n await tx.invitation.update({\n where: { id: pendingInvite.id },\n data: {\n accepted_at: new Date(),\n accepted_by_user_id: created.id,\n },\n });\n }\n\n return created;\n });\n };\n}\n","import \"server-only\";\n\nimport { encode, decode } from \"next-auth/jwt\";\nimport { getSecret } from \"@augmenting-integrations/aws/server\";\n\n// =============================================================================\n// Impersonation cookie + JWT helpers.\n//\n// Pattern: an admin issues POST /api/admin/users/:id/impersonate, which mints\n// a short-lived JWT and sets it as the `__impersonate` httpOnly cookie. On\n// every subsequent authed request, getOrCreateAppUser reads the cookie,\n// verifies the JWT against AUTH_SECRET, and -- if valid -- returns the\n// *target* user instead of the session user with `impersonatedBy` set.\n//\n// The cookie does NOT replace the next-auth session cookie. It is read\n// alongside the session. Invalid / expired tokens silently fall through.\n//\n// JWT library: next-auth re-exports @auth/core's `encode` / `decode` (JWE).\n// Salted differently from session tokens so they can't be cross-replayed.\n// =============================================================================\n\nexport const IMPERSONATE_COOKIE_NAME = \"__impersonate\";\nexport const IMPERSONATE_TTL_SECONDS = 3600;\nconst IMPERSONATE_JWT_SALT = \"impersonate.v1\";\n\nexport type ImpersonationClaims = {\n /** Admin user id who started the impersonation (stringified BigInt). */\n impersonatedBy: string;\n /** Target user id being impersonated (stringified BigInt). */\n sub: string;\n /** Issued-at (seconds since epoch). */\n iat: number;\n /** Expiry (seconds since epoch). */\n exp: number;\n};\n\nlet cachedSecret: string | null = null;\n\nasync function getAuthSecret(): Promise<string> {\n if (cachedSecret) return cachedSecret;\n const arn = process.env.AUTH_SECRET_ARN;\n const fromSm = arn ? await getSecret(arn) : null;\n const secret = fromSm ?? process.env.AUTH_SECRET;\n if (!secret) {\n throw new Error(\n \"AUTH_SECRET (or AUTH_SECRET_ARN) must be set to mint/verify impersonation tokens\",\n );\n }\n cachedSecret = secret;\n return secret;\n}\n\nexport async function mintImpersonationToken(args: {\n adminId: bigint | string;\n targetId: bigint | string;\n now?: Date;\n}): Promise<{ token: string; expiresAt: Date }> {\n const secret = await getAuthSecret();\n const nowSec = Math.floor((args.now?.getTime() ?? Date.now()) / 1000);\n const exp = nowSec + IMPERSONATE_TTL_SECONDS;\n const token = await encode({\n secret,\n salt: IMPERSONATE_JWT_SALT,\n maxAge: IMPERSONATE_TTL_SECONDS,\n token: {\n impersonatedBy: String(args.adminId),\n sub: String(args.targetId),\n iat: nowSec,\n exp,\n },\n });\n return { token, expiresAt: new Date(exp * 1000) };\n}\n\nexport async function verifyImpersonationToken(\n token: string,\n): Promise<ImpersonationClaims | null> {\n try {\n const secret = await getAuthSecret();\n const decoded = await decode({\n token,\n secret,\n salt: IMPERSONATE_JWT_SALT,\n });\n if (!decoded) return null;\n const impersonatedBy = decoded[\"impersonatedBy\"];\n const sub = decoded[\"sub\"];\n const iat = decoded[\"iat\"];\n const exp = decoded[\"exp\"];\n if (\n typeof impersonatedBy !== \"string\" ||\n typeof sub !== \"string\" ||\n typeof iat !== \"number\" ||\n typeof exp !== \"number\"\n ) {\n return null;\n }\n if (exp * 1000 < Date.now()) return null;\n return { impersonatedBy, sub, iat, exp };\n } catch {\n return null;\n }\n}\n","import \"server-only\";\nimport * as React from \"react\";\nimport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantRole,\n type TenantServerConfig,\n} from \"../tenant-types.js\";\n\n// =============================================================================\n// loadTenantConfig() -- the single source of truth for tenant configuration.\n//\n// Every required process.env read happens here. Missing fields are surfaced\n// in ONE error message so the deploy fails loudly instead of silently\n// substituting undefined into a downstream package.\n//\n// Apex apps call loadTenantConfig({ role: \"apex\" }). Spoke apps call\n// loadTenantConfig({ role: \"spoke\" }). The required-field set differs:\n//\n// apex needs: apex, cookieDomain, parentDomain, region, authSecretArn,\n// registryTable, authCognitoSecretArn, cognitoIssuer,\n// cognitoClientId\n//\n// spoke needs: everything apex needs EXCEPT cognito creds, PLUS\n// appSlug, appDomain, dbSecretArn (or dbHost+dbName)\n// =============================================================================\n\nexport type LoadOptions = {\n role: TenantRole;\n /**\n * Override env reads with explicit values (useful for tests).\n */\n overrides?: Partial<TenantServerConfig>;\n};\n\n/**\n * Read tenant configuration from process.env with optional overrides.\n * Throws a single Error listing every missing required field.\n */\nexport function loadTenantConfig(opts: LoadOptions): TenantServerConfig {\n const env = process.env;\n const o = opts.overrides ?? {};\n\n // Apex derives from the parent domain (strip leading dot) when not\n // explicitly set. This is the only env var we synthesize -- everything\n // else maps 1:1 to a process.env name.\n const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;\n const apexFallback = parentDomainRaw?.replace(/^\\./, \"\");\n\n const draft: Partial<TenantServerConfig> = {\n role: opts.role,\n apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,\n cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,\n parentDomain: parentDomainRaw,\n region: o.region ?? env.AWS_REGION ?? \"us-east-1\",\n appSlug: o.appSlug ?? env.APP_SLUG,\n appDomain: o.appDomain ?? env.APP_DOMAIN,\n authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,\n registryTable: o.registryTable ?? env.APP_REGISTRY_TABLE,\n authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,\n cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,\n cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,\n adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,\n dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,\n dbHost: o.dbHost ?? env.DB_HOST,\n dbName: o.dbName ?? env.DB_NAME,\n stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,\n stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN,\n };\n\n // Default appDomain to apex for apex apps.\n if (opts.role === \"apex\" && !draft.appDomain) {\n draft.appDomain = draft.apex;\n }\n\n const required: Array<{ key: keyof TenantServerConfig; env: string }> = [\n { key: \"apex\", env: \"APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)\" },\n { key: \"cookieDomain\", env: \"AUTH_COOKIE_DOMAIN\" },\n { key: \"parentDomain\", env: \"AUTH_ALLOWED_PARENT_DOMAIN\" },\n { key: \"authSecretArn\", env: \"AUTH_SECRET_ARN\" },\n { key: \"appDomain\", env: \"APP_DOMAIN\" },\n ];\n if (opts.role === \"apex\") {\n required.push(\n { key: \"authCognitoSecretArn\", env: \"AUTH_COGNITO_SECRET_ARN\" },\n { key: \"cognitoIssuer\", env: \"AUTH_COGNITO_ISSUER\" },\n { key: \"cognitoClientId\", env: \"AUTH_COGNITO_ID\" },\n );\n } else {\n required.push({ key: \"appSlug\", env: \"APP_SLUG\" });\n }\n\n // Skip validation during `next build`. NEXT_PHASE is set by next.js\n // itself during the production build step; module-init code evaluates\n // but no requests are served. ANY other phase (dev server, prod runtime,\n // localhost E2E) must enforce the required fields.\n if (process.env.NEXT_PHASE === \"phase-production-build\") {\n return draft as TenantServerConfig;\n }\n\n const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);\n if (missing.length > 0) {\n throw new Error(\n `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(\", \")}`,\n );\n }\n\n return draft as TenantServerConfig;\n}\n\n/**\n * Reduce a TenantServerConfig to the public-safe subset. Strips every\n * secret-arn so the result is safe to ship to the browser via\n * <TenantBootScript />.\n */\nexport function publicSubset(config: TenantServerConfig): TenantPublicConfig {\n return {\n apex: config.apex,\n cookieDomain: config.cookieDomain,\n parentDomain: config.parentDomain,\n region: config.region,\n appSlug: config.appSlug,\n appDomain: config.appDomain,\n role: config.role,\n };\n}\n\n// =============================================================================\n// <TenantBootScript /> -- server component that injects window.__TENANT__\n// before paint. Every client widget (ThemeProvider, RoleSwitcher, AppShell,\n// CartDrawer, etc.) reads from this global instead of receiving props\n// threaded through React context.\n//\n// The payload is JSON.stringify of a TYPED struct -- we control every field\n// shape. The </script> escape protects against rare-but-real \"config\n// contains </script>\" payloads.\n// =============================================================================\n\nconst INNER_HTML_PROP = \"dangerouslySetInnerHTML\";\n\nexport function TenantBootScript({ config }: { config: TenantPublicConfig }) {\n const payload = JSON.stringify(config).replace(/</g, \"\\\\u003c\");\n const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;\n const props: Record<string, unknown> = {};\n props[INNER_HTML_PROP] = { __html: body };\n return React.createElement(\"script\", props);\n}\n\nexport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"../tenant-types.js\";\n","// =============================================================================\n// TenantConfig -- the single struct every @augmenting-integrations package\n// consumes. Apex apps and spokes share the same type; spoke-only fields are\n// optional. The `role` discriminator tells loadTenantConfig() which fields\n// to demand.\n//\n// Public fields (apex + parent domain + slug) are safe to ship to the browser\n// via <TenantBootScript />. Secret-arn fields are server-only and never reach\n// the client bundle.\n// =============================================================================\n\nexport type TenantRole = \"apex\" | \"spoke\";\n\nexport type TenantPublicConfig = {\n /** The tenant apex FQDN, e.g. \"agency.aillc.link\". */\n apex: string;\n /**\n * Cookie Domain attribute. Always the apex (no leading dot needed -- the\n * browser implies it for shared cookies). Auth.js session cookie and the\n * theme x-theme/x-theme-variant cookies use this. Without it cookies are\n * host-only and the subdomain ecosystem breaks.\n */\n cookieDomain: string;\n /**\n * The registrable parent domain (e.g. \"aillc.link\"). Used by the auth\n * redirect callback to validate post-login callbacks back to any subdomain\n * of the tenant. Distinct from cookieDomain in two-level apex setups.\n */\n parentDomain: string;\n /** AWS region. Default: us-east-1. */\n region: string;\n /**\n * For spoke apps: this spoke's slug (matches app registry primary key).\n * For apex: undefined.\n */\n appSlug?: string;\n /**\n * For spoke apps: this spoke's FQDN (e.g. \"leads.agency.aillc.link\").\n * For apex: same as `apex`.\n */\n appDomain: string;\n /** \"apex\" or \"spoke\". Affects which secret-arn fields are required. */\n role: TenantRole;\n};\n\nexport type TenantServerConfig = TenantPublicConfig & {\n /** AUTH_SECRET ARN in Secrets Manager. Used by createAuth(). */\n authSecretArn: string;\n /** App registry DynamoDB table name. Apex owns the table; spokes read. */\n registryTable: string;\n /** Cognito client secret ARN. Apex only -- spokes don't run the OAuth dance. */\n authCognitoSecretArn?: string;\n /** Cognito issuer URL (apex only). */\n cognitoIssuer?: string;\n /** Cognito client ID (apex only). */\n cognitoClientId?: string;\n /** Comma-separated admin emails (auto-promoted on first sign-in). */\n adminEmails?: string;\n /** Aurora connection secret ARN (spoke only). */\n dbSecretArn?: string;\n /** Aurora endpoint host (spoke only). */\n dbHost?: string;\n /** Aurora database name (spoke only). */\n dbName?: string;\n /** Stripe credentials bundle ARN (spoke that does billing). */\n stripeSecretArn?: string;\n /** Stripe webhook signing secret ARN (spoke that does billing). */\n stripeWebhookSecretArn?: string;\n};\n\nexport const TENANT_GLOBAL_KEY = \"__TENANT__\" as const;\n\ndeclare global {\n interface Window {\n [TENANT_GLOBAL_KEY]?: TenantPublicConfig;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@augmenting-integrations/auth",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.5",
|
|
4
4
|
"description": "Auth.js v5 factory + JIT user provisioning + impersonation + client-side user menu / sign-out. Subpath exports: /server (createAuth, JIT, impersonation token mint/verify) and /client (AppUserProvider, useAppUser, UserMenu, SignOutButton, ImpersonationBanner).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"next": "^16.0.0",
|
|
34
34
|
"next-auth": "^5.0.0-beta.31",
|
|
35
35
|
"react": "^19.0.0",
|
|
36
|
-
"@augmenting-integrations/aws": "8.0.
|
|
36
|
+
"@augmenting-integrations/aws": "8.0.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "^19.0.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"tsup": "^8.3.5",
|
|
44
44
|
"typescript": "^5.7.2",
|
|
45
45
|
"vitest": "^4.1.5",
|
|
46
|
-
"@augmenting-integrations/aws": "8.0.
|
|
46
|
+
"@augmenting-integrations/aws": "8.0.5"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsup",
|