@getstrata/core 2.2.33 → 2.2.34

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.
@@ -1082,6 +1082,19 @@ function readBasicCredentials(request) {
1082
1082
  // ../../src/core/auth/basicAuthGuard.ts
1083
1083
  init_databaseTenantContext();
1084
1084
 
1085
+ // ../../src/core/auth/accountState.ts
1086
+ function isAccountActive(record) {
1087
+ return record.is_active === undefined || record.is_active === true;
1088
+ }
1089
+ function isAuthUserActive(user) {
1090
+ return user.isActive === undefined || user.isActive === true;
1091
+ }
1092
+ function matchesAuthVersion(issued, current) {
1093
+ if (current === undefined)
1094
+ return true;
1095
+ return Number.isSafeInteger(current) && current >= 0 && typeof issued === "number" && Number.isSafeInteger(issued) && issued >= 0 && issued === current;
1096
+ }
1097
+
1085
1098
  // ../../src/core/auth/password.ts
1086
1099
  async function hashPassword(password) {
1087
1100
  return await Bun.password.hash(password, {
@@ -1574,6 +1587,8 @@ function parseRecoveryHashes(raw) {
1574
1587
  }
1575
1588
  }
1576
1589
  function completePasswordLogin(user, options = {}) {
1590
+ if (!isAccountActive(user))
1591
+ return { ok: false, error: "account_inactive" };
1577
1592
  if (!user.mfa_enabled) {
1578
1593
  return { ok: true };
1579
1594
  }
@@ -1637,6 +1652,8 @@ class BasicAuthGuard {
1637
1652
  return null;
1638
1653
  }
1639
1654
  const record = typeof directory.findByEmail === "function" ? await directory.findByEmail(credentials.username) : null;
1655
+ if (record && !isAccountActive(record))
1656
+ return null;
1640
1657
  if (record?.password) {
1641
1658
  if (!await verifyPassword(credentials.password, record.password)) {
1642
1659
  return null;
@@ -1654,12 +1671,13 @@ class BasicAuthGuard {
1654
1671
  return {
1655
1672
  id: record.id,
1656
1673
  role: record.role,
1674
+ ...record.auth_version !== undefined ? { authVersion: record.auth_version } : {},
1657
1675
  emailVerifiedAt: record.email_verified_at ?? null
1658
1676
  };
1659
1677
  }
1660
1678
  if (typeof directory.verifyCredentials === "function") {
1661
1679
  const user = await directory.verifyCredentials(credentials.username, credentials.password);
1662
- if (!user) {
1680
+ if (!user || !isAuthUserActive(user)) {
1663
1681
  return null;
1664
1682
  }
1665
1683
  let record = typeof directory.findByEmail === "function" ? await directory.findByEmail(credentials.username) : null;
@@ -167,6 +167,19 @@ function readBasicCredentials(request) {
167
167
  }
168
168
  }
169
169
 
170
+ // ../../src/core/auth/accountState.ts
171
+ function isAccountActive(record) {
172
+ return record.is_active === undefined || record.is_active === true;
173
+ }
174
+ function isAuthUserActive(user) {
175
+ return user.isActive === undefined || user.isActive === true;
176
+ }
177
+ function matchesAuthVersion(issued, current) {
178
+ if (current === undefined)
179
+ return true;
180
+ return Number.isSafeInteger(current) && current >= 0 && typeof issued === "number" && Number.isSafeInteger(issued) && issued >= 0 && issued === current;
181
+ }
182
+
170
183
  // ../../src/core/auth/jwt.ts
171
184
  import { createHmac, timingSafeEqual } from "crypto";
172
185
 
@@ -333,6 +346,40 @@ function verifyJwt(token, secret) {
333
346
 
334
347
  // ../../src/core/auth/sessionCookie.ts
335
348
  import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
349
+
350
+ // ../../src/core/http/cookies.ts
351
+ function readRequestCookie(request, name) {
352
+ const cookies = request.cookies;
353
+ if (cookies && typeof cookies.get === "function") {
354
+ const value = cookies.get(name);
355
+ if (value) {
356
+ return value;
357
+ }
358
+ }
359
+ const header = request.headers.get("cookie");
360
+ if (!header) {
361
+ return null;
362
+ }
363
+ for (const part of header.split(";")) {
364
+ const idx = part.indexOf("=");
365
+ if (idx === -1)
366
+ continue;
367
+ const cookieName = part.slice(0, idx).trim();
368
+ if (cookieName !== name)
369
+ continue;
370
+ try {
371
+ return decodeURIComponent(part.slice(idx + 1).trim());
372
+ } catch {
373
+ return null;
374
+ }
375
+ }
376
+ return null;
377
+ }
378
+ function readBunRequestCookie(request, name) {
379
+ return request.cookies.get(name) ?? readRequestCookie(request, name);
380
+ }
381
+
382
+ // ../../src/core/auth/sessionCookie.ts
336
383
  var SESSION_COOKIE = appCookieName("session");
337
384
  var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
338
385
  var SESSION_REMEMBER_TTL_SECONDS = 60 * 60 * 24 * 30;
@@ -352,25 +399,12 @@ function sessionRememberTtlSeconds() {
352
399
  function resolveSessionSecret() {
353
400
  return requireConfiguredSecret(["SESSION_SECRET"], "session-secret");
354
401
  }
355
- function signSession(userId, issuedAt, ttlSeconds) {
356
- const payload = ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
402
+ function signSession(userId, issuedAt, ttlSeconds, authVersion) {
403
+ const payload = authVersion !== undefined ? `v2.${userId}.${issuedAt}.${ttlSeconds}.${authVersion}` : ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
357
404
  const signature = createHmac2("sha256", resolveSessionSecret()).update(payload).digest("hex");
358
405
  return `${payload}.${signature}`;
359
406
  }
360
- function readCookieValue(request, cookieName) {
361
- const cookieHeader = request.headers.get("cookie");
362
- if (!cookieHeader) {
363
- return null;
364
- }
365
- for (const part of cookieHeader.split(";")) {
366
- const [name, ...rest] = part.trim().split("=");
367
- if (name === cookieName) {
368
- return decodeURIComponent(rest.join("="));
369
- }
370
- }
371
- return null;
372
- }
373
- function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds, remember) {
407
+ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds, remember, authVersion) {
374
408
  const userId = Number.parseInt(userIdRaw, 10);
375
409
  const issuedAt = Number.parseInt(issuedAtRaw, 10);
376
410
  if (!Number.isInteger(userId) || userId <= 0 || !Number.isFinite(issuedAt)) {
@@ -382,7 +416,7 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
382
416
  if (Date.now() - issuedAt > ttlSeconds * 1000) {
383
417
  return null;
384
418
  }
385
- const expectedSignature = (remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt)).split(".").pop();
419
+ const expectedSignature = (authVersion !== undefined ? signSession(userId, issuedAt, ttlSeconds, authVersion) : remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt)).split(".").pop();
386
420
  if (!expectedSignature || !cookieSignature) {
387
421
  return null;
388
422
  }
@@ -394,14 +428,20 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
394
428
  if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
395
429
  return null;
396
430
  }
397
- return { userId, issuedAt };
431
+ return { userId, issuedAt, ...authVersion !== undefined ? { authVersion } : {} };
398
432
  }
399
433
  function readSession(request) {
400
- const cookieValue = readCookieValue(request, sessionCookieName());
434
+ const cookieValue = readRequestCookie(request, sessionCookieName());
401
435
  if (!cookieValue) {
402
436
  return null;
403
437
  }
404
438
  const parts = cookieValue.split(".");
439
+ if (parts.length === 6 && parts[0] === "v2") {
440
+ const fields = parts.slice(1, 5);
441
+ if (fields.some((value) => !/^(0|[1-9][0-9]*)$/.test(value) || !Number.isSafeInteger(Number(value))))
442
+ return null;
443
+ return readSignedSession(String(parts[1]), String(parts[2]), parts[5], Number(parts[3]), true, Number(parts[4]));
444
+ }
405
445
  if (parts.length === 4) {
406
446
  const [userIdRaw, issuedAtRaw, ttlRaw, cookieSignature] = parts;
407
447
  return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, Number.parseInt(String(ttlRaw), 10), true);
@@ -426,15 +466,18 @@ function isSessionInvalidated(issuedAt, validAfter) {
426
466
  return issuedAt < timestamp;
427
467
  }
428
468
  function createSessionCookieDetails(userId, options = {}) {
469
+ if (options.authVersion !== undefined && (!Number.isSafeInteger(options.authVersion) || options.authVersion < 0))
470
+ throw new RangeError("authVersion must be a non-negative safe integer.");
429
471
  const issuedAt = Date.now();
430
472
  const ttlSeconds = options.remember ? sessionRememberTtlSeconds() : sessionTtlSeconds();
431
- const value = options.remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt);
473
+ const value = options.authVersion !== undefined ? signSession(userId, issuedAt, ttlSeconds, options.authVersion) : options.remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt);
432
474
  const secure = isProductionEnv() ? "; Secure" : "";
433
475
  return {
434
476
  header: `${sessionCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ttlSeconds}${secure}`,
435
477
  userId,
436
478
  issuedAt,
437
- ttlSeconds
479
+ ttlSeconds,
480
+ ...options.authVersion !== undefined ? { authVersion: options.authVersion } : {}
438
481
  };
439
482
  }
440
483
  function createSessionCookie(userId, options = {}) {
@@ -496,10 +539,15 @@ class JwtGuard {
496
539
  } catch {
497
540
  return null;
498
541
  }
499
- if (isSessionInvalidated(payload.iat * 1000, record.session_valid_after)) {
542
+ if (!isAccountActive(record) || !matchesAuthVersion(payload.authVersion, record.auth_version))
543
+ return null;
544
+ if (record.auth_version === undefined && isSessionInvalidated(payload.iat * 1000, record.session_valid_after)) {
500
545
  return null;
501
546
  }
502
- return authUserFromJwt(payload);
547
+ return {
548
+ ...authUserFromJwt(payload),
549
+ ...record.auth_version !== undefined ? { authVersion: record.auth_version } : {}
550
+ };
503
551
  }
504
552
  }
505
553
  export {
@@ -341,6 +341,8 @@ function hashOneTimeToken(plain) {
341
341
  return hashApiToken(plain.trim());
342
342
  }
343
343
  function isMissingRelation(error) {
344
+ if (error && typeof error === "object" && "code" in error && error.code === "ER_NO_SUCH_TABLE")
345
+ return true;
344
346
  const message = error instanceof Error ? error.message : String(error);
345
347
  return /no such table|does not exist|unknown table|er_no_such_table/i.test(message);
346
348
  }
@@ -374,23 +376,25 @@ async function consumeOneTimeToken(sql, purpose, plain, now = new Date) {
374
376
  }
375
377
  async function revokeUserSessions(sql, userId, options = {}) {
376
378
  const dialect = currentSqlDialect();
377
- await sql.unsafe(`UPDATE users SET session_valid_after = ${dialect.placeholder(1)} WHERE id = ${dialect.placeholder(2)}`, [new Date().toISOString(), userId]);
378
- try {
379
- await sql.unsafe(`DELETE FROM sessions WHERE user_id = ${dialect.placeholder(1)}`, [userId]);
380
- } catch (error) {
381
- if (!isMissingRelation(error)) {
382
- throw error;
383
- }
379
+ const versionUpdate = options.incrementAuthVersion ? ", auth_version = auth_version + 1" : "";
380
+ await sql.unsafe(`UPDATE users SET session_valid_after = ${dialect.placeholder(1)}${versionUpdate} WHERE id = ${dialect.placeholder(2)}`, [new Date().toISOString(), userId]);
381
+ await deleteOptionalCredentials(sql, "sessions", userId);
382
+ if (options.revokeApiTokens !== false) {
383
+ await deleteOptionalCredentials(sql, "api_tokens", userId);
384
384
  }
385
- if (options.revokeApiTokens === false) {
386
- return;
385
+ }
386
+ async function deleteOptionalCredentials(sql, table, userId) {
387
+ const dialect = currentSqlDialect();
388
+ if (dialect.driver === "pgsql") {
389
+ const [row] = await sql.unsafe(`SELECT to_regclass(${dialect.placeholder(1)}) AS relation`, [table]);
390
+ if (row?.relation == null)
391
+ return;
387
392
  }
388
393
  try {
389
- await sql.unsafe(`DELETE FROM api_tokens WHERE user_id = ${dialect.placeholder(1)}`, [userId]);
394
+ await sql.unsafe(`DELETE FROM ${table} WHERE user_id = ${dialect.placeholder(1)}`, [userId]);
390
395
  } catch (error) {
391
- if (!isMissingRelation(error)) {
396
+ if (dialect.driver === "pgsql" || !isMissingRelation(error))
392
397
  throw error;
393
- }
394
398
  }
395
399
  }
396
400
  export {
@@ -2,6 +2,38 @@
2
2
  // ../../src/core/auth/passwordConfirmCookie.ts
3
3
  import { createHmac, timingSafeEqual } from "crypto";
4
4
 
5
+ // ../../src/core/http/cookies.ts
6
+ function readRequestCookie(request, name) {
7
+ const cookies = request.cookies;
8
+ if (cookies && typeof cookies.get === "function") {
9
+ const value = cookies.get(name);
10
+ if (value) {
11
+ return value;
12
+ }
13
+ }
14
+ const header = request.headers.get("cookie");
15
+ if (!header) {
16
+ return null;
17
+ }
18
+ for (const part of header.split(";")) {
19
+ const idx = part.indexOf("=");
20
+ if (idx === -1)
21
+ continue;
22
+ const cookieName = part.slice(0, idx).trim();
23
+ if (cookieName !== name)
24
+ continue;
25
+ try {
26
+ return decodeURIComponent(part.slice(idx + 1).trim());
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ function readBunRequestCookie(request, name) {
34
+ return request.cookies.get(name) ?? readRequestCookie(request, name);
35
+ }
36
+
5
37
  // ../../src/core/runtime/appEnv.ts
6
38
  var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
7
39
  function normalizeEnvValue(value) {
@@ -88,6 +120,19 @@ function sdkClientClassName() {
88
120
  return fromName ? `${fromName}Client` : "AppClient";
89
121
  }
90
122
 
123
+ // ../../src/core/auth/accountState.ts
124
+ function isAccountActive(record) {
125
+ return record.is_active === undefined || record.is_active === true;
126
+ }
127
+ function isAuthUserActive(user) {
128
+ return user.isActive === undefined || user.isActive === true;
129
+ }
130
+ function matchesAuthVersion(issued, current) {
131
+ if (current === undefined)
132
+ return true;
133
+ return Number.isSafeInteger(current) && current >= 0 && typeof issued === "number" && Number.isSafeInteger(issued) && issued >= 0 && issued === current;
134
+ }
135
+
91
136
  // ../../src/core/auth/passwordConfirmCookie.ts
92
137
  var PASSWORD_CONFIRM_COOKIE = appCookieName("password_confirmed");
93
138
  var DEFAULT_PASSWORD_CONFIRM_TTL_SECONDS = 3 * 60 * 60;
@@ -101,33 +146,27 @@ function passwordConfirmTtlSeconds() {
101
146
  function resolvePasswordConfirmSecret() {
102
147
  return requireConfiguredSecret(["SESSION_SECRET", "OAUTH_STATE_SECRET"], "session-secret");
103
148
  }
104
- function signPasswordConfirm(userId, confirmedAt) {
105
- const payload = `${userId}.${confirmedAt}`;
149
+ function signPasswordConfirm(userId, confirmedAt, authVersion) {
150
+ const payload = authVersion === undefined ? `${userId}.${confirmedAt}` : `v2.${userId}.${confirmedAt}.${authVersion}`;
106
151
  const signature = createHmac("sha256", resolvePasswordConfirmSecret()).update(payload).digest("hex");
107
152
  return `${payload}.${signature}`;
108
153
  }
109
- function readCookieValue(request, cookieName) {
110
- const cookieHeader = request.headers.get("cookie");
111
- if (!cookieHeader) {
112
- return null;
113
- }
114
- for (const part of cookieHeader.split(";")) {
115
- const [name, ...rest] = part.trim().split("=");
116
- if (name === cookieName) {
117
- return decodeURIComponent(rest.join("="));
118
- }
119
- }
120
- return null;
121
- }
122
- function hasFreshPasswordConfirmation(request, userId) {
123
- const cookieValue = readCookieValue(request, passwordConfirmCookieName());
154
+ function hasFreshPasswordConfirmation(request, userId, authVersion) {
155
+ const cookieValue = readRequestCookie(request, passwordConfirmCookieName());
124
156
  if (!cookieValue) {
125
157
  return false;
126
158
  }
127
159
  const parts = cookieValue.split(".");
128
- if (parts.length !== 3) {
129
- return false;
160
+ let proofVersion;
161
+ if (parts.length === 5 && parts[0] === "v2") {
162
+ if (parts.slice(1, 4).some((value) => !/^(0|[1-9][0-9]*)$/.test(value) || !Number.isSafeInteger(Number(value))))
163
+ return false;
164
+ proofVersion = Number(parts[3]);
165
+ parts.splice(3, 1);
166
+ parts.shift();
130
167
  }
168
+ if (parts.length !== 3 || !matchesAuthVersion(proofVersion, authVersion))
169
+ return false;
131
170
  const [userIdRaw, confirmedAtRaw, cookieSignature] = parts;
132
171
  const cookieUserId = Number.parseInt(String(userIdRaw), 10);
133
172
  const confirmedAt = Number.parseInt(String(confirmedAtRaw), 10);
@@ -137,7 +176,7 @@ function hasFreshPasswordConfirmation(request, userId) {
137
176
  if (Date.now() - confirmedAt > passwordConfirmTtlSeconds() * 1000) {
138
177
  return false;
139
178
  }
140
- const expectedSignature = signPasswordConfirm(cookieUserId, confirmedAt).split(".").pop();
179
+ const expectedSignature = signPasswordConfirm(cookieUserId, confirmedAt, proofVersion).split(".").pop();
141
180
  if (!expectedSignature || !cookieSignature) {
142
181
  return false;
143
182
  }
@@ -148,9 +187,11 @@ function hasFreshPasswordConfirmation(request, userId) {
148
187
  }
149
188
  return timingSafeEqual(expectedBuffer, actualBuffer);
150
189
  }
151
- function createPasswordConfirmCookie(userId) {
190
+ function createPasswordConfirmCookie(userId, authVersion) {
191
+ if (authVersion !== undefined && (!Number.isSafeInteger(authVersion) || authVersion < 0))
192
+ throw new Error("Invalid authentication version.");
152
193
  const confirmedAt = Date.now();
153
- const value = signPasswordConfirm(userId, confirmedAt);
194
+ const value = signPasswordConfirm(userId, confirmedAt, authVersion);
154
195
  const secure = isProductionEnv() ? "; Secure" : "";
155
196
  return `${passwordConfirmCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${passwordConfirmTtlSeconds()}${secure}`;
156
197
  }
@@ -712,6 +712,19 @@ function verifyTotp(secret, token, window = 1) {
712
712
  return matched;
713
713
  }
714
714
 
715
+ // ../../src/core/auth/accountState.ts
716
+ function isAccountActive(record) {
717
+ return record.is_active === undefined || record.is_active === true;
718
+ }
719
+ function isAuthUserActive(user) {
720
+ return user.isActive === undefined || user.isActive === true;
721
+ }
722
+ function matchesAuthVersion(issued, current) {
723
+ if (current === undefined)
724
+ return true;
725
+ return Number.isSafeInteger(current) && current >= 0 && typeof issued === "number" && Number.isSafeInteger(issued) && issued >= 0 && issued === current;
726
+ }
727
+
715
728
  // ../../src/core/auth/passwordLogin.ts
716
729
  function parseRecoveryHashes(raw) {
717
730
  if (!raw?.trim()) {
@@ -725,6 +738,8 @@ function parseRecoveryHashes(raw) {
725
738
  }
726
739
  }
727
740
  function completePasswordLogin(user, options = {}) {
741
+ if (!isAccountActive(user))
742
+ return { ok: false, error: "account_inactive" };
728
743
  if (!user.mfa_enabled) {
729
744
  return { ok: true };
730
745
  }
@@ -0,0 +1,100 @@
1
+ // @bun
2
+ // ../../src/core/auth/pendingMfaCookie.ts
3
+ import { createHmac, timingSafeEqual } from "crypto";
4
+
5
+ // ../../src/core/http/cookies.ts
6
+ function readRequestCookie(request, name) {
7
+ const cookies = request.cookies;
8
+ if (cookies && typeof cookies.get === "function") {
9
+ const value = cookies.get(name);
10
+ if (value) {
11
+ return value;
12
+ }
13
+ }
14
+ const header = request.headers.get("cookie");
15
+ if (!header) {
16
+ return null;
17
+ }
18
+ for (const part of header.split(";")) {
19
+ const idx = part.indexOf("=");
20
+ if (idx === -1)
21
+ continue;
22
+ const cookieName = part.slice(0, idx).trim();
23
+ if (cookieName !== name)
24
+ continue;
25
+ try {
26
+ return decodeURIComponent(part.slice(idx + 1).trim());
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ function readBunRequestCookie(request, name) {
34
+ return request.cookies.get(name) ?? readRequestCookie(request, name);
35
+ }
36
+
37
+ // ../../src/core/runtime/appEnv.ts
38
+ var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
39
+ function normalizeEnvValue(value) {
40
+ return (value ?? "").trim().toLowerCase();
41
+ }
42
+ function isProductionEnv(env = process.env) {
43
+ const appEnv = normalizeEnvValue(env.APP_ENV);
44
+ const nodeEnv = normalizeEnvValue(env.NODE_ENV);
45
+ if (appEnv === "production" || nodeEnv === "production") {
46
+ return true;
47
+ }
48
+ if (appEnv === "") {
49
+ return false;
50
+ }
51
+ return !NON_PRODUCTION_APP_ENVS.has(appEnv);
52
+ }
53
+ function envFlagEnabled(value) {
54
+ return value === "true";
55
+ }
56
+
57
+ // ../../src/core/auth/pendingMfaCookie.ts
58
+ var COOKIE = "strata_mfa_pending";
59
+ var TTL = 600;
60
+ function signature(payload, secret) {
61
+ if (!secret.trim())
62
+ throw new Error("A pending MFA secret is required.");
63
+ return createHmac("sha256", secret).update(`pending-mfa:${payload}`).digest("hex");
64
+ }
65
+ function header(value, ttl) {
66
+ return `${COOKIE}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ttl}${isProductionEnv() ? "; Secure" : ""}`;
67
+ }
68
+ function createPendingMfaCookie(userId, authVersion, secret) {
69
+ if (!Number.isSafeInteger(userId) || userId <= 0 || !Number.isSafeInteger(authVersion) || authVersion < 0)
70
+ throw new Error("Invalid pending MFA account identity or version.");
71
+ const payload = `v1.${userId}.${authVersion}.${Date.now()}`;
72
+ return header(`${payload}.${signature(payload, secret)}`, TTL);
73
+ }
74
+ function readPendingMfaCookie(request, secret) {
75
+ const value = readRequestCookie(request, COOKIE);
76
+ if (!value)
77
+ return null;
78
+ const parts = value.split(".");
79
+ if (parts.length !== 5 || parts[0] !== "v1")
80
+ return null;
81
+ const fields = parts.slice(1, 4);
82
+ if (fields.some((part) => !/^(0|[1-9][0-9]*)$/.test(part) || !Number.isSafeInteger(Number(part))))
83
+ return null;
84
+ const userId = Number(parts[1]), authVersion = Number(parts[2]), issuedAt = Number(parts[3]);
85
+ if (userId <= 0 || issuedAt > Date.now() || Date.now() - issuedAt > TTL * 1000)
86
+ return null;
87
+ const expected = Buffer.from(signature(parts.slice(0, 4).join("."), secret));
88
+ const actual = Buffer.from(parts[4] ?? "");
89
+ if (expected.length !== actual.length || !timingSafeEqual(expected, actual))
90
+ return null;
91
+ return { userId, authVersion };
92
+ }
93
+ function clearPendingMfaCookie() {
94
+ return header("", 0);
95
+ }
96
+ export {
97
+ clearPendingMfaCookie,
98
+ createPendingMfaCookie,
99
+ readPendingMfaCookie
100
+ };
@@ -2,6 +2,38 @@
2
2
  // ../../src/core/auth/sessionCookie.ts
3
3
  import { createHmac, timingSafeEqual } from "crypto";
4
4
 
5
+ // ../../src/core/http/cookies.ts
6
+ function readRequestCookie(request, name) {
7
+ const cookies = request.cookies;
8
+ if (cookies && typeof cookies.get === "function") {
9
+ const value = cookies.get(name);
10
+ if (value) {
11
+ return value;
12
+ }
13
+ }
14
+ const header = request.headers.get("cookie");
15
+ if (!header) {
16
+ return null;
17
+ }
18
+ for (const part of header.split(";")) {
19
+ const idx = part.indexOf("=");
20
+ if (idx === -1)
21
+ continue;
22
+ const cookieName = part.slice(0, idx).trim();
23
+ if (cookieName !== name)
24
+ continue;
25
+ try {
26
+ return decodeURIComponent(part.slice(idx + 1).trim());
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ function readBunRequestCookie(request, name) {
34
+ return request.cookies.get(name) ?? readRequestCookie(request, name);
35
+ }
36
+
5
37
  // ../../src/core/runtime/appEnv.ts
6
38
  var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
7
39
  function normalizeEnvValue(value) {
@@ -108,25 +140,12 @@ function sessionRememberTtlSeconds() {
108
140
  function resolveSessionSecret() {
109
141
  return requireConfiguredSecret(["SESSION_SECRET"], "session-secret");
110
142
  }
111
- function signSession(userId, issuedAt, ttlSeconds) {
112
- const payload = ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
143
+ function signSession(userId, issuedAt, ttlSeconds, authVersion) {
144
+ const payload = authVersion !== undefined ? `v2.${userId}.${issuedAt}.${ttlSeconds}.${authVersion}` : ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
113
145
  const signature = createHmac("sha256", resolveSessionSecret()).update(payload).digest("hex");
114
146
  return `${payload}.${signature}`;
115
147
  }
116
- function readCookieValue(request, cookieName) {
117
- const cookieHeader = request.headers.get("cookie");
118
- if (!cookieHeader) {
119
- return null;
120
- }
121
- for (const part of cookieHeader.split(";")) {
122
- const [name, ...rest] = part.trim().split("=");
123
- if (name === cookieName) {
124
- return decodeURIComponent(rest.join("="));
125
- }
126
- }
127
- return null;
128
- }
129
- function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds, remember) {
148
+ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds, remember, authVersion) {
130
149
  const userId = Number.parseInt(userIdRaw, 10);
131
150
  const issuedAt = Number.parseInt(issuedAtRaw, 10);
132
151
  if (!Number.isInteger(userId) || userId <= 0 || !Number.isFinite(issuedAt)) {
@@ -138,7 +157,7 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
138
157
  if (Date.now() - issuedAt > ttlSeconds * 1000) {
139
158
  return null;
140
159
  }
141
- const expectedSignature = (remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt)).split(".").pop();
160
+ const expectedSignature = (authVersion !== undefined ? signSession(userId, issuedAt, ttlSeconds, authVersion) : remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt)).split(".").pop();
142
161
  if (!expectedSignature || !cookieSignature) {
143
162
  return null;
144
163
  }
@@ -150,14 +169,20 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
150
169
  if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
151
170
  return null;
152
171
  }
153
- return { userId, issuedAt };
172
+ return { userId, issuedAt, ...authVersion !== undefined ? { authVersion } : {} };
154
173
  }
155
174
  function readSession(request) {
156
- const cookieValue = readCookieValue(request, sessionCookieName());
175
+ const cookieValue = readRequestCookie(request, sessionCookieName());
157
176
  if (!cookieValue) {
158
177
  return null;
159
178
  }
160
179
  const parts = cookieValue.split(".");
180
+ if (parts.length === 6 && parts[0] === "v2") {
181
+ const fields = parts.slice(1, 5);
182
+ if (fields.some((value) => !/^(0|[1-9][0-9]*)$/.test(value) || !Number.isSafeInteger(Number(value))))
183
+ return null;
184
+ return readSignedSession(String(parts[1]), String(parts[2]), parts[5], Number(parts[3]), true, Number(parts[4]));
185
+ }
161
186
  if (parts.length === 4) {
162
187
  const [userIdRaw, issuedAtRaw, ttlRaw, cookieSignature] = parts;
163
188
  return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, Number.parseInt(String(ttlRaw), 10), true);
@@ -182,15 +207,18 @@ function isSessionInvalidated(issuedAt, validAfter) {
182
207
  return issuedAt < timestamp;
183
208
  }
184
209
  function createSessionCookieDetails(userId, options = {}) {
210
+ if (options.authVersion !== undefined && (!Number.isSafeInteger(options.authVersion) || options.authVersion < 0))
211
+ throw new RangeError("authVersion must be a non-negative safe integer.");
185
212
  const issuedAt = Date.now();
186
213
  const ttlSeconds = options.remember ? sessionRememberTtlSeconds() : sessionTtlSeconds();
187
- const value = options.remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt);
214
+ const value = options.authVersion !== undefined ? signSession(userId, issuedAt, ttlSeconds, options.authVersion) : options.remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt);
188
215
  const secure = isProductionEnv() ? "; Secure" : "";
189
216
  return {
190
217
  header: `${sessionCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ttlSeconds}${secure}`,
191
218
  userId,
192
219
  issuedAt,
193
- ttlSeconds
220
+ ttlSeconds,
221
+ ...options.authVersion !== undefined ? { authVersion: options.authVersion } : {}
194
222
  };
195
223
  }
196
224
  function createSessionCookie(userId, options = {}) {