@venturekit/auth 0.0.0-dev.20260701100017 → 0.0.0-dev.20260704225856

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.
Files changed (62) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -1
  4. package/dist/index.js.map +1 -1
  5. package/{migrations → dist/migrations}/vk_auth_001_verification_codes.sql +7 -4
  6. package/dist/migrations/vk_auth_003_role_scopes.sql +43 -0
  7. package/dist/roles/index.d.ts +5 -1
  8. package/dist/roles/index.d.ts.map +1 -1
  9. package/dist/roles/index.js +4 -1
  10. package/dist/roles/index.js.map +1 -1
  11. package/dist/roles/role-scopes.d.ts +92 -0
  12. package/dist/roles/role-scopes.d.ts.map +1 -0
  13. package/dist/roles/role-scopes.js +122 -0
  14. package/dist/roles/role-scopes.js.map +1 -0
  15. package/dist/server/cookies.d.ts +98 -13
  16. package/dist/server/cookies.d.ts.map +1 -1
  17. package/dist/server/cookies.js +77 -19
  18. package/dist/server/cookies.js.map +1 -1
  19. package/dist/server/federated-routes.d.ts +29 -22
  20. package/dist/server/federated-routes.d.ts.map +1 -1
  21. package/dist/server/federated-routes.js +31 -4
  22. package/dist/server/federated-routes.js.map +1 -1
  23. package/dist/server/federated.d.ts.map +1 -1
  24. package/dist/server/federated.js +7 -11
  25. package/dist/server/federated.js.map +1 -1
  26. package/dist/server/forgot-password.js +0 -1
  27. package/dist/server/forgot-password.js.map +1 -1
  28. package/dist/server/handoff-routes.d.ts +130 -0
  29. package/dist/server/handoff-routes.d.ts.map +1 -0
  30. package/dist/server/handoff-routes.js +178 -0
  31. package/dist/server/handoff-routes.js.map +1 -0
  32. package/dist/server/handoff.d.ts +112 -0
  33. package/dist/server/handoff.d.ts.map +1 -0
  34. package/dist/server/handoff.js +102 -0
  35. package/dist/server/handoff.js.map +1 -0
  36. package/dist/server/index.d.ts +11 -4
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +9 -3
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/middleware.d.ts +35 -0
  41. package/dist/server/middleware.d.ts.map +1 -1
  42. package/dist/server/middleware.js +50 -10
  43. package/dist/server/middleware.js.map +1 -1
  44. package/dist/server/passwordless.d.ts +68 -0
  45. package/dist/server/passwordless.d.ts.map +1 -0
  46. package/dist/server/passwordless.js +136 -0
  47. package/dist/server/passwordless.js.map +1 -0
  48. package/dist/server/revoke.d.ts +10 -0
  49. package/dist/server/revoke.d.ts.map +1 -1
  50. package/dist/server/revoke.js +19 -2
  51. package/dist/server/revoke.js.map +1 -1
  52. package/dist/server/store/postgres.d.ts +35 -0
  53. package/dist/server/store/postgres.d.ts.map +1 -0
  54. package/dist/server/store/postgres.js +88 -0
  55. package/dist/server/store/postgres.js.map +1 -0
  56. package/dist/server/token-utils.d.ts +12 -2
  57. package/dist/server/token-utils.d.ts.map +1 -1
  58. package/dist/server/token-utils.js +9 -4
  59. package/dist/server/token-utils.js.map +1 -1
  60. package/package.json +21 -8
  61. package/src/migrations/vk_auth_001_verification_codes.sql +55 -0
  62. package/src/migrations/vk_auth_003_role_scopes.sql +43 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Session handoff route factory — the SSO flow that moves a session
3
+ * between hosts that cannot share cookies (different registrable
4
+ * domains, e.g. a platform subdomain and a white-label custom domain).
5
+ *
6
+ * Emits two Lambda handlers:
7
+ *
8
+ * - `start` — `POST /auth/handoff/start` on the host WITH a session.
9
+ * Body `{ target: "https://tenant.io/dashboard" }`.
10
+ * Verifies the session cookies, runs the app's
11
+ * `authorize` hook (membership check — THE gate that
12
+ * keeps foreign domains out), stashes a single-use
13
+ * code via the app's `HandoffStore`, and returns
14
+ * `{ url }` for the SPA to navigate to.
15
+ * - `complete` — `GET /auth/handoff?code=…&next=…` on the TARGET
16
+ * host (a top-level browser navigation, typically
17
+ * through the app's same-origin API proxy). Redeems
18
+ * the code, mints fresh tokens from the stashed
19
+ * refresh token, sets first-party session cookies for
20
+ * THIS host, and 302-redirects to `next`. Every
21
+ * failure redirects to `failurePath` — the user is
22
+ * mid-navigation, JSON errors would dead-end them.
23
+ *
24
+ * The refresh token moves server→store→server; only the opaque
25
+ * single-use code (256-bit, 60s TTL, host-bound, hashed at rest)
26
+ * transits the browser. See `handoff.ts` for the security model.
27
+ */
28
+ import { type APIGatewayProxyEventV2, type APIGatewayProxyResultV2, type Context as LambdaContext } from 'aws-lambda';
29
+ import { type RequestContext } from '@venturekit/runtime';
30
+ import { type CookieOptions } from './cookies.js';
31
+ import { type AuthServerConfig } from './config.js';
32
+ import { type HandoffStore } from './handoff.js';
33
+ /** Body shape for the `start` route. */
34
+ export interface HandoffStartBody {
35
+ /**
36
+ * Absolute URL the user wants to land on, on the other host —
37
+ * `https://tenant.io/dashboard`. The host is what `authorize` gets
38
+ * to approve; path + query become the post-handoff redirect.
39
+ */
40
+ target: string;
41
+ }
42
+ /** Argument bag for the app's `authorize` hook. */
43
+ export interface HandoffAuthorizeArgs {
44
+ /** Cognito `sub` from the verified id token. */
45
+ userSub: string;
46
+ /** All claims of the verified id token. */
47
+ claims: Record<string, unknown>;
48
+ /** Host (lowercased, port included) the user wants a session on. */
49
+ targetHost: string;
50
+ /** Live request context (tenancy middleware output, headers, …). */
51
+ ctx: RequestContext;
52
+ }
53
+ export interface SessionHandoffRoutesOptions {
54
+ /** App-provided single-use code storage. See {@link HandoffStore}. */
55
+ store: HandoffStore;
56
+ /**
57
+ * THE authorization gate: may this user get a session on
58
+ * `targetHost`? Apps typically map the host to a tenant and check
59
+ * membership. Return `false` to refuse (403). Unknown hosts MUST
60
+ * return `false` — this hook is the only thing standing between a
61
+ * session and an arbitrary domain.
62
+ */
63
+ authorize: (args: HandoffAuthorizeArgs) => Promise<boolean>;
64
+ /**
65
+ * Builds the absolute URL of the `complete` endpoint on a target
66
+ * host, WITHOUT query string. Defaults to
67
+ * `https://<targetHost><completePath>`. Override in dev (http, ports)
68
+ * or when the browser-visible API prefix differs per host.
69
+ */
70
+ completeUrl?: (targetHost: string, ctx: RequestContext) => string;
71
+ /**
72
+ * Browser-visible path of the `complete` route used by the default
73
+ * `completeUrl`. Include the app's proxy prefix if there is one
74
+ * (mycohort: `/api/auth/handoff`). Default `/auth/handoff`.
75
+ */
76
+ completePath?: string;
77
+ /**
78
+ * Where `complete` redirects when redemption fails (missing /
79
+ * expired / wrong-host code). Relative path on the target host.
80
+ * Default `/login`.
81
+ */
82
+ failurePath?: string;
83
+ /**
84
+ * Cookie attributes for the session cookies minted on the target
85
+ * host — same semantics as the federated factory: pass a function
86
+ * to resolve per request (Domain depends on the host being landed
87
+ * on). Defaults to VK's host-only defaults.
88
+ */
89
+ cookieOptions?: CookieOptions | ((ctx: RequestContext) => CookieOptions);
90
+ /**
91
+ * Resolve the current (browser-facing) host on the `complete` leg.
92
+ *
93
+ * NOTE: `complete` is a top-level GET navigation — there is no
94
+ * `origin` header and `referer` still points at the ISSUING host,
95
+ * so the general-purpose `getRequestHost()` chain would pick the
96
+ * wrong host. The default here reads `x-forwarded-host` (set by
97
+ * same-origin proxies) then `host`. Override when your proxy uses
98
+ * a different header.
99
+ */
100
+ resolveRequestHost?: (ctx: RequestContext) => string;
101
+ /** Code lifetime in seconds. Default {@link DEFAULT_HANDOFF_TTL_SECONDS}. */
102
+ ttlSeconds?: number;
103
+ /** Explicit Cognito config; defaults to env-var loading per request. */
104
+ config?: AuthServerConfig;
105
+ /** Extra middleware for both routes (tenancy, rate limiting, …). */
106
+ middleware?: import('@venturekit/runtime').Middleware<RequestContext>[];
107
+ }
108
+ /** Route bundle returned by {@link createSessionHandoffRoutes}. */
109
+ export interface SessionHandoffRoutes {
110
+ start: (event: APIGatewayProxyEventV2, context: LambdaContext) => Promise<APIGatewayProxyResultV2>;
111
+ complete: (event: APIGatewayProxyEventV2, context: LambdaContext) => Promise<APIGatewayProxyResultV2>;
112
+ }
113
+ /** Default browser-facing path of the `complete` route. */
114
+ export declare const DEFAULT_HANDOFF_COMPLETE_PATH = "/auth/handoff";
115
+ /**
116
+ * Build the session-handoff route pair. See module docs for the flow.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * export const handoffRoutes = createSessionHandoffRoutes({
121
+ * store: postgresHandoffStore,
122
+ * authorize: async ({ userSub, targetHost }) =>
123
+ * isApprovedMemberOfTenantOwning(targetHost, userSub),
124
+ * completeUrl: (host) => `https://${host}/api/auth/handoff`,
125
+ * cookieOptions: (ctx) => sessionCookieOptions(getRequestHost(ctx)),
126
+ * });
127
+ * ```
128
+ */
129
+ export declare function createSessionHandoffRoutes(options: SessionHandoffRoutesOptions): SessionHandoffRoutes;
130
+ //# sourceMappingURL=handoff-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-routes.d.ts","sourceRoot":"","sources":["../../src/server/handoff-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,OAAO,IAAI,aAAa,EAC9B,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,KAAK,cAAc,EAQpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAwB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,cAAc,CAAC;AAItB,wCAAwC;AACxC,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mDAAmD;AACnD,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,GAAG,EAAE,cAAc,CAAC;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC1C,sEAAsE;IACtE,KAAK,EAAE,YAAY,CAAC;IACpB;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,KAAK,MAAM,CAAC;IAClE;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,GAAG,CAAC,CAAC,GAAG,EAAE,cAAc,KAAK,aAAa,CAAC,CAAC;IACzE;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,CAAC;IACrD,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,oEAAoE;IACpE,UAAU,CAAC,EAAE,OAAO,qBAAqB,EAAE,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;CACzE;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,CACL,KAAK,EAAE,sBAAsB,EAC7B,OAAO,EAAE,aAAa,KACnB,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACtC,QAAQ,EAAE,CACR,KAAK,EAAE,sBAAsB,EAC7B,OAAO,EAAE,aAAa,KACnB,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACvC;AAED,2DAA2D;AAC3D,eAAO,MAAM,6BAA6B,kBAAkB,CAAC;AA6D7D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAgJtB"}
@@ -0,0 +1,178 @@
1
+ import { BadRequestError, ForbiddenError, UnauthorizedError, handler, setCookie, rawResult, redirect, } from '@venturekit/runtime';
2
+ import { buildSessionCookies, readCookieFromHeader, ID_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, } from './cookies.js';
3
+ import { loadAuthServerConfig } from './config.js';
4
+ import { DEFAULT_HANDOFF_TTL_SECONDS, issueHandoffCode, redeemHandoffCode, } from './handoff.js';
5
+ import { refreshSession } from './refresh.js';
6
+ import { verifyAndDecode } from './verify.js';
7
+ /** Default browser-facing path of the `complete` route. */
8
+ export const DEFAULT_HANDOFF_COMPLETE_PATH = '/auth/handoff';
9
+ function headersOf(ctx) {
10
+ return (ctx.rawEvent
11
+ ?.headers ?? {});
12
+ }
13
+ function headerValue(headers, name) {
14
+ for (const [key, value] of Object.entries(headers)) {
15
+ if (key.toLowerCase() === name && value)
16
+ return value;
17
+ }
18
+ return undefined;
19
+ }
20
+ /** Default current-host resolution for the `complete` navigation leg. */
21
+ function defaultResolveRequestHost(ctx) {
22
+ const headers = headersOf(ctx);
23
+ return (headerValue(headers, 'x-forwarded-host') ??
24
+ headerValue(headers, 'host') ??
25
+ '');
26
+ }
27
+ /**
28
+ * `next` must be a same-host path: absolute (`/dashboard`), not
29
+ * protocol-relative (`//evil.io`), not an absolute URL, and not a
30
+ * backslash variant (`/\evil.io` — browsers normalize `\` to `/` in
31
+ * URLs, turning it protocol-relative). Otherwise `complete` would be
32
+ * an open redirector.
33
+ */
34
+ function sanitizeNextPath(next, fallback) {
35
+ if (!next)
36
+ return fallback;
37
+ if (!/^\/(?![/\\])/.test(next))
38
+ return fallback;
39
+ return next;
40
+ }
41
+ /**
42
+ * Redirect for the browser-navigation legs, with cache/referrer
43
+ * hygiene: the request URL carries the one-time code, so the response
44
+ * must never be cached by shared caches (`no-store`) and the landing
45
+ * page must not leak the code onward via `Referer`.
46
+ */
47
+ function navRedirect(location) {
48
+ const base = redirect(location);
49
+ const result = typeof base === 'string' ? { statusCode: 302, body: '' } : base;
50
+ return rawResult({
51
+ ...result,
52
+ headers: {
53
+ ...(typeof base === 'string' ? {} : base.headers),
54
+ 'Cache-Control': 'no-store',
55
+ 'Referrer-Policy': 'no-referrer',
56
+ },
57
+ });
58
+ }
59
+ /**
60
+ * Build the session-handoff route pair. See module docs for the flow.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * export const handoffRoutes = createSessionHandoffRoutes({
65
+ * store: postgresHandoffStore,
66
+ * authorize: async ({ userSub, targetHost }) =>
67
+ * isApprovedMemberOfTenantOwning(targetHost, userSub),
68
+ * completeUrl: (host) => `https://${host}/api/auth/handoff`,
69
+ * cookieOptions: (ctx) => sessionCookieOptions(getRequestHost(ctx)),
70
+ * });
71
+ * ```
72
+ */
73
+ export function createSessionHandoffRoutes(options) {
74
+ const { store, authorize, completePath = DEFAULT_HANDOFF_COMPLETE_PATH, failurePath = '/login', ttlSeconds = DEFAULT_HANDOFF_TTL_SECONDS, resolveRequestHost = defaultResolveRequestHost, middleware: extraMiddleware = [], } = options;
75
+ const cookieOptionsFor = (ctx) => {
76
+ if (!options.cookieOptions)
77
+ return {};
78
+ return typeof options.cookieOptions === 'function'
79
+ ? options.cookieOptions(ctx)
80
+ : options.cookieOptions;
81
+ };
82
+ const buildCompleteUrl = (targetHost, ctx) => options.completeUrl
83
+ ? options.completeUrl(targetHost, ctx)
84
+ : `https://${targetHost}${completePath}`;
85
+ // ─── start ──────────────────────────────────────────────
86
+ const start = handler(async (body, ctx) => {
87
+ if (!body?.target) {
88
+ throw new BadRequestError('target is required');
89
+ }
90
+ let target;
91
+ try {
92
+ target = new URL(body.target);
93
+ }
94
+ catch {
95
+ throw new BadRequestError('target must be an absolute URL');
96
+ }
97
+ if (target.protocol !== 'https:' && target.protocol !== 'http:') {
98
+ throw new BadRequestError('target must be http(s)');
99
+ }
100
+ const targetHost = target.host.toLowerCase();
101
+ const nextPath = sanitizeNextPath(`${target.pathname}${target.search}`, '/');
102
+ // The session being handed off — both cookies are required: the
103
+ // id token proves WHO is asking, the refresh token is what the
104
+ // target host will mint its session from.
105
+ const cookieHeader = ctx.rawEvent.headers?.['cookie'] ?? ctx.rawEvent.headers?.['Cookie'] ?? null;
106
+ const idToken = readCookieFromHeader(cookieHeader, ID_TOKEN_COOKIE);
107
+ const refreshToken = readCookieFromHeader(cookieHeader, REFRESH_TOKEN_COOKIE);
108
+ if (!idToken || !refreshToken) {
109
+ throw new UnauthorizedError('No session to hand off');
110
+ }
111
+ const config = options.config ?? loadAuthServerConfig();
112
+ const claims = await verifyAndDecode(idToken, {
113
+ userPoolId: config.userPoolId,
114
+ clientId: config.appClientId,
115
+ tokenUse: 'id',
116
+ endpoint: config.endpoint,
117
+ });
118
+ const userSub = claims && typeof claims['sub'] === 'string' ? claims['sub'] : null;
119
+ if (!userSub) {
120
+ throw new UnauthorizedError('Invalid session');
121
+ }
122
+ const allowed = await authorize({
123
+ userSub,
124
+ claims: claims,
125
+ targetHost,
126
+ ctx,
127
+ });
128
+ if (!allowed) {
129
+ throw new ForbiddenError('Not authorized for this destination');
130
+ }
131
+ const code = await issueHandoffCode(store, { sub: userSub, refreshToken, targetHost }, ttlSeconds);
132
+ const url = `${buildCompleteUrl(targetHost, ctx)}?code=${encodeURIComponent(code)}` +
133
+ `&next=${encodeURIComponent(nextPath)}`;
134
+ return { url };
135
+ }, {
136
+ status: 200,
137
+ middleware: extraMiddleware,
138
+ });
139
+ // ─── complete ───────────────────────────────────────────
140
+ const complete = handler(async (_body, ctx) => {
141
+ const code = ctx.queryParams?.['code'] ?? '';
142
+ const next = sanitizeNextPath(ctx.queryParams?.['next'], '/');
143
+ const currentHost = resolveRequestHost(ctx);
144
+ const payload = await redeemHandoffCode(store, code, currentHost);
145
+ if (!payload) {
146
+ // Missing / expired / wrong-host code. The user is mid-
147
+ // navigation on the target host: land them on the login page,
148
+ // never on a JSON error.
149
+ return navRedirect(failurePath);
150
+ }
151
+ const config = options.config ?? loadAuthServerConfig();
152
+ let tokens;
153
+ try {
154
+ tokens = await refreshSession(payload.refreshToken, config);
155
+ }
156
+ catch {
157
+ return navRedirect(failurePath);
158
+ }
159
+ // `refreshSession` never returns a refresh token (Cognito does
160
+ // not rotate it) — carry the original over so the target host
161
+ // can refresh on its own later.
162
+ const cookieOptions = cookieOptionsFor(ctx);
163
+ const sessionCookies = buildSessionCookies({
164
+ idToken: tokens.idToken,
165
+ accessToken: tokens.accessToken,
166
+ refreshToken: payload.refreshToken,
167
+ expiresIn: tokens.expiresIn,
168
+ }, cookieOptions);
169
+ for (const cookie of sessionCookies) {
170
+ setCookie(ctx, cookie);
171
+ }
172
+ return navRedirect(next);
173
+ }, {
174
+ middleware: extraMiddleware,
175
+ });
176
+ return { start, complete };
177
+ }
178
+ //# sourceMappingURL=handoff-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-routes.js","sourceRoot":"","sources":["../../src/server/handoff-routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Cross-domain session handoff — core primitives.
3
+ *
4
+ * **Why this exists.** Browser cookies cannot span registrable domains:
5
+ * a session on `app.platform.com` can never be read by `tenant.io`, no
6
+ * matter what `Domain=` attribute it carries. Multi-tenant products
7
+ * with white-label custom domains therefore need an SSO-style handoff
8
+ * to "move" a session between hosts: the host WITH a session mints a
9
+ * short-lived, single-use **handoff code** bound to the target host;
10
+ * the target host redeems it server-side and mints its own first-party
11
+ * cookies. The user's refresh token travels server-to-server through
12
+ * the app's {@link HandoffStore} — only the opaque code ever appears
13
+ * in a URL.
14
+ *
15
+ * **Security properties**
16
+ * - Codes are 256-bit `crypto.randomBytes` values — unguessable.
17
+ * - The store is keyed by the **SHA-256 hash** of the code, so a
18
+ * leaked store (DB dump, logs) cannot be replayed into sessions.
19
+ * - Codes are single-use: {@link HandoffStore.take} must delete
20
+ * atomically (e.g. SQL `DELETE … RETURNING`).
21
+ * - Codes expire after {@link DEFAULT_HANDOFF_TTL_SECONDS}; expiry is
22
+ * enforced BOTH by the store's TTL mechanism and re-checked here on
23
+ * redeem, so a lagging store cleanup can't extend the window.
24
+ * - Codes are bound to the target host at issue time; redeeming on
25
+ * any other host fails — and a wrong-host attempt BURNS the code,
26
+ * so it cannot be probed and then replayed on the right host.
27
+ * Combined with the route factory's `authorize` hook this
28
+ * guarantees a session can only ever materialize on hosts the app
29
+ * explicitly approved for THIS user.
30
+ * - The `complete` redirects carry `Cache-Control: no-store` and
31
+ * `Referrer-Policy: no-referrer` so the code-bearing URL never
32
+ * sits in a shared cache and never leaks via `Referer`.
33
+ *
34
+ * **Residual risks — know what this does NOT protect against**
35
+ * - *Login CSRF*: like every URL-based SSO handoff (OAuth code flow
36
+ * included), an attacker can mint a code for THEIR OWN session and
37
+ * lure a victim into opening the complete URL, logging the victim
38
+ * in as the attacker on that host. The 60s single-use window and
39
+ * the membership-gated `authorize` hook narrow this to hosts the
40
+ * ATTACKER belongs to; apps for which this matters should show the
41
+ * signed-in identity prominently post-handoff.
42
+ * - *Store contents*: payloads hold live refresh tokens. The hashed
43
+ * key prevents lookup-by-code, rows die in ≤60s, but a hostile DBA
44
+ * window exists — encrypt payloads at rest if that is in scope.
45
+ *
46
+ * The route-level flow lives in `handoff-routes.ts`
47
+ * ({@link createSessionHandoffRoutes}); apps supply the storage
48
+ * (Postgres / DynamoDB / Redis — anything with atomic take).
49
+ */
50
+ /** Default lifetime of a handoff code: one browser redirect, not more. */
51
+ export declare const DEFAULT_HANDOFF_TTL_SECONDS = 60;
52
+ /**
53
+ * What the issuing host stashes for the target host to redeem.
54
+ *
55
+ * Contains the user's **refresh token** — treat stored payloads as
56
+ * secrets (the code-hash key already prevents lookup-by-code, and rows
57
+ * are deleted on redeem / expiry, but encrypt at rest if your store's
58
+ * threat model calls for it).
59
+ */
60
+ export interface HandoffPayload {
61
+ /** Cognito subject of the user the session belongs to. */
62
+ sub: string;
63
+ /** The session's refresh token — used by the target host to mint fresh tokens. */
64
+ refreshToken: string;
65
+ /** Host (including port, lowercased) the code was issued FOR. */
66
+ targetHost: string;
67
+ /** Epoch millis after which the code is dead even if the store still has it. */
68
+ expiresAt: number;
69
+ }
70
+ /**
71
+ * Storage the app provides for in-flight handoff codes.
72
+ *
73
+ * Keys are code **hashes** (see module docs). Implementations must make
74
+ * `take` atomic — two concurrent redeems of the same code must never
75
+ * both succeed:
76
+ *
77
+ * ```sql
78
+ * -- Postgres example
79
+ * DELETE FROM auth_handoff_codes WHERE code_hash = $1 RETURNING payload;
80
+ * ```
81
+ */
82
+ export interface HandoffStore {
83
+ /** Persist a payload under `codeHash`, expiring after `ttlSeconds`. */
84
+ put(codeHash: string, payload: HandoffPayload, ttlSeconds: number): Promise<void>;
85
+ /** Atomically fetch AND delete. Returns null when absent (or already taken). */
86
+ take(codeHash: string): Promise<HandoffPayload | null>;
87
+ }
88
+ /** Generate an unguessable handoff code (256-bit, base64url). */
89
+ export declare function generateHandoffCode(): string;
90
+ /** Hash a handoff code for use as a store key. */
91
+ export declare function hashHandoffCode(code: string): string;
92
+ /**
93
+ * Issue a handoff code for `targetHost`.
94
+ *
95
+ * @returns the RAW code — put it in the redirect URL. Only its hash
96
+ * touches the store.
97
+ */
98
+ export declare function issueHandoffCode(store: HandoffStore, input: {
99
+ sub: string;
100
+ refreshToken: string;
101
+ targetHost: string;
102
+ }, ttlSeconds?: number): Promise<string>;
103
+ /**
104
+ * Redeem a handoff code on `currentHost`.
105
+ *
106
+ * Returns the payload when the code exists, hasn't expired, and was
107
+ * issued for exactly this host — null otherwise. The code is consumed
108
+ * either way (single-use: a failed redeem must burn it too, so an
109
+ * attacker can't probe).
110
+ */
111
+ export declare function redeemHandoffCode(store: HandoffStore, code: string, currentHost: string): Promise<HandoffPayload | null>;
112
+ //# sourceMappingURL=handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff.d.ts","sourceRoot":"","sources":["../../src/server/handoff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAIH,0EAA0E;AAC1E,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,gFAAgF;IAChF,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACxD;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,kDAAkD;AAClD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAOD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,EAChE,UAAU,GAAE,MAAoC,GAC/C,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAOhC"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Cross-domain session handoff — core primitives.
3
+ *
4
+ * **Why this exists.** Browser cookies cannot span registrable domains:
5
+ * a session on `app.platform.com` can never be read by `tenant.io`, no
6
+ * matter what `Domain=` attribute it carries. Multi-tenant products
7
+ * with white-label custom domains therefore need an SSO-style handoff
8
+ * to "move" a session between hosts: the host WITH a session mints a
9
+ * short-lived, single-use **handoff code** bound to the target host;
10
+ * the target host redeems it server-side and mints its own first-party
11
+ * cookies. The user's refresh token travels server-to-server through
12
+ * the app's {@link HandoffStore} — only the opaque code ever appears
13
+ * in a URL.
14
+ *
15
+ * **Security properties**
16
+ * - Codes are 256-bit `crypto.randomBytes` values — unguessable.
17
+ * - The store is keyed by the **SHA-256 hash** of the code, so a
18
+ * leaked store (DB dump, logs) cannot be replayed into sessions.
19
+ * - Codes are single-use: {@link HandoffStore.take} must delete
20
+ * atomically (e.g. SQL `DELETE … RETURNING`).
21
+ * - Codes expire after {@link DEFAULT_HANDOFF_TTL_SECONDS}; expiry is
22
+ * enforced BOTH by the store's TTL mechanism and re-checked here on
23
+ * redeem, so a lagging store cleanup can't extend the window.
24
+ * - Codes are bound to the target host at issue time; redeeming on
25
+ * any other host fails — and a wrong-host attempt BURNS the code,
26
+ * so it cannot be probed and then replayed on the right host.
27
+ * Combined with the route factory's `authorize` hook this
28
+ * guarantees a session can only ever materialize on hosts the app
29
+ * explicitly approved for THIS user.
30
+ * - The `complete` redirects carry `Cache-Control: no-store` and
31
+ * `Referrer-Policy: no-referrer` so the code-bearing URL never
32
+ * sits in a shared cache and never leaks via `Referer`.
33
+ *
34
+ * **Residual risks — know what this does NOT protect against**
35
+ * - *Login CSRF*: like every URL-based SSO handoff (OAuth code flow
36
+ * included), an attacker can mint a code for THEIR OWN session and
37
+ * lure a victim into opening the complete URL, logging the victim
38
+ * in as the attacker on that host. The 60s single-use window and
39
+ * the membership-gated `authorize` hook narrow this to hosts the
40
+ * ATTACKER belongs to; apps for which this matters should show the
41
+ * signed-in identity prominently post-handoff.
42
+ * - *Store contents*: payloads hold live refresh tokens. The hashed
43
+ * key prevents lookup-by-code, rows die in ≤60s, but a hostile DBA
44
+ * window exists — encrypt payloads at rest if that is in scope.
45
+ *
46
+ * The route-level flow lives in `handoff-routes.ts`
47
+ * ({@link createSessionHandoffRoutes}); apps supply the storage
48
+ * (Postgres / DynamoDB / Redis — anything with atomic take).
49
+ */
50
+ import { createHash, randomBytes } from 'node:crypto';
51
+ /** Default lifetime of a handoff code: one browser redirect, not more. */
52
+ export const DEFAULT_HANDOFF_TTL_SECONDS = 60;
53
+ /** Generate an unguessable handoff code (256-bit, base64url). */
54
+ export function generateHandoffCode() {
55
+ return randomBytes(32).toString('base64url');
56
+ }
57
+ /** Hash a handoff code for use as a store key. */
58
+ export function hashHandoffCode(code) {
59
+ return createHash('sha256').update(code).digest('base64url');
60
+ }
61
+ /** Lowercase a host for comparison. Ports are significant (dev hosts). */
62
+ function normalizeHost(host) {
63
+ return host.trim().toLowerCase();
64
+ }
65
+ /**
66
+ * Issue a handoff code for `targetHost`.
67
+ *
68
+ * @returns the RAW code — put it in the redirect URL. Only its hash
69
+ * touches the store.
70
+ */
71
+ export async function issueHandoffCode(store, input, ttlSeconds = DEFAULT_HANDOFF_TTL_SECONDS) {
72
+ const code = generateHandoffCode();
73
+ const payload = {
74
+ sub: input.sub,
75
+ refreshToken: input.refreshToken,
76
+ targetHost: normalizeHost(input.targetHost),
77
+ expiresAt: Date.now() + ttlSeconds * 1000,
78
+ };
79
+ await store.put(hashHandoffCode(code), payload, ttlSeconds);
80
+ return code;
81
+ }
82
+ /**
83
+ * Redeem a handoff code on `currentHost`.
84
+ *
85
+ * Returns the payload when the code exists, hasn't expired, and was
86
+ * issued for exactly this host — null otherwise. The code is consumed
87
+ * either way (single-use: a failed redeem must burn it too, so an
88
+ * attacker can't probe).
89
+ */
90
+ export async function redeemHandoffCode(store, code, currentHost) {
91
+ if (!code)
92
+ return null;
93
+ const payload = await store.take(hashHandoffCode(code));
94
+ if (!payload)
95
+ return null;
96
+ if (Date.now() > payload.expiresAt)
97
+ return null;
98
+ if (payload.targetHost !== normalizeHost(currentHost))
99
+ return null;
100
+ return payload;
101
+ }
102
+ //# sourceMappingURL=handoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff.js","sourceRoot":"","sources":["../../src/server/handoff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAwC9C,iEAAiE;AACjE,MAAM,UAAU,mBAAmB;IACjC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAC/D,CAAC;AAED,0EAA0E;AAC1E,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAmB,EACnB,KAAgE,EAChE,aAAqB,2BAA2B;IAEhD,MAAM,IAAI,GAAG,mBAAmB,EAAE,CAAC;IACnC,MAAM,OAAO,GAAmB;QAC9B,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,UAAU,CAAC;QAC3C,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI;KAC1C,CAAC;IACF,MAAM,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAmB,EACnB,IAAY,EACZ,WAAmB;IAEnB,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAChD,IAAI,OAAO,CAAC,UAAU,KAAK,aAAa,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -34,19 +34,26 @@ export type { AdminInviteUserInput, AdminInviteUserResult, AdminUpdateAttributes
34
34
  export { adminInviteUser, adminUpdateUserAttributes, adminDisableUser, adminEnableUser, adminDeleteUser, } from './admin-users.js';
35
35
  export type { RefreshResult } from './refresh.js';
36
36
  export { refreshSession } from './refresh.js';
37
- export { revokeRefreshToken } from './revoke.js';
37
+ export { revokeRefreshToken, globalSignOut } from './revoke.js';
38
+ export type { EnsureUserForVerifiedEmailInput, EnsureUserForVerifiedEmailResult, } from './passwordless.js';
39
+ export { ensureUserForVerifiedEmail, mintSessionForVerifiedEmail, } from './passwordless.js';
38
40
  export type { ChangePasswordInput } from './change-password.js';
39
41
  export { changePassword } from './change-password.js';
40
42
  export type { ForgotPasswordInput, ForgotPasswordResult, ConfirmForgotPasswordInput, CodeDeliveryDetails, } from './forgot-password.js';
41
43
  export { forgotPassword, confirmForgotPassword } from './forgot-password.js';
42
44
  export type { VerifyOptions } from './verify.js';
43
45
  export { verifyAndDecode } from './verify.js';
44
- export type { SessionTokens, CookieOptions, OAuthStateCookieOptions, } from './cookies.js';
45
- export { ID_TOKEN_COOKIE, ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, buildSessionCookies, buildClearSessionCookies, readCookieFromHeader, buildOAuthStateCookie, clearOAuthStateCookie, oauthStateCookieName, } from './cookies.js';
46
+ export { decodeJwtClaims } from './token-utils.js';
47
+ export type { SessionTokens, CookieOptions, CookieDomainOptions, OAuthStateCookieOptions, } from './cookies.js';
48
+ export { ID_TOKEN_COOKIE, ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, buildSessionCookies, buildClearSessionCookies, readCookieFromHeader, buildOAuthStateCookie, clearOAuthStateCookie, oauthStateCookieName, resolveCookieDomain, } from './cookies.js';
46
49
  export type { CookieAuthMiddlewareOptions } from './middleware.js';
47
- export { cookieAuthMiddleware, extractToken } from './middleware.js';
50
+ export { cookieAuthMiddleware, extractToken, defaultScopeMapper } from './middleware.js';
48
51
  export type { FederatedAuthRoutes, FederatedAuthRoutesOptions, FederatedCallbackBody, FederatedCallbackResult, FederatedOnSignInArgs, FederatedOnSignInResult, FederatedStartBody, } from './federated-routes.js';
49
52
  export { createFederatedAuthRoutes } from './federated-routes.js';
53
+ export type { HandoffPayload, HandoffStore } from './handoff.js';
54
+ export { DEFAULT_HANDOFF_TTL_SECONDS, generateHandoffCode, hashHandoffCode, issueHandoffCode, redeemHandoffCode, } from './handoff.js';
55
+ export type { HandoffAuthorizeArgs, HandoffStartBody, SessionHandoffRoutes, SessionHandoffRoutesOptions, } from './handoff-routes.js';
56
+ export { createSessionHandoffRoutes, DEFAULT_HANDOFF_COMPLETE_PATH, } from './handoff-routes.js';
50
57
  export type { FederatedProvider, FederatedProfile, FederatedProviderCredentials, SignInAsFederatedUserInput, BuildAuthorizeUrlInput, ExchangeAuthorizationCodeInput, } from './federated.js';
51
58
  export { loadFederatedProviderCredentials, generateOAuthState, verifyOAuthState, buildAuthorizeUrl, exchangeAuthorizationCode, signInAsFederatedUser, } from './federated.js';
52
59
  export type { VerificationChannel, VerificationCodeStore, VerificationCodeRecord, RequestVerificationCodeInput, RequestVerificationCodeResult, VerifyVerificationCodeInput, } from './verification.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,YAAY,EACV,eAAe,EACf,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,GAC9B,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,YAAY,EACV,mBAAmB,EACnB,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7E,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,YAAY,EACV,aAAa,EACb,aAAa,EACb,uBAAuB,GACxB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAEtB,YAAY,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAErE,YAAY,EACV,mBAAmB,EACnB,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAElE,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,4BAA4B,EAC5B,0BAA0B,EAC1B,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,gCAAgC,EAChC,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,4BAA4B,EAC5B,6BAA6B,EAC7B,2BAA2B,GAC5B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,mCAAmC,GACpC,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
@@ -28,13 +28,19 @@ export { signInWithPasswordOrChallenge, respondToNewPasswordChallenge, } from '.
28
28
  export { signUpUser } from './sign-up.js';
29
29
  export { adminInviteUser, adminUpdateUserAttributes, adminDisableUser, adminEnableUser, adminDeleteUser, } from './admin-users.js';
30
30
  export { refreshSession } from './refresh.js';
31
- export { revokeRefreshToken } from './revoke.js';
31
+ export { revokeRefreshToken, globalSignOut } from './revoke.js';
32
+ export { ensureUserForVerifiedEmail, mintSessionForVerifiedEmail, } from './passwordless.js';
32
33
  export { changePassword } from './change-password.js';
33
34
  export { forgotPassword, confirmForgotPassword } from './forgot-password.js';
34
35
  export { verifyAndDecode } from './verify.js';
35
- export { ID_TOKEN_COOKIE, ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, buildSessionCookies, buildClearSessionCookies, readCookieFromHeader, buildOAuthStateCookie, clearOAuthStateCookie, oauthStateCookieName, } from './cookies.js';
36
- export { cookieAuthMiddleware, extractToken } from './middleware.js';
36
+ // Unverified claim extraction ONLY for tokens received directly
37
+ // from the provider (e.g. reading profile claims right after sign-in).
38
+ export { decodeJwtClaims } from './token-utils.js';
39
+ export { ID_TOKEN_COOKIE, ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, buildSessionCookies, buildClearSessionCookies, readCookieFromHeader, buildOAuthStateCookie, clearOAuthStateCookie, oauthStateCookieName, resolveCookieDomain, } from './cookies.js';
40
+ export { cookieAuthMiddleware, extractToken, defaultScopeMapper } from './middleware.js';
37
41
  export { createFederatedAuthRoutes } from './federated-routes.js';
42
+ export { DEFAULT_HANDOFF_TTL_SECONDS, generateHandoffCode, hashHandoffCode, issueHandoffCode, redeemHandoffCode, } from './handoff.js';
43
+ export { createSessionHandoffRoutes, DEFAULT_HANDOFF_COMPLETE_PATH, } from './handoff-routes.js';
38
44
  export { loadFederatedProviderCredentials, generateOAuthState, verifyOAuthState, buildAuthorizeUrl, exchangeAuthorizationCode, signInAsFederatedUser, } from './federated.js';
39
45
  export { generateVerificationCode, hashVerificationCode, requestVerificationCode, verifyVerificationCode, createInMemoryVerificationCodeStore, } from './verification.js';
40
46
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAOlD,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,GAC9B,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO1C,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAQtD,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAG7E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAO9C,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAWrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAUlE,OAAO,EACL,gCAAgC,EAChC,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,mCAAmC,GACpC,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAOlD,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,GAC9B,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO1C,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAQhE,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAQtD,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAG7E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,kEAAkE;AAClE,uEAAuE;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAQnD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAWzF,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAGlE,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAQtB,OAAO,EACL,0BAA0B,EAC1B,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAU7B,OAAO,EACL,gCAAgC,EAChC,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,mCAAmC,GACpC,MAAM,mBAAmB,CAAC"}