@jcoder-stack/abp-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,700 @@
1
+ // src/auth/base64url.ts
2
+ function bytesToBase64Url(bytes) {
3
+ let bin = "";
4
+ for (const byte of bytes) bin += String.fromCharCode(byte);
5
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6
+ }
7
+ function base64UrlToBytes(value) {
8
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
9
+ const bin = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
10
+ const bytes = new Uint8Array(bin.length);
11
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
12
+ return bytes;
13
+ }
14
+
15
+ // src/auth/codec.ts
16
+ var IV_LENGTH = 12;
17
+ var VERSION = 1;
18
+ async function deriveKey(secret, usage) {
19
+ const ikm = await crypto.subtle.importKey(
20
+ "raw",
21
+ new TextEncoder().encode(secret),
22
+ "HKDF",
23
+ false,
24
+ ["deriveKey"]
25
+ );
26
+ return crypto.subtle.deriveKey(
27
+ {
28
+ name: "HKDF",
29
+ hash: "SHA-256",
30
+ salt: new TextEncoder().encode("jc-abp-auth-codec-v1"),
31
+ info: new TextEncoder().encode(usage)
32
+ },
33
+ ikm,
34
+ { name: "AES-GCM", length: 256 },
35
+ false,
36
+ ["encrypt", "decrypt"]
37
+ );
38
+ }
39
+ function createCodec(secret, schema, opts) {
40
+ if (secret.length < 32) throw new Error("codec secret must be at least 32 characters");
41
+ const keyPromise = deriveKey(secret, opts.usage);
42
+ return {
43
+ async seal(data) {
44
+ const key = await keyPromise;
45
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
46
+ const plaintext = new TextEncoder().encode(JSON.stringify(data));
47
+ const ciphertext = new Uint8Array(
48
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext)
49
+ );
50
+ const combined = new Uint8Array(1 + iv.length + ciphertext.length);
51
+ combined[0] = VERSION;
52
+ combined.set(iv, 1);
53
+ combined.set(ciphertext, 1 + iv.length);
54
+ return bytesToBase64Url(combined);
55
+ },
56
+ async open(token) {
57
+ try {
58
+ const key = await keyPromise;
59
+ const combined = base64UrlToBytes(token);
60
+ if (combined.length <= 1 + IV_LENGTH || combined[0] !== VERSION) return null;
61
+ const iv = Uint8Array.from(combined.slice(1, 1 + IV_LENGTH));
62
+ const ciphertext = Uint8Array.from(combined.slice(1 + IV_LENGTH));
63
+ const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
64
+ const parsed = schema.safeParse(JSON.parse(new TextDecoder().decode(plaintext)));
65
+ if (parsed.success) return parsed.data;
66
+ opts.onError?.(new Error("codec payload failed schema validation"));
67
+ return null;
68
+ } catch (error) {
69
+ opts.onError?.(error);
70
+ return null;
71
+ }
72
+ }
73
+ };
74
+ }
75
+
76
+ // src/auth/cookies.ts
77
+ var COOKIE_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
78
+ function serializeCookie(name, value, opts = {}) {
79
+ if (!COOKIE_NAME_PATTERN.test(name)) {
80
+ throw new Error(`invalid cookie name: ${JSON.stringify(name)}`);
81
+ }
82
+ const sameSite = opts.sameSite ?? "Lax";
83
+ if (sameSite === "None" && opts.secure === false) {
84
+ throw new Error("SameSite=None requires Secure");
85
+ }
86
+ const parts = [`${name}=${encodeURIComponent(value)}`, `Path=${opts.path ?? "/"}`];
87
+ if (opts.httpOnly !== false) parts.push("HttpOnly");
88
+ if (opts.secure !== false) parts.push("Secure");
89
+ parts.push(`SameSite=${sameSite}`);
90
+ if (opts.maxAge !== void 0) parts.push(`Max-Age=${opts.maxAge}`);
91
+ return parts.join("; ");
92
+ }
93
+ function clearCookie(name, opts = {}) {
94
+ return serializeCookie(name, "", { ...opts, maxAge: 0 });
95
+ }
96
+ function parseCookieHeader(header) {
97
+ const out = {};
98
+ if (!header) return out;
99
+ for (const part of header.split(/;\s*/)) {
100
+ const eq = part.indexOf("=");
101
+ if (eq <= 0) continue;
102
+ const raw = part.slice(eq + 1);
103
+ try {
104
+ out[part.slice(0, eq).trim()] = decodeURIComponent(raw);
105
+ } catch {
106
+ out[part.slice(0, eq).trim()] = raw;
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+ var COOKIE_CHUNK_SIZE = 3600;
112
+ function splitByEncodedLength(value, limit) {
113
+ const chunks = [];
114
+ let chunk = "";
115
+ let encodedLength = 0;
116
+ for (const char of value) {
117
+ const charLength = encodeURIComponent(char).length;
118
+ if (chunk !== "" && encodedLength + charLength > limit) {
119
+ chunks.push(chunk);
120
+ chunk = "";
121
+ encodedLength = 0;
122
+ }
123
+ chunk += char;
124
+ encodedLength += charLength;
125
+ }
126
+ if (chunk !== "") chunks.push(chunk);
127
+ return chunks;
128
+ }
129
+ function chunkCookieValue(name, value, opts = {}, existing) {
130
+ const cookies = [];
131
+ const fresh = /* @__PURE__ */ new Set();
132
+ let chunkCount = 0;
133
+ const chunks = splitByEncodedLength(value, COOKIE_CHUNK_SIZE);
134
+ if (chunks.length <= 1) {
135
+ cookies.push(serializeCookie(name, value, opts));
136
+ fresh.add(name);
137
+ } else {
138
+ cookies.push(clearCookie(name, opts));
139
+ for (const chunk of chunks) {
140
+ const chunkName = `${name}.${chunkCount}`;
141
+ cookies.push(serializeCookie(chunkName, chunk, opts));
142
+ fresh.add(chunkName);
143
+ chunkCount++;
144
+ }
145
+ }
146
+ const cleared = new Set(fresh.has(name) ? [] : [name]);
147
+ const clearOnce = (chunkName) => {
148
+ if (fresh.has(chunkName) || cleared.has(chunkName)) return;
149
+ cleared.add(chunkName);
150
+ cookies.push(clearCookie(chunkName, opts));
151
+ };
152
+ clearOnce(`${name}.${chunkCount}`);
153
+ if (existing !== void 0) {
154
+ const chunkPattern = new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\d+$`);
155
+ for (const key of Object.keys(existing)) {
156
+ if (chunkPattern.test(key)) clearOnce(key);
157
+ }
158
+ }
159
+ return cookies;
160
+ }
161
+ function readChunkedCookie(cookies, name) {
162
+ const whole = cookies[name];
163
+ if (whole !== void 0) return whole;
164
+ let value = "";
165
+ for (let index = 0; ; index++) {
166
+ const part = cookies[`${name}.${index}`];
167
+ if (part === void 0) break;
168
+ value += part;
169
+ }
170
+ return value === "" ? void 0 : value;
171
+ }
172
+ function clearChunkedCookie(name, cookies, opts = {}) {
173
+ const out = [clearCookie(name, opts)];
174
+ for (let index = 0; ; index++) {
175
+ if (cookies[`${name}.${index}`] === void 0) break;
176
+ out.push(clearCookie(`${name}.${index}`, opts));
177
+ }
178
+ return out;
179
+ }
180
+
181
+ // src/auth/types.ts
182
+ import { z } from "zod";
183
+ var authTokensSchema = z.object({
184
+ accessToken: z.string(),
185
+ refreshToken: z.string().optional(),
186
+ idToken: z.string().optional()
187
+ });
188
+ var authSessionSchema = z.object({
189
+ tokens: authTokensSchema,
190
+ expiresAt: z.number().optional(),
191
+ tenant: z.string().nullable().optional(),
192
+ culture: z.string().nullable().optional()
193
+ });
194
+ var handshakeSchema = z.object({
195
+ state: z.string(),
196
+ nonce: z.string(),
197
+ codeVerifier: z.string(),
198
+ returnUrl: z.string(),
199
+ issuedAt: z.number()
200
+ });
201
+
202
+ // src/auth/cookie-store.ts
203
+ function createCookieSessionStore(opts) {
204
+ const codec = createCodec(opts.secret, authSessionSchema, {
205
+ usage: "session",
206
+ onError: (error) => opts.logger?.debug("session cookie open failed", { error: String(error) })
207
+ });
208
+ return {
209
+ load: async (cookieHeader) => {
210
+ const sealed = readChunkedCookie(parseCookieHeader(cookieHeader), opts.cookieName);
211
+ if (sealed === void 0) return null;
212
+ const session = await codec.open(sealed);
213
+ opts.logger?.debug("session opened", { found: session !== null, sealedBytes: sealed.length });
214
+ return session;
215
+ },
216
+ save: async (session, cookieHeader) => {
217
+ const sealed = await codec.seal(session);
218
+ const cookies = chunkCookieValue(
219
+ opts.cookieName,
220
+ sealed,
221
+ { ...opts.cookieOptions, maxAge: opts.maxAge },
222
+ parseCookieHeader(cookieHeader)
223
+ );
224
+ opts.logger?.debug("session sealed", { sealedBytes: sealed.length, cookies: cookies.length });
225
+ return cookies;
226
+ },
227
+ clear: async (cookieHeader) => clearChunkedCookie(opts.cookieName, parseCookieHeader(cookieHeader), opts.cookieOptions)
228
+ };
229
+ }
230
+
231
+ // src/auth/oidc/token-client.ts
232
+ import { z as z3 } from "zod";
233
+
234
+ // src/auth/errors.ts
235
+ var AuthError = class extends Error {
236
+ code;
237
+ constructor(code, message, opts) {
238
+ super(message ?? code, opts);
239
+ this.name = "AuthError";
240
+ this.code = code;
241
+ }
242
+ };
243
+
244
+ // src/auth/oidc/metadata.ts
245
+ import { z as z2 } from "zod";
246
+ var oidcMetadataSchema = z2.object({
247
+ issuer: z2.string(),
248
+ authorization_endpoint: z2.string(),
249
+ token_endpoint: z2.string(),
250
+ end_session_endpoint: z2.string().optional(),
251
+ revocation_endpoint: z2.string().optional()
252
+ });
253
+ async function discoverMetadata(issuer, opts = {}) {
254
+ const fetchFn = opts.fetchFn ?? fetch;
255
+ const url = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
256
+ try {
257
+ const res = await fetchFn(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? 3e4) });
258
+ if (!res.ok) throw new Error(`discovery returned ${res.status}`);
259
+ const metadata = oidcMetadataSchema.parse(await res.json());
260
+ const normalize = (v) => v.replace(/\/$/, "");
261
+ if (normalize(metadata.issuer) !== normalize(issuer)) {
262
+ throw new Error(`issuer mismatch: expected ${issuer}, got ${metadata.issuer}`);
263
+ }
264
+ return metadata;
265
+ } catch (error) {
266
+ throw new AuthError("discovery_failed", `OIDC discovery failed for ${issuer}`, {
267
+ cause: error
268
+ });
269
+ }
270
+ }
271
+
272
+ // src/auth/oidc/token-client.ts
273
+ var tokenResponseSchema = z3.object({
274
+ access_token: z3.string(),
275
+ refresh_token: z3.string().optional(),
276
+ id_token: z3.string().optional(),
277
+ expires_in: z3.number().optional()
278
+ });
279
+ function toTokenResult(grant, nowMs) {
280
+ return {
281
+ tokens: {
282
+ accessToken: grant.accessToken,
283
+ refreshToken: grant.refreshToken,
284
+ idToken: grant.idToken
285
+ },
286
+ expiresAt: grant.expiresIn !== void 0 ? nowMs + grant.expiresIn * 1e3 : void 0
287
+ };
288
+ }
289
+ function createTokenClient(cfg) {
290
+ const fetchFn = cfg.fetchFn ?? fetch;
291
+ const timeoutMs = cfg.timeoutMs ?? 3e4;
292
+ let metadataPromise;
293
+ const metadata = () => {
294
+ metadataPromise ??= discoverMetadata(cfg.issuer, { fetchFn, timeoutMs }).catch((error) => {
295
+ metadataPromise = void 0;
296
+ throw error;
297
+ });
298
+ return metadataPromise;
299
+ };
300
+ async function postToken(form, errorOf, headers = {}) {
301
+ const grantType = form.grant_type;
302
+ const md = await metadata();
303
+ let res;
304
+ try {
305
+ res = await fetchFn(md.token_endpoint, {
306
+ method: "POST",
307
+ headers: {
308
+ "Content-Type": "application/x-www-form-urlencoded",
309
+ Accept: "application/json",
310
+ ...headers
311
+ },
312
+ body: new URLSearchParams({ client_id: cfg.clientId, ...form }).toString(),
313
+ signal: AbortSignal.timeout(timeoutMs)
314
+ });
315
+ } catch (error) {
316
+ throw new AuthError(errorOf(0), `token request (${grantType}) failed`, { cause: error });
317
+ }
318
+ cfg.logger?.debug("token endpoint responded", { grantType, status: res.status });
319
+ if (!res.ok) {
320
+ throw new AuthError(
321
+ errorOf(res.status),
322
+ `token request (${grantType}) returned ${res.status}`
323
+ );
324
+ }
325
+ try {
326
+ const parsed = tokenResponseSchema.parse(await res.json());
327
+ return {
328
+ accessToken: parsed.access_token,
329
+ refreshToken: parsed.refresh_token,
330
+ idToken: parsed.id_token,
331
+ expiresIn: parsed.expires_in
332
+ };
333
+ } catch (error) {
334
+ throw new AuthError(errorOf(0), `token response (${grantType}) malformed`, { cause: error });
335
+ }
336
+ }
337
+ const withSecret = (form) => cfg.clientSecret === void 0 ? form : { ...form, client_secret: cfg.clientSecret };
338
+ const tenantParts = (tenant) => tenant && cfg.tenantPropagation !== void 0 ? cfg.tenantPropagation(tenant) : {};
339
+ return {
340
+ metadata,
341
+ exchangeCode: (p) => postToken(
342
+ withSecret({
343
+ grant_type: "authorization_code",
344
+ code: p.code,
345
+ code_verifier: p.codeVerifier,
346
+ redirect_uri: p.redirectUri
347
+ }),
348
+ () => "exchange_failed"
349
+ ),
350
+ passwordGrant: (p) => postToken(
351
+ withSecret({
352
+ grant_type: "password",
353
+ username: p.userName,
354
+ password: p.password,
355
+ ...cfg.scope === void 0 ? {} : { scope: cfg.scope }
356
+ }),
357
+ (status) => status === 400 ? "invalid_credentials" : "exchange_failed",
358
+ tenantParts(p.tenant).headers
359
+ ),
360
+ refreshGrant: (refreshToken) => postToken(
361
+ withSecret({ grant_type: "refresh_token", refresh_token: refreshToken }),
362
+ () => "refresh_failed"
363
+ ),
364
+ revoke: async (refreshToken) => {
365
+ const md = await metadata();
366
+ if (md.revocation_endpoint === void 0) return;
367
+ await fetchFn(md.revocation_endpoint, {
368
+ method: "POST",
369
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
370
+ body: new URLSearchParams(
371
+ withSecret({
372
+ token: refreshToken,
373
+ token_type_hint: "refresh_token",
374
+ client_id: cfg.clientId
375
+ })
376
+ ).toString(),
377
+ signal: AbortSignal.timeout(timeoutMs)
378
+ });
379
+ },
380
+ authorizeUrl: async (p) => {
381
+ const md = await metadata();
382
+ const url = new URL(md.authorization_endpoint);
383
+ url.searchParams.set("response_type", "code");
384
+ url.searchParams.set("client_id", cfg.clientId);
385
+ url.searchParams.set("redirect_uri", p.redirectUri);
386
+ url.searchParams.set("scope", cfg.scope ?? "openid profile");
387
+ url.searchParams.set("state", p.state);
388
+ url.searchParams.set("nonce", p.nonce);
389
+ url.searchParams.set("code_challenge", p.codeChallenge);
390
+ url.searchParams.set("code_challenge_method", "S256");
391
+ for (const [key, value] of Object.entries(tenantParts(p.tenant).query ?? {})) {
392
+ url.searchParams.set(key, value);
393
+ }
394
+ return url.toString();
395
+ },
396
+ endSessionUrl: async (p) => {
397
+ const md = await metadata();
398
+ if (md.end_session_endpoint === void 0) return null;
399
+ const url = new URL(md.end_session_endpoint);
400
+ if (p.idToken !== void 0) url.searchParams.set("id_token_hint", p.idToken);
401
+ if (p.postLogoutRedirectUri !== void 0)
402
+ url.searchParams.set("post_logout_redirect_uri", p.postLogoutRedirectUri);
403
+ return url.toString();
404
+ }
405
+ };
406
+ }
407
+
408
+ // src/auth/manager.ts
409
+ function createSessionManager(deps) {
410
+ const now = deps.now ?? (() => Date.now());
411
+ const skewMs = (deps.skewSeconds ?? 60) * 1e3;
412
+ const ttlMs = deps.coalesceTtlMs ?? 1e4;
413
+ const revokeTimeoutMs = deps.revokeTimeoutMs ?? 2e3;
414
+ const inflight = /* @__PURE__ */ new Map();
415
+ const coalescedGrant = (refreshToken) => {
416
+ const at = now();
417
+ for (const [key, entry] of inflight) {
418
+ if (at - entry.at >= ttlMs) inflight.delete(key);
419
+ }
420
+ const hit = inflight.get(refreshToken);
421
+ if (hit !== void 0) return hit.promise;
422
+ const promise = deps.refreshGrant(refreshToken);
423
+ inflight.set(refreshToken, { at, promise });
424
+ promise.catch(() => inflight.delete(refreshToken));
425
+ return promise;
426
+ };
427
+ const revokeWithTimeout = async (revoke, refreshToken) => {
428
+ const revoking = revoke(refreshToken);
429
+ revoking.catch(() => {
430
+ });
431
+ let timer;
432
+ try {
433
+ await Promise.race([
434
+ revoking,
435
+ new Promise((_, reject) => {
436
+ timer = setTimeout(
437
+ () => reject(new Error(`revoke timed out after ${revokeTimeoutMs}ms`)),
438
+ revokeTimeoutMs
439
+ );
440
+ })
441
+ ]);
442
+ } catch (error) {
443
+ deps.logger?.warn("token revocation failed", { error: String(error) });
444
+ } finally {
445
+ clearTimeout(timer);
446
+ }
447
+ };
448
+ return {
449
+ establish: (result, ctx) => deps.store.save(
450
+ {
451
+ tokens: result.tokens,
452
+ expiresAt: result.expiresAt,
453
+ tenant: ctx?.tenant,
454
+ culture: ctx?.culture
455
+ },
456
+ ctx?.cookieHeader
457
+ ),
458
+ current: (cookieHeader) => deps.store.load(cookieHeader),
459
+ isExpired: (session) => session.expiresAt !== void 0 && now() >= session.expiresAt - skewMs,
460
+ refresh: async (session, cookieHeader) => {
461
+ const refreshToken = session.tokens.refreshToken;
462
+ if (refreshToken === void 0) return null;
463
+ try {
464
+ const grant = await coalescedGrant(refreshToken);
465
+ const result = toTokenResult(grant, now());
466
+ const next = {
467
+ tokens: {
468
+ ...result.tokens,
469
+ // IdP 未轮换时沿用旧 refresh token,否则会话将失去续期能力。
470
+ refreshToken: result.tokens.refreshToken ?? refreshToken
471
+ },
472
+ expiresAt: result.expiresAt,
473
+ tenant: session.tenant,
474
+ culture: session.culture
475
+ };
476
+ const setCookies = await deps.store.save(next, cookieHeader);
477
+ deps.logger?.debug("session refreshed", {
478
+ rotated: result.tokens.refreshToken !== void 0 && result.tokens.refreshToken !== refreshToken
479
+ });
480
+ return { session: next, setCookies };
481
+ } catch (error) {
482
+ deps.logger?.warn("session refresh failed", { error: String(error) });
483
+ return null;
484
+ }
485
+ },
486
+ destroy: async (cookieHeader) => {
487
+ const session = await deps.store.load(cookieHeader);
488
+ const refreshToken = session?.tokens.refreshToken;
489
+ if (refreshToken !== void 0 && deps.revoke !== void 0) {
490
+ await revokeWithTimeout(deps.revoke, refreshToken);
491
+ }
492
+ deps.logger?.debug("session destroyed", { hadSession: session !== null });
493
+ return deps.store.clear(cookieHeader);
494
+ }
495
+ };
496
+ }
497
+
498
+ // src/auth/create-auth.ts
499
+ function createAuth(opts) {
500
+ const strategies = new Map(opts.strategies.map((s) => [s.name, s]));
501
+ const session = createSessionManager({
502
+ store: opts.store,
503
+ refreshGrant: opts.refreshGrant,
504
+ revoke: opts.revoke,
505
+ logger: opts.logger,
506
+ now: opts.now,
507
+ revokeTimeoutMs: opts.revokeTimeoutMs,
508
+ skewSeconds: opts.skewSeconds,
509
+ coalesceTtlMs: opts.coalesceTtlMs
510
+ });
511
+ return {
512
+ strategy: (name) => {
513
+ const strategy = strategies.get(name);
514
+ if (strategy === void 0) throw new Error(`unknown auth strategy: ${name}`);
515
+ return strategy;
516
+ },
517
+ session,
518
+ identity: async (session2, ctx = { cookieHeader: null }) => {
519
+ const startedAt = (opts.now ?? Date.now)();
520
+ const identity = await opts.resolveIdentity(session2, ctx);
521
+ opts.logger?.debug("identity resolved", {
522
+ isAuthenticated: identity.isAuthenticated,
523
+ policies: Object.keys(identity.grantedPolicies).length,
524
+ ms: (opts.now ?? Date.now)() - startedAt
525
+ });
526
+ return identity;
527
+ }
528
+ };
529
+ }
530
+
531
+ // src/auth/culture.ts
532
+ function parseCultureCookie(value) {
533
+ if (!value) return null;
534
+ for (const part of value.split("|")) {
535
+ if (part.startsWith("c=")) return part.slice(2);
536
+ }
537
+ return null;
538
+ }
539
+ function formatCultureCookie(culture) {
540
+ return `c=${culture}|uic=${culture}`;
541
+ }
542
+
543
+ // src/auth/oidc/claims.ts
544
+ function decodeIdTokenClaims(idToken) {
545
+ const payload = idToken.split(".")[1];
546
+ if (payload === void 0) return {};
547
+ try {
548
+ return JSON.parse(new TextDecoder().decode(base64UrlToBytes(payload)));
549
+ } catch {
550
+ return {};
551
+ }
552
+ }
553
+
554
+ // src/auth/pkce.ts
555
+ function generateRandomString(byteLength = 32) {
556
+ const bytes = new Uint8Array(byteLength);
557
+ crypto.getRandomValues(bytes);
558
+ return bytesToBase64Url(bytes);
559
+ }
560
+ async function generatePkce() {
561
+ const verifier = generateRandomString(32);
562
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
563
+ return { verifier, challenge: bytesToBase64Url(new Uint8Array(digest)) };
564
+ }
565
+
566
+ // src/auth/return-url.ts
567
+ function hasBrowserStrippedChars(value) {
568
+ for (const char of value) {
569
+ const code = char.codePointAt(0) ?? 0;
570
+ if (code <= 32 || code === 127) return true;
571
+ }
572
+ return false;
573
+ }
574
+ function sanitizeReturnUrl(value) {
575
+ if (!value?.startsWith("/") || value.startsWith("//") || value.startsWith("/\\")) {
576
+ return "/";
577
+ }
578
+ if (hasBrowserStrippedChars(value)) {
579
+ return "/";
580
+ }
581
+ return value;
582
+ }
583
+
584
+ // src/auth/strategies/oidc.ts
585
+ function oidcStrategy(cfg) {
586
+ const now = cfg.now ?? (() => Date.now());
587
+ const random = cfg.random ?? generateRandomString;
588
+ const pkce = cfg.pkce ?? generatePkce;
589
+ const handshakeMaxAgeMs = (cfg.handshakeMaxAgeSeconds ?? 600) * 1e3;
590
+ return {
591
+ name: "oidc",
592
+ async begin(input) {
593
+ const { verifier, challenge } = await pkce();
594
+ const handshake = {
595
+ state: random(),
596
+ nonce: random(),
597
+ codeVerifier: verifier,
598
+ returnUrl: input.returnUrl,
599
+ issuedAt: now()
600
+ };
601
+ const redirectUrl = await cfg.tokenClient.authorizeUrl({
602
+ state: handshake.state,
603
+ nonce: handshake.nonce,
604
+ codeChallenge: challenge,
605
+ redirectUri: cfg.redirectUri,
606
+ tenant: input.tenant
607
+ });
608
+ cfg.logger?.debug("oidc begin", {
609
+ host: new URL(redirectUrl).host,
610
+ stateBytes: handshake.state.length
611
+ });
612
+ return { redirectUrl, handshake };
613
+ },
614
+ async complete(input) {
615
+ if (input.kind !== "callback") {
616
+ throw new AuthError("invalid_input", "oidc strategy only completes callback inputs");
617
+ }
618
+ if (now() - input.handshake.issuedAt > handshakeMaxAgeMs) {
619
+ throw new AuthError("handshake_expired", "handshake outlived its server-side lifetime");
620
+ }
621
+ const code = input.params.get("code");
622
+ const state = input.params.get("state");
623
+ if (state !== input.handshake.state) throw new AuthError("invalid_state");
624
+ const providerError = input.params.get("error");
625
+ if (providerError !== null) {
626
+ cfg.logger?.debug("oidc provider error", {
627
+ error: providerError,
628
+ description: input.params.get("error_description") ?? void 0
629
+ });
630
+ throw new AuthError("provider_denied", `provider returned error: ${providerError}`);
631
+ }
632
+ if (code === null) throw new AuthError("exchange_failed", "callback carried no code");
633
+ const grant = await cfg.tokenClient.exchangeCode({
634
+ code,
635
+ codeVerifier: input.handshake.codeVerifier,
636
+ redirectUri: cfg.redirectUri
637
+ });
638
+ if (grant.idToken !== void 0) {
639
+ if (decodeIdTokenClaims(grant.idToken).nonce !== input.handshake.nonce) {
640
+ throw new AuthError("invalid_nonce");
641
+ }
642
+ }
643
+ cfg.logger?.debug("oidc complete", { hasRefreshToken: grant.refreshToken !== void 0 });
644
+ return toTokenResult(grant, now());
645
+ },
646
+ logoutUrl: (p) => cfg.tokenClient.endSessionUrl(p)
647
+ };
648
+ }
649
+
650
+ // src/auth/strategies/password.ts
651
+ function passwordStrategy(cfg) {
652
+ const now = cfg.now ?? (() => Date.now());
653
+ return {
654
+ name: "password",
655
+ async complete(input) {
656
+ if (input.kind !== "credentials") {
657
+ throw new AuthError("invalid_input", "password strategy only completes credentials");
658
+ }
659
+ const grant = await cfg.tokenClient.passwordGrant({
660
+ userName: input.userName,
661
+ password: input.password,
662
+ tenant: input.tenant
663
+ });
664
+ cfg.logger?.debug("password attempt succeeded", {
665
+ userName: `${input.userName[0] ?? ""}***`
666
+ });
667
+ return toTokenResult(grant, now());
668
+ }
669
+ };
670
+ }
671
+
672
+ export {
673
+ createCodec,
674
+ serializeCookie,
675
+ clearCookie,
676
+ parseCookieHeader,
677
+ COOKIE_CHUNK_SIZE,
678
+ chunkCookieValue,
679
+ readChunkedCookie,
680
+ clearChunkedCookie,
681
+ authTokensSchema,
682
+ authSessionSchema,
683
+ handshakeSchema,
684
+ createCookieSessionStore,
685
+ AuthError,
686
+ oidcMetadataSchema,
687
+ discoverMetadata,
688
+ toTokenResult,
689
+ createTokenClient,
690
+ createSessionManager,
691
+ createAuth,
692
+ parseCultureCookie,
693
+ formatCultureCookie,
694
+ decodeIdTokenClaims,
695
+ generateRandomString,
696
+ generatePkce,
697
+ sanitizeReturnUrl,
698
+ oidcStrategy,
699
+ passwordStrategy
700
+ };