@augmenting-integrations/auth 4.2.0 → 5.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.
@@ -0,0 +1,49 @@
1
+ import * as React from "react";
2
+ export type DbAppUser = {
3
+ /** Stringified BigInt user id from the DB. */
4
+ id: string;
5
+ email: string;
6
+ name: string;
7
+ role: string;
8
+ parent_id: string | null;
9
+ credit_balance: number;
10
+ is_active: boolean;
11
+ must_change_password: boolean;
12
+ /** Stringified BigInt admin id when this session is impersonated. */
13
+ impersonatedBy?: string;
14
+ };
15
+ export type SessionAppUser = {
16
+ email: string;
17
+ name?: string | null;
18
+ groups: string[];
19
+ };
20
+ export type AppUserState = {
21
+ kind: "db";
22
+ user: DbAppUser;
23
+ } | {
24
+ kind: "session";
25
+ user: SessionAppUser;
26
+ } | {
27
+ kind: "anonymous";
28
+ };
29
+ export declare function AppUserProvider({ value, children, }: {
30
+ value: AppUserState;
31
+ children: React.ReactNode;
32
+ }): React.JSX.Element;
33
+ /**
34
+ * Read the current user state. Throws if no <AppUserProvider> ancestor
35
+ * exists. Narrow with `state.kind` before accessing user fields.
36
+ */
37
+ export declare function useAppUser(): AppUserState;
38
+ /**
39
+ * Convenience helper: returns the DB user if the state is `kind: "db"`,
40
+ * otherwise null. Used by UI gates that only make sense with a DB row.
41
+ */
42
+ export declare function useDbAppUser(): DbAppUser | null;
43
+ /**
44
+ * Convenience helper: returns the user's primary role, regardless of state
45
+ * shape. DB state -> `state.user.role`. Session state -> `groups[0] ?? null`.
46
+ * Anonymous -> null.
47
+ */
48
+ export declare function useRole(): string | null;
49
+ //# sourceMappingURL=AppUserProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppUserProvider.d.ts","sourceRoot":"","sources":["../../src/client/AppUserProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAc/B,MAAM,MAAM,SAAS,GAAG;IACtB,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAI1B,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,qBAEA;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,YAAY,CAQzC;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI,SAAS,GAAG,IAAI,CAG/C;AAED;;;;GAIG;AACH,wBAAgB,OAAO,IAAI,MAAM,GAAG,IAAI,CAKvC"}
@@ -0,0 +1,9 @@
1
+ import * as React from "react";
2
+ export declare function ImpersonationBanner({ endpoint, }: {
3
+ /**
4
+ * Base endpoint where the impersonate DELETE lives. The component appends
5
+ * `/${user.id}/impersonate`. Default matches the spoke's convention.
6
+ */
7
+ endpoint?: string;
8
+ }): React.JSX.Element | null;
9
+ //# sourceMappingURL=ImpersonationBanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImpersonationBanner.d.ts","sourceRoot":"","sources":["../../src/client/ImpersonationBanner.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAe/B,wBAAgB,mBAAmB,CAAC,EAClC,QAA6B,GAC9B,EAAE;IACD;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,4BAwDA"}
@@ -0,0 +1,10 @@
1
+ import * as React from "react";
2
+ export declare function SignOutButton({ callbackUrl, variant, size, showIcon, className, label, }: {
3
+ callbackUrl?: string;
4
+ variant?: "outline" | "ghost" | "default";
5
+ size?: "sm" | "md";
6
+ showIcon?: boolean;
7
+ className?: string;
8
+ label?: string;
9
+ }): React.JSX.Element;
10
+ //# sourceMappingURL=SignOutButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SignOutButton.d.ts","sourceRoot":"","sources":["../../src/client/SignOutButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAY/B,wBAAgB,aAAa,CAAC,EAC5B,WAAiB,EACjB,OAAmB,EACnB,IAAW,EACX,QAAe,EACf,SAAS,EACT,KAAkB,GACnB,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAC1C,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,qBAuBA"}
@@ -0,0 +1,6 @@
1
+ import * as React from "react";
2
+ export declare function UserMenu({ signOutCallbackUrl, className, }: {
3
+ signOutCallbackUrl?: string;
4
+ className?: string;
5
+ }): React.JSX.Element | null;
6
+ //# sourceMappingURL=UserMenu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserMenu.d.ts","sourceRoot":"","sources":["../../src/client/UserMenu.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAc/B,wBAAgB,QAAQ,CAAC,EACvB,kBAAwB,EACxB,SAAS,GACV,EAAE;IACD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,4BAuBA"}
@@ -0,0 +1,39 @@
1
+ export type EffectiveUser = {
2
+ id: string;
3
+ email: string;
4
+ name: string;
5
+ role: string;
6
+ is_active: boolean;
7
+ credit_balance: number;
8
+ };
9
+ export type ImpersonatedBy = {
10
+ id: string;
11
+ name: string;
12
+ email: string;
13
+ };
14
+ export type MeResponse = {
15
+ user: EffectiveUser;
16
+ impersonatedBy: ImpersonatedBy | null;
17
+ };
18
+ type State = {
19
+ status: "loading";
20
+ data: null;
21
+ error: null;
22
+ } | {
23
+ status: "anonymous";
24
+ data: null;
25
+ error: null;
26
+ } | {
27
+ status: "ready";
28
+ data: MeResponse;
29
+ error: null;
30
+ } | {
31
+ status: "error";
32
+ data: null;
33
+ error: string;
34
+ };
35
+ export declare function useImpersonation(): State & {
36
+ refresh: () => Promise<void>;
37
+ };
38
+ export {};
39
+ //# sourceMappingURL=use-impersonation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-impersonation.d.ts","sourceRoot":"","sources":["../../src/client/use-impersonation.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,aAAa,CAAC;IACpB,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;CACvC,CAAC;AAEF,KAAK,KAAK,GACN;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GAChD;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GAClD;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,gBAAgB,IAAI,KAAK,GAAG;IAAE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAuC3E"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var client_exports = {};
20
+ __export(client_exports, {
21
+ AppUserProvider: () => import_AppUserProvider.AppUserProvider,
22
+ ImpersonationBanner: () => import_ImpersonationBanner.ImpersonationBanner,
23
+ SignOutButton: () => import_SignOutButton.SignOutButton,
24
+ UserMenu: () => import_UserMenu.UserMenu,
25
+ useAppUser: () => import_AppUserProvider.useAppUser,
26
+ useDbAppUser: () => import_AppUserProvider.useDbAppUser,
27
+ useImpersonation: () => import_use_impersonation.useImpersonation,
28
+ useRole: () => import_AppUserProvider.useRole
29
+ });
30
+ module.exports = __toCommonJS(client_exports);
31
+ var import_AppUserProvider = require("./client/AppUserProvider.js");
32
+ var import_UserMenu = require("./client/UserMenu.js");
33
+ var import_SignOutButton = require("./client/SignOutButton.js");
34
+ var import_ImpersonationBanner = require("./client/ImpersonationBanner.js");
35
+ var import_use_impersonation = require("./client/use-impersonation.js");
36
+ // Annotate the CommonJS export names for ESM import in node:
37
+ 0 && (module.exports = {
38
+ AppUserProvider,
39
+ ImpersonationBanner,
40
+ SignOutButton,
41
+ UserMenu,
42
+ useAppUser,
43
+ useDbAppUser,
44
+ useImpersonation,
45
+ useRole
46
+ });
47
+ //# sourceMappingURL=client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["export {\n AppUserProvider,\n useAppUser,\n useDbAppUser,\n useRole,\n type AppUserState,\n type DbAppUser,\n type SessionAppUser,\n} from \"./client/AppUserProvider.js\";\nexport { UserMenu } from \"./client/UserMenu.js\";\nexport { SignOutButton } from \"./client/SignOutButton.js\";\nexport { ImpersonationBanner } from \"./client/ImpersonationBanner.js\";\nexport {\n useImpersonation,\n type EffectiveUser,\n type ImpersonatedBy,\n type MeResponse,\n} from \"./client/use-impersonation.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAQO;AACP,sBAAyB;AACzB,2BAA8B;AAC9B,iCAAoC;AACpC,+BAKO;","names":[]}
@@ -0,0 +1,6 @@
1
+ export { AppUserProvider, useAppUser, useDbAppUser, useRole, type AppUserState, type DbAppUser, type SessionAppUser, } from "./client/AppUserProvider.js";
2
+ export { UserMenu } from "./client/UserMenu.js";
3
+ export { SignOutButton } from "./client/SignOutButton.js";
4
+ export { ImpersonationBanner } from "./client/ImpersonationBanner.js";
5
+ export { useImpersonation, type EffectiveUser, type ImpersonatedBy, type MeResponse, } from "./client/use-impersonation.js";
6
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,YAAY,EACZ,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,cAAc,GACpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EACL,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,UAAU,GAChB,MAAM,+BAA+B,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ AppUserProvider,
3
+ useAppUser,
4
+ useDbAppUser,
5
+ useRole
6
+ } from "./client/AppUserProvider.js";
7
+ import { UserMenu } from "./client/UserMenu.js";
8
+ import { SignOutButton } from "./client/SignOutButton.js";
9
+ import { ImpersonationBanner } from "./client/ImpersonationBanner.js";
10
+ import {
11
+ useImpersonation
12
+ } from "./client/use-impersonation.js";
13
+ export {
14
+ AppUserProvider,
15
+ ImpersonationBanner,
16
+ SignOutButton,
17
+ UserMenu,
18
+ useAppUser,
19
+ useDbAppUser,
20
+ useImpersonation,
21
+ useRole
22
+ };
23
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["export {\n AppUserProvider,\n useAppUser,\n useDbAppUser,\n useRole,\n type AppUserState,\n type DbAppUser,\n type SessionAppUser,\n} from \"./client/AppUserProvider.js\";\nexport { UserMenu } from \"./client/UserMenu.js\";\nexport { SignOutButton } from \"./client/SignOutButton.js\";\nexport { ImpersonationBanner } from \"./client/ImpersonationBanner.js\";\nexport {\n useImpersonation,\n type EffectiveUser,\n type ImpersonatedBy,\n type MeResponse,\n} from \"./client/use-impersonation.js\";\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,OAIK;","names":[]}
@@ -70,4 +70,4 @@ export declare function hasGroup(session: Session | null | undefined, name: stri
70
70
  export declare function requireGroup(session: Session | null | undefined, ...names: string[]): void;
71
71
  export declare function createAuth(opts: CreateAuthOptions): import("next-auth").NextAuthResult;
72
72
  export type { NextAuthConfig } from "next-auth";
73
- //# sourceMappingURL=index.d.ts.map
73
+ //# sourceMappingURL=createAuth.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,23 @@
1
+ import "server-only";
2
+ export declare const IMPERSONATE_COOKIE_NAME = "__impersonate";
3
+ export declare const IMPERSONATE_TTL_SECONDS = 3600;
4
+ export type ImpersonationClaims = {
5
+ /** Admin user id who started the impersonation (stringified BigInt). */
6
+ impersonatedBy: string;
7
+ /** Target user id being impersonated (stringified BigInt). */
8
+ sub: string;
9
+ /** Issued-at (seconds since epoch). */
10
+ iat: number;
11
+ /** Expiry (seconds since epoch). */
12
+ exp: number;
13
+ };
14
+ export declare function mintImpersonationToken(args: {
15
+ adminId: bigint | string;
16
+ targetId: bigint | string;
17
+ now?: Date;
18
+ }): Promise<{
19
+ token: string;
20
+ expiresAt: Date;
21
+ }>;
22
+ export declare function verifyImpersonationToken(token: string): Promise<ImpersonationClaims | null>;
23
+ //# sourceMappingURL=impersonation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impersonation.d.ts","sourceRoot":"","sources":["../../src/server/impersonation.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAqBrB,eAAO,MAAM,uBAAuB,kBAAkB,CAAC;AACvD,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAG5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,wEAAwE;IACxE,cAAc,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAkBF,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CAgB9C;AAED,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA0BrC"}
@@ -0,0 +1,97 @@
1
+ import "server-only";
2
+ import type { Session } from "next-auth";
3
+ /**
4
+ * Minimum contract every spoke User row must satisfy. Spokes can widen this
5
+ * with additional fields (credit_balance, must_change_password, etc.) and the
6
+ * factory will preserve them through the returned `Promise<TUser>`.
7
+ */
8
+ export type BaseAppUser = {
9
+ id: bigint | string | number;
10
+ email: string;
11
+ name: string;
12
+ role: string;
13
+ parent_id: bigint | string | number | null;
14
+ };
15
+ /**
16
+ * Loose typing for the Prisma delegates the factory touches. Each spoke has
17
+ * its own generated client whose actual types are concrete; we use loose
18
+ * shapes here so the factory works with any spoke's schema.
19
+ */
20
+ export type PrismaLikeUserDelegate<TUser> = {
21
+ findUnique: (args: {
22
+ where: {
23
+ id?: unknown;
24
+ email?: string;
25
+ };
26
+ }) => Promise<TUser | null>;
27
+ create: (args: {
28
+ data: unknown;
29
+ }) => Promise<TUser>;
30
+ };
31
+ export type PrismaLikeInvitationDelegate = {
32
+ findFirst: (args: {
33
+ where: {
34
+ email: string;
35
+ accepted_at: null;
36
+ expires_at: {
37
+ gt: Date;
38
+ };
39
+ };
40
+ orderBy?: unknown;
41
+ }) => Promise<{
42
+ id: bigint | string | number;
43
+ intended_role: string;
44
+ parent_id: bigint | string | number | null;
45
+ } | null>;
46
+ update: (args: {
47
+ where: {
48
+ id: unknown;
49
+ };
50
+ data: {
51
+ accepted_at: Date;
52
+ accepted_by_user_id: unknown;
53
+ };
54
+ }) => Promise<unknown>;
55
+ };
56
+ export type PrismaLikeClient<TUser> = {
57
+ user: PrismaLikeUserDelegate<TUser>;
58
+ invitation: PrismaLikeInvitationDelegate;
59
+ $transaction: <T>(fn: (tx: {
60
+ user: PrismaLikeUserDelegate<TUser>;
61
+ invitation: PrismaLikeInvitationDelegate;
62
+ }) => Promise<T>) => Promise<T>;
63
+ };
64
+ export type CreateGetOrCreateAppUserOptions<TUser extends BaseAppUser> = {
65
+ /** Returns the spoke's PrismaClient (lazily). */
66
+ db: () => Promise<PrismaLikeClient<TUser>>;
67
+ /** Fallback role when no admin email + no Cognito groups. */
68
+ defaultRole: string;
69
+ /** Starting credit balance per role. */
70
+ computeCreditBalance: (role: string) => number;
71
+ /** Emails auto-promoted to "admin" role on first sign-in (case-insensitive). */
72
+ adminEmails?: string[];
73
+ /**
74
+ * Hash value written to User.password on creation. Schema-inherited
75
+ * not-null constraint; never used to authenticate (Cognito does that).
76
+ * Default: a recognizable placeholder string.
77
+ */
78
+ placeholderPasswordHash?: string;
79
+ /**
80
+ * Extra column values written on creation. Use this for spoke-specific
81
+ * defaults (e.g. is_active: true, must_change_password: false).
82
+ */
83
+ extraCreateFields?: Record<string, unknown>;
84
+ };
85
+ export type AppUserWithImpersonation<TUser extends BaseAppUser> = TUser & {
86
+ /** Stringified admin id when this session is impersonated; absent otherwise. */
87
+ impersonatedBy?: string;
88
+ };
89
+ /**
90
+ * Build a `getOrCreateAppUser(session)` function configured for this spoke.
91
+ *
92
+ * Returned function is idempotent: subsequent calls with the same email
93
+ * return the existing row. First-time emails are created inside a transaction
94
+ * that also auto-accepts a matching Invitation row if present.
95
+ */
96
+ export declare function createGetOrCreateAppUser<TUser extends BaseAppUser>(opts: CreateGetOrCreateAppUserOptions<TUser>): (session: Session) => Promise<AppUserWithImpersonation<TUser>>;
97
+ //# sourceMappingURL=jit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jit.d.ts","sourceRoot":"","sources":["../../src/server/jit.ts"],"names":[],"mappings":"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"}
@@ -27,16 +27,23 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  ));
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
 
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
30
+ // src/server.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
33
  AuthError: () => AuthError,
34
+ IMPERSONATE_COOKIE_NAME: () => IMPERSONATE_COOKIE_NAME,
35
+ IMPERSONATE_TTL_SECONDS: () => IMPERSONATE_TTL_SECONDS,
34
36
  createAuth: () => createAuth,
37
+ createGetOrCreateAppUser: () => createGetOrCreateAppUser,
35
38
  getUserGroups: () => getUserGroups,
36
39
  hasGroup: () => hasGroup,
37
- requireGroup: () => requireGroup
40
+ mintImpersonationToken: () => mintImpersonationToken,
41
+ requireGroup: () => requireGroup,
42
+ verifyImpersonationToken: () => verifyImpersonationToken
38
43
  });
39
- module.exports = __toCommonJS(index_exports);
44
+ module.exports = __toCommonJS(server_exports);
45
+
46
+ // src/server/createAuth.ts
40
47
  var import_next_auth = __toESM(require("next-auth"));
41
48
  var import_credentials = __toESM(require("next-auth/providers/credentials"));
42
49
  var import_cognito = __toESM(require("next-auth/providers/cognito"));
@@ -233,12 +240,153 @@ function createAuth(opts) {
233
240
  };
234
241
  return (0, import_next_auth.default)(config);
235
242
  }
243
+
244
+ // src/server/jit.ts
245
+ var import_server_only2 = require("server-only");
246
+ var import_headers = require("next/headers");
247
+
248
+ // src/server/impersonation.ts
249
+ var import_server_only = require("server-only");
250
+ var import_jwt = require("next-auth/jwt");
251
+ var import_server = require("@augmenting-integrations/aws/server");
252
+ var IMPERSONATE_COOKIE_NAME = "__impersonate";
253
+ var IMPERSONATE_TTL_SECONDS = 3600;
254
+ var IMPERSONATE_JWT_SALT = "impersonate.v1";
255
+ var cachedSecret = null;
256
+ async function getAuthSecret() {
257
+ if (cachedSecret) return cachedSecret;
258
+ const arn = process.env.AUTH_SECRET_ARN;
259
+ const fromSm = arn ? await (0, import_server.getSecret)(arn) : null;
260
+ const secret = fromSm ?? process.env.AUTH_SECRET;
261
+ if (!secret) {
262
+ throw new Error(
263
+ "AUTH_SECRET (or AUTH_SECRET_ARN) must be set to mint/verify impersonation tokens"
264
+ );
265
+ }
266
+ cachedSecret = secret;
267
+ return secret;
268
+ }
269
+ async function mintImpersonationToken(args) {
270
+ const secret = await getAuthSecret();
271
+ const nowSec = Math.floor((args.now?.getTime() ?? Date.now()) / 1e3);
272
+ const exp = nowSec + IMPERSONATE_TTL_SECONDS;
273
+ const token = await (0, import_jwt.encode)({
274
+ secret,
275
+ salt: IMPERSONATE_JWT_SALT,
276
+ maxAge: IMPERSONATE_TTL_SECONDS,
277
+ token: {
278
+ impersonatedBy: String(args.adminId),
279
+ sub: String(args.targetId),
280
+ iat: nowSec,
281
+ exp
282
+ }
283
+ });
284
+ return { token, expiresAt: new Date(exp * 1e3) };
285
+ }
286
+ async function verifyImpersonationToken(token) {
287
+ try {
288
+ const secret = await getAuthSecret();
289
+ const decoded = await (0, import_jwt.decode)({
290
+ token,
291
+ secret,
292
+ salt: IMPERSONATE_JWT_SALT
293
+ });
294
+ if (!decoded) return null;
295
+ const impersonatedBy = decoded["impersonatedBy"];
296
+ const sub = decoded["sub"];
297
+ const iat = decoded["iat"];
298
+ const exp = decoded["exp"];
299
+ if (typeof impersonatedBy !== "string" || typeof sub !== "string" || typeof iat !== "number" || typeof exp !== "number") {
300
+ return null;
301
+ }
302
+ if (exp * 1e3 < Date.now()) return null;
303
+ return { impersonatedBy, sub, iat, exp };
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
308
+
309
+ // src/server/jit.ts
310
+ var DEFAULT_PLACEHOLDER_HASH = "$2y$12$.cognito-managed.never.used-for-login.placeholder";
311
+ function createGetOrCreateAppUser(opts) {
312
+ const adminEmailsLower = (opts.adminEmails ?? []).map((s) => s.toLowerCase());
313
+ const placeholder = opts.placeholderPasswordHash ?? DEFAULT_PLACEHOLDER_HASH;
314
+ return async function getOrCreateAppUser(session) {
315
+ const email = session.user?.email;
316
+ if (!email) {
317
+ throw new Error("getOrCreateAppUser called with a session that has no user.email");
318
+ }
319
+ const db = await opts.db();
320
+ try {
321
+ const cookieStore = await (0, import_headers.cookies)();
322
+ const cookie = cookieStore.get(IMPERSONATE_COOKIE_NAME);
323
+ if (cookie?.value) {
324
+ const claims = await verifyImpersonationToken(cookie.value);
325
+ if (claims) {
326
+ const [admin, target] = await Promise.all([
327
+ db.user.findUnique({ where: { id: claims.impersonatedBy } }),
328
+ db.user.findUnique({ where: { id: claims.sub } })
329
+ ]);
330
+ if (admin && admin.role === "admin" && target) {
331
+ return Object.assign(target, {
332
+ impersonatedBy: claims.impersonatedBy
333
+ });
334
+ }
335
+ }
336
+ }
337
+ } catch {
338
+ }
339
+ const existing = await db.user.findUnique({ where: { email } });
340
+ if (existing) return existing;
341
+ const groups = session.user.groups ?? [];
342
+ const fallbackRole = adminEmailsLower.includes(email.toLowerCase()) ? "admin" : groups[0] ?? opts.defaultRole;
343
+ const name = session.user.name ?? email.split("@")[0];
344
+ return db.$transaction(async (tx) => {
345
+ const pendingInvite = await tx.invitation.findFirst({
346
+ where: {
347
+ email,
348
+ accepted_at: null,
349
+ expires_at: { gt: /* @__PURE__ */ new Date() }
350
+ },
351
+ orderBy: { created_at: "desc" }
352
+ });
353
+ const role = pendingInvite ? pendingInvite.intended_role : fallbackRole;
354
+ const parent_id = pendingInvite ? pendingInvite.parent_id : null;
355
+ const created = await tx.user.create({
356
+ data: {
357
+ email,
358
+ name,
359
+ role,
360
+ parent_id,
361
+ password: placeholder,
362
+ credit_balance: opts.computeCreditBalance(role),
363
+ ...opts.extraCreateFields ?? {}
364
+ }
365
+ });
366
+ if (pendingInvite) {
367
+ await tx.invitation.update({
368
+ where: { id: pendingInvite.id },
369
+ data: {
370
+ accepted_at: /* @__PURE__ */ new Date(),
371
+ accepted_by_user_id: created.id
372
+ }
373
+ });
374
+ }
375
+ return created;
376
+ });
377
+ };
378
+ }
236
379
  // Annotate the CommonJS export names for ESM import in node:
237
380
  0 && (module.exports = {
238
381
  AuthError,
382
+ IMPERSONATE_COOKIE_NAME,
383
+ IMPERSONATE_TTL_SECONDS,
239
384
  createAuth,
385
+ createGetOrCreateAppUser,
240
386
  getUserGroups,
241
387
  hasGroup,
242
- requireGroup
388
+ mintImpersonationToken,
389
+ requireGroup,
390
+ verifyImpersonationToken
243
391
  });
244
- //# sourceMappingURL=index.cjs.map
392
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,4 @@
1
+ export { createAuth, type CreateAuthOptions, type NextAuthConfig, AuthError, getUserGroups, hasGroup, requireGroup, } from "./server/createAuth.js";
2
+ export { createGetOrCreateAppUser, type BaseAppUser, type AppUserWithImpersonation, type CreateGetOrCreateAppUserOptions, type PrismaLikeClient, type PrismaLikeUserDelegate, type PrismaLikeInvitationDelegate, } from "./server/jit.js";
3
+ export { mintImpersonationToken, verifyImpersonationToken, IMPERSONATE_COOKIE_NAME, IMPERSONATE_TTL_SECONDS, type ImpersonationClaims, } from "./server/impersonation.js";
4
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,SAAS,EACT,aAAa,EACb,QAAQ,EACR,YAAY,GACb,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,wBAAwB,EACxB,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,GAClC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,KAAK,mBAAmB,GACzB,MAAM,2BAA2B,CAAC"}
@@ -1,4 +1,4 @@
1
- // src/index.ts
1
+ // src/server/createAuth.ts
2
2
  import NextAuth from "next-auth";
3
3
  import Credentials from "next-auth/providers/credentials";
4
4
  import Cognito from "next-auth/providers/cognito";
@@ -195,11 +195,152 @@ function createAuth(opts) {
195
195
  };
196
196
  return NextAuth(config);
197
197
  }
198
+
199
+ // src/server/jit.ts
200
+ import "server-only";
201
+ import { cookies } from "next/headers";
202
+
203
+ // src/server/impersonation.ts
204
+ import "server-only";
205
+ import { encode, decode } from "next-auth/jwt";
206
+ import { getSecret } from "@augmenting-integrations/aws/server";
207
+ var IMPERSONATE_COOKIE_NAME = "__impersonate";
208
+ var IMPERSONATE_TTL_SECONDS = 3600;
209
+ var IMPERSONATE_JWT_SALT = "impersonate.v1";
210
+ var cachedSecret = null;
211
+ async function getAuthSecret() {
212
+ if (cachedSecret) return cachedSecret;
213
+ const arn = process.env.AUTH_SECRET_ARN;
214
+ const fromSm = arn ? await getSecret(arn) : null;
215
+ const secret = fromSm ?? process.env.AUTH_SECRET;
216
+ if (!secret) {
217
+ throw new Error(
218
+ "AUTH_SECRET (or AUTH_SECRET_ARN) must be set to mint/verify impersonation tokens"
219
+ );
220
+ }
221
+ cachedSecret = secret;
222
+ return secret;
223
+ }
224
+ async function mintImpersonationToken(args) {
225
+ const secret = await getAuthSecret();
226
+ const nowSec = Math.floor((args.now?.getTime() ?? Date.now()) / 1e3);
227
+ const exp = nowSec + IMPERSONATE_TTL_SECONDS;
228
+ const token = await encode({
229
+ secret,
230
+ salt: IMPERSONATE_JWT_SALT,
231
+ maxAge: IMPERSONATE_TTL_SECONDS,
232
+ token: {
233
+ impersonatedBy: String(args.adminId),
234
+ sub: String(args.targetId),
235
+ iat: nowSec,
236
+ exp
237
+ }
238
+ });
239
+ return { token, expiresAt: new Date(exp * 1e3) };
240
+ }
241
+ async function verifyImpersonationToken(token) {
242
+ try {
243
+ const secret = await getAuthSecret();
244
+ const decoded = await decode({
245
+ token,
246
+ secret,
247
+ salt: IMPERSONATE_JWT_SALT
248
+ });
249
+ if (!decoded) return null;
250
+ const impersonatedBy = decoded["impersonatedBy"];
251
+ const sub = decoded["sub"];
252
+ const iat = decoded["iat"];
253
+ const exp = decoded["exp"];
254
+ if (typeof impersonatedBy !== "string" || typeof sub !== "string" || typeof iat !== "number" || typeof exp !== "number") {
255
+ return null;
256
+ }
257
+ if (exp * 1e3 < Date.now()) return null;
258
+ return { impersonatedBy, sub, iat, exp };
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ // src/server/jit.ts
265
+ var DEFAULT_PLACEHOLDER_HASH = "$2y$12$.cognito-managed.never.used-for-login.placeholder";
266
+ function createGetOrCreateAppUser(opts) {
267
+ const adminEmailsLower = (opts.adminEmails ?? []).map((s) => s.toLowerCase());
268
+ const placeholder = opts.placeholderPasswordHash ?? DEFAULT_PLACEHOLDER_HASH;
269
+ return async function getOrCreateAppUser(session) {
270
+ const email = session.user?.email;
271
+ if (!email) {
272
+ throw new Error("getOrCreateAppUser called with a session that has no user.email");
273
+ }
274
+ const db = await opts.db();
275
+ try {
276
+ const cookieStore = await cookies();
277
+ const cookie = cookieStore.get(IMPERSONATE_COOKIE_NAME);
278
+ if (cookie?.value) {
279
+ const claims = await verifyImpersonationToken(cookie.value);
280
+ if (claims) {
281
+ const [admin, target] = await Promise.all([
282
+ db.user.findUnique({ where: { id: claims.impersonatedBy } }),
283
+ db.user.findUnique({ where: { id: claims.sub } })
284
+ ]);
285
+ if (admin && admin.role === "admin" && target) {
286
+ return Object.assign(target, {
287
+ impersonatedBy: claims.impersonatedBy
288
+ });
289
+ }
290
+ }
291
+ }
292
+ } catch {
293
+ }
294
+ const existing = await db.user.findUnique({ where: { email } });
295
+ if (existing) return existing;
296
+ const groups = session.user.groups ?? [];
297
+ const fallbackRole = adminEmailsLower.includes(email.toLowerCase()) ? "admin" : groups[0] ?? opts.defaultRole;
298
+ const name = session.user.name ?? email.split("@")[0];
299
+ return db.$transaction(async (tx) => {
300
+ const pendingInvite = await tx.invitation.findFirst({
301
+ where: {
302
+ email,
303
+ accepted_at: null,
304
+ expires_at: { gt: /* @__PURE__ */ new Date() }
305
+ },
306
+ orderBy: { created_at: "desc" }
307
+ });
308
+ const role = pendingInvite ? pendingInvite.intended_role : fallbackRole;
309
+ const parent_id = pendingInvite ? pendingInvite.parent_id : null;
310
+ const created = await tx.user.create({
311
+ data: {
312
+ email,
313
+ name,
314
+ role,
315
+ parent_id,
316
+ password: placeholder,
317
+ credit_balance: opts.computeCreditBalance(role),
318
+ ...opts.extraCreateFields ?? {}
319
+ }
320
+ });
321
+ if (pendingInvite) {
322
+ await tx.invitation.update({
323
+ where: { id: pendingInvite.id },
324
+ data: {
325
+ accepted_at: /* @__PURE__ */ new Date(),
326
+ accepted_by_user_id: created.id
327
+ }
328
+ });
329
+ }
330
+ return created;
331
+ });
332
+ };
333
+ }
198
334
  export {
199
335
  AuthError,
336
+ IMPERSONATE_COOKIE_NAME,
337
+ IMPERSONATE_TTL_SECONDS,
200
338
  createAuth,
339
+ createGetOrCreateAppUser,
201
340
  getUserGroups,
202
341
  hasGroup,
203
- requireGroup
342
+ mintImpersonationToken,
343
+ requireGroup,
344
+ verifyImpersonationToken
204
345
  };
205
- //# sourceMappingURL=index.js.map
346
+ //# sourceMappingURL=server.js.map
@@ -0,0 +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":[]}
package/package.json CHANGED
@@ -1,38 +1,49 @@
1
1
  {
2
2
  "name": "@augmenting-integrations/auth",
3
- "version": "4.2.0",
4
- "description": "Auth.js v5 factory: Cognito in prod, Credentials role-picker in dev. Same JWT shape (sub, email, cognito:groups) regardless of provider.",
3
+ "version": "5.0.0",
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": {
7
7
  "access": "public"
8
8
  },
9
9
  "sideEffects": false,
10
- "main": "./dist/index.cjs",
11
- "module": "./dist/index.js",
12
- "types": "./dist/index.d.ts",
10
+ "main": "./dist/server.cjs",
11
+ "module": "./dist/server.js",
12
+ "types": "./dist/server.d.ts",
13
13
  "exports": {
14
- ".": {
15
- "types": "./dist/index.d.ts",
16
- "import": "./dist/index.js",
17
- "require": "./dist/index.cjs"
14
+ "./server": {
15
+ "types": "./dist/server.d.ts",
16
+ "import": "./dist/server.js",
17
+ "require": "./dist/server.cjs"
18
+ },
19
+ "./client": {
20
+ "types": "./dist/client.d.ts",
21
+ "import": "./dist/client.js",
22
+ "require": "./dist/client.cjs"
18
23
  }
19
24
  },
20
25
  "files": [
21
26
  "dist",
22
27
  "README.md"
23
28
  ],
29
+ "dependencies": {
30
+ "lucide-react": "^1.14.0"
31
+ },
24
32
  "peerDependencies": {
25
33
  "next": "^16.0.0",
26
34
  "next-auth": "^5.0.0-beta.31",
27
- "react": "^19.0.0"
35
+ "react": "^19.0.0",
36
+ "@augmenting-integrations/aws": "5.0.0"
28
37
  },
29
38
  "devDependencies": {
39
+ "@types/react": "^19.0.0",
30
40
  "next": "^16.2.5",
31
41
  "next-auth": "^5.0.0-beta.31",
32
42
  "react": "^19.0.0",
33
43
  "tsup": "^8.3.5",
34
44
  "typescript": "^5.7.2",
35
- "vitest": "^4.1.5"
45
+ "vitest": "^4.1.5",
46
+ "@augmenting-integrations/aws": "5.0.0"
36
47
  },
37
48
  "scripts": {
38
49
  "build": "tsup",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.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"],"mappings":"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","names":["Cognito","Credentials","NextAuth"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.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"}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.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"],"mappings":"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","names":[]}