@aooth/auth 0.1.50 → 0.1.51
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/dist/atscript-db.cjs +3 -1
- package/dist/atscript-db.d.cts +3 -2
- package/dist/atscript-db.d.mts +3 -2
- package/dist/atscript-db.mjs +3 -1
- package/dist/{auth-credential-C3qebKuy.d.mts → auth-credential-C5zD8b0J.d.cts} +21 -9
- package/dist/{auth-credential-CEijxoGU.d.cts → auth-credential-C9xy4X_k.d.mts} +21 -9
- package/dist/authz.cjs +4 -1
- package/dist/authz.d.cts +2 -2
- package/dist/authz.d.mts +2 -2
- package/dist/authz.mjs +4 -1
- package/dist/{clock-DJ_eroHW.d.cts → clock-C0O0FCCN.d.cts} +63 -8
- package/dist/{clock-DJ_eroHW.d.mts → clock-C0O0FCCN.d.mts} +63 -8
- package/dist/{dynamic-client-store-DJgtMjPw.d.mts → dynamic-client-store-Cujxec8n.d.cts} +12 -2
- package/dist/{dynamic-client-store-DbiSLOj0.d.cts → dynamic-client-store-DEGFMiTK.d.mts} +12 -2
- package/dist/index.cjs +46 -11
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +46 -11
- package/dist/{payload-D-DzH5-J.mjs → payload-DP5zRiXT.mjs} +1 -0
- package/dist/{payload-BJjvj8AH.cjs → payload-HPgyPiek.cjs} +1 -0
- package/dist/redis.d.cts +2 -2
- package/dist/redis.d.mts +2 -2
- package/dist/{store-BYnpA7VK.d.cts → store-C6DU6VZk.d.cts} +1 -1
- package/dist/{store-pKh0ZaGG.d.mts → store-y7K7z1EF.d.mts} +1 -1
- package/package.json +8 -8
- package/src/atscript-db/auth-credential.as +19 -0
- package/src/atscript-db/auth-credential.as.d.ts +10 -1
package/dist/atscript-db.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_clock = require("./clock-Bl-H3eqE.cjs");
|
|
3
|
-
const require_payload = require("./payload-
|
|
3
|
+
const require_payload = require("./payload-HPgyPiek.cjs");
|
|
4
4
|
const require_dynamic_client_store = require("./dynamic-client-store-DykM9QOT.cjs");
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
6
6
|
//#region src/atscript-db/authz-stores.ts
|
|
@@ -324,6 +324,7 @@ function stateToRow(state, token, expiresAt, metadataField) {
|
|
|
324
324
|
kind: state.kind,
|
|
325
325
|
parentCredentialId: state.parentCredentialId,
|
|
326
326
|
rotatedAt: state.rotatedAt,
|
|
327
|
+
successor: state.successor,
|
|
327
328
|
sessionId: state.sessionId,
|
|
328
329
|
lastSeenAt: state.lastSeenAt
|
|
329
330
|
};
|
|
@@ -351,6 +352,7 @@ function rowToState(row, metadataField) {
|
|
|
351
352
|
if (row.kind === "access" || row.kind === "refresh") state.kind = row.kind;
|
|
352
353
|
if (row.parentCredentialId !== void 0) state.parentCredentialId = row.parentCredentialId;
|
|
353
354
|
if (row.rotatedAt !== void 0) state.rotatedAt = row.rotatedAt;
|
|
355
|
+
if (row.successor !== void 0 && row.successor !== null) state.successor = row.successor;
|
|
354
356
|
if (row.sessionId !== void 0) state.sessionId = row.sessionId;
|
|
355
357
|
if (row.lastSeenAt !== void 0) state.lastSeenAt = row.lastSeenAt;
|
|
356
358
|
return state;
|
package/dist/atscript-db.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as CredentialStore, s as CredentialState, t as Clock } from "./clock-
|
|
2
|
-
import { c as AuthCodeStore, d as NewAuthCode, h as PendingAuthorizationStore, m as PendingAuthorization, o as NewDynamicClient, p as NewPendingAuthorization, r as DynamicClientStore, s as AuthCode, t as DynamicClient } from "./dynamic-client-store-
|
|
1
|
+
import { c as CredentialSuccessorRef, r as CredentialStore, s as CredentialState, t as Clock } from "./clock-C0O0FCCN.cjs";
|
|
2
|
+
import { c as AuthCodeStore, d as NewAuthCode, h as PendingAuthorizationStore, m as PendingAuthorization, o as NewDynamicClient, p as NewPendingAuthorization, r as DynamicClientStore, s as AuthCode, t as DynamicClient } from "./dynamic-client-store-Cujxec8n.cjs";
|
|
3
3
|
|
|
4
4
|
//#region src/atscript-db/authz-stores.d.ts
|
|
5
5
|
/**
|
|
@@ -225,6 +225,7 @@ type AuthCredentialRow<TPayload extends object = object> = {
|
|
|
225
225
|
kind?: string;
|
|
226
226
|
parentCredentialId?: string;
|
|
227
227
|
rotatedAt?: number;
|
|
228
|
+
successor?: CredentialSuccessorRef;
|
|
228
229
|
sessionId?: string;
|
|
229
230
|
lastSeenAt?: number;
|
|
230
231
|
} & TPayload;
|
package/dist/atscript-db.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as CredentialStore, s as CredentialState, t as Clock } from "./clock-
|
|
2
|
-
import { c as AuthCodeStore, d as NewAuthCode, h as PendingAuthorizationStore, m as PendingAuthorization, o as NewDynamicClient, p as NewPendingAuthorization, r as DynamicClientStore, s as AuthCode, t as DynamicClient } from "./dynamic-client-store-
|
|
1
|
+
import { c as CredentialSuccessorRef, r as CredentialStore, s as CredentialState, t as Clock } from "./clock-C0O0FCCN.mjs";
|
|
2
|
+
import { c as AuthCodeStore, d as NewAuthCode, h as PendingAuthorizationStore, m as PendingAuthorization, o as NewDynamicClient, p as NewPendingAuthorization, r as DynamicClientStore, s as AuthCode, t as DynamicClient } from "./dynamic-client-store-DEGFMiTK.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/atscript-db/authz-stores.d.ts
|
|
5
5
|
/**
|
|
@@ -225,6 +225,7 @@ type AuthCredentialRow<TPayload extends object = object> = {
|
|
|
225
225
|
kind?: string;
|
|
226
226
|
parentCredentialId?: string;
|
|
227
227
|
rotatedAt?: number;
|
|
228
|
+
successor?: CredentialSuccessorRef;
|
|
228
229
|
sessionId?: string;
|
|
229
230
|
lastSeenAt?: number;
|
|
230
231
|
} & TPayload;
|
package/dist/atscript-db.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as defaultClock } from "./clock-Bdsep_1j.mjs";
|
|
2
|
-
import { t as credentialPayloadOf } from "./payload-
|
|
2
|
+
import { t as credentialPayloadOf } from "./payload-DP5zRiXT.mjs";
|
|
3
3
|
import { r as AuthCodeStore, s as PendingAuthorizationStore, t as DynamicClientStore } from "./dynamic-client-store-B1JwFHdP.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
//#region src/atscript-db/authz-stores.ts
|
|
@@ -323,6 +323,7 @@ function stateToRow(state, token, expiresAt, metadataField) {
|
|
|
323
323
|
kind: state.kind,
|
|
324
324
|
parentCredentialId: state.parentCredentialId,
|
|
325
325
|
rotatedAt: state.rotatedAt,
|
|
326
|
+
successor: state.successor,
|
|
326
327
|
sessionId: state.sessionId,
|
|
327
328
|
lastSeenAt: state.lastSeenAt
|
|
328
329
|
};
|
|
@@ -350,6 +351,7 @@ function rowToState(row, metadataField) {
|
|
|
350
351
|
if (row.kind === "access" || row.kind === "refresh") state.kind = row.kind;
|
|
351
352
|
if (row.parentCredentialId !== void 0) state.parentCredentialId = row.parentCredentialId;
|
|
352
353
|
if (row.rotatedAt !== void 0) state.rotatedAt = row.rotatedAt;
|
|
354
|
+
if (row.successor !== void 0 && row.successor !== null) state.successor = row.successor;
|
|
353
355
|
if (row.sessionId !== void 0) state.sessionId = row.sessionId;
|
|
354
356
|
if (row.lastSeenAt !== void 0) state.lastSeenAt = row.lastSeenAt;
|
|
355
357
|
return state;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AuthContext,
|
|
1
|
+
import { a as AuthContext, d as RefreshConfig, f as RefreshResult, i as DenylistStore, l as EnrichedSession, m as SessionInfo, o as CredentialMetadata, p as SessionEnricher, r as CredentialStore, s as CredentialState, t as Clock, u as IssueResult } from "./clock-C0O0FCCN.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/credential/auth-credential.d.ts
|
|
4
4
|
interface AuthCredentialOptions<TPayload extends object = object> {
|
|
@@ -84,16 +84,21 @@ type IssueOptions<TPayload extends object = object> = TPayload & {
|
|
|
84
84
|
* - `false` — mint NO refresh token even when the instance config exists
|
|
85
85
|
* (e.g. the authz token endpoint suppressing the paired refresh for a
|
|
86
86
|
* policy that didn't opt in — no orphaned refresh row).
|
|
87
|
-
* - `{ ttl? }` — mint a refresh token for this credential; `ttl`
|
|
88
|
-
* overrides the instance `refresh.ttl`. Works WITHOUT an instance
|
|
89
|
-
* (then `ttl` is required). Per-mint families ALWAYS redeem with
|
|
87
|
+
* - `{ ttl?, graceMs? }` — mint a refresh token for this credential; `ttl`
|
|
88
|
+
* (ms) overrides the instance `refresh.ttl`. Works WITHOUT an instance
|
|
89
|
+
* config (then `ttl` is required). Per-mint families ALWAYS redeem with
|
|
90
90
|
* fixed-ceiling (`'always'`) rotation semantics — the family is stamped
|
|
91
91
|
* `metadata.refreshRotation: "always"` at mint, which `refresh()` honors
|
|
92
92
|
* over the instance rotation, so rotation never extends the family's
|
|
93
93
|
* lifetime even on an instance whose sessions rotate `'sliding'`.
|
|
94
|
+
* `graceMs` (ms, ≥ 0) fixes the family's rotation grace window the same
|
|
95
|
+
* mint-time-authority way (stamped `metadata.rotationGraceMs`, honored
|
|
96
|
+
* over the instance `rotationGraceMs`); `0` is strict single-use. Omit to
|
|
97
|
+
* inherit the instance window (or its 30 s default).
|
|
94
98
|
*/
|
|
95
99
|
refresh?: false | {
|
|
96
100
|
ttl?: number;
|
|
101
|
+
graceMs?: number;
|
|
97
102
|
};
|
|
98
103
|
};
|
|
99
104
|
/** Options for {@link AuthCredential.refresh}. */
|
|
@@ -168,11 +173,18 @@ declare class AuthCredential<TPayload extends object = object> {
|
|
|
168
173
|
private refreshAlways;
|
|
169
174
|
/**
|
|
170
175
|
* Shared rotation-with-grace mechanism for `sliding` and `always` on stateful
|
|
171
|
-
* stores.
|
|
172
|
-
*
|
|
173
|
-
* re-
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
+
* stores. The first rotation keeps the old refresh valid and stamps
|
|
177
|
+
* `rotatedAt` + the {@link CredentialState.successor} pair on it; within the
|
|
178
|
+
* grace window a re-presentation **re-delivers that same pair verbatim** —
|
|
179
|
+
* idempotent, minting nothing, with no liveness side effects (no expiry
|
|
180
|
+
* slide, no `lastSeenAt`) — so a captured stale token teaches an attacker
|
|
181
|
+
* nothing beyond capturing the original rotation response, and a multi-tab
|
|
182
|
+
* race converges on one successor. Beyond grace — or once the successor has
|
|
183
|
+
* itself been rotated or revoked (superseded) — the re-presentation is
|
|
184
|
+
* treated as theft. `preserveExpiry` selects fixed-ceiling (`always`) vs
|
|
185
|
+
* sliding (`sliding`) expiry for the new refresh token; the family's
|
|
186
|
+
* mint-time `metadata.rotationGraceMs` (when stamped) wins over the instance
|
|
187
|
+
* window, and a window of `0` is strict single-use.
|
|
176
188
|
*/
|
|
177
189
|
private rotateWithGrace;
|
|
178
190
|
revoke(token: string): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AuthContext,
|
|
1
|
+
import { a as AuthContext, d as RefreshConfig, f as RefreshResult, i as DenylistStore, l as EnrichedSession, m as SessionInfo, o as CredentialMetadata, p as SessionEnricher, r as CredentialStore, s as CredentialState, t as Clock, u as IssueResult } from "./clock-C0O0FCCN.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/credential/auth-credential.d.ts
|
|
4
4
|
interface AuthCredentialOptions<TPayload extends object = object> {
|
|
@@ -84,16 +84,21 @@ type IssueOptions<TPayload extends object = object> = TPayload & {
|
|
|
84
84
|
* - `false` — mint NO refresh token even when the instance config exists
|
|
85
85
|
* (e.g. the authz token endpoint suppressing the paired refresh for a
|
|
86
86
|
* policy that didn't opt in — no orphaned refresh row).
|
|
87
|
-
* - `{ ttl? }` — mint a refresh token for this credential; `ttl`
|
|
88
|
-
* overrides the instance `refresh.ttl`. Works WITHOUT an instance
|
|
89
|
-
* (then `ttl` is required). Per-mint families ALWAYS redeem with
|
|
87
|
+
* - `{ ttl?, graceMs? }` — mint a refresh token for this credential; `ttl`
|
|
88
|
+
* (ms) overrides the instance `refresh.ttl`. Works WITHOUT an instance
|
|
89
|
+
* config (then `ttl` is required). Per-mint families ALWAYS redeem with
|
|
90
90
|
* fixed-ceiling (`'always'`) rotation semantics — the family is stamped
|
|
91
91
|
* `metadata.refreshRotation: "always"` at mint, which `refresh()` honors
|
|
92
92
|
* over the instance rotation, so rotation never extends the family's
|
|
93
93
|
* lifetime even on an instance whose sessions rotate `'sliding'`.
|
|
94
|
+
* `graceMs` (ms, ≥ 0) fixes the family's rotation grace window the same
|
|
95
|
+
* mint-time-authority way (stamped `metadata.rotationGraceMs`, honored
|
|
96
|
+
* over the instance `rotationGraceMs`); `0` is strict single-use. Omit to
|
|
97
|
+
* inherit the instance window (or its 30 s default).
|
|
94
98
|
*/
|
|
95
99
|
refresh?: false | {
|
|
96
100
|
ttl?: number;
|
|
101
|
+
graceMs?: number;
|
|
97
102
|
};
|
|
98
103
|
};
|
|
99
104
|
/** Options for {@link AuthCredential.refresh}. */
|
|
@@ -168,11 +173,18 @@ declare class AuthCredential<TPayload extends object = object> {
|
|
|
168
173
|
private refreshAlways;
|
|
169
174
|
/**
|
|
170
175
|
* Shared rotation-with-grace mechanism for `sliding` and `always` on stateful
|
|
171
|
-
* stores.
|
|
172
|
-
*
|
|
173
|
-
* re-
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
+
* stores. The first rotation keeps the old refresh valid and stamps
|
|
177
|
+
* `rotatedAt` + the {@link CredentialState.successor} pair on it; within the
|
|
178
|
+
* grace window a re-presentation **re-delivers that same pair verbatim** —
|
|
179
|
+
* idempotent, minting nothing, with no liveness side effects (no expiry
|
|
180
|
+
* slide, no `lastSeenAt`) — so a captured stale token teaches an attacker
|
|
181
|
+
* nothing beyond capturing the original rotation response, and a multi-tab
|
|
182
|
+
* race converges on one successor. Beyond grace — or once the successor has
|
|
183
|
+
* itself been rotated or revoked (superseded) — the re-presentation is
|
|
184
|
+
* treated as theft. `preserveExpiry` selects fixed-ceiling (`always`) vs
|
|
185
|
+
* sliding (`sliding`) expiry for the new refresh token; the family's
|
|
186
|
+
* mint-time `metadata.rotationGraceMs` (when stamped) wins over the instance
|
|
187
|
+
* window, and a window of `0` is strict single-use.
|
|
176
188
|
*/
|
|
177
189
|
private rotateWithGrace;
|
|
178
190
|
revoke(token: string): Promise<void>;
|
package/dist/authz.cjs
CHANGED
|
@@ -28,7 +28,10 @@ function tokenPolicyToIssueOptions(policy, clientId) {
|
|
|
28
28
|
...policy.payload,
|
|
29
29
|
...policy.kind !== void 0 && { kind: policy.kind },
|
|
30
30
|
...policy.ttl !== void 0 && { ttl: policy.ttl },
|
|
31
|
-
refresh: withRefresh ? {
|
|
31
|
+
refresh: withRefresh ? {
|
|
32
|
+
ttl: policy.refresh?.ttl ?? 2592e6,
|
|
33
|
+
...policy.refresh?.graceMs !== void 0 && { graceMs: policy.refresh.graceMs }
|
|
34
|
+
} : false,
|
|
32
35
|
...withRefresh && { metadata: { authzClientId: clientId } }
|
|
33
36
|
};
|
|
34
37
|
}
|
package/dist/authz.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as Clock } from "./clock-
|
|
2
|
-
import { _ as PendingAuthorizationStoreMemoryOptions, a as DynamicClientStoreMemoryOptions, b as tokenPolicyToIssueOptions, c as AuthCodeStore, d as NewAuthCode, f as DEFAULT_PENDING_TTL_MS, g as PendingAuthorizationStoreMemory, h as PendingAuthorizationStore, i as DynamicClientStoreMemory, l as AuthCodeStoreMemory, m as PendingAuthorization, n as DynamicClientAuthMethod, o as NewDynamicClient, p as NewPendingAuthorization, r as DynamicClientStore, s as AuthCode, t as DynamicClient, u as AuthCodeStoreMemoryOptions, v as DEFAULT_AUTHZ_REFRESH_TTL_MS, y as TokenPolicy } from "./dynamic-client-store-
|
|
1
|
+
import { t as Clock } from "./clock-C0O0FCCN.cjs";
|
|
2
|
+
import { _ as PendingAuthorizationStoreMemoryOptions, a as DynamicClientStoreMemoryOptions, b as tokenPolicyToIssueOptions, c as AuthCodeStore, d as NewAuthCode, f as DEFAULT_PENDING_TTL_MS, g as PendingAuthorizationStoreMemory, h as PendingAuthorizationStore, i as DynamicClientStoreMemory, l as AuthCodeStoreMemory, m as PendingAuthorization, n as DynamicClientAuthMethod, o as NewDynamicClient, p as NewPendingAuthorization, r as DynamicClientStore, s as AuthCode, t as DynamicClient, u as AuthCodeStoreMemoryOptions, v as DEFAULT_AUTHZ_REFRESH_TTL_MS, y as TokenPolicy } from "./dynamic-client-store-Cujxec8n.cjs";
|
|
3
3
|
import { JWK } from "jose";
|
|
4
4
|
|
|
5
5
|
//#region src/authz/authz-errors.d.ts
|
package/dist/authz.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as Clock } from "./clock-
|
|
2
|
-
import { _ as PendingAuthorizationStoreMemoryOptions, a as DynamicClientStoreMemoryOptions, b as tokenPolicyToIssueOptions, c as AuthCodeStore, d as NewAuthCode, f as DEFAULT_PENDING_TTL_MS, g as PendingAuthorizationStoreMemory, h as PendingAuthorizationStore, i as DynamicClientStoreMemory, l as AuthCodeStoreMemory, m as PendingAuthorization, n as DynamicClientAuthMethod, o as NewDynamicClient, p as NewPendingAuthorization, r as DynamicClientStore, s as AuthCode, t as DynamicClient, u as AuthCodeStoreMemoryOptions, v as DEFAULT_AUTHZ_REFRESH_TTL_MS, y as TokenPolicy } from "./dynamic-client-store-
|
|
1
|
+
import { t as Clock } from "./clock-C0O0FCCN.mjs";
|
|
2
|
+
import { _ as PendingAuthorizationStoreMemoryOptions, a as DynamicClientStoreMemoryOptions, b as tokenPolicyToIssueOptions, c as AuthCodeStore, d as NewAuthCode, f as DEFAULT_PENDING_TTL_MS, g as PendingAuthorizationStoreMemory, h as PendingAuthorizationStore, i as DynamicClientStoreMemory, l as AuthCodeStoreMemory, m as PendingAuthorization, n as DynamicClientAuthMethod, o as NewDynamicClient, p as NewPendingAuthorization, r as DynamicClientStore, s as AuthCode, t as DynamicClient, u as AuthCodeStoreMemoryOptions, v as DEFAULT_AUTHZ_REFRESH_TTL_MS, y as TokenPolicy } from "./dynamic-client-store-DEGFMiTK.mjs";
|
|
3
3
|
import { JWK } from "jose";
|
|
4
4
|
|
|
5
5
|
//#region src/authz/authz-errors.d.ts
|
package/dist/authz.mjs
CHANGED
|
@@ -27,7 +27,10 @@ function tokenPolicyToIssueOptions(policy, clientId) {
|
|
|
27
27
|
...policy.payload,
|
|
28
28
|
...policy.kind !== void 0 && { kind: policy.kind },
|
|
29
29
|
...policy.ttl !== void 0 && { ttl: policy.ttl },
|
|
30
|
-
refresh: withRefresh ? {
|
|
30
|
+
refresh: withRefresh ? {
|
|
31
|
+
ttl: policy.refresh?.ttl ?? 2592e6,
|
|
32
|
+
...policy.refresh?.graceMs !== void 0 && { graceMs: policy.refresh.graceMs }
|
|
33
|
+
} : false,
|
|
31
34
|
...withRefresh && { metadata: { authzClientId: clientId } }
|
|
32
35
|
};
|
|
33
36
|
}
|
|
@@ -78,6 +78,29 @@ interface CredentialMetadata {
|
|
|
78
78
|
* sessions are unaffected).
|
|
79
79
|
*/
|
|
80
80
|
refreshRotation?: "none" | "always" | "sliding";
|
|
81
|
+
/**
|
|
82
|
+
* Per-family rotation grace window in ms — stamped by `issue()` when a
|
|
83
|
+
* refresh token is minted with a per-mint `graceMs` (e.g. an authz grant's
|
|
84
|
+
* `TokenPolicy.refresh.graceMs`), and honored by `refresh()` OVER the
|
|
85
|
+
* instance {@link RefreshConfig.rotationGraceMs}. Same mint-time-authority
|
|
86
|
+
* mechanism as {@link accessTtl} / {@link refreshRotation}. Absent ⇒ the
|
|
87
|
+
* instance window (or its 30 s default) applies.
|
|
88
|
+
*/
|
|
89
|
+
rotationGraceMs?: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The successor pair persisted on a rotated refresh row (see
|
|
93
|
+
* {@link CredentialState.successor}) so a within-grace re-presentation of the
|
|
94
|
+
* consumed token re-delivers the SAME tokens the original rotation produced.
|
|
95
|
+
* Raw token material in a store column matches the store contract's existing
|
|
96
|
+
* posture: stateful stores already key rows on the raw token, and
|
|
97
|
+
* {@link CredentialState.parentCredentialId} already carries one.
|
|
98
|
+
*/
|
|
99
|
+
interface CredentialSuccessorRef {
|
|
100
|
+
accessToken: string;
|
|
101
|
+
accessExpiresAt: number;
|
|
102
|
+
refreshToken: string;
|
|
103
|
+
refreshExpiresAt: number;
|
|
81
104
|
}
|
|
82
105
|
/**
|
|
83
106
|
* Persisted state of a credential — the fixed **envelope**. A consumer's
|
|
@@ -99,8 +122,17 @@ interface CredentialState {
|
|
|
99
122
|
kind?: "access" | "refresh";
|
|
100
123
|
/** For rotated refresh tokens — id of the parent credential this one replaced */
|
|
101
124
|
parentCredentialId?: string;
|
|
102
|
-
/** Timestamp of rotation; used by sliding rotation grace
|
|
125
|
+
/** Timestamp of rotation; used by the sliding/always rotation grace window */
|
|
103
126
|
rotatedAt?: number;
|
|
127
|
+
/**
|
|
128
|
+
* Set alongside {@link rotatedAt} on the OLD refresh row when it is rotated:
|
|
129
|
+
* the successor pair the rotation produced, re-delivered VERBATIM on a
|
|
130
|
+
* within-grace re-presentation of this token (idempotent re-delivery — a
|
|
131
|
+
* grace hit mints nothing, so it grants an attacker no capability beyond
|
|
132
|
+
* capturing the original rotation response). Dies with the row; only the
|
|
133
|
+
* family's latest rotation carries a live grace record.
|
|
134
|
+
*/
|
|
135
|
+
successor?: CredentialSuccessorRef;
|
|
104
136
|
/**
|
|
105
137
|
* Stable id of the session (token family) this credential belongs to. Minted
|
|
106
138
|
* once by `issue()` and copied forward onto every rotation (access + refresh),
|
|
@@ -200,11 +232,17 @@ interface RefreshConfig {
|
|
|
200
232
|
* activity.
|
|
201
233
|
*
|
|
202
234
|
* Both `'sliding'` and `'always'` are **grace-tolerant** on stateful stores:
|
|
203
|
-
* a benign
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
235
|
+
* a benign re-presentation of the just-rotated token (multi-tab race, a
|
|
236
|
+
* rotation response lost to a rolling deploy) within {@link rotationGraceMs}
|
|
237
|
+
* **re-delivers the SAME successor pair the rotation produced** — idempotent,
|
|
238
|
+
* minting nothing — instead of being mistaken for token theft. A grace hit
|
|
239
|
+
* has no liveness side effects: it never slides the refresh expiry and never
|
|
240
|
+
* stamps `lastSeenAt`, so a captured stale token cannot be used as a
|
|
241
|
+
* keep-alive. Because the grace window is tracked in the store (via
|
|
242
|
+
* {@link CredentialState.rotatedAt} + {@link CredentialState.successor}), it
|
|
243
|
+
* is correct across multiple app instances — and the moment the successor is
|
|
244
|
+
* itself rotated (or revoked), the old token is dead regardless of the
|
|
245
|
+
* window (superseded ⇒ the strict theft path).
|
|
208
246
|
*
|
|
209
247
|
* Note: the grace window needs a stateful store (one with `listForUser`).
|
|
210
248
|
* Stateless stores (JWT, Encapsulated) cannot mutate an issued token in place;
|
|
@@ -213,7 +251,14 @@ interface RefreshConfig {
|
|
|
213
251
|
* with a process-local reuse signal (no cross-instance grace).
|
|
214
252
|
*/
|
|
215
253
|
rotation?: "none" | "always" | "sliding";
|
|
216
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* Grace window for sliding/always rotation, in milliseconds. Defaults to
|
|
256
|
+
* 30_000. Size it to survive a rolling-deploy drain + client retry (30–60 s);
|
|
257
|
+
* it stays meaningless for offline token theft. An explicit `0` is strict
|
|
258
|
+
* mode: every re-presentation of a rotated token is treated as theft.
|
|
259
|
+
* Overridden per family by a per-mint `graceMs`
|
|
260
|
+
* ({@link CredentialMetadata.rotationGraceMs}).
|
|
261
|
+
*/
|
|
217
262
|
rotationGraceMs?: number;
|
|
218
263
|
/**
|
|
219
264
|
* Revocation scope when refresh-token reuse is detected (replay after grace,
|
|
@@ -228,6 +273,16 @@ interface RefreshConfig {
|
|
|
228
273
|
reuseResponse?: "session" | "user";
|
|
229
274
|
/** Theft detection hook — invoked when a previously-rotated refresh is reused. */
|
|
230
275
|
onRotationReuse?: (state: CredentialState) => void;
|
|
276
|
+
/**
|
|
277
|
+
* Audit hook for within-grace re-presentations — invoked with the OLD
|
|
278
|
+
* (rotated) refresh credential's envelope just before its successor pair is
|
|
279
|
+
* re-delivered. The benign twin of {@link onRotationReuse}: a trickle of
|
|
280
|
+
* grace hits correlated with deploys is expected (a rotation response died
|
|
281
|
+
* with a drained task and the client retried); a spike is someone replaying
|
|
282
|
+
* tokens. Wire it to the same sink as `onRotationReuse` (e.g. a
|
|
283
|
+
* `refresh-grace-hit` audit event) to tell the two apart.
|
|
284
|
+
*/
|
|
285
|
+
onRotationGraceHit?: (state: CredentialState) => void;
|
|
231
286
|
}
|
|
232
287
|
//#endregion
|
|
233
288
|
//#region src/stores/store.d.ts
|
|
@@ -309,4 +364,4 @@ interface Clock {
|
|
|
309
364
|
}
|
|
310
365
|
declare const defaultClock: Clock;
|
|
311
366
|
//#endregion
|
|
312
|
-
export { AuthContext as a,
|
|
367
|
+
export { AuthContext as a, CredentialSuccessorRef as c, RefreshConfig as d, RefreshResult as f, DenylistStore as i, EnrichedSession as l, SessionInfo as m, defaultClock as n, CredentialMetadata as o, SessionEnricher as p, CredentialStore as r, CredentialState as s, Clock as t, IssueResult as u };
|
|
@@ -78,6 +78,29 @@ interface CredentialMetadata {
|
|
|
78
78
|
* sessions are unaffected).
|
|
79
79
|
*/
|
|
80
80
|
refreshRotation?: "none" | "always" | "sliding";
|
|
81
|
+
/**
|
|
82
|
+
* Per-family rotation grace window in ms — stamped by `issue()` when a
|
|
83
|
+
* refresh token is minted with a per-mint `graceMs` (e.g. an authz grant's
|
|
84
|
+
* `TokenPolicy.refresh.graceMs`), and honored by `refresh()` OVER the
|
|
85
|
+
* instance {@link RefreshConfig.rotationGraceMs}. Same mint-time-authority
|
|
86
|
+
* mechanism as {@link accessTtl} / {@link refreshRotation}. Absent ⇒ the
|
|
87
|
+
* instance window (or its 30 s default) applies.
|
|
88
|
+
*/
|
|
89
|
+
rotationGraceMs?: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The successor pair persisted on a rotated refresh row (see
|
|
93
|
+
* {@link CredentialState.successor}) so a within-grace re-presentation of the
|
|
94
|
+
* consumed token re-delivers the SAME tokens the original rotation produced.
|
|
95
|
+
* Raw token material in a store column matches the store contract's existing
|
|
96
|
+
* posture: stateful stores already key rows on the raw token, and
|
|
97
|
+
* {@link CredentialState.parentCredentialId} already carries one.
|
|
98
|
+
*/
|
|
99
|
+
interface CredentialSuccessorRef {
|
|
100
|
+
accessToken: string;
|
|
101
|
+
accessExpiresAt: number;
|
|
102
|
+
refreshToken: string;
|
|
103
|
+
refreshExpiresAt: number;
|
|
81
104
|
}
|
|
82
105
|
/**
|
|
83
106
|
* Persisted state of a credential — the fixed **envelope**. A consumer's
|
|
@@ -99,8 +122,17 @@ interface CredentialState {
|
|
|
99
122
|
kind?: "access" | "refresh";
|
|
100
123
|
/** For rotated refresh tokens — id of the parent credential this one replaced */
|
|
101
124
|
parentCredentialId?: string;
|
|
102
|
-
/** Timestamp of rotation; used by sliding rotation grace
|
|
125
|
+
/** Timestamp of rotation; used by the sliding/always rotation grace window */
|
|
103
126
|
rotatedAt?: number;
|
|
127
|
+
/**
|
|
128
|
+
* Set alongside {@link rotatedAt} on the OLD refresh row when it is rotated:
|
|
129
|
+
* the successor pair the rotation produced, re-delivered VERBATIM on a
|
|
130
|
+
* within-grace re-presentation of this token (idempotent re-delivery — a
|
|
131
|
+
* grace hit mints nothing, so it grants an attacker no capability beyond
|
|
132
|
+
* capturing the original rotation response). Dies with the row; only the
|
|
133
|
+
* family's latest rotation carries a live grace record.
|
|
134
|
+
*/
|
|
135
|
+
successor?: CredentialSuccessorRef;
|
|
104
136
|
/**
|
|
105
137
|
* Stable id of the session (token family) this credential belongs to. Minted
|
|
106
138
|
* once by `issue()` and copied forward onto every rotation (access + refresh),
|
|
@@ -200,11 +232,17 @@ interface RefreshConfig {
|
|
|
200
232
|
* activity.
|
|
201
233
|
*
|
|
202
234
|
* Both `'sliding'` and `'always'` are **grace-tolerant** on stateful stores:
|
|
203
|
-
* a benign
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
235
|
+
* a benign re-presentation of the just-rotated token (multi-tab race, a
|
|
236
|
+
* rotation response lost to a rolling deploy) within {@link rotationGraceMs}
|
|
237
|
+
* **re-delivers the SAME successor pair the rotation produced** — idempotent,
|
|
238
|
+
* minting nothing — instead of being mistaken for token theft. A grace hit
|
|
239
|
+
* has no liveness side effects: it never slides the refresh expiry and never
|
|
240
|
+
* stamps `lastSeenAt`, so a captured stale token cannot be used as a
|
|
241
|
+
* keep-alive. Because the grace window is tracked in the store (via
|
|
242
|
+
* {@link CredentialState.rotatedAt} + {@link CredentialState.successor}), it
|
|
243
|
+
* is correct across multiple app instances — and the moment the successor is
|
|
244
|
+
* itself rotated (or revoked), the old token is dead regardless of the
|
|
245
|
+
* window (superseded ⇒ the strict theft path).
|
|
208
246
|
*
|
|
209
247
|
* Note: the grace window needs a stateful store (one with `listForUser`).
|
|
210
248
|
* Stateless stores (JWT, Encapsulated) cannot mutate an issued token in place;
|
|
@@ -213,7 +251,14 @@ interface RefreshConfig {
|
|
|
213
251
|
* with a process-local reuse signal (no cross-instance grace).
|
|
214
252
|
*/
|
|
215
253
|
rotation?: "none" | "always" | "sliding";
|
|
216
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* Grace window for sliding/always rotation, in milliseconds. Defaults to
|
|
256
|
+
* 30_000. Size it to survive a rolling-deploy drain + client retry (30–60 s);
|
|
257
|
+
* it stays meaningless for offline token theft. An explicit `0` is strict
|
|
258
|
+
* mode: every re-presentation of a rotated token is treated as theft.
|
|
259
|
+
* Overridden per family by a per-mint `graceMs`
|
|
260
|
+
* ({@link CredentialMetadata.rotationGraceMs}).
|
|
261
|
+
*/
|
|
217
262
|
rotationGraceMs?: number;
|
|
218
263
|
/**
|
|
219
264
|
* Revocation scope when refresh-token reuse is detected (replay after grace,
|
|
@@ -228,6 +273,16 @@ interface RefreshConfig {
|
|
|
228
273
|
reuseResponse?: "session" | "user";
|
|
229
274
|
/** Theft detection hook — invoked when a previously-rotated refresh is reused. */
|
|
230
275
|
onRotationReuse?: (state: CredentialState) => void;
|
|
276
|
+
/**
|
|
277
|
+
* Audit hook for within-grace re-presentations — invoked with the OLD
|
|
278
|
+
* (rotated) refresh credential's envelope just before its successor pair is
|
|
279
|
+
* re-delivered. The benign twin of {@link onRotationReuse}: a trickle of
|
|
280
|
+
* grace hits correlated with deploys is expected (a rotation response died
|
|
281
|
+
* with a drained task and the client retried); a spike is someone replaying
|
|
282
|
+
* tokens. Wire it to the same sink as `onRotationReuse` (e.g. a
|
|
283
|
+
* `refresh-grace-hit` audit event) to tell the two apart.
|
|
284
|
+
*/
|
|
285
|
+
onRotationGraceHit?: (state: CredentialState) => void;
|
|
231
286
|
}
|
|
232
287
|
//#endregion
|
|
233
288
|
//#region src/stores/store.d.ts
|
|
@@ -309,4 +364,4 @@ interface Clock {
|
|
|
309
364
|
}
|
|
310
365
|
declare const defaultClock: Clock;
|
|
311
366
|
//#endregion
|
|
312
|
-
export { AuthContext as a,
|
|
367
|
+
export { AuthContext as a, CredentialSuccessorRef as c, RefreshConfig as d, RefreshResult as f, DenylistStore as i, EnrichedSession as l, SessionInfo as m, defaultClock as n, CredentialMetadata as o, SessionEnricher as p, CredentialStore as r, CredentialState as s, Clock as t, IssueResult as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as Clock } from "./clock-
|
|
2
|
-
import { r as IssueOptions } from "./auth-credential-
|
|
1
|
+
import { t as Clock } from "./clock-C0O0FCCN.cjs";
|
|
2
|
+
import { r as IssueOptions } from "./auth-credential-C5zD8b0J.cjs";
|
|
3
3
|
|
|
4
4
|
//#region src/authz/token-policy.d.ts
|
|
5
5
|
/**
|
|
@@ -37,9 +37,19 @@ interface TokenPolicy {
|
|
|
37
37
|
* dynamic clients) — the family is stamped with `metadata.authzClientId` and
|
|
38
38
|
* a refresh token redeems only for that client. A clientless (Tier-1
|
|
39
39
|
* loopback) grant has nothing to bind to, so the flag is IGNORED there.
|
|
40
|
+
*
|
|
41
|
+
* `graceMs` (ms, ≥ 0) fixes the family's rotation grace window — the window
|
|
42
|
+
* within which re-presenting a just-rotated refresh token idempotently
|
|
43
|
+
* re-delivers the same successor pair instead of tripping theft detection
|
|
44
|
+
* (a connector refreshing against a redeploying server survives the lost
|
|
45
|
+
* response instead of bricking until re-consent). Stamped at mint
|
|
46
|
+
* (`metadata.rotationGraceMs`) and honored over the credential instance's
|
|
47
|
+
* `rotationGraceMs`; `0` is strict single-use. Absent ⇒ the instance window
|
|
48
|
+
* (or its 30 s default).
|
|
40
49
|
*/
|
|
41
50
|
refresh?: {
|
|
42
51
|
ttl?: number;
|
|
52
|
+
graceMs?: number;
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as Clock } from "./clock-
|
|
2
|
-
import { r as IssueOptions } from "./auth-credential-
|
|
1
|
+
import { t as Clock } from "./clock-C0O0FCCN.mjs";
|
|
2
|
+
import { r as IssueOptions } from "./auth-credential-C9xy4X_k.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/authz/token-policy.d.ts
|
|
5
5
|
/**
|
|
@@ -37,9 +37,19 @@ interface TokenPolicy {
|
|
|
37
37
|
* dynamic clients) — the family is stamped with `metadata.authzClientId` and
|
|
38
38
|
* a refresh token redeems only for that client. A clientless (Tier-1
|
|
39
39
|
* loopback) grant has nothing to bind to, so the flag is IGNORED there.
|
|
40
|
+
*
|
|
41
|
+
* `graceMs` (ms, ≥ 0) fixes the family's rotation grace window — the window
|
|
42
|
+
* within which re-presenting a just-rotated refresh token idempotently
|
|
43
|
+
* re-delivers the same successor pair instead of tripping theft detection
|
|
44
|
+
* (a connector refreshing against a redeploying server survives the lost
|
|
45
|
+
* response instead of bricking until re-consent). Stamped at mint
|
|
46
|
+
* (`metadata.rotationGraceMs`) and honored over the credential instance's
|
|
47
|
+
* `rotationGraceMs`; `0` is strict single-use. Absent ⇒ the instance window
|
|
48
|
+
* (or its 30 s default).
|
|
40
49
|
*/
|
|
41
50
|
refresh?: {
|
|
42
51
|
ttl?: number;
|
|
52
|
+
graceMs?: number;
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
/**
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_clock = require("./clock-Bl-H3eqE.cjs");
|
|
3
|
-
const require_payload = require("./payload-
|
|
3
|
+
const require_payload = require("./payload-HPgyPiek.cjs");
|
|
4
4
|
const require_opaque_token = require("./opaque-token-CaZgD2i6.cjs");
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
6
6
|
let jose = require("jose");
|
|
@@ -583,6 +583,8 @@ var AuthCredential = class {
|
|
|
583
583
|
this.clock = opts.clock ?? require_clock.defaultClock;
|
|
584
584
|
if (this.accessTtl <= 0) throw new AuthError("INVALID_CONFIG", `accessTtl must be > 0 (got ${this.accessTtl})`);
|
|
585
585
|
if (this.refreshConfig && this.refreshConfig.ttl <= 0) throw new AuthError("INVALID_CONFIG", `refresh.ttl must be > 0 (got ${this.refreshConfig.ttl})`);
|
|
586
|
+
const graceMs = this.refreshConfig?.rotationGraceMs;
|
|
587
|
+
if (graceMs !== void 0 && graceMs < 0) throw new AuthError("INVALID_CONFIG", `refresh.rotationGraceMs must be >= 0 (got ${graceMs})`);
|
|
586
588
|
}
|
|
587
589
|
async issue(userId, options) {
|
|
588
590
|
if (this.maxConcurrent !== void 0 && this.store.listForUser) await this.enforceConcurrencyLimit(userId);
|
|
@@ -594,6 +596,7 @@ var AuthCredential = class {
|
|
|
594
596
|
refreshTtl = refresh.ttl ?? this.refreshConfig?.ttl;
|
|
595
597
|
if (refreshTtl === void 0) throw new AuthError("INVALID_CONFIG", "issue: per-mint refresh needs a ttl when no instance refresh config exists");
|
|
596
598
|
if (refreshTtl <= 0) throw new AuthError("INVALID_CONFIG", `issue: refresh.ttl must be > 0 (got ${refreshTtl})`);
|
|
599
|
+
if (refresh.graceMs !== void 0 && refresh.graceMs < 0) throw new AuthError("INVALID_CONFIG", `issue: refresh.graceMs must be >= 0 (got ${refresh.graceMs})`);
|
|
597
600
|
}
|
|
598
601
|
const perMintRefresh = refresh !== void 0 && refresh !== false;
|
|
599
602
|
const stampAccessTtl = refreshTtl !== void 0 && ttl !== void 0;
|
|
@@ -601,7 +604,8 @@ var AuthCredential = class {
|
|
|
601
604
|
...metadata,
|
|
602
605
|
...kind !== void 0 && { credentialKind: kind },
|
|
603
606
|
...stampAccessTtl && { accessTtl: ttl },
|
|
604
|
-
...perMintRefresh && { refreshRotation: "always" }
|
|
607
|
+
...perMintRefresh && { refreshRotation: "always" },
|
|
608
|
+
...perMintRefresh && refresh.graceMs !== void 0 && { rotationGraceMs: refresh.graceMs }
|
|
605
609
|
} : metadata;
|
|
606
610
|
if (ttl !== void 0 && expiresAt !== void 0) throw new AuthError("INVALID_CONFIG", "issue: pass either `ttl` or `expiresAt`, not both");
|
|
607
611
|
if (ttl !== void 0 && ttl <= 0) throw new AuthError("INVALID_CONFIG", `issue: ttl must be > 0 (got ${ttl})`);
|
|
@@ -715,23 +719,48 @@ var AuthCredential = class {
|
|
|
715
719
|
}
|
|
716
720
|
/**
|
|
717
721
|
* Shared rotation-with-grace mechanism for `sliding` and `always` on stateful
|
|
718
|
-
* stores.
|
|
719
|
-
*
|
|
720
|
-
* re-
|
|
721
|
-
*
|
|
722
|
-
*
|
|
722
|
+
* stores. The first rotation keeps the old refresh valid and stamps
|
|
723
|
+
* `rotatedAt` + the {@link CredentialState.successor} pair on it; within the
|
|
724
|
+
* grace window a re-presentation **re-delivers that same pair verbatim** —
|
|
725
|
+
* idempotent, minting nothing, with no liveness side effects (no expiry
|
|
726
|
+
* slide, no `lastSeenAt`) — so a captured stale token teaches an attacker
|
|
727
|
+
* nothing beyond capturing the original rotation response, and a multi-tab
|
|
728
|
+
* race converges on one successor. Beyond grace — or once the successor has
|
|
729
|
+
* itself been rotated or revoked (superseded) — the re-presentation is
|
|
730
|
+
* treated as theft. `preserveExpiry` selects fixed-ceiling (`always`) vs
|
|
731
|
+
* sliding (`sliding`) expiry for the new refresh token; the family's
|
|
732
|
+
* mint-time `metadata.rotationGraceMs` (when stamped) wins over the instance
|
|
733
|
+
* window, and a window of `0` is strict single-use.
|
|
723
734
|
*/
|
|
724
735
|
async rotateWithGrace(oldState, refreshToken, now, preserveExpiry) {
|
|
725
|
-
const graceMs = this.refreshConfig?.rotationGraceMs ?? DEFAULT_ROTATION_GRACE_MS;
|
|
736
|
+
const graceMs = oldState.metadata?.rotationGraceMs ?? this.refreshConfig?.rotationGraceMs ?? DEFAULT_ROTATION_GRACE_MS;
|
|
726
737
|
if (typeof oldState.rotatedAt !== "number") return await this.issueRotatedPair(oldState, refreshToken, true, now, preserveExpiry);
|
|
727
|
-
|
|
738
|
+
const reuse = {
|
|
728
739
|
userId: oldState.userId,
|
|
729
740
|
sessionId: oldState.sessionId,
|
|
730
741
|
issuedAt: oldState.issuedAt,
|
|
731
742
|
expiresAt: oldState.expiresAt,
|
|
732
743
|
rotatedAt: oldState.rotatedAt
|
|
744
|
+
};
|
|
745
|
+
if (graceMs <= 0 || now - oldState.rotatedAt > graceMs) return await this.respondToRefreshReuse(reuse);
|
|
746
|
+
const successor = oldState.successor;
|
|
747
|
+
const successorState = successor === void 0 ? null : await this.store.retrieve(successor.refreshToken);
|
|
748
|
+
if (successor === void 0 || successorState === null || typeof successorState.rotatedAt === "number") return await this.respondToRefreshReuse(reuse);
|
|
749
|
+
this.refreshConfig?.onRotationGraceHit?.({
|
|
750
|
+
userId: oldState.userId,
|
|
751
|
+
issuedAt: oldState.issuedAt,
|
|
752
|
+
expiresAt: oldState.expiresAt,
|
|
753
|
+
kind: "refresh",
|
|
754
|
+
rotatedAt: oldState.rotatedAt,
|
|
755
|
+
...oldState.sessionId !== void 0 && { sessionId: oldState.sessionId }
|
|
733
756
|
});
|
|
734
|
-
return
|
|
757
|
+
return {
|
|
758
|
+
accessToken: successor.accessToken,
|
|
759
|
+
accessExpiresAt: successor.accessExpiresAt,
|
|
760
|
+
refreshToken: successor.refreshToken,
|
|
761
|
+
refreshExpiresAt: successor.refreshExpiresAt,
|
|
762
|
+
userId: oldState.userId
|
|
763
|
+
};
|
|
735
764
|
}
|
|
736
765
|
async revoke(token) {
|
|
737
766
|
await this.store.revoke(token);
|
|
@@ -909,7 +938,13 @@ var AuthCredential = class {
|
|
|
909
938
|
if (rotateOld) {
|
|
910
939
|
const rotatedState = {
|
|
911
940
|
...oldRefreshState,
|
|
912
|
-
rotatedAt: now
|
|
941
|
+
rotatedAt: now,
|
|
942
|
+
successor: {
|
|
943
|
+
accessToken: access.token,
|
|
944
|
+
accessExpiresAt: access.expiresAt,
|
|
945
|
+
refreshToken: newRefreshToken,
|
|
946
|
+
refreshExpiresAt
|
|
947
|
+
}
|
|
913
948
|
};
|
|
914
949
|
await this.store.update(oldRefreshToken, rotatedState);
|
|
915
950
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as AuthContext,
|
|
2
|
-
import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-
|
|
3
|
-
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-
|
|
1
|
+
import { a as AuthContext, d as RefreshConfig, f as RefreshResult, i as DenylistStore, l as EnrichedSession, m as SessionInfo, n as defaultClock, o as CredentialMetadata, p as SessionEnricher, r as CredentialStore, s as CredentialState, t as Clock, u as IssueResult } from "./clock-C0O0FCCN.cjs";
|
|
2
|
+
import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-C5zD8b0J.cjs";
|
|
3
|
+
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-C6DU6VZk.cjs";
|
|
4
4
|
import { CryptoKey } from "jose";
|
|
5
5
|
|
|
6
6
|
//#region src/errors.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as AuthContext,
|
|
2
|
-
import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-
|
|
3
|
-
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-
|
|
1
|
+
import { a as AuthContext, d as RefreshConfig, f as RefreshResult, i as DenylistStore, l as EnrichedSession, m as SessionInfo, n as defaultClock, o as CredentialMetadata, p as SessionEnricher, r as CredentialStore, s as CredentialState, t as Clock, u as IssueResult } from "./clock-C0O0FCCN.mjs";
|
|
2
|
+
import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-C9xy4X_k.mjs";
|
|
3
|
+
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-y7K7z1EF.mjs";
|
|
4
4
|
import { CryptoKey } from "jose";
|
|
5
5
|
|
|
6
6
|
//#region src/errors.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as defaultClock } from "./clock-Bdsep_1j.mjs";
|
|
2
|
-
import { t as credentialPayloadOf } from "./payload-
|
|
2
|
+
import { t as credentialPayloadOf } from "./payload-DP5zRiXT.mjs";
|
|
3
3
|
import { t as generateOpaqueToken } from "./opaque-token-BaMOrC1A.mjs";
|
|
4
4
|
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID, scryptSync } from "node:crypto";
|
|
5
5
|
import { SignJWT, jwtVerify } from "jose";
|
|
@@ -582,6 +582,8 @@ var AuthCredential = class {
|
|
|
582
582
|
this.clock = opts.clock ?? defaultClock;
|
|
583
583
|
if (this.accessTtl <= 0) throw new AuthError("INVALID_CONFIG", `accessTtl must be > 0 (got ${this.accessTtl})`);
|
|
584
584
|
if (this.refreshConfig && this.refreshConfig.ttl <= 0) throw new AuthError("INVALID_CONFIG", `refresh.ttl must be > 0 (got ${this.refreshConfig.ttl})`);
|
|
585
|
+
const graceMs = this.refreshConfig?.rotationGraceMs;
|
|
586
|
+
if (graceMs !== void 0 && graceMs < 0) throw new AuthError("INVALID_CONFIG", `refresh.rotationGraceMs must be >= 0 (got ${graceMs})`);
|
|
585
587
|
}
|
|
586
588
|
async issue(userId, options) {
|
|
587
589
|
if (this.maxConcurrent !== void 0 && this.store.listForUser) await this.enforceConcurrencyLimit(userId);
|
|
@@ -593,6 +595,7 @@ var AuthCredential = class {
|
|
|
593
595
|
refreshTtl = refresh.ttl ?? this.refreshConfig?.ttl;
|
|
594
596
|
if (refreshTtl === void 0) throw new AuthError("INVALID_CONFIG", "issue: per-mint refresh needs a ttl when no instance refresh config exists");
|
|
595
597
|
if (refreshTtl <= 0) throw new AuthError("INVALID_CONFIG", `issue: refresh.ttl must be > 0 (got ${refreshTtl})`);
|
|
598
|
+
if (refresh.graceMs !== void 0 && refresh.graceMs < 0) throw new AuthError("INVALID_CONFIG", `issue: refresh.graceMs must be >= 0 (got ${refresh.graceMs})`);
|
|
596
599
|
}
|
|
597
600
|
const perMintRefresh = refresh !== void 0 && refresh !== false;
|
|
598
601
|
const stampAccessTtl = refreshTtl !== void 0 && ttl !== void 0;
|
|
@@ -600,7 +603,8 @@ var AuthCredential = class {
|
|
|
600
603
|
...metadata,
|
|
601
604
|
...kind !== void 0 && { credentialKind: kind },
|
|
602
605
|
...stampAccessTtl && { accessTtl: ttl },
|
|
603
|
-
...perMintRefresh && { refreshRotation: "always" }
|
|
606
|
+
...perMintRefresh && { refreshRotation: "always" },
|
|
607
|
+
...perMintRefresh && refresh.graceMs !== void 0 && { rotationGraceMs: refresh.graceMs }
|
|
604
608
|
} : metadata;
|
|
605
609
|
if (ttl !== void 0 && expiresAt !== void 0) throw new AuthError("INVALID_CONFIG", "issue: pass either `ttl` or `expiresAt`, not both");
|
|
606
610
|
if (ttl !== void 0 && ttl <= 0) throw new AuthError("INVALID_CONFIG", `issue: ttl must be > 0 (got ${ttl})`);
|
|
@@ -714,23 +718,48 @@ var AuthCredential = class {
|
|
|
714
718
|
}
|
|
715
719
|
/**
|
|
716
720
|
* Shared rotation-with-grace mechanism for `sliding` and `always` on stateful
|
|
717
|
-
* stores.
|
|
718
|
-
*
|
|
719
|
-
* re-
|
|
720
|
-
*
|
|
721
|
-
*
|
|
721
|
+
* stores. The first rotation keeps the old refresh valid and stamps
|
|
722
|
+
* `rotatedAt` + the {@link CredentialState.successor} pair on it; within the
|
|
723
|
+
* grace window a re-presentation **re-delivers that same pair verbatim** —
|
|
724
|
+
* idempotent, minting nothing, with no liveness side effects (no expiry
|
|
725
|
+
* slide, no `lastSeenAt`) — so a captured stale token teaches an attacker
|
|
726
|
+
* nothing beyond capturing the original rotation response, and a multi-tab
|
|
727
|
+
* race converges on one successor. Beyond grace — or once the successor has
|
|
728
|
+
* itself been rotated or revoked (superseded) — the re-presentation is
|
|
729
|
+
* treated as theft. `preserveExpiry` selects fixed-ceiling (`always`) vs
|
|
730
|
+
* sliding (`sliding`) expiry for the new refresh token; the family's
|
|
731
|
+
* mint-time `metadata.rotationGraceMs` (when stamped) wins over the instance
|
|
732
|
+
* window, and a window of `0` is strict single-use.
|
|
722
733
|
*/
|
|
723
734
|
async rotateWithGrace(oldState, refreshToken, now, preserveExpiry) {
|
|
724
|
-
const graceMs = this.refreshConfig?.rotationGraceMs ?? DEFAULT_ROTATION_GRACE_MS;
|
|
735
|
+
const graceMs = oldState.metadata?.rotationGraceMs ?? this.refreshConfig?.rotationGraceMs ?? DEFAULT_ROTATION_GRACE_MS;
|
|
725
736
|
if (typeof oldState.rotatedAt !== "number") return await this.issueRotatedPair(oldState, refreshToken, true, now, preserveExpiry);
|
|
726
|
-
|
|
737
|
+
const reuse = {
|
|
727
738
|
userId: oldState.userId,
|
|
728
739
|
sessionId: oldState.sessionId,
|
|
729
740
|
issuedAt: oldState.issuedAt,
|
|
730
741
|
expiresAt: oldState.expiresAt,
|
|
731
742
|
rotatedAt: oldState.rotatedAt
|
|
743
|
+
};
|
|
744
|
+
if (graceMs <= 0 || now - oldState.rotatedAt > graceMs) return await this.respondToRefreshReuse(reuse);
|
|
745
|
+
const successor = oldState.successor;
|
|
746
|
+
const successorState = successor === void 0 ? null : await this.store.retrieve(successor.refreshToken);
|
|
747
|
+
if (successor === void 0 || successorState === null || typeof successorState.rotatedAt === "number") return await this.respondToRefreshReuse(reuse);
|
|
748
|
+
this.refreshConfig?.onRotationGraceHit?.({
|
|
749
|
+
userId: oldState.userId,
|
|
750
|
+
issuedAt: oldState.issuedAt,
|
|
751
|
+
expiresAt: oldState.expiresAt,
|
|
752
|
+
kind: "refresh",
|
|
753
|
+
rotatedAt: oldState.rotatedAt,
|
|
754
|
+
...oldState.sessionId !== void 0 && { sessionId: oldState.sessionId }
|
|
732
755
|
});
|
|
733
|
-
return
|
|
756
|
+
return {
|
|
757
|
+
accessToken: successor.accessToken,
|
|
758
|
+
accessExpiresAt: successor.accessExpiresAt,
|
|
759
|
+
refreshToken: successor.refreshToken,
|
|
760
|
+
refreshExpiresAt: successor.refreshExpiresAt,
|
|
761
|
+
userId: oldState.userId
|
|
762
|
+
};
|
|
734
763
|
}
|
|
735
764
|
async revoke(token) {
|
|
736
765
|
await this.store.revoke(token);
|
|
@@ -908,7 +937,13 @@ var AuthCredential = class {
|
|
|
908
937
|
if (rotateOld) {
|
|
909
938
|
const rotatedState = {
|
|
910
939
|
...oldRefreshState,
|
|
911
|
-
rotatedAt: now
|
|
940
|
+
rotatedAt: now,
|
|
941
|
+
successor: {
|
|
942
|
+
accessToken: access.token,
|
|
943
|
+
accessExpiresAt: access.expiresAt,
|
|
944
|
+
refreshToken: newRefreshToken,
|
|
945
|
+
refreshExpiresAt
|
|
946
|
+
}
|
|
912
947
|
};
|
|
913
948
|
await this.store.update(oldRefreshToken, rotatedState);
|
|
914
949
|
}
|
package/dist/redis.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as DenylistStore, r as CredentialStore, s as CredentialState } from "./clock-
|
|
2
|
-
import { t as RateLimitStore } from "./store-
|
|
1
|
+
import { i as DenylistStore, r as CredentialStore, s as CredentialState } from "./clock-C0O0FCCN.cjs";
|
|
2
|
+
import { t as RateLimitStore } from "./store-C6DU6VZk.cjs";
|
|
3
3
|
|
|
4
4
|
//#region src/redis/index.d.ts
|
|
5
5
|
/**
|
package/dist/redis.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as DenylistStore, r as CredentialStore, s as CredentialState } from "./clock-
|
|
2
|
-
import { t as RateLimitStore } from "./store-
|
|
1
|
+
import { i as DenylistStore, r as CredentialStore, s as CredentialState } from "./clock-C0O0FCCN.mjs";
|
|
2
|
+
import { t as RateLimitStore } from "./store-y7K7z1EF.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/redis/index.d.ts
|
|
5
5
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
4
4
|
"description": "Auth method layer for aoothjs (sessions, tokens, password reset, MFA primitives)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aoothjs",
|
|
@@ -104,18 +104,18 @@
|
|
|
104
104
|
},
|
|
105
105
|
"dependencies": {
|
|
106
106
|
"jose": "^6.2.3",
|
|
107
|
-
"@aooth/user": "0.1.
|
|
107
|
+
"@aooth/user": "0.1.51"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
|
-
"@atscript/core": "^0.1.
|
|
111
|
-
"@atscript/db": "^0.1.
|
|
112
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
113
|
-
"@atscript/db-sqlite": "^0.1.
|
|
114
|
-
"@atscript/typescript": "^0.1.
|
|
110
|
+
"@atscript/core": "^0.1.83",
|
|
111
|
+
"@atscript/db": "^0.1.116",
|
|
112
|
+
"@atscript/db-sql-tools": "^0.1.116",
|
|
113
|
+
"@atscript/db-sqlite": "^0.1.116",
|
|
114
|
+
"@atscript/typescript": "^0.1.83",
|
|
115
115
|
"@types/better-sqlite3": "^7.6.13",
|
|
116
116
|
"better-sqlite3": "^12.6.2",
|
|
117
117
|
"ioredis": "^5.11.1",
|
|
118
|
-
"unplugin-atscript": "^0.1.
|
|
118
|
+
"unplugin-atscript": "^0.1.83"
|
|
119
119
|
},
|
|
120
120
|
"peerDependencies": {
|
|
121
121
|
"@atscript/db": ">=0.1.79"
|
|
@@ -45,6 +45,12 @@ export type AoothCredentialMetadataBase = {
|
|
|
45
45
|
* slides. Stored as a plain string (same portability rule as `kind`).
|
|
46
46
|
*/
|
|
47
47
|
refreshRotation?: string
|
|
48
|
+
/**
|
|
49
|
+
* Per-family rotation grace window (ms) — stamped when a refresh token
|
|
50
|
+
* is minted with a per-mint `graceMs` (e.g. `TokenPolicy.refresh.graceMs`),
|
|
51
|
+
* honored over the instance `rotationGraceMs`. 0 = strict single-use.
|
|
52
|
+
*/
|
|
53
|
+
rotationGraceMs?: number
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
@db.table 'aooth_credentials'
|
|
@@ -84,6 +90,19 @@ export interface AoothAuthCredential {
|
|
|
84
90
|
parentCredentialId?: string
|
|
85
91
|
rotatedAt?: number.timestamp
|
|
86
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Set alongside `rotatedAt` on a rotated refresh row: the successor pair
|
|
95
|
+
* the rotation produced, re-delivered verbatim on a within-grace
|
|
96
|
+
* re-presentation of this token (idempotent grace hits mint nothing).
|
|
97
|
+
*/
|
|
98
|
+
@db.json
|
|
99
|
+
successor?: {
|
|
100
|
+
accessToken: string
|
|
101
|
+
accessExpiresAt: number.timestamp
|
|
102
|
+
refreshToken: string
|
|
103
|
+
refreshExpiresAt: number.timestamp
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
/**
|
|
88
107
|
* Stable session-family id. Minted once at login, copied forward on every
|
|
89
108
|
* rotation. Indexed so a store could group/revoke a family natively (the
|
|
@@ -22,6 +22,7 @@ export type AoothCredentialMetadataBase = {
|
|
|
22
22
|
authzClientId?: string
|
|
23
23
|
accessTtl?: number
|
|
24
24
|
refreshRotation?: string
|
|
25
|
+
rotationGraceMs?: number
|
|
25
26
|
}
|
|
26
27
|
declare namespace AoothCredentialMetadataBase {
|
|
27
28
|
const __is_atscript_annotated_type: true
|
|
@@ -36,7 +37,7 @@ declare namespace AoothCredentialMetadataBase {
|
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Atscript interface **AoothAuthCredential**
|
|
39
|
-
* @see {@link ./auth-credential.as:
|
|
40
|
+
* @see {@link ./auth-credential.as:58:18}
|
|
40
41
|
*/
|
|
41
42
|
export declare class AoothAuthCredential {
|
|
42
43
|
token: string
|
|
@@ -46,6 +47,12 @@ export declare class AoothAuthCredential {
|
|
|
46
47
|
kind?: string
|
|
47
48
|
parentCredentialId?: string
|
|
48
49
|
rotatedAt?: number /* timestamp */
|
|
50
|
+
successor?: {
|
|
51
|
+
accessToken: string
|
|
52
|
+
accessExpiresAt: number /* timestamp */
|
|
53
|
+
refreshToken: string
|
|
54
|
+
refreshExpiresAt: number /* timestamp */
|
|
55
|
+
}
|
|
49
56
|
sessionId?: string
|
|
50
57
|
lastSeenAt?: number /* timestamp */
|
|
51
58
|
static __is_atscript_annotated_type: true
|
|
@@ -64,6 +71,7 @@ export declare class AoothAuthCredential {
|
|
|
64
71
|
"kind"?: string
|
|
65
72
|
"parentCredentialId"?: string
|
|
66
73
|
"rotatedAt"?: number /* timestamp */
|
|
74
|
+
"successor"?: string
|
|
67
75
|
"sessionId"?: string
|
|
68
76
|
"lastSeenAt"?: number /* timestamp */
|
|
69
77
|
}
|
|
@@ -75,6 +83,7 @@ export declare class AoothAuthCredential {
|
|
|
75
83
|
"kind"?: string
|
|
76
84
|
"parentCredentialId"?: string
|
|
77
85
|
"rotatedAt"?: number /* timestamp */
|
|
86
|
+
"successor"?: string
|
|
78
87
|
"sessionId"?: string
|
|
79
88
|
"lastSeenAt"?: number /* timestamp */
|
|
80
89
|
}
|