okengine 0.4.3 → 0.5.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/package.json +3 -1
- package/site/content/docs/ai/index.mdx +24 -0
- package/site/content/docs/ai/llms-txt.mdx +3 -0
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/console/gates.mdx +46 -8
- package/site/content/docs/console/index.mdx +54 -0
- package/site/content/docs/console/meta.json +1 -0
- package/site/content/docs/elements/gate.mdx +187 -48
- package/site/content/docs/elements/index.mdx +45 -0
- package/site/content/docs/elements/meta.json +1 -1
- package/site/content/docs/get-started/basic-usage.mdx +4 -3
- package/site/content/docs/get-started/index.mdx +33 -0
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/index.mdx +9 -31
- package/site/content/docs/plugins/anonymous.mdx +95 -0
- package/site/content/docs/plugins/compression.mdx +2 -2
- package/site/content/docs/plugins/cors.mdx +2 -2
- package/site/content/docs/plugins/csrf.mdx +2 -2
- package/site/content/docs/plugins/email-otp.mdx +111 -0
- package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +1 -1
- package/site/content/docs/plugins/index.mdx +69 -0
- package/site/content/docs/plugins/magic-link.mdx +112 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
- package/site/content/docs/plugins/meta.json +10 -1
- package/site/content/docs/plugins/passkey.mdx +128 -0
- package/site/content/docs/plugins/phone-number.mdx +111 -0
- package/site/content/docs/plugins/two-factor.mdx +116 -0
- package/site/content/docs/plugins/username.mdx +117 -0
- package/site/content/docs/reference/client.mdx +331 -0
- package/site/content/docs/reference/fx.mdx +20 -5
- package/site/content/docs/reference/index.mdx +45 -0
- package/site/content/docs/reference/meta.json +11 -1
- package/site/content/docs/reference/plugins.mdx +25 -14
- package/src/auth/auth.test.ts +20 -2
- package/src/auth/bindings.ts +439 -0
- package/src/auth/breach-check.ts +112 -0
- package/src/auth/config.ts +288 -0
- package/src/auth/cookies.ts +123 -0
- package/src/auth/gate-auth.test.ts +379 -0
- package/src/auth/identity.ts +190 -0
- package/src/auth/index.ts +117 -1
- package/src/auth/method-context.ts +33 -0
- package/src/auth/operator.ts +27 -1
- package/src/auth/password-policy.test.ts +126 -0
- package/src/auth/password-policy.ts +77 -0
- package/src/auth/plugin.ts +62 -4
- package/src/auth/rate.ts +45 -0
- package/src/auth/schema.ts +260 -0
- package/src/auth/secondary-storage.ts +37 -0
- package/src/auth/sessions.ts +58 -1
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +78 -0
- package/src/cli/dev.test.ts +3 -3
- package/src/cli/schema.ts +95 -23
- package/src/client/auth.ts +120 -0
- package/src/client-react/index.ts +93 -0
- package/src/compiler/aot.test.ts +2 -1
- package/src/compiler/extract.ts +19 -0
- package/src/compiler/response.ts +16 -2
- package/src/console/server/app.ts +10 -6
- package/src/console/server/auth-rate.test.ts +3 -3
- package/src/console/server/bind.ts +12 -1
- package/src/console/server/channels.test.ts +1 -1
- package/src/console/server/console-gates.ts +14 -0
- package/src/console/server/console.test.ts +6 -6
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +2 -0
- package/src/console/server/gates.ts +8 -1
- package/src/console/server/operator-db.test.ts +4 -4
- package/src/console/server/operator-db.ts +22 -4
- package/src/console/server/security.gate.test.ts +3 -3
- package/src/console/ui/gates/fixture.ts +4 -0
- package/src/console/ui/gates/types.ts +2 -0
- package/src/console/ui/shell/client.ts +1 -0
- package/src/elements/gate/boot.ts +136 -0
- package/src/elements/gate/config.ts +69 -0
- package/src/elements/gate/declare.ts +51 -1
- package/src/elements/gate/runtime.ts +3 -1
- package/src/elements/gate.test.ts +77 -0
- package/src/elements/gate.ts +20 -1
- package/src/elements/index.ts +8 -0
- package/src/index.ts +11 -0
- package/src/kernel/app.ts +253 -32
- package/src/kernel/boot.test.ts +40 -3
- package/src/kernel/boot.ts +8 -0
- package/src/kernel/call.test.ts +46 -2
- package/src/kernel/edge.test.ts +3 -3
- package/src/kernel/flow.test.ts +2 -2
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +60 -0
- package/src/kernel/hooks.test.ts +4 -4
- package/src/kernel/index.ts +12 -0
- package/src/kernel/pipeline.test.ts +12 -8
- package/src/kernel/pipeline.ts +23 -4
- package/src/kernel/plugin/decorate.test.ts +3 -3
- package/src/kernel/plugin/scoping.test.ts +3 -3
- package/src/kernel/plugin-elements.test.ts +51 -0
- package/src/kernel/plugin-needs.test.ts +83 -0
- package/src/kernel/plugin-needs.ts +129 -0
- package/src/kernel/plugin.ts +101 -0
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/registry.ts +102 -3
- package/src/manifest/types.ts +2 -0
- package/src/plugins/anonymous.ts +58 -0
- package/src/plugins/auth/shared.ts +121 -0
- package/src/plugins/auth-methods.test.ts +176 -0
- package/src/plugins/compression.test.ts +5 -5
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/cors.test.ts +16 -10
- package/src/plugins/csrf.test.ts +1 -1
- package/src/plugins/email-otp.ts +161 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/ip-allowlist.test.ts +19 -9
- package/src/plugins/magic-link.ts +163 -0
- package/src/plugins/maintenance-mode.test.ts +9 -5
- package/src/plugins/passkey.ts +216 -0
- package/src/plugins/phone-number.ts +149 -0
- package/src/plugins/security-headers.test.ts +14 -14
- package/src/plugins/two-factor.ts +249 -0
- package/src/plugins/username.ts +148 -0
- package/src/runs/runs.test.ts +6 -2
- package/src/runtime/primitives.ts +37 -4
- package/src/runtime/serve.test.ts +3 -2
- package/src/runtime/types.ts +25 -2
- package/src/test/create-test-app.test.ts +1 -1
- package/src/test/create-test-app.ts +4 -1
- package/src/test/provisions.integration.test.ts +1 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate auth options — resolved from `oke({ gate: { auth } })`.
|
|
3
|
+
*
|
|
4
|
+
* Better Auth is inspiration for shape only; this is OKE-native.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PasswordHashOptions } from "../runtime/types.ts";
|
|
8
|
+
import type { SessionStore } from "./sessions.ts";
|
|
9
|
+
import type { BreachCheckFn } from "./breach-check.ts";
|
|
10
|
+
import type { PasswordPolicyOptions } from "./password-policy.ts";
|
|
11
|
+
import {
|
|
12
|
+
resolveAuthSchema,
|
|
13
|
+
type AuthModelOptions,
|
|
14
|
+
type AuthSchemaOptions,
|
|
15
|
+
type ResolvedAuthSchema,
|
|
16
|
+
} from "./schema.ts";
|
|
17
|
+
import { sessionCryptoFromAuthOptions, type AuthSessionOptions } from "./plugin.ts";
|
|
18
|
+
|
|
19
|
+
/** Email + password method knobs. */
|
|
20
|
+
export interface EmailAndPasswordOptions {
|
|
21
|
+
readonly enabled?: boolean;
|
|
22
|
+
/** Require verified email before sign-in (default false). */
|
|
23
|
+
readonly requireEmailVerification?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Account-linking policy (Phase 1 stub — enforced later with OAuth). */
|
|
27
|
+
export interface AccountLinkingOptions {
|
|
28
|
+
readonly enabled?: boolean;
|
|
29
|
+
readonly trustedProviders?: readonly string[];
|
|
30
|
+
readonly allowDifferentEmails?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Opt-in cookie session mirror (Phase 1a). Bearer remains default. */
|
|
34
|
+
export interface AuthCookieOptions {
|
|
35
|
+
readonly enabled?: boolean;
|
|
36
|
+
/** Cookie name prefix (default `oke`). */
|
|
37
|
+
readonly prefix?: string;
|
|
38
|
+
readonly secure?: boolean;
|
|
39
|
+
/** Share across subdomains (`.example.com`). */
|
|
40
|
+
readonly crossSubdomain?: boolean;
|
|
41
|
+
readonly domain?: string;
|
|
42
|
+
readonly sameSite?: "strict" | "lax" | "none";
|
|
43
|
+
readonly path?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Secondary hot-path storage (Phase 1a) — uses `store.kv` when configured. */
|
|
47
|
+
export interface AuthSecondaryStorageOptions {
|
|
48
|
+
readonly enabled?: boolean;
|
|
49
|
+
/** Kv namespace prefix (default `auth:`). */
|
|
50
|
+
readonly prefix?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Lifecycle hooks (Phase 1a) — fx-safe, no side-channel I/O. */
|
|
54
|
+
export interface AuthDatabaseHooks {
|
|
55
|
+
readonly user?: {
|
|
56
|
+
readonly create?: {
|
|
57
|
+
readonly before?: (user: Record<string, unknown>) => unknown | Promise<unknown>;
|
|
58
|
+
readonly after?: (user: Record<string, unknown>) => void | Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
readonly session?: {
|
|
62
|
+
readonly create?: {
|
|
63
|
+
readonly after?: (session: Record<string, unknown>) => void | Promise<void>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
readonly account?: {
|
|
67
|
+
readonly create?: {
|
|
68
|
+
readonly after?: (account: Record<string, unknown>) => void | Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Session options under `gate.auth.session` (extends plugin session + schema). */
|
|
74
|
+
export interface GateAuthSessionOptions extends AuthSessionOptions, AuthModelOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Max age (ms) from session creation for "fresh" step-up policies.
|
|
77
|
+
* Default 24h. `fx.auth` / gate policies can require freshness.
|
|
78
|
+
*/
|
|
79
|
+
readonly freshAgeMs?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Public `gate.auth` bag. */
|
|
83
|
+
export interface GateAuthOptions extends AuthSchemaOptions {
|
|
84
|
+
/** HMAC secret for access tokens. Required in prod; minted in dev when omitted. */
|
|
85
|
+
readonly secret?: string;
|
|
86
|
+
/** HTTP base path for auth Flows (default `/auth`). */
|
|
87
|
+
readonly basePath?: string;
|
|
88
|
+
/**
|
|
89
|
+
* When `false`, skip materializing `/auth/*` HTTP Bindings
|
|
90
|
+
* (embedding / Console — secret + tables only). Default `true`.
|
|
91
|
+
*/
|
|
92
|
+
readonly http?: boolean;
|
|
93
|
+
/** Audience stamped on access tokens (default `oke-app`). */
|
|
94
|
+
readonly audience?: string;
|
|
95
|
+
readonly emailAndPassword?: EmailAndPasswordOptions;
|
|
96
|
+
readonly session?: GateAuthSessionOptions;
|
|
97
|
+
readonly accountLinking?: AccountLinkingOptions;
|
|
98
|
+
readonly password?: PasswordHashOptions;
|
|
99
|
+
readonly passwordPolicy?: PasswordPolicyOptions;
|
|
100
|
+
readonly breachCheck?: BreachCheckFn;
|
|
101
|
+
/** Opt-in HttpOnly cookie mirror (Phase 1a). */
|
|
102
|
+
readonly cookies?: AuthCookieOptions;
|
|
103
|
+
/** Opt-in secondary kv storage (Phase 1a). */
|
|
104
|
+
readonly secondaryStorage?: AuthSecondaryStorageOptions;
|
|
105
|
+
/** Lifecycle hooks (Phase 1a). */
|
|
106
|
+
readonly hooks?: AuthDatabaseHooks;
|
|
107
|
+
/**
|
|
108
|
+
* Pre-built session store (Console / tests). Created when omitted.
|
|
109
|
+
* Not part of the public app DX — escape hatch for embedding.
|
|
110
|
+
*/
|
|
111
|
+
readonly sessions?: SessionStore;
|
|
112
|
+
/** Injectable clock. */
|
|
113
|
+
readonly now?: () => number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Resolved account-linking stub. */
|
|
117
|
+
export interface ResolvedAccountLinking {
|
|
118
|
+
readonly enabled: boolean;
|
|
119
|
+
readonly trustedProviders: readonly string[];
|
|
120
|
+
readonly allowDifferentEmails: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Resolved cookie options. */
|
|
124
|
+
export interface ResolvedAuthCookies {
|
|
125
|
+
readonly enabled: boolean;
|
|
126
|
+
readonly prefix: string;
|
|
127
|
+
readonly secure: boolean;
|
|
128
|
+
readonly crossSubdomain: boolean;
|
|
129
|
+
readonly domain: string | undefined;
|
|
130
|
+
readonly sameSite: "strict" | "lax" | "none";
|
|
131
|
+
readonly path: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Fully resolved Gate auth config. */
|
|
135
|
+
export interface ResolvedGateAuth {
|
|
136
|
+
readonly enabled: true;
|
|
137
|
+
readonly secret: string;
|
|
138
|
+
readonly secretMinted: boolean;
|
|
139
|
+
readonly basePath: string;
|
|
140
|
+
/** When false, no `/auth/*` Bindings (secret / tables / Bearer only). */
|
|
141
|
+
readonly http: boolean;
|
|
142
|
+
readonly audience: string;
|
|
143
|
+
readonly emailAndPassword: {
|
|
144
|
+
readonly enabled: boolean;
|
|
145
|
+
readonly requireEmailVerification: boolean;
|
|
146
|
+
};
|
|
147
|
+
readonly session: {
|
|
148
|
+
readonly accessTtlMs: number | undefined;
|
|
149
|
+
readonly refreshTtlMs: number | undefined;
|
|
150
|
+
readonly idleTtlMs: number | undefined;
|
|
151
|
+
readonly absoluteTtlMs: number | undefined;
|
|
152
|
+
readonly singleSessionPerUser: boolean;
|
|
153
|
+
readonly freshAgeMs: number;
|
|
154
|
+
};
|
|
155
|
+
readonly accountLinking: ResolvedAccountLinking;
|
|
156
|
+
readonly schema: ResolvedAuthSchema;
|
|
157
|
+
readonly password: PasswordHashOptions | undefined;
|
|
158
|
+
readonly passwordPolicy: PasswordPolicyOptions | undefined;
|
|
159
|
+
readonly breachCheck: BreachCheckFn | undefined;
|
|
160
|
+
readonly cookies: ResolvedAuthCookies;
|
|
161
|
+
readonly secondaryStorage: {
|
|
162
|
+
readonly enabled: boolean;
|
|
163
|
+
readonly prefix: string;
|
|
164
|
+
};
|
|
165
|
+
readonly hooks: AuthDatabaseHooks | undefined;
|
|
166
|
+
readonly sessions: SessionStore | undefined;
|
|
167
|
+
readonly now: (() => number) | undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Default fresh-age window (24h). */
|
|
171
|
+
export const DEFAULT_FRESH_AGE_MS = 24 * 60 * 60 * 1000;
|
|
172
|
+
|
|
173
|
+
/** Options for {@link resolveGateAuth}. */
|
|
174
|
+
export interface ResolveGateAuthOptions {
|
|
175
|
+
readonly auth: GateAuthOptions;
|
|
176
|
+
/** Boot / app env (`prod` fails without secret). */
|
|
177
|
+
readonly env?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Mint a development HMAC secret (never used in production boots).
|
|
182
|
+
*/
|
|
183
|
+
export function mintDevAuthSecret(): string {
|
|
184
|
+
const bytes = new Uint8Array(32);
|
|
185
|
+
crypto.getRandomValues(bytes);
|
|
186
|
+
return `oke_dev_${Buffer.from(bytes).toString("base64url")}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve `gate.auth` — fails in prod when `secret` is missing.
|
|
191
|
+
*
|
|
192
|
+
* @param options - Auth bag + env
|
|
193
|
+
*/
|
|
194
|
+
export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAuth {
|
|
195
|
+
const { auth, env } = options;
|
|
196
|
+
const isProd = env === "prod" || env === "production";
|
|
197
|
+
let secret = auth.secret;
|
|
198
|
+
let secretMinted = false;
|
|
199
|
+
if (!secret || secret.length === 0) {
|
|
200
|
+
if (isProd) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET)",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
secret = mintDevAuthSecret();
|
|
206
|
+
secretMinted = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sessionOpts = auth.session ?? {};
|
|
210
|
+
const crypto = sessionCryptoFromAuthOptions({
|
|
211
|
+
accessTtlMs: sessionOpts.accessTtlMs,
|
|
212
|
+
refreshTtlMs: sessionOpts.refreshTtlMs,
|
|
213
|
+
session: sessionOpts,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const schema = resolveAuthSchema({
|
|
217
|
+
user: auth.user,
|
|
218
|
+
account: auth.account,
|
|
219
|
+
session: auth.session,
|
|
220
|
+
refreshToken: auth.refreshToken,
|
|
221
|
+
verification: auth.verification,
|
|
222
|
+
roles: auth.roles,
|
|
223
|
+
apiKeys: auth.apiKeys,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const basePath = normalizeBasePath(auth.basePath ?? "/auth");
|
|
227
|
+
const cookies = auth.cookies ?? {};
|
|
228
|
+
const secondary = auth.secondaryStorage ?? {};
|
|
229
|
+
const linking = auth.accountLinking ?? {};
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
enabled: true,
|
|
233
|
+
secret,
|
|
234
|
+
secretMinted,
|
|
235
|
+
basePath,
|
|
236
|
+
http: auth.http !== false,
|
|
237
|
+
audience: auth.audience ?? "oke-app",
|
|
238
|
+
emailAndPassword: {
|
|
239
|
+
enabled: auth.emailAndPassword?.enabled === true,
|
|
240
|
+
requireEmailVerification: auth.emailAndPassword?.requireEmailVerification === true,
|
|
241
|
+
},
|
|
242
|
+
session: {
|
|
243
|
+
accessTtlMs: crypto.accessTtlMs,
|
|
244
|
+
refreshTtlMs: crypto.refreshTtlMs,
|
|
245
|
+
idleTtlMs: crypto.idleTtlMs,
|
|
246
|
+
absoluteTtlMs: crypto.absoluteTtlMs,
|
|
247
|
+
singleSessionPerUser: crypto.singleSessionPerUser === true,
|
|
248
|
+
freshAgeMs: sessionOpts.freshAgeMs ?? DEFAULT_FRESH_AGE_MS,
|
|
249
|
+
},
|
|
250
|
+
accountLinking: {
|
|
251
|
+
enabled: linking.enabled === true,
|
|
252
|
+
trustedProviders: linking.trustedProviders ?? ["credential"],
|
|
253
|
+
allowDifferentEmails: linking.allowDifferentEmails === true,
|
|
254
|
+
},
|
|
255
|
+
schema,
|
|
256
|
+
password: auth.password,
|
|
257
|
+
passwordPolicy: auth.passwordPolicy,
|
|
258
|
+
breachCheck: auth.breachCheck,
|
|
259
|
+
cookies: {
|
|
260
|
+
enabled: cookies.enabled === true,
|
|
261
|
+
prefix: cookies.prefix ?? "oke",
|
|
262
|
+
secure: cookies.secure !== false,
|
|
263
|
+
crossSubdomain: cookies.crossSubdomain === true,
|
|
264
|
+
domain: cookies.domain,
|
|
265
|
+
sameSite: cookies.sameSite ?? "lax",
|
|
266
|
+
path: cookies.path ?? "/",
|
|
267
|
+
},
|
|
268
|
+
secondaryStorage: {
|
|
269
|
+
enabled: secondary.enabled === true,
|
|
270
|
+
prefix: secondary.prefix ?? "auth:",
|
|
271
|
+
},
|
|
272
|
+
hooks: auth.hooks,
|
|
273
|
+
sessions: auth.sessions,
|
|
274
|
+
now: auth.now,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Normalize auth base path (`/auth`, no trailing slash except root).
|
|
280
|
+
*
|
|
281
|
+
* @param path - Raw base path
|
|
282
|
+
*/
|
|
283
|
+
export function normalizeBasePath(path: string): string {
|
|
284
|
+
const trimmed = path.trim();
|
|
285
|
+
if (trimmed === "" || trimmed === "/") return "/auth";
|
|
286
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
287
|
+
return withSlash.replace(/\/+$/, "") || "/auth";
|
|
288
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in auth cookie helpers (Phase 1a) — Bearer remains the default transport.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Console's cookie→Bearer pattern for app Gate auth when enabled.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ResolvedAuthCookies } from "./config.ts";
|
|
8
|
+
|
|
9
|
+
/** Cookie names derived from prefix. */
|
|
10
|
+
export interface AuthCookieNames {
|
|
11
|
+
readonly access: string;
|
|
12
|
+
readonly refresh: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cookie names for a resolved cookie config.
|
|
17
|
+
*
|
|
18
|
+
* @param cookies - Resolved cookie options
|
|
19
|
+
*/
|
|
20
|
+
export function authCookieNames(cookies: ResolvedAuthCookies): AuthCookieNames {
|
|
21
|
+
const p = cookies.prefix;
|
|
22
|
+
return {
|
|
23
|
+
access: `${p}.session_token`,
|
|
24
|
+
refresh: `${p}.refresh_token`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build `Set-Cookie` header values for access + refresh tokens.
|
|
30
|
+
*
|
|
31
|
+
* @param cookies - Resolved cookie options
|
|
32
|
+
* @param tokens - Issued tokens
|
|
33
|
+
* @param maxAgeSec - Max-Age for refresh (access uses shorter when provided)
|
|
34
|
+
*/
|
|
35
|
+
export function buildAuthSetCookies(
|
|
36
|
+
cookies: ResolvedAuthCookies,
|
|
37
|
+
tokens: { readonly accessToken: string; readonly refreshToken: string },
|
|
38
|
+
maxAgeSec: { readonly access: number; readonly refresh: number },
|
|
39
|
+
): string[] {
|
|
40
|
+
if (!cookies.enabled) return [];
|
|
41
|
+
const names = authCookieNames(cookies);
|
|
42
|
+
return [
|
|
43
|
+
serializeCookie(names.access, tokens.accessToken, cookies, maxAgeSec.access),
|
|
44
|
+
serializeCookie(names.refresh, tokens.refreshToken, cookies, maxAgeSec.refresh),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build clearing `Set-Cookie` headers (logout).
|
|
50
|
+
*
|
|
51
|
+
* @param cookies - Resolved cookie options
|
|
52
|
+
*/
|
|
53
|
+
export function clearAuthSetCookies(cookies: ResolvedAuthCookies): string[] {
|
|
54
|
+
if (!cookies.enabled) return [];
|
|
55
|
+
const names = authCookieNames(cookies);
|
|
56
|
+
return [
|
|
57
|
+
serializeCookie(names.access, "", cookies, 0),
|
|
58
|
+
serializeCookie(names.refresh, "", cookies, 0),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract Bearer-equivalent token from Cookie header when cookies enabled.
|
|
64
|
+
* Prefers access cookie; falls back to refresh only when caller asks.
|
|
65
|
+
*
|
|
66
|
+
* @param cookieHeader - Raw `Cookie` header
|
|
67
|
+
* @param cookies - Resolved cookie options
|
|
68
|
+
*/
|
|
69
|
+
export function tokenFromCookieHeader(
|
|
70
|
+
cookieHeader: string | null | undefined,
|
|
71
|
+
cookies: ResolvedAuthCookies,
|
|
72
|
+
): string | undefined {
|
|
73
|
+
if (!cookies.enabled || !cookieHeader) return undefined;
|
|
74
|
+
const names = authCookieNames(cookies);
|
|
75
|
+
const map = parseCookieHeader(cookieHeader);
|
|
76
|
+
return map.get(names.access) || undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function serializeCookie(
|
|
80
|
+
name: string,
|
|
81
|
+
value: string,
|
|
82
|
+
cookies: ResolvedAuthCookies,
|
|
83
|
+
maxAge: number,
|
|
84
|
+
): string {
|
|
85
|
+
const parts = [
|
|
86
|
+
`${name}=${encodeURIComponent(value)}`,
|
|
87
|
+
`Path=${cookies.path}`,
|
|
88
|
+
`Max-Age=${Math.max(0, Math.floor(maxAge))}`,
|
|
89
|
+
"HttpOnly",
|
|
90
|
+
`SameSite=${capitalizeSameSite(cookies.sameSite)}`,
|
|
91
|
+
];
|
|
92
|
+
if (cookies.secure) parts.push("Secure");
|
|
93
|
+
const domain = cookies.domain ?? (cookies.crossSubdomain ? undefined : undefined);
|
|
94
|
+
if (domain) parts.push(`Domain=${domain}`);
|
|
95
|
+
return parts.join("; ");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function capitalizeSameSite(v: "strict" | "lax" | "none"): string {
|
|
99
|
+
if (v === "none") return "None";
|
|
100
|
+
if (v === "lax") return "Lax";
|
|
101
|
+
return "Strict";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse a Cookie header into a map.
|
|
106
|
+
*
|
|
107
|
+
* @param header - Raw Cookie header
|
|
108
|
+
*/
|
|
109
|
+
export function parseCookieHeader(header: string): Map<string, string> {
|
|
110
|
+
const out = new Map<string, string>();
|
|
111
|
+
for (const part of header.split(";")) {
|
|
112
|
+
const idx = part.indexOf("=");
|
|
113
|
+
if (idx <= 0) continue;
|
|
114
|
+
const k = part.slice(0, idx).trim();
|
|
115
|
+
const v = part.slice(idx + 1).trim();
|
|
116
|
+
try {
|
|
117
|
+
out.set(k, decodeURIComponent(v));
|
|
118
|
+
} catch {
|
|
119
|
+
out.set(k, v);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|