@c9up/warden 0.1.17 → 0.1.19

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.
Files changed (124) hide show
  1. package/dist/AuthManager.d.ts +86 -1
  2. package/dist/AuthManager.d.ts.map +1 -1
  3. package/dist/AuthManager.js +113 -10
  4. package/dist/AuthManager.js.map +1 -1
  5. package/dist/Authenticator.d.ts +13 -1
  6. package/dist/Authenticator.d.ts.map +1 -1
  7. package/dist/Authenticator.js +37 -3
  8. package/dist/Authenticator.js.map +1 -1
  9. package/dist/RememberMeToken.d.ts +169 -0
  10. package/dist/RememberMeToken.d.ts.map +1 -0
  11. package/dist/RememberMeToken.js +269 -0
  12. package/dist/RememberMeToken.js.map +1 -0
  13. package/dist/ResilientBlacklistDriver.d.ts +15 -9
  14. package/dist/ResilientBlacklistDriver.d.ts.map +1 -1
  15. package/dist/ResilientBlacklistDriver.js +53 -15
  16. package/dist/ResilientBlacklistDriver.js.map +1 -1
  17. package/dist/Secret.d.ts +11 -0
  18. package/dist/Secret.d.ts.map +1 -0
  19. package/dist/Secret.js +36 -0
  20. package/dist/Secret.js.map +1 -0
  21. package/dist/WardenProvider.d.ts +1 -0
  22. package/dist/WardenProvider.d.ts.map +1 -1
  23. package/dist/WardenProvider.js +64 -0
  24. package/dist/WardenProvider.js.map +1 -1
  25. package/dist/bouncer/Bouncer.d.ts +46 -1
  26. package/dist/bouncer/Bouncer.d.ts.map +1 -1
  27. package/dist/bouncer/Bouncer.js +77 -4
  28. package/dist/bouncer/Bouncer.js.map +1 -1
  29. package/dist/bouncer/PolicyAuthorizer.d.ts +9 -1
  30. package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -1
  31. package/dist/bouncer/PolicyAuthorizer.js +17 -1
  32. package/dist/bouncer/PolicyAuthorizer.js.map +1 -1
  33. package/dist/bouncer/evaluate.d.ts +14 -2
  34. package/dist/bouncer/evaluate.d.ts.map +1 -1
  35. package/dist/bouncer/evaluate.js +16 -5
  36. package/dist/bouncer/evaluate.js.map +1 -1
  37. package/dist/bouncer/types.d.ts +4 -2
  38. package/dist/bouncer/types.d.ts.map +1 -1
  39. package/dist/config.d.ts +3 -0
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/config.js +5 -0
  42. package/dist/config.js.map +1 -1
  43. package/dist/firstcontact/drivers/GitHubDriver.d.ts.map +1 -1
  44. package/dist/firstcontact/drivers/GitHubDriver.js +2 -3
  45. package/dist/firstcontact/drivers/GitHubDriver.js.map +1 -1
  46. package/dist/firstcontact/drivers/GoogleDriver.d.ts.map +1 -1
  47. package/dist/firstcontact/drivers/GoogleDriver.js +2 -3
  48. package/dist/firstcontact/drivers/GoogleDriver.js.map +1 -1
  49. package/dist/firstcontact/types.d.ts +10 -0
  50. package/dist/firstcontact/types.d.ts.map +1 -1
  51. package/dist/firstcontact/types.js +18 -1
  52. package/dist/firstcontact/types.js.map +1 -1
  53. package/dist/index.d.ts +8 -4
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +6 -3
  56. package/dist/index.js.map +1 -1
  57. package/dist/mfa/MfaManager.d.ts.map +1 -1
  58. package/dist/mfa/MfaManager.js +14 -5
  59. package/dist/mfa/MfaManager.js.map +1 -1
  60. package/dist/mfa/TotpProvider.d.ts +34 -1
  61. package/dist/mfa/TotpProvider.d.ts.map +1 -1
  62. package/dist/mfa/TotpProvider.js +60 -4
  63. package/dist/mfa/TotpProvider.js.map +1 -1
  64. package/dist/mfa/webauthn-codec.d.ts +6 -1
  65. package/dist/mfa/webauthn-codec.d.ts.map +1 -1
  66. package/dist/mfa/webauthn-codec.js +50 -6
  67. package/dist/mfa/webauthn-codec.js.map +1 -1
  68. package/dist/middleware.d.ts.map +1 -1
  69. package/dist/middleware.js +46 -1
  70. package/dist/middleware.js.map +1 -1
  71. package/dist/quasar.d.ts.map +1 -1
  72. package/dist/quasar.js +6 -14
  73. package/dist/quasar.js.map +1 -1
  74. package/dist/sanitize.d.ts +10 -0
  75. package/dist/sanitize.d.ts.map +1 -0
  76. package/dist/sanitize.js +15 -0
  77. package/dist/sanitize.js.map +1 -0
  78. package/dist/strategies/ApiKeyStrategy.d.ts +10 -1
  79. package/dist/strategies/ApiKeyStrategy.d.ts.map +1 -1
  80. package/dist/strategies/ApiKeyStrategy.js +16 -0
  81. package/dist/strategies/ApiKeyStrategy.js.map +1 -1
  82. package/dist/strategies/BasicAuthStrategy.d.ts +59 -0
  83. package/dist/strategies/BasicAuthStrategy.d.ts.map +1 -0
  84. package/dist/strategies/BasicAuthStrategy.js +97 -0
  85. package/dist/strategies/BasicAuthStrategy.js.map +1 -0
  86. package/dist/strategies/JwtStrategy.d.ts +10 -1
  87. package/dist/strategies/JwtStrategy.d.ts.map +1 -1
  88. package/dist/strategies/JwtStrategy.js +11 -0
  89. package/dist/strategies/JwtStrategy.js.map +1 -1
  90. package/dist/strategies/SessionStrategy.d.ts +84 -2
  91. package/dist/strategies/SessionStrategy.d.ts.map +1 -1
  92. package/dist/strategies/SessionStrategy.js +124 -1
  93. package/dist/strategies/SessionStrategy.js.map +1 -1
  94. package/index.darwin-arm64.node +0 -0
  95. package/index.darwin-x64.node +0 -0
  96. package/index.linux-arm64-gnu.node +0 -0
  97. package/index.linux-x64-gnu.node +0 -0
  98. package/index.win32-x64-msvc.node +0 -0
  99. package/package.json +4 -2
  100. package/src/AuthManager.ts +163 -7
  101. package/src/Authenticator.ts +58 -10
  102. package/src/RememberMeToken.ts +357 -0
  103. package/src/ResilientBlacklistDriver.ts +58 -18
  104. package/src/Secret.ts +41 -0
  105. package/src/WardenProvider.ts +90 -0
  106. package/src/bouncer/Bouncer.ts +93 -4
  107. package/src/bouncer/PolicyAuthorizer.ts +20 -1
  108. package/src/bouncer/evaluate.ts +26 -6
  109. package/src/bouncer/types.ts +4 -2
  110. package/src/config.ts +9 -0
  111. package/src/firstcontact/drivers/GitHubDriver.ts +2 -3
  112. package/src/firstcontact/drivers/GoogleDriver.ts +2 -3
  113. package/src/firstcontact/types.ts +24 -0
  114. package/src/index.ts +30 -2
  115. package/src/mfa/MfaManager.ts +15 -8
  116. package/src/mfa/TotpProvider.ts +90 -6
  117. package/src/mfa/webauthn-codec.ts +48 -6
  118. package/src/middleware.ts +55 -2
  119. package/src/quasar.ts +6 -14
  120. package/src/sanitize.ts +17 -0
  121. package/src/strategies/ApiKeyStrategy.ts +23 -1
  122. package/src/strategies/BasicAuthStrategy.ts +127 -0
  123. package/src/strategies/JwtStrategy.ts +18 -1
  124. package/src/strategies/SessionStrategy.ts +167 -2
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Remember-me tokens — "keep me signed in" that survives the session cookie.
3
+ *
4
+ * Modelled on `@adonisjs/auth`'s `RememberMeToken`
5
+ * (modules/session_guard/remember_me_token.ts), because an app moving over
6
+ * carries the same table and the same cookie:
7
+ *
8
+ * value = base64url(identifier) + "." + base64url(secret)
9
+ * stored = sha256(secret), never the secret itself
10
+ *
11
+ * The three properties that make this safe, all taken from upstream:
12
+ *
13
+ * 1. **Only a hash is stored.** A dump of the tokens table does not let anyone
14
+ * log in — the secret exists solely in the user's cookie.
15
+ * 2. **The comparison is constant-time.** Comparing hashes with `===` leaks
16
+ * how many leading bytes matched, which is enough to forge one byte at a
17
+ * time.
18
+ * 3. **A used token is recycled.** Authenticating rotates the secret and
19
+ * invalidates the old one, so a stolen cookie stops working the moment the
20
+ * legitimate user comes back.
21
+ */
22
+ import { Secret } from "./Secret.js";
23
+ /** Bytes of entropy in a token secret. Adonis defaults to 40. */
24
+ export declare const DEFAULT_SECRET_LENGTH = 40;
25
+ /** A token as persisted. The secret is NOT here — only its hash. */
26
+ export interface StoredRememberMeToken {
27
+ /** Unique id of the row; the public half of the token value. */
28
+ identifier: string;
29
+ /** Whose token this is. */
30
+ tokenableId: string | number;
31
+ /** `sha256(secret)`, hex. */
32
+ hash: string;
33
+ createdAt: number;
34
+ updatedAt: number;
35
+ expiresAt: number;
36
+ }
37
+ /** What a store must implement. Structural: warden owns no database. */
38
+ export interface RememberMeTokenDriver {
39
+ create(token: StoredRememberMeToken): Promise<void>;
40
+ find(identifier: string): Promise<StoredRememberMeToken | null>;
41
+ /** Replace the hash + expiry of an existing row (recycling). */
42
+ update(identifier: string, hash: string, expiresAt: number): Promise<void>;
43
+ delete(identifier: string): Promise<void>;
44
+ deleteAllForUser(userId: string | number): Promise<void>;
45
+ }
46
+ /** `sha256(secret)` as hex — what gets stored. */
47
+ export declare function hashSecret(secret: string): string;
48
+ /**
49
+ * Compare two hex hashes without leaking where they diverge. Length is checked
50
+ * first because `timingSafeEqual` throws on a mismatch — and the length of a
51
+ * sha256 hex digest is public anyway.
52
+ */
53
+ export declare function safeCompareHashes(a: string, b: string): boolean;
54
+ /** Split a cookie value into its identifier and secret, or `null` if malformed. */
55
+ export declare function decodeTokenValue(value: unknown): {
56
+ identifier: string;
57
+ secret: string;
58
+ } | null;
59
+ /** Build the cookie value from its two halves. */
60
+ export declare function encodeTokenValue(identifier: string, secret: string): string;
61
+ /** A freshly minted token: what to store, and what to hand the browser. */
62
+ export interface MintedRememberMeToken {
63
+ stored: StoredRememberMeToken;
64
+ /** Goes in the cookie. Never persisted. */
65
+ value: string;
66
+ }
67
+ /**
68
+ * Mint a token for `userId`, valid for `ageSeconds`.
69
+ *
70
+ * `identifier` is random too, not a counter: a guessable identifier would let
71
+ * an attacker enumerate rows and target a specific user's token.
72
+ */
73
+ export declare function mintRememberMeToken(userId: string | number, ageSeconds: number, secretLength?: number): MintedRememberMeToken;
74
+ /**
75
+ * Verify a cookie value against the store, and RECYCLE the token on success:
76
+ * the row keeps its identifier but gets a fresh secret and expiry, and the
77
+ * caller must send the returned value back as the new cookie.
78
+ *
79
+ * Recycling is what limits the damage of a stolen cookie — the thief's copy
80
+ * dies as soon as the real user authenticates again.
81
+ *
82
+ * Returns `null` for anything wrong: malformed value, unknown identifier,
83
+ * expired row, or a secret that does not match. The caller cannot tell these
84
+ * apart, which is deliberate — a distinguishable "unknown identifier" tells an
85
+ * attacker which identifiers exist.
86
+ */
87
+ export declare function verifyAndRecycleRememberMeToken(driver: RememberMeTokenDriver, cookieValue: unknown, ageSeconds: number, secretLength?: number): Promise<{
88
+ userId: string | number;
89
+ value: string;
90
+ } | null>;
91
+ /** In-memory store — for tests and single-process apps. Not for a cluster. */
92
+ export declare class MemoryRememberMeTokenDriver implements RememberMeTokenDriver {
93
+ #private;
94
+ create(token: StoredRememberMeToken): Promise<void>;
95
+ find(identifier: string): Promise<StoredRememberMeToken | null>;
96
+ update(identifier: string, hash: string, expiresAt: number): Promise<void>;
97
+ delete(identifier: string): Promise<void>;
98
+ deleteAllForUser(userId: string | number): Promise<void>;
99
+ }
100
+ /**
101
+ * A remember-me token as an object, matching `@adonisjs/auth`'s
102
+ * `RememberMeToken` (modules/session_guard/remember_me_token.ts).
103
+ *
104
+ * The functions above are what warden's own guard uses and stay the primary
105
+ * API. This class exists because an app moving over calls
106
+ * `RememberMeToken.decode(...)` / `token.verify(secret)` directly — upstream
107
+ * exports it — and finding nothing under that name is a migration stopping at
108
+ * an import line. Both sit on the same encoding and the same hashing, so they
109
+ * cannot disagree about what a token is.
110
+ *
111
+ * Secrets come back wrapped in {@link Secret} so one cannot end up in a log by
112
+ * being interpolated into a string, which is what upstream does too.
113
+ */
114
+ export declare class RememberMeToken {
115
+ /**
116
+ * Split a public token value into its identifier and secret. Returns `null`
117
+ * for anything malformed rather than throwing — a bad cookie is an ordinary
118
+ * event, not an exception.
119
+ */
120
+ static decode(value: string): {
121
+ identifier: string;
122
+ secret: Secret<string>;
123
+ } | null;
124
+ /** A fresh secret and the hash to store for it. */
125
+ static seed(size?: number): {
126
+ secret: Secret<string>;
127
+ hash: string;
128
+ };
129
+ /**
130
+ * Everything the persistence layer needs for a new token, without deciding
131
+ * how it is stored (upstream `createTransientToken`).
132
+ *
133
+ * `expiresIn` is in SECONDS, as everywhere else in this module. AdonisJS
134
+ * also accepts a duration string there; warden takes the number, so the
135
+ * unit is never ambiguous at a call site.
136
+ */
137
+ static createTransientToken(userId: string | number, size: number, expiresIn: number): {
138
+ secret: Secret<string>;
139
+ hash: string;
140
+ userId: string | number;
141
+ expiresAt: Date;
142
+ };
143
+ identifier: string;
144
+ tokenableId: string | number;
145
+ hash: string;
146
+ createdAt: Date;
147
+ updatedAt: Date;
148
+ expiresAt: Date;
149
+ /** The public `identifier.secret` value — only on a token just minted. */
150
+ value?: Secret<string>;
151
+ constructor(attributes: {
152
+ identifier: string;
153
+ tokenableId: string | number;
154
+ hash: string;
155
+ createdAt: Date;
156
+ updatedAt: Date;
157
+ expiresAt: Date;
158
+ secret?: Secret<string>;
159
+ });
160
+ /** Build one from a stored row, whose timestamps are epoch milliseconds. */
161
+ static fromStored(stored: StoredRememberMeToken): RememberMeToken;
162
+ /** The row to persist, in the shape {@link RememberMeTokenDriver} takes. */
163
+ toStored(): StoredRememberMeToken;
164
+ /** Whether the token is past its expiry. */
165
+ isExpired(): boolean;
166
+ /** Constant-time check of a presented secret against the stored hash. */
167
+ verify(secret: Secret<string>): boolean;
168
+ }
169
+ //# sourceMappingURL=RememberMeToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RememberMeToken.d.ts","sourceRoot":"","sources":["../src/RememberMeToken.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Remember-me tokens — "keep me signed in" that survives the session cookie.
3
+ *
4
+ * Modelled on `@adonisjs/auth`'s `RememberMeToken`
5
+ * (modules/session_guard/remember_me_token.ts), because an app moving over
6
+ * carries the same table and the same cookie:
7
+ *
8
+ * value = base64url(identifier) + "." + base64url(secret)
9
+ * stored = sha256(secret), never the secret itself
10
+ *
11
+ * The three properties that make this safe, all taken from upstream:
12
+ *
13
+ * 1. **Only a hash is stored.** A dump of the tokens table does not let anyone
14
+ * log in — the secret exists solely in the user's cookie.
15
+ * 2. **The comparison is constant-time.** Comparing hashes with `===` leaks
16
+ * how many leading bytes matched, which is enough to forge one byte at a
17
+ * time.
18
+ * 3. **A used token is recycled.** Authenticating rotates the secret and
19
+ * invalidates the old one, so a stolen cookie stops working the moment the
20
+ * legitimate user comes back.
21
+ */
22
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
23
+ import { Secret } from "./Secret.js";
24
+ /** Bytes of entropy in a token secret. Adonis defaults to 40. */
25
+ export const DEFAULT_SECRET_LENGTH = 40;
26
+ /** Base64url without padding — the encoding Adonis uses for the token halves. */
27
+ function base64UrlEncode(value) {
28
+ return Buffer.from(value, "utf8").toString("base64url");
29
+ }
30
+ function base64UrlDecode(value) {
31
+ try {
32
+ const decoded = Buffer.from(value, "base64url").toString("utf8");
33
+ // A non-base64url character decodes to something that does not round-trip;
34
+ // reject rather than authenticate against a mangled identifier.
35
+ return Buffer.from(decoded, "utf8").toString("base64url") === value
36
+ ? decoded
37
+ : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ /** `sha256(secret)` as hex — what gets stored. */
44
+ export function hashSecret(secret) {
45
+ return createHash("sha256").update(secret).digest("hex");
46
+ }
47
+ /**
48
+ * Compare two hex hashes without leaking where they diverge. Length is checked
49
+ * first because `timingSafeEqual` throws on a mismatch — and the length of a
50
+ * sha256 hex digest is public anyway.
51
+ */
52
+ export function safeCompareHashes(a, b) {
53
+ const left = Buffer.from(a, "utf8");
54
+ const right = Buffer.from(b, "utf8");
55
+ if (left.length !== right.length)
56
+ return false;
57
+ return timingSafeEqual(left, right);
58
+ }
59
+ /** Split a cookie value into its identifier and secret, or `null` if malformed. */
60
+ export function decodeTokenValue(value) {
61
+ if (typeof value !== "string" || value.length === 0)
62
+ return null;
63
+ const [encodedIdentifier, ...rest] = value.split(".");
64
+ if (!encodedIdentifier || rest.length === 0)
65
+ return null;
66
+ const identifier = base64UrlDecode(encodedIdentifier);
67
+ const secret = base64UrlDecode(rest.join("."));
68
+ if (identifier === null || secret === null)
69
+ return null;
70
+ if (identifier === "" || secret === "")
71
+ return null;
72
+ return { identifier, secret };
73
+ }
74
+ /** Build the cookie value from its two halves. */
75
+ export function encodeTokenValue(identifier, secret) {
76
+ return `${base64UrlEncode(identifier)}.${base64UrlEncode(secret)}`;
77
+ }
78
+ /**
79
+ * Mint a token for `userId`, valid for `ageSeconds`.
80
+ *
81
+ * `identifier` is random too, not a counter: a guessable identifier would let
82
+ * an attacker enumerate rows and target a specific user's token.
83
+ */
84
+ export function mintRememberMeToken(userId, ageSeconds, secretLength = DEFAULT_SECRET_LENGTH) {
85
+ const identifier = randomBytes(16).toString("hex");
86
+ const secret = randomBytes(secretLength).toString("base64url");
87
+ const now = Date.now();
88
+ return {
89
+ stored: {
90
+ identifier,
91
+ tokenableId: userId,
92
+ hash: hashSecret(secret),
93
+ createdAt: now,
94
+ updatedAt: now,
95
+ expiresAt: now + ageSeconds * 1000,
96
+ },
97
+ value: encodeTokenValue(identifier, secret),
98
+ };
99
+ }
100
+ /**
101
+ * Verify a cookie value against the store, and RECYCLE the token on success:
102
+ * the row keeps its identifier but gets a fresh secret and expiry, and the
103
+ * caller must send the returned value back as the new cookie.
104
+ *
105
+ * Recycling is what limits the damage of a stolen cookie — the thief's copy
106
+ * dies as soon as the real user authenticates again.
107
+ *
108
+ * Returns `null` for anything wrong: malformed value, unknown identifier,
109
+ * expired row, or a secret that does not match. The caller cannot tell these
110
+ * apart, which is deliberate — a distinguishable "unknown identifier" tells an
111
+ * attacker which identifiers exist.
112
+ */
113
+ export async function verifyAndRecycleRememberMeToken(driver, cookieValue, ageSeconds, secretLength = DEFAULT_SECRET_LENGTH) {
114
+ const decoded = decodeTokenValue(cookieValue);
115
+ if (!decoded)
116
+ return null;
117
+ const stored = await driver.find(decoded.identifier);
118
+ if (!stored)
119
+ return null;
120
+ if (stored.expiresAt <= Date.now()) {
121
+ // Expired rows are removed on sight rather than left to a cleanup job.
122
+ await driver.delete(stored.identifier);
123
+ return null;
124
+ }
125
+ if (!safeCompareHashes(stored.hash, hashSecret(decoded.secret))) {
126
+ return null;
127
+ }
128
+ const secret = randomBytes(secretLength).toString("base64url");
129
+ const expiresAt = Date.now() + ageSeconds * 1000;
130
+ await driver.update(stored.identifier, hashSecret(secret), expiresAt);
131
+ return {
132
+ userId: stored.tokenableId,
133
+ value: encodeTokenValue(stored.identifier, secret),
134
+ };
135
+ }
136
+ /** In-memory store — for tests and single-process apps. Not for a cluster. */
137
+ export class MemoryRememberMeTokenDriver {
138
+ #rows = new Map();
139
+ async create(token) {
140
+ this.#rows.set(token.identifier, { ...token });
141
+ }
142
+ async find(identifier) {
143
+ const row = this.#rows.get(identifier);
144
+ return row ? { ...row } : null;
145
+ }
146
+ async update(identifier, hash, expiresAt) {
147
+ const row = this.#rows.get(identifier);
148
+ if (!row)
149
+ return;
150
+ this.#rows.set(identifier, {
151
+ ...row,
152
+ hash,
153
+ expiresAt,
154
+ updatedAt: Date.now(),
155
+ });
156
+ }
157
+ async delete(identifier) {
158
+ this.#rows.delete(identifier);
159
+ }
160
+ async deleteAllForUser(userId) {
161
+ for (const [identifier, row] of this.#rows) {
162
+ if (String(row.tokenableId) === String(userId)) {
163
+ this.#rows.delete(identifier);
164
+ }
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * A remember-me token as an object, matching `@adonisjs/auth`'s
170
+ * `RememberMeToken` (modules/session_guard/remember_me_token.ts).
171
+ *
172
+ * The functions above are what warden's own guard uses and stay the primary
173
+ * API. This class exists because an app moving over calls
174
+ * `RememberMeToken.decode(...)` / `token.verify(secret)` directly — upstream
175
+ * exports it — and finding nothing under that name is a migration stopping at
176
+ * an import line. Both sit on the same encoding and the same hashing, so they
177
+ * cannot disagree about what a token is.
178
+ *
179
+ * Secrets come back wrapped in {@link Secret} so one cannot end up in a log by
180
+ * being interpolated into a string, which is what upstream does too.
181
+ */
182
+ export class RememberMeToken {
183
+ /**
184
+ * Split a public token value into its identifier and secret. Returns `null`
185
+ * for anything malformed rather than throwing — a bad cookie is an ordinary
186
+ * event, not an exception.
187
+ */
188
+ static decode(value) {
189
+ const decoded = decodeTokenValue(value);
190
+ if (!decoded)
191
+ return null;
192
+ return {
193
+ identifier: decoded.identifier,
194
+ secret: new Secret(decoded.secret),
195
+ };
196
+ }
197
+ /** A fresh secret and the hash to store for it. */
198
+ static seed(size = DEFAULT_SECRET_LENGTH) {
199
+ const secret = randomBytes(size).toString("base64url");
200
+ return { secret: new Secret(secret), hash: hashSecret(secret) };
201
+ }
202
+ /**
203
+ * Everything the persistence layer needs for a new token, without deciding
204
+ * how it is stored (upstream `createTransientToken`).
205
+ *
206
+ * `expiresIn` is in SECONDS, as everywhere else in this module. AdonisJS
207
+ * also accepts a duration string there; warden takes the number, so the
208
+ * unit is never ambiguous at a call site.
209
+ */
210
+ static createTransientToken(userId, size, expiresIn) {
211
+ const { secret, hash } = RememberMeToken.seed(size);
212
+ return {
213
+ secret,
214
+ hash,
215
+ userId,
216
+ expiresAt: new Date(Date.now() + expiresIn * 1000),
217
+ };
218
+ }
219
+ identifier;
220
+ tokenableId;
221
+ hash;
222
+ createdAt;
223
+ updatedAt;
224
+ expiresAt;
225
+ /** The public `identifier.secret` value — only on a token just minted. */
226
+ value;
227
+ constructor(attributes) {
228
+ this.identifier = attributes.identifier;
229
+ this.tokenableId = attributes.tokenableId;
230
+ this.hash = attributes.hash;
231
+ this.createdAt = attributes.createdAt;
232
+ this.updatedAt = attributes.updatedAt;
233
+ this.expiresAt = attributes.expiresAt;
234
+ if (attributes.secret) {
235
+ this.value = new Secret(encodeTokenValue(attributes.identifier, attributes.secret.release()));
236
+ }
237
+ }
238
+ /** Build one from a stored row, whose timestamps are epoch milliseconds. */
239
+ static fromStored(stored) {
240
+ return new RememberMeToken({
241
+ identifier: stored.identifier,
242
+ tokenableId: stored.tokenableId,
243
+ hash: stored.hash,
244
+ createdAt: new Date(stored.createdAt),
245
+ updatedAt: new Date(stored.updatedAt),
246
+ expiresAt: new Date(stored.expiresAt),
247
+ });
248
+ }
249
+ /** The row to persist, in the shape {@link RememberMeTokenDriver} takes. */
250
+ toStored() {
251
+ return {
252
+ identifier: this.identifier,
253
+ tokenableId: this.tokenableId,
254
+ hash: this.hash,
255
+ createdAt: this.createdAt.getTime(),
256
+ updatedAt: this.updatedAt.getTime(),
257
+ expiresAt: this.expiresAt.getTime(),
258
+ };
259
+ }
260
+ /** Whether the token is past its expiry. */
261
+ isExpired() {
262
+ return this.expiresAt.getTime() <= Date.now();
263
+ }
264
+ /** Constant-time check of a presented secret against the stored hash. */
265
+ verify(secret) {
266
+ return safeCompareHashes(this.hash, hashSecret(secret.release()));
267
+ }
268
+ }
269
+ //# sourceMappingURL=RememberMeToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RememberMeToken.js","sourceRoot":"","sources":["../src/RememberMeToken.ts"],"names":[],"mappings":"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"}
@@ -4,17 +4,23 @@
4
4
  * takes the whole app down.
5
5
  *
6
6
  * Policy (fail-open on reads):
7
- * - `add()` — if the primary throws, the revocation is recorded in the
8
- * fallback so logout stays effective on this instance.
9
- * - `has()` — if the primary throws, consult the fallback: a token revoked
10
- * during the outage is still caught here; anything else is
11
- * allowed through (fail-open). Access tokens are short-lived, so
12
- * the window a missed revocation survives is bounded by the token
13
- * TTL — the blacklist is defense-in-depth above expiry, not the
7
+ * - `add()` — records the revocation in BOTH, always. The fallback is this
8
+ * instance's own record of what it has revoked, not a log of its
9
+ * outages: writing only on failure meant a token revoked while
10
+ * the primary was HEALTHY was absent from the fallback, so the
11
+ * first check during a later outage let that logged-out token
12
+ * through. A primary failure still does not fail the logout.
13
+ * - `has()` asks the fallback FIRST: what this instance revoked is known
14
+ * locally and needs no round trip. Otherwise it asks the
15
+ * primary, and if that throws, allows the token through
16
+ * (fail-open). Access tokens are short-lived, so the window a
17
+ * revocation missed by BOTH survives is bounded by the token TTL
18
+ * — the blacklist is defense-in-depth above expiry, not the
14
19
  * primary boundary.
15
20
  *
16
- * Every degradation invokes `onDegrade` (defaults to a stderr warning) so the
17
- * outage is observable.
21
+ * Every degradation invokes `onDegrade`. The default reports the first failure
22
+ * per operation and then a periodic count: an outage that logs on every request
23
+ * buries itself, and the reader stops seeing what the flood is about.
18
24
  */
19
25
  import { type BlacklistDriver } from "./TokenBlacklist.js";
20
26
  export interface BlacklistDegradeEvent {
@@ -1 +1 @@
1
- {"version":3,"file":"ResilientBlacklistDriver.d.ts","sourceRoot":"","sources":["../src/ResilientBlacklistDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACN,KAAK,eAAe,EAEpB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAAC;IAC9B,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACxC,0EAA0E;IAC1E,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;CACnD;AASD,qBAAa,wBAAyB,YAAW,eAAe;;gBAM9D,OAAO,EAAE,eAAe,EACxB,QAAQ,GAAE,eAA6C,EACvD,MAAM,GAAE,wBAA6B;IAOhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASlD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUlC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ9B"}
1
+ {"version":3,"file":"ResilientBlacklistDriver.d.ts","sourceRoot":"","sources":["../src/ResilientBlacklistDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EACN,KAAK,eAAe,EAEpB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAAC;IAC9B,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACxC,0EAA0E;IAC1E,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;CACnD;AAkCD,qBAAa,wBAAyB,YAAW,eAAe;;gBAM9D,OAAO,EAAE,eAAe,EACxB,QAAQ,GAAE,eAA6C,EACvD,MAAM,GAAE,wBAA6B;IAOhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAelD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAalC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ9B"}
@@ -4,21 +4,49 @@
4
4
  * takes the whole app down.
5
5
  *
6
6
  * Policy (fail-open on reads):
7
- * - `add()` — if the primary throws, the revocation is recorded in the
8
- * fallback so logout stays effective on this instance.
9
- * - `has()` — if the primary throws, consult the fallback: a token revoked
10
- * during the outage is still caught here; anything else is
11
- * allowed through (fail-open). Access tokens are short-lived, so
12
- * the window a missed revocation survives is bounded by the token
13
- * TTL — the blacklist is defense-in-depth above expiry, not the
7
+ * - `add()` — records the revocation in BOTH, always. The fallback is this
8
+ * instance's own record of what it has revoked, not a log of its
9
+ * outages: writing only on failure meant a token revoked while
10
+ * the primary was HEALTHY was absent from the fallback, so the
11
+ * first check during a later outage let that logged-out token
12
+ * through. A primary failure still does not fail the logout.
13
+ * - `has()` asks the fallback FIRST: what this instance revoked is known
14
+ * locally and needs no round trip. Otherwise it asks the
15
+ * primary, and if that throws, allows the token through
16
+ * (fail-open). Access tokens are short-lived, so the window a
17
+ * revocation missed by BOTH survives is bounded by the token TTL
18
+ * — the blacklist is defense-in-depth above expiry, not the
14
19
  * primary boundary.
15
20
  *
16
- * Every degradation invokes `onDegrade` (defaults to a stderr warning) so the
17
- * outage is observable.
21
+ * Every degradation invokes `onDegrade`. The default reports the first failure
22
+ * per operation and then a periodic count: an outage that logs on every request
23
+ * buries itself, and the reader stops seeing what the flood is about.
18
24
  */
19
25
  import { MemoryBlacklistDriver, } from "./TokenBlacklist.js";
20
- function defaultOnDegrade({ op, error }) {
21
- console.warn(`[warden] blacklist primary failed on ${op}; using in-memory fallback:`, error);
26
+ /** How long a repeat of the same failing operation stays folded into a count. */
27
+ const DEGRADE_LOG_WINDOW_MS = 60_000;
28
+ /**
29
+ * Report the first failure of each operation, then one line per window with
30
+ * how many were suppressed.
31
+ *
32
+ * Keyed by operation — a fixed, three-value set — never by the error text,
33
+ * which an attacker could influence and which would make the key set unbounded.
34
+ */
35
+ function makeDefaultOnDegrade() {
36
+ const windows = new Map();
37
+ return ({ op, error }) => {
38
+ const now = Date.now();
39
+ const open = windows.get(op);
40
+ if (open === undefined || open.until <= now) {
41
+ if (open !== undefined && open.suppressed > 0) {
42
+ console.warn(`[warden] blacklist primary failed on ${op} ${open.suppressed} more time(s) in the last minute.`);
43
+ }
44
+ windows.set(op, { until: now + DEGRADE_LOG_WINDOW_MS, suppressed: 0 });
45
+ console.warn(`[warden] blacklist primary failed on ${op}; using in-memory fallback:`, error);
46
+ return;
47
+ }
48
+ open.suppressed++;
49
+ };
22
50
  }
23
51
  export class ResilientBlacklistDriver {
24
52
  #primary;
@@ -27,25 +55,35 @@ export class ResilientBlacklistDriver {
27
55
  constructor(primary, fallback = new MemoryBlacklistDriver(), config = {}) {
28
56
  this.#primary = primary;
29
57
  this.#fallback = fallback;
30
- this.#onDegrade = config.onDegrade ?? defaultOnDegrade;
58
+ this.#onDegrade = config.onDegrade ?? makeDefaultOnDegrade();
31
59
  }
32
60
  async add(jti, expiresAt) {
61
+ // The fallback first, and unconditionally: it is what this instance
62
+ // knows it revoked. Recording only on failure left every revocation made
63
+ // while the primary was healthy invisible to the fallback — and those are
64
+ // most of them.
65
+ await this.#fallback.add(jti, expiresAt);
33
66
  try {
34
67
  await this.#primary.add(jti, expiresAt);
35
68
  }
36
69
  catch (error) {
70
+ // The revocation is already durable enough for this instance, so a
71
+ // primary failure must not fail the logout.
37
72
  this.#onDegrade({ op: "add", error });
38
- await this.#fallback.add(jti, expiresAt);
39
73
  }
40
74
  }
41
75
  async has(jti) {
76
+ // What this instance revoked is known locally — no round trip needed, and
77
+ // it stays correct through an outage that starts a moment later.
78
+ if (await this.#fallback.has(jti))
79
+ return true;
42
80
  try {
43
81
  return await this.#primary.has(jti);
44
82
  }
45
83
  catch (error) {
46
84
  this.#onDegrade({ op: "has", error });
47
- // Fail-open: honour in-outage revocations on this instance, allow rest.
48
- return this.#fallback.has(jti);
85
+ // Fail-open on what neither knows: access tokens are short-lived.
86
+ return false;
49
87
  }
50
88
  }
51
89
  async cleanup() {
@@ -1 +1 @@
1
- {"version":3,"file":"ResilientBlacklistDriver.js","sourceRoot":"","sources":["../src/ResilientBlacklistDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAEN,qBAAqB,GACrB,MAAM,qBAAqB,CAAC;AAY7B,SAAS,gBAAgB,CAAC,EAAE,EAAE,EAAE,KAAK,EAAyB;IAC7D,OAAO,CAAC,IAAI,CACX,wCAAwC,EAAE,6BAA6B,EACvE,KAAK,CACL,CAAC;AACH,CAAC;AAED,MAAM,OAAO,wBAAwB;IAC3B,QAAQ,CAAkB;IAC1B,SAAS,CAAkB;IAC3B,UAAU,CAAyC;IAE5D,YACC,OAAwB,EACxB,WAA4B,IAAI,qBAAqB,EAAE,EACvD,SAAmC,EAAE;QAErC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;QAC1B,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,SAAS,IAAI,gBAAgB,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,SAAiB;QACvC,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACtC,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC1C,CAAC;IACF,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACpB,IAAI,CAAC;YACJ,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACtC,wEAAwE;YACxE,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;IACF,CAAC;IAED,KAAK,CAAC,OAAO;QACZ,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QACjC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;IAClC,CAAC;CACD"}
1
+ {"version":3,"file":"ResilientBlacklistDriver.js","sourceRoot":"","sources":["../src/ResilientBlacklistDriver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export declare class Secret<T> {
2
+ #private;
3
+ constructor(value: T, redactedKeyword?: string);
4
+ /** Redacted placeholder, so `JSON.stringify` never emits the secret. */
5
+ toJSON(): string;
6
+ /** Redacted placeholder for string coercion and template literals. */
7
+ toString(): string;
8
+ /** Release the underlying value — the one deliberate way to read it. */
9
+ release(): T;
10
+ }
11
+ //# sourceMappingURL=Secret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Secret.d.ts","sourceRoot":"","sources":["../src/Secret.ts"],"names":[],"mappings":"AAYA,qBAAa,MAAM,CAAC,CAAC;;gBAIR,KAAK,EAAE,CAAC,EAAE,eAAe,GAAE,MAAiB;IAKxD,wEAAwE;IACxE,MAAM,IAAI,MAAM;IAIhB,sEAAsE;IACtE,QAAQ,IAAI,MAAM;IASlB,wEAAwE;IACxE,OAAO,IAAI,CAAC;CAGZ"}