@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.
- package/dist/client/AppUserProvider.d.ts +49 -0
- package/dist/client/AppUserProvider.d.ts.map +1 -0
- package/dist/client/ImpersonationBanner.d.ts +9 -0
- package/dist/client/ImpersonationBanner.d.ts.map +1 -0
- package/dist/client/SignOutButton.d.ts +10 -0
- package/dist/client/SignOutButton.d.ts.map +1 -0
- package/dist/client/UserMenu.d.ts +6 -0
- package/dist/client/UserMenu.d.ts.map +1 -0
- package/dist/client/use-impersonation.d.ts +39 -0
- package/dist/client/use-impersonation.d.ts.map +1 -0
- package/dist/client.cjs +47 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +23 -0
- package/dist/client.js.map +1 -0
- package/dist/{index.d.ts → server/createAuth.d.ts} +1 -1
- package/dist/server/createAuth.d.ts.map +1 -0
- package/dist/server/impersonation.d.ts +23 -0
- package/dist/server/impersonation.d.ts.map +1 -0
- package/dist/server/jit.d.ts +97 -0
- package/dist/server/jit.d.ts.map +1 -0
- package/dist/{index.cjs → server.cjs} +155 -7
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/{index.js → server.js} +144 -3
- package/dist/server.js.map +1 -0
- package/package.json +22 -11
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -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 @@
|
|
|
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"}
|
package/dist/client.cjs
ADDED
|
@@ -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":[]}
|
package/dist/client.d.ts
ADDED
|
@@ -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=
|
|
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/
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
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
|
-
|
|
40
|
+
mintImpersonationToken: () => mintImpersonationToken,
|
|
41
|
+
requireGroup: () => requireGroup,
|
|
42
|
+
verifyImpersonationToken: () => verifyImpersonationToken
|
|
38
43
|
});
|
|
39
|
-
module.exports = __toCommonJS(
|
|
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
|
-
|
|
388
|
+
mintImpersonationToken,
|
|
389
|
+
requireGroup,
|
|
390
|
+
verifyImpersonationToken
|
|
243
391
|
});
|
|
244
|
-
//# sourceMappingURL=
|
|
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"]}
|
package/dist/server.d.ts
ADDED
|
@@ -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/
|
|
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
|
-
|
|
342
|
+
mintImpersonationToken,
|
|
343
|
+
requireGroup,
|
|
344
|
+
verifyImpersonationToken
|
|
204
345
|
};
|
|
205
|
-
//# sourceMappingURL=
|
|
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
|
-
"description": "Auth.js v5 factory
|
|
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/
|
|
11
|
-
"module": "./dist/
|
|
12
|
-
"types": "./dist/
|
|
10
|
+
"main": "./dist/server.cjs",
|
|
11
|
+
"module": "./dist/server.js",
|
|
12
|
+
"types": "./dist/server.d.ts",
|
|
13
13
|
"exports": {
|
|
14
|
-
"
|
|
15
|
-
"types": "./dist/
|
|
16
|
-
"import": "./dist/
|
|
17
|
-
"require": "./dist/
|
|
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",
|
package/dist/index.cjs.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":["Cognito","Credentials","NextAuth"]}
|
package/dist/index.d.ts.map
DELETED
|
@@ -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":[]}
|