@astram/guard 0.12.0 → 0.12.1

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.
@@ -472,6 +472,8 @@ export declare class AuthController {
472
472
  * this seam today (flagged core-API gap).
473
473
  */
474
474
  refresh(body: RefreshRequest): Promise<SessionSuccess>;
475
+ /** KI #104: audit a tolerated repeat once its re-bind is decided (best-effort, never blocks). */
476
+ private recordReplay;
475
477
  /**
476
478
  * The refresh-time projection for one node: coarse grants + live roles (ADR-0035) and the
477
479
  * subtree with its version stamp (Phase 1C: the version is read FIRST). Each part is best-effort
@@ -1 +1 @@
1
- {"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"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"}
@@ -69,6 +69,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
69
69
  exports.AuthController = void 0;
70
70
  const common_1 = require("@nestjs/common");
71
71
  const node_crypto_1 = require("node:crypto");
72
+ const replay_outcome_js_1 = require("../session/replay-outcome.js");
72
73
  const common_2 = require("@nestjs/common");
73
74
  const classification_decorator_js_1 = require("../enforcement/classification.decorator.js");
74
75
  const tokens_js_1 = require("./tokens.js");
@@ -1451,8 +1452,12 @@ let AuthController = class AuthController {
1451
1452
  if (typeof probe?.isAvailable === 'function' && !(await probe.isAvailable())) {
1452
1453
  retryableUnavailable();
1453
1454
  }
1455
+ // KI #104: a tolerated repeat is audited after the re-bind, with its real outcome.
1456
+ let replay;
1454
1457
  try {
1455
- const est = await this.sessionAuthority.rotate(refreshToken);
1458
+ const rotated = await (0, replay_outcome_js_1.captureReplayDuring)(() => this.sessionAuthority.rotate(refreshToken));
1459
+ const est = rotated.result;
1460
+ replay = rotated.replay;
1456
1461
  // Carry the principal claims forward to the NEW access token (the durable
1457
1462
  // session row lacks roles/kind — the session-claims entry has them).
1458
1463
  if (this.tokenBinder !== undefined) {
@@ -1477,7 +1482,9 @@ let AuthController = class AuthController {
1477
1482
  ? await peek.sessionTenant(est.session.sessionId)
1478
1483
  : null) ?? est.session.tenantId;
1479
1484
  const fresh = await this.projectForNode(est.session.principalId, activeTenant);
1480
- const rebound = await this.tokenBinder.rebindOnRotate(est.tokens.accessToken, est.session.sessionId, ttlMs, fresh.facts, fresh.subtree, fresh.subtreeVersion, fresh.roles, activeTenant);
1485
+ const rebound = await this.tokenBinder.rebindOnRotate(est.tokens.accessToken, est.session.sessionId, ttlMs, fresh.facts, fresh.subtree, fresh.subtreeVersion, fresh.roles, activeTenant,
1486
+ // KI #110: a repeat never moves the pointer; its original takes the full bind.
1487
+ { repeat: replay !== undefined });
1481
1488
  // The claims were gone (Redis loss) or a logout won the race (`SET XX` refused):
1482
1489
  // SILENT RECOVERY (invariant #12, ADR-0013/0035) rebuilds them from durable state; a
1483
1490
  // rebuild that cannot be proven safe is a generic 401 — never an unbound token.
@@ -1490,15 +1497,25 @@ let AuthController = class AuthController {
1490
1497
  genericUnauthorized();
1491
1498
  }
1492
1499
  }
1500
+ this.recordReplay(replay, 'tolerated');
1493
1501
  return toSessionSuccess(est);
1494
1502
  }
1495
1503
  catch {
1496
1504
  // The engine throws only its generic SessionRejectedError (reuse, invalid,
1497
1505
  // expiry, or a swallowed store outage — all indistinguishable here). Map to a
1498
1506
  // generic 401; never leak which, never echo the token.
1507
+ this.recordReplay(replay, 'refused');
1499
1508
  genericUnauthorized();
1500
1509
  }
1501
1510
  }
1511
+ /** KI #104: audit a tolerated repeat once its re-bind is decided (best-effort, never blocks). */
1512
+ recordReplay(replay, outcome) {
1513
+ if (replay === undefined)
1514
+ return;
1515
+ void this.auditSink?.record((0, replay_outcome_js_1.replayAuditEvent)(replay, outcome, Date.now())).catch(() => {
1516
+ /* best-effort: an audit failure never changes the response */
1517
+ });
1518
+ }
1502
1519
  /**
1503
1520
  * The refresh-time projection for one node: coarse grants + live roles (ADR-0035) and the
1504
1521
  * subtree with its version stamp (Phase 1C: the version is read FIRST). Each part is best-effort
@@ -1 +1 @@
1
- {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"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"}
@@ -130,6 +130,7 @@ const mariadb_or_totp_verifier_js_1 = require("../authentication/mariadb-or-totp
130
130
  const permission_facts_adapter_js_1 = require("../identity/permission-facts.adapter.js");
131
131
  const auth_facts_controller_js_1 = require("../identity/auth-facts.controller.js");
132
132
  const index_js_1 = require("../session/index.js");
133
+ const replay_outcome_js_1 = require("../session/replay-outcome.js");
133
134
  const persistence_module_js_1 = require("./persistence.module.js");
134
135
  const auth_controller_js_1 = require("./auth.controller.js");
135
136
  const account_controller_js_1 = require("./account.controller.js");
@@ -512,16 +513,10 @@ const sessionAuthorityProvider = {
512
513
  ? {
513
514
  recentRotations,
514
515
  onReplayTolerated: (event) => {
515
- void auditSink
516
- ?.record({
517
- type: 'authentication',
518
- occurredAtMs: Date.now(),
519
- actorId: event.principalId,
520
- tenantId: event.tenantId,
521
- action: 'session.refresh_replay_tolerated',
522
- attributes: { sessionId: event.sessionId },
523
- })
524
- .catch(() => {
516
+ // KI #104: inside the refresh route the event is recorded after the re-bind.
517
+ if ((0, replay_outcome_js_1.noteReplayTolerated)(event))
518
+ return;
519
+ void auditSink?.record((0, replay_outcome_js_1.replayAuditEvent)(event, 'tolerated', Date.now())).catch(() => {
525
520
  /* best-effort: an audit failure never changes the response */
526
521
  });
527
522
  },
@@ -17,8 +17,10 @@
17
17
  export declare function canonicalEmail(raw: string): string;
18
18
  /**
19
19
  * A deliberately minimal email shape check (non-empty, a single `@`, a `.` in the domain, no
20
- * whitespace). A full RFC-5322 validator adds no security value here, only false rejections; the
21
- * authoritative check is the confirm link reaching the inbox.
20
+ * whitespace, at most 255 characters). A full RFC-5322 validator adds no security value here, only
21
+ * false rejections; the authoritative check is the confirm link reaching the inbox. The length cap
22
+ * keeps every path (create, administrator change, self-service change) from reaching the store with
23
+ * a value it would refuse as errno 1406.
22
24
  */
23
25
  export declare function isEmailShaped(value: string): boolean;
24
26
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"email-address.d.ts","sourceRoot":"","sources":["../../src/auth/email-address.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMpD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI/C"}
1
+ {"version":3,"file":"email-address.d.ts","sourceRoot":"","sources":["../../src/auth/email-address.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAQD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI/C"}
@@ -22,14 +22,23 @@ exports.maskEmail = maskEmail;
22
22
  function canonicalEmail(raw) {
23
23
  return raw.trim().normalize('NFKC').trim().toLowerCase();
24
24
  }
25
+ /**
26
+ * The longest address any store takes: `users.u_email` and `email_change_token.ect_new_email` are
27
+ * VARCHAR(255), and MariaDB counts characters (code points), not UTF-16 units (KI #107).
28
+ */
29
+ const MAX_EMAIL_CHARS = 255;
25
30
  /**
26
31
  * A deliberately minimal email shape check (non-empty, a single `@`, a `.` in the domain, no
27
- * whitespace). A full RFC-5322 validator adds no security value here, only false rejections; the
28
- * authoritative check is the confirm link reaching the inbox.
32
+ * whitespace, at most 255 characters). A full RFC-5322 validator adds no security value here, only
33
+ * false rejections; the authoritative check is the confirm link reaching the inbox. The length cap
34
+ * keeps every path (create, administrator change, self-service change) from reaching the store with
35
+ * a value it would refuse as errno 1406.
29
36
  */
30
37
  function isEmailShaped(value) {
31
38
  if (/\s/.test(value))
32
39
  return false;
40
+ if ([...value].length > MAX_EMAIL_CHARS)
41
+ return false;
33
42
  const at = value.indexOf('@');
34
43
  if (at <= 0 || at !== value.lastIndexOf('@') || at === value.length - 1)
35
44
  return false;
@@ -118,6 +118,25 @@ export interface SessionTokenBinderOptions {
118
118
  */
119
119
  readonly rotateGraceMs?: number;
120
120
  }
121
+ /**
122
+ * The rotate re-bind as ONE script (KI #105, KI #110). KEYS: 1 the new token's binding, 2 the session
123
+ * claims, 3 the current-token pointer, 4 the previous token's binding (= KEYS[1] when there is none),
124
+ * 5 the pending marker (the token a repeat bound for the grace only). ARGV: 1 payload, 2 access TTL,
125
+ * 3 session TTL, 4 grace-only TTL (min(access TTL, grace)), 5 the pointer value read before the
126
+ * rebind ('' = none), 6 the new token, 7 grace, 8 '1' when core answered a tolerated repeat.
127
+ *
128
+ * 0 the claims are gone (a logout won): nothing is written, the caller refuses.
129
+ * 2 nothing changes. A repeat whose token is bound (its original, or an earlier repeat, bound it);
130
+ * or an original whose token is bound but not pending (the session rotated on past it, and the
131
+ * binding keeps the grace it has).
132
+ * 3 bound for the grace TTL only; the claims, the pointer and the current token are left alone. A
133
+ * repeat whose token is unbound (its original has not re-bound yet: the token is marked pending
134
+ * so the original can still take the full bind), or an original whose pointer moved since it
135
+ * was read (the session rotated on).
136
+ * 1 the normal rotate (an original; its token unbound, or pending): bind for the access TTL,
137
+ * refresh the claims, move the pointer, grace-expire the previous token, clear the marker.
138
+ */
139
+ export declare const REBIND_ON_ROTATE_SCRIPT = "if redis.call('EXISTS', KEYS[2]) == 0 then return 0 end\nlocal bound = redis.call('EXISTS', KEYS[1]) == 1\nif ARGV[8] == '1' then\n if bound then return 2 end\n redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[4])\n redis.call('SET', KEYS[5], ARGV[6], 'PX', ARGV[4])\n return 3\nend\nif bound and redis.call('GET', KEYS[5]) ~= ARGV[6] then return 2 end\nlocal current = redis.call('GET', KEYS[3])\nif current == false then current = '' end\nif current ~= ARGV[5] then\n if bound then return 2 end\n redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[4])\n return 3\nend\nredis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\nredis.call('SET', KEYS[2], ARGV[1], 'PX', ARGV[3], 'XX')\nredis.call('SET', KEYS[3], ARGV[6], 'PX', ARGV[3])\nif ARGV[5] ~= '' and KEYS[4] ~= KEYS[1] then\n local remaining = redis.call('PTTL', KEYS[4])\n if remaining == -1 or remaining > tonumber(ARGV[7]) then\n redis.call('PEXPIRE', KEYS[4], ARGV[7])\n end\nend\nredis.call('DEL', KEYS[5])\nreturn 1";
121
140
  export declare class SessionTokenBinder {
122
141
  private readonly redis;
123
142
  private readonly options;
@@ -134,12 +153,6 @@ export declare class SessionTokenBinder {
134
153
  * token so a rotation can grace-expire it and a revoke can drop it.
135
154
  */
136
155
  bind(accessToken: string, claims: BoundPrincipalClaims, ttlMs: number): Promise<void>;
137
- /**
138
- * Grace-expire the session's previous access token after a rotation (plan D7): it stays
139
- * usable for at most `rotateGraceMs` (never longer than it already had), then expires.
140
- * Best-effort — a failure leaves the old binding to its own (≤ access TTL) expiry.
141
- */
142
- private graceExpire;
143
156
  /**
144
157
  * On refresh/rotate the controller has the NEW access token + the session, but not
145
158
  * the full Principal (roles/kind/root live only in the binding, not the
@@ -149,7 +162,13 @@ export declare class SessionTokenBinder {
149
162
  * then refuse the refresh (the auth controller answers 401: an unbound token is never
150
163
  * handed out). A Redis error on the writes propagates (the caller fails closed too).
151
164
  * The new binding's TTL is `ttlMs` (≤ the access TTL); the session's previous access
152
- * token is grace-expired (≤ 30 s by default, plan D7).
165
+ * token is grace-expired (≤ 30 s by default, plan D7). All writes are one script
166
+ * ({@link REBIND_ON_ROTATE_SCRIPT}, KI #105, KI #110). `opts.repeat` (core answered the rotation
167
+ * from the replay window) never moves the pointer: a bound token changes nothing, an unbound one is
168
+ * bound for the grace only and marked pending. An original rotation takes the full bind when its
169
+ * token is unbound or pending and the pointer has not moved since it was read; otherwise its token
170
+ * is left as it is (or bound for the grace only), so no rotation can take the session back from a
171
+ * newer token.
153
172
  *
154
173
  * `freshFacts` (ADR-0011 eager projection at REFRESH):
155
174
  * - `undefined` (omitted, the default) → carry whatever `cachedFacts` the session
@@ -181,7 +200,11 @@ export declare class SessionTokenBinder {
181
200
  * switch raced the refresh) → nothing fresh is applied and the cached facts are stripped (the
182
201
  * next request reloads for the right node). Absent → applied as before.
183
202
  */
184
- freshForTenantId?: string): Promise<boolean>;
203
+ freshForTenantId?: string,
204
+ /** `repeat`: core answered this rotation from the replay window (KI #110). Absent → an original. */
205
+ opts?: {
206
+ readonly repeat?: boolean;
207
+ }): Promise<boolean>;
185
208
  /**
186
209
  * SILENT RECOVERY (ADR-0013 / ADR-0035, GAPS #5): write a session binding REBUILT from durable
187
210
  * state after the session-claims entry was lost (Redis restart/flush). The caller (refresh) has
@@ -1 +1 @@
1
- {"version":3,"file":"session-token-binder.d.ts","sourceRoot":"","sources":["../../src/request-auth/session-token-binder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-token-binder.d.ts","sourceRoot":"","sources":["../../src/request-auth/session-token-binder.ts"],"names":[],"mappings":"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"}
@@ -24,7 +24,7 @@
24
24
  * and the stored value carries no secret (claims only — ids, not credentials).
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.SessionTokenBinder = void 0;
27
+ exports.SessionTokenBinder = exports.REBIND_ON_ROTATE_SCRIPT = void 0;
28
28
  exports.factsSlot = factsSlot;
29
29
  exports.liveRoleIds = liveRoleIds;
30
30
  /** A valid stamp: a non-negative safe integer. */
@@ -148,8 +148,62 @@ function sessClaimsKey(sessionId) {
148
148
  function sessAtKey(sessionId) {
149
149
  return `${SESS_AT_PREFIX}${sessionId}`;
150
150
  }
151
+ /**
152
+ * Redis key prefix for the session-id → the token a tolerated repeat bound for the grace only
153
+ * (KI #110): the one token the original rotation may still upgrade to the full bind.
154
+ */
155
+ const SESS_GRACE_PREFIX = 'shield:sessgrace:';
156
+ function sessGraceKey(sessionId) {
157
+ return `${SESS_GRACE_PREFIX}${sessionId}`;
158
+ }
151
159
  /** Default grace for the previous access token after a rotation (plan decision D7). */
152
160
  const DEFAULT_ROTATE_GRACE_MS = 30_000;
161
+ /**
162
+ * The rotate re-bind as ONE script (KI #105, KI #110). KEYS: 1 the new token's binding, 2 the session
163
+ * claims, 3 the current-token pointer, 4 the previous token's binding (= KEYS[1] when there is none),
164
+ * 5 the pending marker (the token a repeat bound for the grace only). ARGV: 1 payload, 2 access TTL,
165
+ * 3 session TTL, 4 grace-only TTL (min(access TTL, grace)), 5 the pointer value read before the
166
+ * rebind ('' = none), 6 the new token, 7 grace, 8 '1' when core answered a tolerated repeat.
167
+ *
168
+ * 0 the claims are gone (a logout won): nothing is written, the caller refuses.
169
+ * 2 nothing changes. A repeat whose token is bound (its original, or an earlier repeat, bound it);
170
+ * or an original whose token is bound but not pending (the session rotated on past it, and the
171
+ * binding keeps the grace it has).
172
+ * 3 bound for the grace TTL only; the claims, the pointer and the current token are left alone. A
173
+ * repeat whose token is unbound (its original has not re-bound yet: the token is marked pending
174
+ * so the original can still take the full bind), or an original whose pointer moved since it
175
+ * was read (the session rotated on).
176
+ * 1 the normal rotate (an original; its token unbound, or pending): bind for the access TTL,
177
+ * refresh the claims, move the pointer, grace-expire the previous token, clear the marker.
178
+ */
179
+ exports.REBIND_ON_ROTATE_SCRIPT = `if redis.call('EXISTS', KEYS[2]) == 0 then return 0 end
180
+ local bound = redis.call('EXISTS', KEYS[1]) == 1
181
+ if ARGV[8] == '1' then
182
+ if bound then return 2 end
183
+ redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[4])
184
+ redis.call('SET', KEYS[5], ARGV[6], 'PX', ARGV[4])
185
+ return 3
186
+ end
187
+ if bound and redis.call('GET', KEYS[5]) ~= ARGV[6] then return 2 end
188
+ local current = redis.call('GET', KEYS[3])
189
+ if current == false then current = '' end
190
+ if current ~= ARGV[5] then
191
+ if bound then return 2 end
192
+ redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[4])
193
+ return 3
194
+ end
195
+ redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
196
+ redis.call('SET', KEYS[2], ARGV[1], 'PX', ARGV[3], 'XX')
197
+ redis.call('SET', KEYS[3], ARGV[6], 'PX', ARGV[3])
198
+ if ARGV[5] ~= '' and KEYS[4] ~= KEYS[1] then
199
+ local remaining = redis.call('PTTL', KEYS[4])
200
+ if remaining == -1 or remaining > tonumber(ARGV[7]) then
201
+ redis.call('PEXPIRE', KEYS[4], ARGV[7])
202
+ end
203
+ end
204
+ redis.call('DEL', KEYS[5])
205
+ return 1`;
206
+ const REBIND_REFUSED = 0;
153
207
  /** How long refresh waits for Redis to answer PING before answering 503 (ADR-0035, F1). */
154
208
  const DEFAULT_PING_TIMEOUT_MS = 1_000;
155
209
  class SessionTokenBinder {
@@ -184,25 +238,6 @@ class SessionTokenBinder {
184
238
  await this.redis.set(sessClaimsKey(claims.sessionId), payload, 'PX', sessTtl);
185
239
  await this.redis.set(sessAtKey(claims.sessionId), accessToken, 'PX', sessTtl);
186
240
  }
187
- /**
188
- * Grace-expire the session's previous access token after a rotation (plan D7): it stays
189
- * usable for at most `rotateGraceMs` (never longer than it already had), then expires.
190
- * Best-effort — a failure leaves the old binding to its own (≤ access TTL) expiry.
191
- */
192
- async graceExpire(previousToken, newToken) {
193
- if (previousToken === null || previousToken === '' || previousToken === newToken)
194
- return;
195
- const grace = this.options.rotateGraceMs ?? DEFAULT_ROTATE_GRACE_MS;
196
- try {
197
- const remaining = await this.redis.pttl(atKey(previousToken));
198
- if (remaining === -1 || remaining > grace) {
199
- await this.redis.pexpire(atKey(previousToken), grace);
200
- }
201
- }
202
- catch {
203
- // best-effort; the old binding still expires at its own access TTL.
204
- }
205
- }
206
241
  /**
207
242
  * On refresh/rotate the controller has the NEW access token + the session, but not
208
243
  * the full Principal (roles/kind/root live only in the binding, not the
@@ -212,7 +247,13 @@ class SessionTokenBinder {
212
247
  * then refuse the refresh (the auth controller answers 401: an unbound token is never
213
248
  * handed out). A Redis error on the writes propagates (the caller fails closed too).
214
249
  * The new binding's TTL is `ttlMs` (≤ the access TTL); the session's previous access
215
- * token is grace-expired (≤ 30 s by default, plan D7).
250
+ * token is grace-expired (≤ 30 s by default, plan D7). All writes are one script
251
+ * ({@link REBIND_ON_ROTATE_SCRIPT}, KI #105, KI #110). `opts.repeat` (core answered the rotation
252
+ * from the replay window) never moves the pointer: a bound token changes nothing, an unbound one is
253
+ * bound for the grace only and marked pending. An original rotation takes the full bind when its
254
+ * token is unbound or pending and the pointer has not moved since it was read; otherwise its token
255
+ * is left as it is (or bound for the grace only), so no rotation can take the session back from a
256
+ * newer token.
216
257
  *
217
258
  * `freshFacts` (ADR-0011 eager projection at REFRESH):
218
259
  * - `undefined` (omitted, the default) → carry whatever `cachedFacts` the session
@@ -244,9 +285,15 @@ class SessionTokenBinder {
244
285
  * switch raced the refresh) → nothing fresh is applied and the cached facts are stripped (the
245
286
  * next request reloads for the right node). Absent → applied as before.
246
287
  */
247
- freshForTenantId) {
288
+ freshForTenantId,
289
+ /** `repeat`: core answered this rotation from the replay window (KI #110). Absent → an original. */
290
+ opts) {
291
+ // The session's current access token, read FIRST: the script moves the pointer only if it still
292
+ // holds this value (KI #105).
293
+ let previousToken;
248
294
  let raw;
249
295
  try {
296
+ previousToken = await this.redis.get(sessAtKey(sessionId));
250
297
  raw = await this.redis.get(sessClaimsKey(sessionId));
251
298
  }
252
299
  catch {
@@ -304,28 +351,12 @@ class SessionTokenBinder {
304
351
  payload = raw;
305
352
  }
306
353
  }
307
- // The session's previous access token (best-effort read — a miss only skips the grace).
308
- let previousToken = null;
309
- try {
310
- previousToken = await this.redis.get(sessAtKey(sessionId));
311
- }
312
- catch {
313
- previousToken = null;
314
- }
315
354
  const sessTtl = this.sessionTtl(ttlMs);
316
- await this.redis.set(atKey(newAccessToken), payload, 'PX', ttlMs);
317
- // Refresh the session-claims TTL (with the same, possibly-updated payload) + pointer.
318
- // `XX` (Phase 2 review F2): only while the entry still exists. A logout that deleted it
319
- // between our read and this write wins — the claims are NOT re-created, the new token's
320
- // binding is dropped, and the caller refuses the refresh (no resurrected session).
321
- const kept = await this.redis.set(sessClaimsKey(sessionId), payload, 'PX', sessTtl, 'XX');
322
- if (kept !== 'OK') {
323
- await this.unbind(newAccessToken);
324
- return false;
325
- }
326
- await this.redis.set(sessAtKey(sessionId), newAccessToken, 'PX', sessTtl);
327
- await this.graceExpire(previousToken, newAccessToken);
328
- return true;
355
+ const grace = this.options.rotateGraceMs ?? DEFAULT_ROTATE_GRACE_MS;
356
+ const expected = previousToken ?? '';
357
+ // KI #105: one script, so nothing can land between the checks and the writes.
358
+ const outcome = await this.redis.eval(exports.REBIND_ON_ROTATE_SCRIPT, 5, atKey(newAccessToken), sessClaimsKey(sessionId), sessAtKey(sessionId), expected === '' ? atKey(newAccessToken) : atKey(expected), sessGraceKey(sessionId), payload, String(ttlMs), String(sessTtl), String(Math.max(1, Math.min(ttlMs, grace))), expected, newAccessToken, String(grace), opts?.repeat === true ? '1' : '0');
359
+ return Number(outcome) !== REBIND_REFUSED;
329
360
  }
330
361
  /**
331
362
  * SILENT RECOVERY (ADR-0013 / ADR-0035, GAPS #5): write a session binding REBUILT from durable
@@ -0,0 +1,12 @@
1
+ import type { ReplayToleratedEvent } from '@astram/core';
2
+ import type { AuthAuditEvent } from '../auth/ports/audit-sink.js';
3
+ /** Runs `fn` and returns its result with the tolerated-repeat event core reported during it, if any. */
4
+ export declare function captureReplayDuring<T>(fn: () => Promise<T>): Promise<{
5
+ result: T;
6
+ replay: ReplayToleratedEvent | undefined;
7
+ }>;
8
+ /** Notes the event for the enclosing {@link captureReplayDuring}; false when there is none. */
9
+ export declare function noteReplayTolerated(event: ReplayToleratedEvent): boolean;
10
+ /** The audit event for a tolerated repeat: answered (`tolerated`) or refused at the re-bind. */
11
+ export declare function replayAuditEvent(event: ReplayToleratedEvent, outcome: 'tolerated' | 'refused', occurredAtMs: number): AuthAuditEvent;
12
+ //# sourceMappingURL=replay-outcome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-outcome.d.ts","sourceRoot":"","sources":["../../src/session/replay-outcome.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAQlE,wGAAwG;AACxG,wBAAsB,mBAAmB,CAAC,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,CAAA;CAAE,CAAC,CAIlE;AAED,+FAA+F;AAC/F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAKxE;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,oBAAoB,EAC3B,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,YAAY,EAAE,MAAM,GACnB,cAAc,CAYhB"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.captureReplayDuring = captureReplayDuring;
4
+ exports.noteReplayTolerated = noteReplayTolerated;
5
+ exports.replayAuditEvent = replayAuditEvent;
6
+ /**
7
+ * KI #104: core reports a tolerated refresh repeat (`onReplayTolerated`) from inside `rotate`, before
8
+ * the controller has re-bound the access token. The refresh route runs `rotate` inside
9
+ * {@link captureReplayDuring}; the callback then only notes the event, and the route records the
10
+ * audit AFTER the re-bind with the real outcome (`session.refresh_replay_tolerated`, or
11
+ * `session.refresh_replay_refused` when the re-bind refuses). A rotate outside that scope (another
12
+ * caller) is recorded at once, as before.
13
+ */
14
+ const node_async_hooks_1 = require("node:async_hooks");
15
+ const capture = new node_async_hooks_1.AsyncLocalStorage();
16
+ /** Runs `fn` and returns its result with the tolerated-repeat event core reported during it, if any. */
17
+ async function captureReplayDuring(fn) {
18
+ const slot = {};
19
+ const result = await capture.run(slot, fn);
20
+ return { result, replay: slot.event };
21
+ }
22
+ /** Notes the event for the enclosing {@link captureReplayDuring}; false when there is none. */
23
+ function noteReplayTolerated(event) {
24
+ const slot = capture.getStore();
25
+ if (slot === undefined)
26
+ return false;
27
+ slot.event = event;
28
+ return true;
29
+ }
30
+ /** The audit event for a tolerated repeat: answered (`tolerated`) or refused at the re-bind. */
31
+ function replayAuditEvent(event, outcome, occurredAtMs) {
32
+ return {
33
+ type: 'authentication',
34
+ occurredAtMs,
35
+ actorId: event.principalId,
36
+ tenantId: event.tenantId,
37
+ action: outcome === 'tolerated'
38
+ ? 'session.refresh_replay_tolerated'
39
+ : 'session.refresh_replay_refused',
40
+ attributes: { sessionId: event.sessionId },
41
+ };
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guard",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/Redis/crypto adapters that connect the sealed core to a real backend. Depends on @astram/core (ADR-0003, ADR-0023).",
6
6
  "type": "commonjs",
@@ -35,8 +35,8 @@
35
35
  "argon2": "^0.41.1",
36
36
  "ioredis": "^5.4.1",
37
37
  "mariadb": "^3.5.4",
38
- "@astram/engine": "0.5.3",
39
- "@astram/core": "1.5.0"
38
+ "@astram/core": "1.5.0",
39
+ "@astram/engine": "0.5.3"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@nestjs/common": "^11",