@augmenting-integrations/auth 5.0.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,11 +3,9 @@ declare module "next-auth" {
3
3
  interface Session {
4
4
  user: {
5
5
  groups: string[];
6
- role: string;
7
6
  } & DefaultSession["user"];
8
7
  }
9
8
  interface User {
10
- role?: string;
11
9
  groups?: string[];
12
10
  }
13
11
  }
@@ -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;AAInB,OAAO,QAAQ,WAAW,CAAC;IACzB,UAAU,OAAO;QACf,IAAI,EAAE;YACJ,MAAM,EAAE,MAAM,EAAE,CAAC;YACjB,IAAI,EAAE,MAAM,CAAC;SACd,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;KAC5B;IACD,UAAU,IAAI;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB;CACF;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,2DAA2D;IAC3D,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,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;AAyFD,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,sCAgJjD;AAED,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
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;AAQnB,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,2DAA2D;IAC3D,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,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;AAkFD,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,sCA+IjD;AAED,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
package/dist/server.cjs CHANGED
@@ -116,12 +116,6 @@ function deriveSignInPage(args) {
116
116
  }
117
117
  return "/login";
118
118
  }
119
- function roleFromGroups(groups) {
120
- if (Array.isArray(groups) && groups.length > 0) {
121
- return String(groups[0]).toLowerCase();
122
- }
123
- return "visitor";
124
- }
125
119
  function createAuth(opts) {
126
120
  const isProd = opts.isProd ?? process.env.NODE_ENV === "production";
127
121
  const cookieDomain = isProd ? opts.cookieDomain ?? process.env.AUTH_COOKIE_DOMAIN : void 0;
@@ -215,7 +209,6 @@ function createAuth(opts) {
215
209
  session: ({ session, token }) => {
216
210
  const groups = token["cognito:groups"] ?? [];
217
211
  session.user.groups = groups;
218
- session.user.role = roleFromGroups(groups);
219
212
  return session;
220
213
  },
221
214
  authorized: ({ auth: session, request: { nextUrl } }) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/server/createAuth.ts","../src/server/jit.ts","../src/server/impersonation.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\";\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\";\n\ndeclare module \"next-auth\" {\n interface Session {\n user: {\n groups: string[];\n role: string;\n } & DefaultSession[\"user\"];\n }\n interface User {\n role?: string;\n groups?: string[];\n }\n}\n\nexport type CreateAuthOptions = {\n /** Path prefixes that require an authenticated session. */\n authedRoutePrefixes: string[];\n /**\n * Page to redirect to when an unauthed user hits a gated route.\n * If omitted, derived automatically from appDomain + allowedParentDomain:\n * apex app gets `/login`; subdomain apps get `https://<apex>/login`.\n */\n signInPage?: string;\n /**\n * Cookie Domain attribute. In subdomain ecosystems, set to the parent\n * (e.g. `.agency.aillc.link`). Default: process.env.AUTH_COOKIE_DOMAIN.\n * In dev (NODE_ENV !== \"production\") this is ignored — cookies stay\n * host-only so per-port localhost apps don't collide.\n */\n cookieDomain?: string;\n /**\n * The parent domain that all subdomain apps share (e.g.\n * `.agency.aillc.link`). The redirect callback uses this to allow\n * post-login redirects back to any subdomain of the parent (apex or\n * `<sub>.agency.aillc.link`). Default: process.env.AUTH_ALLOWED_PARENT_DOMAIN.\n */\n allowedParentDomain?: string;\n /**\n * This app's full FQDN (e.g. `agency.aillc.link` for the apex app, or\n * `leads.agency.aillc.link` for a subdomain app). Used to derive the\n * default signInPage. Default: process.env.APP_DOMAIN.\n */\n appDomain?: string;\n /** Override prod/dev detection. Default reads NODE_ENV. */\n isProd?: boolean;\n /**\n * The JWT signing secret. Default: process.env.AUTH_SECRET.\n * In prod, pass this from a runtime fetch (Secrets Manager) to keep the\n * secret out of Lambda env vars and to support rotation without redeploy.\n */\n secret?: string;\n cognito?: {\n clientId?: string;\n clientSecret?: string;\n issuer?: string;\n };\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// ----- Env validation -----\n\nfunction validateProdEnv(args: {\n isProd: boolean;\n cookieDomain: string | undefined;\n allowedParentDomain: string | undefined;\n appDomain: string | undefined;\n secret: string | undefined;\n cognitoClientId: string | undefined;\n cognitoClientSecret: string | undefined;\n cognitoIssuer: string | undefined;\n}): void {\n if (!args.isProd) return;\n // Skip when not actually running inside an AWS Lambda. Cognito values\n // come from SSM dynamic refs in the deployed Lambda environment;\n // they're not present at `next build` time. Throwing here would break\n // the build with no actionable fix. AWS_LAMBDA_FUNCTION_NAME is set\n // only by the Lambda runtime, so its presence is a reliable runtime\n // marker. We also keep the NEXT_PHASE check as a belt-and-suspenders\n // exit for cases where the build env happens to expose Lambda-shaped\n // env vars (e.g. local sam local invoke).\n if (process.env.NEXT_PHASE === \"phase-production-build\") return;\n if (!process.env.AWS_LAMBDA_FUNCTION_NAME) return;\n const missing: string[] = [];\n if (!args.secret) missing.push(\"AUTH_SECRET\");\n if (!args.cognitoClientId) missing.push(\"AUTH_COGNITO_ID\");\n if (!args.cognitoClientSecret) missing.push(\"AUTH_COGNITO_SECRET\");\n if (!args.cognitoIssuer) missing.push(\"AUTH_COGNITO_ISSUER\");\n // Subdomain mode: if any of the three multi-domain values is set, all three must be.\n const hasAny = !!(args.cookieDomain || args.allowedParentDomain || args.appDomain);\n if (hasAny) {\n if (!args.cookieDomain) missing.push(\"AUTH_COOKIE_DOMAIN\");\n if (!args.allowedParentDomain) missing.push(\"AUTH_ALLOWED_PARENT_DOMAIN\");\n if (!args.appDomain) missing.push(\"APP_DOMAIN\");\n }\n if (missing.length > 0) {\n throw new Error(\n `[@augmenting-integrations/auth] Missing required prod env vars: ${missing.join(\n \", \",\n )}. Provide via createAuth() opts or process.env.`,\n );\n }\n}\n\n// ----- Redirect callback factory -----\n\nfunction buildRedirectCallback(allowedParentDomain: string | undefined) {\n return ({ url, baseUrl }: { url: string; baseUrl: string }): string => {\n try {\n const target = new URL(url, baseUrl);\n if (!allowedParentDomain) {\n return target.origin === new URL(baseUrl).origin ? target.toString() : baseUrl;\n }\n const apex = allowedParentDomain.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 | undefined;\n allowedParentDomain: string | undefined;\n}): string {\n if (args.signInPage) return args.signInPage;\n if (args.appDomain && args.allowedParentDomain) {\n const apex = args.allowedParentDomain.replace(/^\\./, \"\");\n return args.appDomain === apex ? \"/login\" : `https://${apex}/login`;\n }\n return \"/login\";\n}\n\nfunction roleFromGroups(groups: unknown): string {\n if (Array.isArray(groups) && groups.length > 0) {\n return String(groups[0]).toLowerCase();\n }\n return \"visitor\";\n}\n\n// ----- Main factory -----\n\nexport function createAuth(opts: CreateAuthOptions) {\n const isProd = opts.isProd ?? process.env.NODE_ENV === \"production\";\n\n const cookieDomain = isProd\n ? (opts.cookieDomain ?? process.env.AUTH_COOKIE_DOMAIN)\n : undefined;\n const allowedParentDomain =\n opts.allowedParentDomain ?? process.env.AUTH_ALLOWED_PARENT_DOMAIN;\n const appDomain = opts.appDomain ?? process.env.APP_DOMAIN;\n\n const SECRET =\n opts.secret ??\n process.env.AUTH_SECRET ??\n (isProd ? undefined : \"dev-only-fallback-not-for-prod\");\n const cognitoClientId = opts.cognito?.clientId ?? process.env.AUTH_COGNITO_ID;\n const cognitoClientSecret =\n opts.cognito?.clientSecret ?? process.env.AUTH_COGNITO_SECRET;\n const cognitoIssuer = opts.cognito?.issuer ?? process.env.AUTH_COGNITO_ISSUER;\n\n validateProdEnv({\n isProd,\n cookieDomain,\n allowedParentDomain,\n appDomain,\n secret: SECRET,\n cognitoClientId,\n cognitoClientSecret,\n cognitoIssuer,\n });\n\n const signInPage = deriveSignInPage({\n signInPage: opts.signInPage,\n appDomain,\n allowedParentDomain,\n });\n\n const config: NextAuthConfig = {\n secret: SECRET,\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: cognitoClientId,\n clientSecret: cognitoClientSecret,\n issuer: 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 session.user.role = roleFromGroups(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(allowedParentDomain),\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"],"mappings":"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","names":["Cognito","Credentials","NextAuth","import_server_only"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/server/createAuth.ts","../src/server/jit.ts","../src/server/impersonation.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\";\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\";\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 /** Path prefixes that require an authenticated session. */\n authedRoutePrefixes: string[];\n /**\n * Page to redirect to when an unauthed user hits a gated route.\n * If omitted, derived automatically from appDomain + allowedParentDomain:\n * apex app gets `/login`; subdomain apps get `https://<apex>/login`.\n */\n signInPage?: string;\n /**\n * Cookie Domain attribute. In subdomain ecosystems, set to the parent\n * (e.g. `.agency.aillc.link`). Default: process.env.AUTH_COOKIE_DOMAIN.\n * In dev (NODE_ENV !== \"production\") this is ignored — cookies stay\n * host-only so per-port localhost apps don't collide.\n */\n cookieDomain?: string;\n /**\n * The parent domain that all subdomain apps share (e.g.\n * `.agency.aillc.link`). The redirect callback uses this to allow\n * post-login redirects back to any subdomain of the parent (apex or\n * `<sub>.agency.aillc.link`). Default: process.env.AUTH_ALLOWED_PARENT_DOMAIN.\n */\n allowedParentDomain?: string;\n /**\n * This app's full FQDN (e.g. `agency.aillc.link` for the apex app, or\n * `leads.agency.aillc.link` for a subdomain app). Used to derive the\n * default signInPage. Default: process.env.APP_DOMAIN.\n */\n appDomain?: string;\n /** Override prod/dev detection. Default reads NODE_ENV. */\n isProd?: boolean;\n /**\n * The JWT signing secret. Default: process.env.AUTH_SECRET.\n * In prod, pass this from a runtime fetch (Secrets Manager) to keep the\n * secret out of Lambda env vars and to support rotation without redeploy.\n */\n secret?: string;\n cognito?: {\n clientId?: string;\n clientSecret?: string;\n issuer?: string;\n };\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// ----- Env validation -----\n\nfunction validateProdEnv(args: {\n isProd: boolean;\n cookieDomain: string | undefined;\n allowedParentDomain: string | undefined;\n appDomain: string | undefined;\n secret: string | undefined;\n cognitoClientId: string | undefined;\n cognitoClientSecret: string | undefined;\n cognitoIssuer: string | undefined;\n}): void {\n if (!args.isProd) return;\n // Skip when not actually running inside an AWS Lambda. Cognito values\n // come from SSM dynamic refs in the deployed Lambda environment;\n // they're not present at `next build` time. Throwing here would break\n // the build with no actionable fix. AWS_LAMBDA_FUNCTION_NAME is set\n // only by the Lambda runtime, so its presence is a reliable runtime\n // marker. We also keep the NEXT_PHASE check as a belt-and-suspenders\n // exit for cases where the build env happens to expose Lambda-shaped\n // env vars (e.g. local sam local invoke).\n if (process.env.NEXT_PHASE === \"phase-production-build\") return;\n if (!process.env.AWS_LAMBDA_FUNCTION_NAME) return;\n const missing: string[] = [];\n if (!args.secret) missing.push(\"AUTH_SECRET\");\n if (!args.cognitoClientId) missing.push(\"AUTH_COGNITO_ID\");\n if (!args.cognitoClientSecret) missing.push(\"AUTH_COGNITO_SECRET\");\n if (!args.cognitoIssuer) missing.push(\"AUTH_COGNITO_ISSUER\");\n // Subdomain mode: if any of the three multi-domain values is set, all three must be.\n const hasAny = !!(args.cookieDomain || args.allowedParentDomain || args.appDomain);\n if (hasAny) {\n if (!args.cookieDomain) missing.push(\"AUTH_COOKIE_DOMAIN\");\n if (!args.allowedParentDomain) missing.push(\"AUTH_ALLOWED_PARENT_DOMAIN\");\n if (!args.appDomain) missing.push(\"APP_DOMAIN\");\n }\n if (missing.length > 0) {\n throw new Error(\n `[@augmenting-integrations/auth] Missing required prod env vars: ${missing.join(\n \", \",\n )}. Provide via createAuth() opts or process.env.`,\n );\n }\n}\n\n// ----- Redirect callback factory -----\n\nfunction buildRedirectCallback(allowedParentDomain: string | undefined) {\n return ({ url, baseUrl }: { url: string; baseUrl: string }): string => {\n try {\n const target = new URL(url, baseUrl);\n if (!allowedParentDomain) {\n return target.origin === new URL(baseUrl).origin ? target.toString() : baseUrl;\n }\n const apex = allowedParentDomain.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 | undefined;\n allowedParentDomain: string | undefined;\n}): string {\n if (args.signInPage) return args.signInPage;\n if (args.appDomain && args.allowedParentDomain) {\n const apex = args.allowedParentDomain.replace(/^\\./, \"\");\n return args.appDomain === apex ? \"/login\" : `https://${apex}/login`;\n }\n return \"/login\";\n}\n\n// ----- Main factory -----\n\nexport function createAuth(opts: CreateAuthOptions) {\n const isProd = opts.isProd ?? process.env.NODE_ENV === \"production\";\n\n const cookieDomain = isProd\n ? (opts.cookieDomain ?? process.env.AUTH_COOKIE_DOMAIN)\n : undefined;\n const allowedParentDomain =\n opts.allowedParentDomain ?? process.env.AUTH_ALLOWED_PARENT_DOMAIN;\n const appDomain = opts.appDomain ?? process.env.APP_DOMAIN;\n\n const SECRET =\n opts.secret ??\n process.env.AUTH_SECRET ??\n (isProd ? undefined : \"dev-only-fallback-not-for-prod\");\n const cognitoClientId = opts.cognito?.clientId ?? process.env.AUTH_COGNITO_ID;\n const cognitoClientSecret =\n opts.cognito?.clientSecret ?? process.env.AUTH_COGNITO_SECRET;\n const cognitoIssuer = opts.cognito?.issuer ?? process.env.AUTH_COGNITO_ISSUER;\n\n validateProdEnv({\n isProd,\n cookieDomain,\n allowedParentDomain,\n appDomain,\n secret: SECRET,\n cognitoClientId,\n cognitoClientSecret,\n cognitoIssuer,\n });\n\n const signInPage = deriveSignInPage({\n signInPage: opts.signInPage,\n appDomain,\n allowedParentDomain,\n });\n\n const config: NextAuthConfig = {\n secret: SECRET,\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: cognitoClientId,\n clientSecret: cognitoClientSecret,\n issuer: 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(allowedParentDomain),\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"],"mappings":"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","names":["Cognito","Credentials","NextAuth","import_server_only"]}
package/dist/server.js CHANGED
@@ -71,12 +71,6 @@ function deriveSignInPage(args) {
71
71
  }
72
72
  return "/login";
73
73
  }
74
- function roleFromGroups(groups) {
75
- if (Array.isArray(groups) && groups.length > 0) {
76
- return String(groups[0]).toLowerCase();
77
- }
78
- return "visitor";
79
- }
80
74
  function createAuth(opts) {
81
75
  const isProd = opts.isProd ?? process.env.NODE_ENV === "production";
82
76
  const cookieDomain = isProd ? opts.cookieDomain ?? process.env.AUTH_COOKIE_DOMAIN : void 0;
@@ -170,7 +164,6 @@ function createAuth(opts) {
170
164
  session: ({ session, token }) => {
171
165
  const groups = token["cognito:groups"] ?? [];
172
166
  session.user.groups = groups;
173
- session.user.role = roleFromGroups(groups);
174
167
  return session;
175
168
  },
176
169
  authorized: ({ auth: session, request: { nextUrl } }) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/createAuth.ts","../src/server/jit.ts","../src/server/impersonation.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\";\n\ndeclare module \"next-auth\" {\n interface Session {\n user: {\n groups: string[];\n role: string;\n } & DefaultSession[\"user\"];\n }\n interface User {\n role?: string;\n groups?: string[];\n }\n}\n\nexport type CreateAuthOptions = {\n /** Path prefixes that require an authenticated session. */\n authedRoutePrefixes: string[];\n /**\n * Page to redirect to when an unauthed user hits a gated route.\n * If omitted, derived automatically from appDomain + allowedParentDomain:\n * apex app gets `/login`; subdomain apps get `https://<apex>/login`.\n */\n signInPage?: string;\n /**\n * Cookie Domain attribute. In subdomain ecosystems, set to the parent\n * (e.g. `.agency.aillc.link`). Default: process.env.AUTH_COOKIE_DOMAIN.\n * In dev (NODE_ENV !== \"production\") this is ignored — cookies stay\n * host-only so per-port localhost apps don't collide.\n */\n cookieDomain?: string;\n /**\n * The parent domain that all subdomain apps share (e.g.\n * `.agency.aillc.link`). The redirect callback uses this to allow\n * post-login redirects back to any subdomain of the parent (apex or\n * `<sub>.agency.aillc.link`). Default: process.env.AUTH_ALLOWED_PARENT_DOMAIN.\n */\n allowedParentDomain?: string;\n /**\n * This app's full FQDN (e.g. `agency.aillc.link` for the apex app, or\n * `leads.agency.aillc.link` for a subdomain app). Used to derive the\n * default signInPage. Default: process.env.APP_DOMAIN.\n */\n appDomain?: string;\n /** Override prod/dev detection. Default reads NODE_ENV. */\n isProd?: boolean;\n /**\n * The JWT signing secret. Default: process.env.AUTH_SECRET.\n * In prod, pass this from a runtime fetch (Secrets Manager) to keep the\n * secret out of Lambda env vars and to support rotation without redeploy.\n */\n secret?: string;\n cognito?: {\n clientId?: string;\n clientSecret?: string;\n issuer?: string;\n };\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// ----- Env validation -----\n\nfunction validateProdEnv(args: {\n isProd: boolean;\n cookieDomain: string | undefined;\n allowedParentDomain: string | undefined;\n appDomain: string | undefined;\n secret: string | undefined;\n cognitoClientId: string | undefined;\n cognitoClientSecret: string | undefined;\n cognitoIssuer: string | undefined;\n}): void {\n if (!args.isProd) return;\n // Skip when not actually running inside an AWS Lambda. Cognito values\n // come from SSM dynamic refs in the deployed Lambda environment;\n // they're not present at `next build` time. Throwing here would break\n // the build with no actionable fix. AWS_LAMBDA_FUNCTION_NAME is set\n // only by the Lambda runtime, so its presence is a reliable runtime\n // marker. We also keep the NEXT_PHASE check as a belt-and-suspenders\n // exit for cases where the build env happens to expose Lambda-shaped\n // env vars (e.g. local sam local invoke).\n if (process.env.NEXT_PHASE === \"phase-production-build\") return;\n if (!process.env.AWS_LAMBDA_FUNCTION_NAME) return;\n const missing: string[] = [];\n if (!args.secret) missing.push(\"AUTH_SECRET\");\n if (!args.cognitoClientId) missing.push(\"AUTH_COGNITO_ID\");\n if (!args.cognitoClientSecret) missing.push(\"AUTH_COGNITO_SECRET\");\n if (!args.cognitoIssuer) missing.push(\"AUTH_COGNITO_ISSUER\");\n // Subdomain mode: if any of the three multi-domain values is set, all three must be.\n const hasAny = !!(args.cookieDomain || args.allowedParentDomain || args.appDomain);\n if (hasAny) {\n if (!args.cookieDomain) missing.push(\"AUTH_COOKIE_DOMAIN\");\n if (!args.allowedParentDomain) missing.push(\"AUTH_ALLOWED_PARENT_DOMAIN\");\n if (!args.appDomain) missing.push(\"APP_DOMAIN\");\n }\n if (missing.length > 0) {\n throw new Error(\n `[@augmenting-integrations/auth] Missing required prod env vars: ${missing.join(\n \", \",\n )}. Provide via createAuth() opts or process.env.`,\n );\n }\n}\n\n// ----- Redirect callback factory -----\n\nfunction buildRedirectCallback(allowedParentDomain: string | undefined) {\n return ({ url, baseUrl }: { url: string; baseUrl: string }): string => {\n try {\n const target = new URL(url, baseUrl);\n if (!allowedParentDomain) {\n return target.origin === new URL(baseUrl).origin ? target.toString() : baseUrl;\n }\n const apex = allowedParentDomain.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 | undefined;\n allowedParentDomain: string | undefined;\n}): string {\n if (args.signInPage) return args.signInPage;\n if (args.appDomain && args.allowedParentDomain) {\n const apex = args.allowedParentDomain.replace(/^\\./, \"\");\n return args.appDomain === apex ? \"/login\" : `https://${apex}/login`;\n }\n return \"/login\";\n}\n\nfunction roleFromGroups(groups: unknown): string {\n if (Array.isArray(groups) && groups.length > 0) {\n return String(groups[0]).toLowerCase();\n }\n return \"visitor\";\n}\n\n// ----- Main factory -----\n\nexport function createAuth(opts: CreateAuthOptions) {\n const isProd = opts.isProd ?? process.env.NODE_ENV === \"production\";\n\n const cookieDomain = isProd\n ? (opts.cookieDomain ?? process.env.AUTH_COOKIE_DOMAIN)\n : undefined;\n const allowedParentDomain =\n opts.allowedParentDomain ?? process.env.AUTH_ALLOWED_PARENT_DOMAIN;\n const appDomain = opts.appDomain ?? process.env.APP_DOMAIN;\n\n const SECRET =\n opts.secret ??\n process.env.AUTH_SECRET ??\n (isProd ? undefined : \"dev-only-fallback-not-for-prod\");\n const cognitoClientId = opts.cognito?.clientId ?? process.env.AUTH_COGNITO_ID;\n const cognitoClientSecret =\n opts.cognito?.clientSecret ?? process.env.AUTH_COGNITO_SECRET;\n const cognitoIssuer = opts.cognito?.issuer ?? process.env.AUTH_COGNITO_ISSUER;\n\n validateProdEnv({\n isProd,\n cookieDomain,\n allowedParentDomain,\n appDomain,\n secret: SECRET,\n cognitoClientId,\n cognitoClientSecret,\n cognitoIssuer,\n });\n\n const signInPage = deriveSignInPage({\n signInPage: opts.signInPage,\n appDomain,\n allowedParentDomain,\n });\n\n const config: NextAuthConfig = {\n secret: SECRET,\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: cognitoClientId,\n clientSecret: cognitoClientSecret,\n issuer: 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 session.user.role = roleFromGroups(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(allowedParentDomain),\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server/createAuth.ts","../src/server/jit.ts","../src/server/impersonation.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\";\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 /** Path prefixes that require an authenticated session. */\n authedRoutePrefixes: string[];\n /**\n * Page to redirect to when an unauthed user hits a gated route.\n * If omitted, derived automatically from appDomain + allowedParentDomain:\n * apex app gets `/login`; subdomain apps get `https://<apex>/login`.\n */\n signInPage?: string;\n /**\n * Cookie Domain attribute. In subdomain ecosystems, set to the parent\n * (e.g. `.agency.aillc.link`). Default: process.env.AUTH_COOKIE_DOMAIN.\n * In dev (NODE_ENV !== \"production\") this is ignored — cookies stay\n * host-only so per-port localhost apps don't collide.\n */\n cookieDomain?: string;\n /**\n * The parent domain that all subdomain apps share (e.g.\n * `.agency.aillc.link`). The redirect callback uses this to allow\n * post-login redirects back to any subdomain of the parent (apex or\n * `<sub>.agency.aillc.link`). Default: process.env.AUTH_ALLOWED_PARENT_DOMAIN.\n */\n allowedParentDomain?: string;\n /**\n * This app's full FQDN (e.g. `agency.aillc.link` for the apex app, or\n * `leads.agency.aillc.link` for a subdomain app). Used to derive the\n * default signInPage. Default: process.env.APP_DOMAIN.\n */\n appDomain?: string;\n /** Override prod/dev detection. Default reads NODE_ENV. */\n isProd?: boolean;\n /**\n * The JWT signing secret. Default: process.env.AUTH_SECRET.\n * In prod, pass this from a runtime fetch (Secrets Manager) to keep the\n * secret out of Lambda env vars and to support rotation without redeploy.\n */\n secret?: string;\n cognito?: {\n clientId?: string;\n clientSecret?: string;\n issuer?: string;\n };\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// ----- Env validation -----\n\nfunction validateProdEnv(args: {\n isProd: boolean;\n cookieDomain: string | undefined;\n allowedParentDomain: string | undefined;\n appDomain: string | undefined;\n secret: string | undefined;\n cognitoClientId: string | undefined;\n cognitoClientSecret: string | undefined;\n cognitoIssuer: string | undefined;\n}): void {\n if (!args.isProd) return;\n // Skip when not actually running inside an AWS Lambda. Cognito values\n // come from SSM dynamic refs in the deployed Lambda environment;\n // they're not present at `next build` time. Throwing here would break\n // the build with no actionable fix. AWS_LAMBDA_FUNCTION_NAME is set\n // only by the Lambda runtime, so its presence is a reliable runtime\n // marker. We also keep the NEXT_PHASE check as a belt-and-suspenders\n // exit for cases where the build env happens to expose Lambda-shaped\n // env vars (e.g. local sam local invoke).\n if (process.env.NEXT_PHASE === \"phase-production-build\") return;\n if (!process.env.AWS_LAMBDA_FUNCTION_NAME) return;\n const missing: string[] = [];\n if (!args.secret) missing.push(\"AUTH_SECRET\");\n if (!args.cognitoClientId) missing.push(\"AUTH_COGNITO_ID\");\n if (!args.cognitoClientSecret) missing.push(\"AUTH_COGNITO_SECRET\");\n if (!args.cognitoIssuer) missing.push(\"AUTH_COGNITO_ISSUER\");\n // Subdomain mode: if any of the three multi-domain values is set, all three must be.\n const hasAny = !!(args.cookieDomain || args.allowedParentDomain || args.appDomain);\n if (hasAny) {\n if (!args.cookieDomain) missing.push(\"AUTH_COOKIE_DOMAIN\");\n if (!args.allowedParentDomain) missing.push(\"AUTH_ALLOWED_PARENT_DOMAIN\");\n if (!args.appDomain) missing.push(\"APP_DOMAIN\");\n }\n if (missing.length > 0) {\n throw new Error(\n `[@augmenting-integrations/auth] Missing required prod env vars: ${missing.join(\n \", \",\n )}. Provide via createAuth() opts or process.env.`,\n );\n }\n}\n\n// ----- Redirect callback factory -----\n\nfunction buildRedirectCallback(allowedParentDomain: string | undefined) {\n return ({ url, baseUrl }: { url: string; baseUrl: string }): string => {\n try {\n const target = new URL(url, baseUrl);\n if (!allowedParentDomain) {\n return target.origin === new URL(baseUrl).origin ? target.toString() : baseUrl;\n }\n const apex = allowedParentDomain.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 | undefined;\n allowedParentDomain: string | undefined;\n}): string {\n if (args.signInPage) return args.signInPage;\n if (args.appDomain && args.allowedParentDomain) {\n const apex = args.allowedParentDomain.replace(/^\\./, \"\");\n return args.appDomain === apex ? \"/login\" : `https://${apex}/login`;\n }\n return \"/login\";\n}\n\n// ----- Main factory -----\n\nexport function createAuth(opts: CreateAuthOptions) {\n const isProd = opts.isProd ?? process.env.NODE_ENV === \"production\";\n\n const cookieDomain = isProd\n ? (opts.cookieDomain ?? process.env.AUTH_COOKIE_DOMAIN)\n : undefined;\n const allowedParentDomain =\n opts.allowedParentDomain ?? process.env.AUTH_ALLOWED_PARENT_DOMAIN;\n const appDomain = opts.appDomain ?? process.env.APP_DOMAIN;\n\n const SECRET =\n opts.secret ??\n process.env.AUTH_SECRET ??\n (isProd ? undefined : \"dev-only-fallback-not-for-prod\");\n const cognitoClientId = opts.cognito?.clientId ?? process.env.AUTH_COGNITO_ID;\n const cognitoClientSecret =\n opts.cognito?.clientSecret ?? process.env.AUTH_COGNITO_SECRET;\n const cognitoIssuer = opts.cognito?.issuer ?? process.env.AUTH_COGNITO_ISSUER;\n\n validateProdEnv({\n isProd,\n cookieDomain,\n allowedParentDomain,\n appDomain,\n secret: SECRET,\n cognitoClientId,\n cognitoClientSecret,\n cognitoIssuer,\n });\n\n const signInPage = deriveSignInPage({\n signInPage: opts.signInPage,\n appDomain,\n allowedParentDomain,\n });\n\n const config: NextAuthConfig = {\n secret: SECRET,\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: cognitoClientId,\n clientSecret: cognitoClientSecret,\n issuer: 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(allowedParentDomain),\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"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augmenting-integrations/auth",
3
- "version": "5.0.1",
3
+ "version": "6.0.0",
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": "5.0.1"
36
+ "@augmenting-integrations/aws": "6.0.0"
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": "5.0.1"
46
+ "@augmenting-integrations/aws": "6.0.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsup",