@holo-js/security 0.3.28 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/.holo-build-fingerprint +1 -0
- package/dist/{chunk-WRMZRO77.mjs → chunk-D3ZN5MLJ.mjs} +112 -6
- package/dist/{chunk-PC2YCWDH.mjs → chunk-EINZXLC2.mjs} +7 -5
- package/dist/chunk-Q3PK4AGL.mjs +84 -0
- package/dist/{chunk-26IYURJG.mjs → chunk-RYQLS2OO.mjs} +428 -211
- package/dist/{chunk-FQ4X7X6L.mjs → chunk-U25HSHLJ.mjs} +4 -16
- package/dist/client.d.ts +1 -1
- package/dist/client.mjs +2 -2
- package/dist/config-BK6q6Yh9.d.ts +145 -0
- package/dist/config.d.ts +2 -136
- package/dist/config.mjs +1 -1
- package/dist/contracts.d.ts +15 -4
- package/dist/contracts.mjs +2 -4
- package/dist/drivers/redis-adapter.d.ts +3 -8
- package/dist/drivers/redis-adapter.mjs +13 -29
- package/dist/index.d.ts +24 -74
- package/dist/index.mjs +5 -6
- package/dist/next/server.d.ts +2 -1
- package/dist/next/server.mjs +28 -10
- package/dist/nuxt/server.mjs +50 -14
- package/dist/sveltekit/server.d.ts +3 -2
- package/dist/sveltekit/server.mjs +11 -8
- package/package.json +19 -13
- package/dist/chunk-PHPA3C6L.mjs +0 -64
|
@@ -1,19 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SECURITY_XSRF_HEADER
|
|
3
|
+
} from "./chunk-EINZXLC2.mjs";
|
|
1
4
|
import {
|
|
2
5
|
SecurityCsrfError,
|
|
3
6
|
SecurityRateLimitError,
|
|
4
|
-
ip,
|
|
5
7
|
limit
|
|
6
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-U25HSHLJ.mjs";
|
|
7
9
|
import {
|
|
10
|
+
createForwardedRequestResolver,
|
|
8
11
|
normalizeCorsConfig,
|
|
9
|
-
normalizeSecurityConfig
|
|
10
|
-
|
|
12
|
+
normalizeSecurityConfig,
|
|
13
|
+
normalizeTrustedProxies,
|
|
14
|
+
resolveForwarding
|
|
15
|
+
} from "./chunk-D3ZN5MLJ.mjs";
|
|
11
16
|
|
|
12
|
-
// src/
|
|
13
|
-
import {
|
|
14
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
17
|
+
// src/proxy.ts
|
|
18
|
+
import { requestUrlInternals as requestUrlInternals2 } from "@holo-js/kernel";
|
|
15
19
|
|
|
16
20
|
// src/runtime.ts
|
|
21
|
+
import { requestBodyInternals, requestUrlInternals } from "@holo-js/kernel";
|
|
17
22
|
function getSecurityRuntimeState() {
|
|
18
23
|
const runtime = globalThis;
|
|
19
24
|
runtime.__holoSecurityRuntime__ ??= {};
|
|
@@ -25,11 +30,16 @@ var SecurityRuntimeNotConfiguredError = class extends Error {
|
|
|
25
30
|
}
|
|
26
31
|
};
|
|
27
32
|
function configureSecurityRuntime(bindings) {
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
const config = bindings ? normalizeSecurityConfig(bindings.config) : void 0;
|
|
34
|
+
requestUrlInternals.configureForwardedRequestResolver(config ? createForwardedRequestResolver(config.trustedProxies) : void 0);
|
|
35
|
+
requestBodyInternals.configureMaxRequestBodyBytes(config?.maxBodyBytes);
|
|
36
|
+
requestBodyInternals.configureMaxUploadBodyBytes(config?.maxUploadBodyBytes);
|
|
37
|
+
getSecurityRuntimeState().bindings = bindings && config ? Object.freeze({
|
|
38
|
+
config,
|
|
30
39
|
cors: normalizeCorsConfig(bindings.cors),
|
|
31
40
|
rateLimitStore: bindings.rateLimitStore,
|
|
32
41
|
csrfSigningKey: bindings.csrfSigningKey,
|
|
42
|
+
csrfSession: bindings.csrfSession,
|
|
33
43
|
defaultKeyResolver: bindings.defaultKeyResolver
|
|
34
44
|
}) : void 0;
|
|
35
45
|
}
|
|
@@ -44,6 +54,9 @@ function getSecurityRuntimeBindings() {
|
|
|
44
54
|
return getSecurityRuntimeState().bindings;
|
|
45
55
|
}
|
|
46
56
|
function resetSecurityRuntime() {
|
|
57
|
+
requestUrlInternals.configureForwardedRequestResolver(void 0);
|
|
58
|
+
requestBodyInternals.configureMaxRequestBodyBytes(void 0);
|
|
59
|
+
requestBodyInternals.configureMaxUploadBodyBytes(void 0);
|
|
47
60
|
getSecurityRuntimeState().bindings = void 0;
|
|
48
61
|
}
|
|
49
62
|
var securityRuntimeInternals = {
|
|
@@ -51,24 +64,27 @@ var securityRuntimeInternals = {
|
|
|
51
64
|
};
|
|
52
65
|
|
|
53
66
|
// src/proxy.ts
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return ip(request, trustedProxyHeaders === "x-forwarded");
|
|
67
|
+
var SecurityClientAddressUnavailableError = class extends Error {
|
|
68
|
+
constructor() {
|
|
69
|
+
super("[@holo-js/security] The client address is unavailable because this framework does not expose the connection address, so an address or CIDR list in trustedProxies can never match. Only '*' works here, and only behind a proxy that overwrites X-Forwarded-For, such as Vercel's edge, or a reverse proxy with the Next.js server bound to localhost or a private network. Set trustedProxies in config/security.ts to '*' (scaffolded apps read it from TRUSTED_PROXIES). If the app doesn't need IP identity, pass an explicit rate-limit key instead. See https://docs.holo-js.com/basics/security/cors-and-proxies#trusted-proxies");
|
|
70
|
+
this.name = "SecurityClientAddressUnavailableError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function ip(request) {
|
|
74
|
+
const trustedProxies = getSecurityRuntimeBindings()?.config.trustedProxies ?? normalizeTrustedProxies(void 0);
|
|
75
|
+
const { address } = resolveForwarding(
|
|
76
|
+
(name) => request.headers.get(name) ?? void 0,
|
|
77
|
+
requestUrlInternals2.currentConnectionAddress(),
|
|
78
|
+
trustedProxies
|
|
79
|
+
);
|
|
80
|
+
if (!address) throw new SecurityClientAddressUnavailableError();
|
|
81
|
+
return address;
|
|
70
82
|
}
|
|
71
83
|
|
|
84
|
+
// src/csrf.ts
|
|
85
|
+
import { cookieEncryptionInternals, HoloRequestBodyTooLargeError, readBoundedRequestBody, resolveRequestUrl } from "@holo-js/kernel";
|
|
86
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
87
|
+
|
|
72
88
|
// src/rate-limit.ts
|
|
73
89
|
function encodeBucketPart(value) {
|
|
74
90
|
return encodeURIComponent(value);
|
|
@@ -107,7 +123,7 @@ async function defaultRateLimitKey(request) {
|
|
|
107
123
|
if (typeof runtimeDefaultKey !== "undefined" && runtimeDefaultKey !== null) {
|
|
108
124
|
return normalizeResolvedLimiterKey(runtimeDefaultKey, "Default rate limiter key resolver");
|
|
109
125
|
}
|
|
110
|
-
return `ip:${
|
|
126
|
+
return `ip:${ip(request)}`;
|
|
111
127
|
}
|
|
112
128
|
async function resolveLimiterKey(name, limiter, options) {
|
|
113
129
|
if (typeof options.key === "string" && options.key.length > 0) {
|
|
@@ -185,32 +201,8 @@ var rateLimitInternals = {
|
|
|
185
201
|
};
|
|
186
202
|
|
|
187
203
|
// src/csrf.ts
|
|
188
|
-
var
|
|
189
|
-
|
|
190
|
-
if (!header) {
|
|
191
|
-
return Object.freeze({});
|
|
192
|
-
}
|
|
193
|
-
const decodeCookiePart = (value) => {
|
|
194
|
-
try {
|
|
195
|
-
return decodeURIComponent(value);
|
|
196
|
-
} catch {
|
|
197
|
-
return void 0;
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
const entries = header.split(";").map((segment) => segment.trim()).filter(Boolean).map((segment) => {
|
|
201
|
-
const separator = segment.indexOf("=");
|
|
202
|
-
if (separator <= 0) {
|
|
203
|
-
return void 0;
|
|
204
|
-
}
|
|
205
|
-
const key = decodeCookiePart(segment.slice(0, separator));
|
|
206
|
-
const value = decodeCookiePart(segment.slice(separator + 1));
|
|
207
|
-
if (!key || typeof value === "undefined") {
|
|
208
|
-
return void 0;
|
|
209
|
-
}
|
|
210
|
-
return [key, value];
|
|
211
|
-
}).filter((entry) => !!entry);
|
|
212
|
-
return Object.freeze(Object.fromEntries(entries));
|
|
213
|
-
}
|
|
204
|
+
var requestTokenCache = /* @__PURE__ */ new WeakMap();
|
|
205
|
+
var persistedTokens = /* @__PURE__ */ new WeakMap();
|
|
214
206
|
function serializeCookie(name, value, options = {}) {
|
|
215
207
|
const attributes = [
|
|
216
208
|
`${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
|
|
@@ -244,59 +236,84 @@ function isSecureRequest(request) {
|
|
|
244
236
|
function createCsrfToken() {
|
|
245
237
|
return randomBytes(32).toString("base64url");
|
|
246
238
|
}
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
if (configured) {
|
|
250
|
-
return configured;
|
|
251
|
-
}
|
|
252
|
-
throw new Error("[@holo-js/security] CSRF signing key is not configured.");
|
|
239
|
+
function csrfCookieName() {
|
|
240
|
+
return getSecurityRuntime().config.csrf.cookie;
|
|
253
241
|
}
|
|
254
|
-
function
|
|
255
|
-
return
|
|
242
|
+
function encryptCsrfToken(token2) {
|
|
243
|
+
return cookieEncryptionInternals.encryptCookieValue(csrfCookieName(), token2, getSecurityRuntime().csrfSigningKey);
|
|
256
244
|
}
|
|
257
|
-
function
|
|
258
|
-
return
|
|
245
|
+
function decryptCsrfToken(value) {
|
|
246
|
+
return cookieEncryptionInternals.decryptCookieValue(csrfCookieName(), value, getSecurityRuntime().csrfSigningKey);
|
|
259
247
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return null;
|
|
248
|
+
function tokensMatch(expected, submitted) {
|
|
249
|
+
if (!submitted) {
|
|
250
|
+
return false;
|
|
264
251
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
});
|
|
252
|
+
const expectedBytes = Buffer.from(expected);
|
|
253
|
+
const submittedBytes = Buffer.from(submitted);
|
|
254
|
+
return expectedBytes.length === submittedBytes.length && timingSafeEqual(expectedBytes, submittedBytes);
|
|
269
255
|
}
|
|
270
|
-
function
|
|
271
|
-
const
|
|
272
|
-
if (!
|
|
273
|
-
|
|
256
|
+
function csrfSession() {
|
|
257
|
+
const session = getSecurityRuntime().csrfSession;
|
|
258
|
+
if (!session) {
|
|
259
|
+
throw new Error("[@holo-js/security] CSRF protection requires a session. Install and configure @holo-js/session.");
|
|
274
260
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
261
|
+
return session;
|
|
262
|
+
}
|
|
263
|
+
async function readSessionToken(request) {
|
|
264
|
+
const stored = await csrfSession().read(request);
|
|
265
|
+
if (stored) {
|
|
266
|
+
persistedTokens.set(request, stored);
|
|
279
267
|
}
|
|
280
|
-
return
|
|
268
|
+
return stored;
|
|
281
269
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
return parseCookieHeader(request.headers.get("cookie"))[cookie2];
|
|
270
|
+
async function getSessionToken(request) {
|
|
271
|
+
return persistedTokens.get(request) ?? requestTokenCache.get(request) ?? await readSessionToken(request);
|
|
285
272
|
}
|
|
286
273
|
function getHeaderToken(request) {
|
|
287
274
|
const { header } = getSecurityRuntime().config.csrf;
|
|
275
|
+
if (header.toLowerCase() === SECURITY_XSRF_HEADER.toLowerCase()) {
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
288
278
|
return request.headers.get(header)?.trim() || void 0;
|
|
289
279
|
}
|
|
290
|
-
|
|
291
|
-
const
|
|
280
|
+
function getXsrfHeaderToken(request) {
|
|
281
|
+
const value = request.headers.get(SECURITY_XSRF_HEADER)?.trim();
|
|
282
|
+
return value ? decryptCsrfToken(value) : void 0;
|
|
283
|
+
}
|
|
284
|
+
function nonEmptyString(value) {
|
|
285
|
+
return typeof value === "string" && value ? value : void 0;
|
|
286
|
+
}
|
|
287
|
+
async function readBodyBytes(request) {
|
|
292
288
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
return await readBoundedRequestBody(request.clone());
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error instanceof HoloRequestBodyTooLargeError) throw error;
|
|
292
|
+
return void 0;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function readBodyToken(request, field2) {
|
|
296
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
297
|
+
const bytes = await readBodyBytes(request);
|
|
298
|
+
if (!bytes) return void 0;
|
|
299
|
+
try {
|
|
300
|
+
if (/[/+]json\b/i.test(contentType)) {
|
|
301
|
+
const body = JSON.parse(new TextDecoder().decode(bytes));
|
|
302
|
+
return body && typeof body === "object" && !Array.isArray(body) && Object.hasOwn(body, field2) ? nonEmptyString(body[field2]) : void 0;
|
|
303
|
+
}
|
|
304
|
+
return nonEmptyString((await new Response(bytes, { headers: { "content-type": contentType } }).formData()).get(field2));
|
|
296
305
|
} catch {
|
|
297
306
|
return void 0;
|
|
298
307
|
}
|
|
299
308
|
}
|
|
309
|
+
async function readInputToken(request) {
|
|
310
|
+
const { field: field2 } = getSecurityRuntime().config.csrf;
|
|
311
|
+
return await readBodyToken(request, field2) ?? nonEmptyString(new URL(request.url).searchParams.get(field2));
|
|
312
|
+
}
|
|
313
|
+
async function getSubmittedToken(request) {
|
|
314
|
+
const token2 = await readInputToken(request) ?? getHeaderToken(request);
|
|
315
|
+
return token2 ? decryptCsrfToken(token2) ?? token2 : getXsrfHeaderToken(request);
|
|
316
|
+
}
|
|
300
317
|
async function verifyRequest(request, options) {
|
|
301
318
|
if (isSafeMethod(request.method)) {
|
|
302
319
|
return;
|
|
@@ -304,21 +321,21 @@ async function verifyRequest(request, options) {
|
|
|
304
321
|
if (options.allowExcludedPath && isExcludedPath(request)) {
|
|
305
322
|
return;
|
|
306
323
|
}
|
|
307
|
-
const
|
|
308
|
-
if (!
|
|
324
|
+
const sessionToken = await getSessionToken(request);
|
|
325
|
+
if (!sessionToken || !tokensMatch(sessionToken, await getSubmittedToken(request))) {
|
|
309
326
|
throw new SecurityCsrfError();
|
|
310
327
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
const requestToken = await readFormToken(request);
|
|
319
|
-
if (requestToken !== cookieToken) {
|
|
320
|
-
throw new SecurityCsrfError();
|
|
328
|
+
}
|
|
329
|
+
async function ensureSessionToken(request) {
|
|
330
|
+
const stored = persistedTokens.get(request) ?? await readSessionToken(request);
|
|
331
|
+
if (stored) {
|
|
332
|
+
return { token: stored, sessionCookies: [] };
|
|
321
333
|
}
|
|
334
|
+
const token2 = requestTokenCache.get(request) ?? createCsrfToken();
|
|
335
|
+
const written = await csrfSession().write(request, token2);
|
|
336
|
+
requestTokenCache.set(request, written.token);
|
|
337
|
+
persistedTokens.set(request, written.token);
|
|
338
|
+
return { token: written.token, sessionCookies: written.cookies };
|
|
322
339
|
}
|
|
323
340
|
function resolveShouldProtect(request, options = {}) {
|
|
324
341
|
if (options.csrf === false) {
|
|
@@ -340,16 +357,12 @@ function resolveShouldProtect(request, options = {}) {
|
|
|
340
357
|
return true;
|
|
341
358
|
}
|
|
342
359
|
async function token(request) {
|
|
343
|
-
const
|
|
344
|
-
if (
|
|
345
|
-
return
|
|
360
|
+
const sessionToken = await getSessionToken(request);
|
|
361
|
+
if (sessionToken) {
|
|
362
|
+
return sessionToken;
|
|
346
363
|
}
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
return cached;
|
|
350
|
-
}
|
|
351
|
-
const created = encodeCsrfToken(createCsrfToken());
|
|
352
|
-
generatedTokenCache.set(request, created);
|
|
364
|
+
const created = createCsrfToken();
|
|
365
|
+
requestTokenCache.set(request, created);
|
|
353
366
|
return created;
|
|
354
367
|
}
|
|
355
368
|
async function field(request) {
|
|
@@ -367,10 +380,11 @@ async function input(request) {
|
|
|
367
380
|
value: csrfField.value
|
|
368
381
|
});
|
|
369
382
|
}
|
|
383
|
+
async function encryptedToken(request, explicitToken) {
|
|
384
|
+
return encryptCsrfToken(explicitToken || await token(request));
|
|
385
|
+
}
|
|
370
386
|
async function cookie(request, explicitToken) {
|
|
371
|
-
|
|
372
|
-
const value = explicitToken ? isValidSignedCsrfToken(explicitToken) ? explicitToken : encodeCsrfToken(explicitToken) : await token(request);
|
|
373
|
-
return serializeCookie(config.cookie, value, {
|
|
387
|
+
return serializeCookie(csrfCookieName(), await encryptedToken(request, explicitToken), {
|
|
374
388
|
secure: isSecureRequest(request)
|
|
375
389
|
});
|
|
376
390
|
}
|
|
@@ -394,22 +408,24 @@ var csrf = Object.freeze({
|
|
|
394
408
|
cookie,
|
|
395
409
|
verify
|
|
396
410
|
});
|
|
411
|
+
function rememberSessionToken(request, token2) {
|
|
412
|
+
persistedTokens.set(request, token2);
|
|
413
|
+
}
|
|
397
414
|
var csrfInternals = {
|
|
398
415
|
createCsrfToken,
|
|
399
|
-
|
|
400
|
-
|
|
416
|
+
rememberSessionToken,
|
|
417
|
+
ensureSessionToken,
|
|
418
|
+
requestTokenCache,
|
|
401
419
|
getHeaderToken,
|
|
420
|
+
getXsrfHeaderToken,
|
|
402
421
|
isSecureRequest,
|
|
403
|
-
|
|
422
|
+
readInputToken,
|
|
404
423
|
isExcludedPath,
|
|
405
424
|
isSafeMethod,
|
|
406
425
|
matchesPathPattern,
|
|
407
|
-
parseCookieHeader,
|
|
408
426
|
serializeCookie,
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
getCsrfSigningKey,
|
|
412
|
-
isValidSignedCsrfToken
|
|
427
|
+
decryptCsrfToken,
|
|
428
|
+
encryptCsrfToken
|
|
413
429
|
};
|
|
414
430
|
|
|
415
431
|
// src/cors.ts
|
|
@@ -465,7 +481,7 @@ function resolveAllowedOrigin(config, origin) {
|
|
|
465
481
|
return origin;
|
|
466
482
|
}
|
|
467
483
|
if (config.origins.includes("*")) {
|
|
468
|
-
return
|
|
484
|
+
return "*";
|
|
469
485
|
}
|
|
470
486
|
return void 0;
|
|
471
487
|
}
|
|
@@ -552,30 +568,37 @@ import { resolve } from "path";
|
|
|
552
568
|
|
|
553
569
|
// src/drivers/file.ts
|
|
554
570
|
import { createHash, randomUUID } from "crypto";
|
|
555
|
-
import { mkdir, readFile, readdir, rename, rm, stat, utimes, writeFile } from "fs/promises";
|
|
571
|
+
import { mkdir, opendir, readFile, readdir, rename, rm, stat, utimes, writeFile } from "fs/promises";
|
|
556
572
|
import { dirname, join } from "path";
|
|
573
|
+
import { reportError } from "@holo-js/kernel/error-reporting";
|
|
557
574
|
var LOCK_OWNER_FILE = "owner";
|
|
558
575
|
var BUCKETS_DIRECTORY = "buckets";
|
|
576
|
+
var PRUNE_INTERVAL_MS = 6e4;
|
|
577
|
+
var SWEEP_BATCH_SIZE = 1e3;
|
|
559
578
|
function createBucketHash(key) {
|
|
560
579
|
return createHash("sha256").update(key).digest("hex");
|
|
561
580
|
}
|
|
562
|
-
function
|
|
563
|
-
const
|
|
564
|
-
return separatorIndex >= 0 ? key.slice(0, separatorIndex + 1) : key;
|
|
565
|
-
}
|
|
566
|
-
function createBucketNamespaceHash(key) {
|
|
567
|
-
return createBucketHash(createBucketNamespace(key));
|
|
568
|
-
}
|
|
569
|
-
function createBucketPrefixHashes(key) {
|
|
581
|
+
function hashBucketKey(key, keyHash) {
|
|
582
|
+
const hash = createHash("sha256");
|
|
570
583
|
const prefixHashes = [];
|
|
571
|
-
|
|
572
|
-
|
|
584
|
+
let namespaceHash;
|
|
585
|
+
let prefixLength = 0;
|
|
586
|
+
for (const character of key) {
|
|
587
|
+
hash.update(character);
|
|
588
|
+
prefixLength += character.length;
|
|
589
|
+
const prefixHash = prefixLength === key.length ? keyHash : hash.copy().digest("hex");
|
|
590
|
+
prefixHashes.push(prefixHash);
|
|
591
|
+
if (namespaceHash === void 0 && character === "|") {
|
|
592
|
+
namespaceHash = prefixHash;
|
|
593
|
+
}
|
|
573
594
|
}
|
|
574
|
-
return
|
|
595
|
+
return Object.freeze({
|
|
596
|
+
namespaceHash: namespaceHash ?? keyHash,
|
|
597
|
+
prefixHashes: Object.freeze(prefixHashes)
|
|
598
|
+
});
|
|
575
599
|
}
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
return join(root, BUCKETS_DIRECTORY, hash.slice(0, 2), hash.slice(2, 4), `${hash}.json`);
|
|
600
|
+
function getBucketPathForKeyHash(root, keyHash) {
|
|
601
|
+
return join(root, BUCKETS_DIRECTORY, keyHash.slice(0, 2), keyHash.slice(2, 4), `${keyHash}.json`);
|
|
579
602
|
}
|
|
580
603
|
function serializeBucket(bucket) {
|
|
581
604
|
return JSON.stringify({
|
|
@@ -727,12 +750,42 @@ async function acquireBucketLock(lockPath, options) {
|
|
|
727
750
|
continue;
|
|
728
751
|
}
|
|
729
752
|
if (Date.now() >= deadline) {
|
|
730
|
-
throw new Error(`[@holo-js/security] Timed out waiting for file rate-limit lock "${lockPath}"
|
|
753
|
+
throw new Error(`[@holo-js/security] Timed out waiting for file rate-limit lock "${lockPath}".`, { cause: error });
|
|
731
754
|
}
|
|
732
755
|
await sleep(options.retryDelayMs);
|
|
733
756
|
}
|
|
734
757
|
}
|
|
735
758
|
}
|
|
759
|
+
async function withBucketLockIfFree(path, options, operation) {
|
|
760
|
+
const lockPath = getBucketLockPath(path);
|
|
761
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
762
|
+
let lock;
|
|
763
|
+
try {
|
|
764
|
+
lock = await tryCreateBucketLock(lockPath);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
if (error.code !== "EEXIST") {
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
if (!await isBucketLockStale(lockPath, options.timeoutMs) || !await reclaimStaleBucketLock(lockPath, options.timeoutMs)) {
|
|
770
|
+
return void 0;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
lock = await tryCreateBucketLock(lockPath);
|
|
774
|
+
} catch (retryError) {
|
|
775
|
+
if (retryError.code === "EEXIST") {
|
|
776
|
+
return void 0;
|
|
777
|
+
}
|
|
778
|
+
throw retryError;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const heartbeat = startLockHeartbeat(lockPath, options.timeoutMs);
|
|
782
|
+
try {
|
|
783
|
+
return await operation();
|
|
784
|
+
} finally {
|
|
785
|
+
clearInterval(heartbeat);
|
|
786
|
+
await removeOwnedBucketLock(lock);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
736
789
|
async function withBucketLock(path, options, operation) {
|
|
737
790
|
const lockPath = getBucketLockPath(path);
|
|
738
791
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
@@ -745,21 +798,92 @@ async function withBucketLock(path, options, operation) {
|
|
|
745
798
|
await removeOwnedBucketLock(acquiredLock);
|
|
746
799
|
}
|
|
747
800
|
}
|
|
748
|
-
|
|
749
|
-
|
|
801
|
+
var UNIT_STEP = { kind: "unit" };
|
|
802
|
+
function* chargeUnits(count) {
|
|
803
|
+
for (let index = 0; index < count; index += 1) {
|
|
804
|
+
yield UNIT_STEP;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async function readShardEntries(directory, onUnreadable) {
|
|
808
|
+
try {
|
|
809
|
+
return await readdir(directory, { withFileTypes: true });
|
|
810
|
+
} catch (error) {
|
|
750
811
|
if (error.code === "ENOENT") {
|
|
751
|
-
return
|
|
812
|
+
return void 0;
|
|
752
813
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
814
|
+
if (!onUnreadable) {
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
onUnreadable(directory, error);
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async function* readShardListing(directory, onUnreadable) {
|
|
822
|
+
yield UNIT_STEP;
|
|
823
|
+
const entries = await readShardEntries(directory, onUnreadable);
|
|
824
|
+
yield* chargeUnits(entries?.length ?? 0);
|
|
825
|
+
yield UNIT_STEP;
|
|
826
|
+
if (!entries) {
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
names: entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort()
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
async function openShard(directory, onUnreadable) {
|
|
834
|
+
try {
|
|
835
|
+
return await opendir(directory);
|
|
836
|
+
} catch (error) {
|
|
837
|
+
if (error.code === "ENOENT") {
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
if (!onUnreadable) {
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
onUnreadable(directory, error);
|
|
844
|
+
return void 0;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function readShardEntry(handle, directory, onUnreadable) {
|
|
848
|
+
try {
|
|
849
|
+
return await handle.read();
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (!onUnreadable) {
|
|
852
|
+
throw error;
|
|
759
853
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
854
|
+
onUnreadable(directory, error);
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async function* walkBucketTree(bucketsRoot, onUnreadable) {
|
|
859
|
+
const tops = yield* readShardListing(bucketsRoot, onUnreadable);
|
|
860
|
+
for (const top of tops?.names ?? []) {
|
|
861
|
+
const topPath = join(bucketsRoot, top);
|
|
862
|
+
const middles = yield* readShardListing(topPath, onUnreadable);
|
|
863
|
+
for (const middle of middles?.names ?? []) {
|
|
864
|
+
const leaf = join(topPath, middle);
|
|
865
|
+
yield UNIT_STEP;
|
|
866
|
+
const handle = await openShard(leaf, onUnreadable);
|
|
867
|
+
if (!handle) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
for (let entry = await readShardEntry(handle, leaf, onUnreadable); entry !== null; entry = await readShardEntry(handle, leaf, onUnreadable)) {
|
|
872
|
+
yield entry.isFile() && entry.name.endsWith(".json") ? { kind: "bucket", path: join(leaf, entry.name) } : UNIT_STEP;
|
|
873
|
+
}
|
|
874
|
+
} finally {
|
|
875
|
+
await handle.close().catch(() => void 0);
|
|
876
|
+
}
|
|
877
|
+
yield UNIT_STEP;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function* bucketFilePaths(bucketsRoot) {
|
|
882
|
+
for await (const step of walkBucketTree(bucketsRoot)) {
|
|
883
|
+
if (step.kind === "bucket") {
|
|
884
|
+
yield step.path;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
763
887
|
}
|
|
764
888
|
function createSnapshot(key, bucket, maxAttempts) {
|
|
765
889
|
const expiresAt = new Date(bucket.expiresAt.getTime());
|
|
@@ -776,16 +900,96 @@ function createFileRateLimitStore(root, options = {}) {
|
|
|
776
900
|
const resolveNow = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
777
901
|
const lockRetryDelayMs = options.lockRetryDelayMs ?? 5;
|
|
778
902
|
const lockTimeoutMs = options.lockTimeoutMs ?? 2e3;
|
|
779
|
-
|
|
903
|
+
const lockOptions = {
|
|
904
|
+
retryDelayMs: lockRetryDelayMs,
|
|
905
|
+
timeoutMs: lockTimeoutMs
|
|
906
|
+
};
|
|
907
|
+
const bucketsRoot = join(root, BUCKETS_DIRECTORY);
|
|
908
|
+
let cycleStartedAt;
|
|
909
|
+
let cursor;
|
|
910
|
+
let inFlight;
|
|
911
|
+
let closed = false;
|
|
912
|
+
const reportSweepError = (error) => {
|
|
913
|
+
reportError(error, { capability: "security", operation: "rate-limit expired-bucket sweep" });
|
|
914
|
+
};
|
|
915
|
+
const reportedUnreadable = /* @__PURE__ */ new Set();
|
|
916
|
+
const reportUnreadable = (directory, error) => {
|
|
917
|
+
if (reportedUnreadable.has(directory)) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
reportedUnreadable.add(directory);
|
|
921
|
+
reportSweepError(error);
|
|
922
|
+
};
|
|
923
|
+
const sweepBucket = async (path, now) => {
|
|
924
|
+
try {
|
|
925
|
+
const candidate = await readBucket(path);
|
|
926
|
+
if (!candidate || !isExpired(candidate, now)) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
await withBucketLockIfFree(path, { timeoutMs: lockTimeoutMs }, async () => {
|
|
930
|
+
const bucket = await readBucket(path);
|
|
931
|
+
if (bucket && isExpired(bucket, now)) {
|
|
932
|
+
await deleteBucket(path);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
} catch (error) {
|
|
936
|
+
reportSweepError(error);
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
const closeCursor = async () => {
|
|
940
|
+
const current = cursor;
|
|
941
|
+
cursor = void 0;
|
|
942
|
+
await current?.return(void 0);
|
|
943
|
+
};
|
|
944
|
+
const runSweep = async (now) => {
|
|
945
|
+
let units = 0;
|
|
946
|
+
let finished = false;
|
|
947
|
+
try {
|
|
948
|
+
cursor ??= walkBucketTree(bucketsRoot, reportUnreadable);
|
|
949
|
+
while (units < SWEEP_BATCH_SIZE && !closed) {
|
|
950
|
+
const next = await cursor.next();
|
|
951
|
+
if (next.done) {
|
|
952
|
+
finished = true;
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
units += 1;
|
|
956
|
+
if (next.value.kind === "bucket") {
|
|
957
|
+
await sweepBucket(next.value.path, now);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch (error) {
|
|
961
|
+
reportSweepError(error);
|
|
962
|
+
finished = true;
|
|
963
|
+
} finally {
|
|
964
|
+
if (finished || closed) {
|
|
965
|
+
await closeCursor();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
const startSweepIfDue = (now) => {
|
|
970
|
+
if (closed || inFlight !== void 0) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (cursor === void 0) {
|
|
974
|
+
if (cycleStartedAt !== void 0 && now.getTime() - cycleStartedAt < PRUNE_INTERVAL_MS) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
cycleStartedAt = now.getTime();
|
|
978
|
+
}
|
|
979
|
+
inFlight = runSweep(now).finally(() => {
|
|
980
|
+
inFlight = void 0;
|
|
981
|
+
});
|
|
982
|
+
};
|
|
983
|
+
const store = {
|
|
780
984
|
async hit(key, hitOptions) {
|
|
781
|
-
const
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}, async () => {
|
|
985
|
+
const sweepNow = resolveNow();
|
|
986
|
+
const keyHash = createBucketHash(key);
|
|
987
|
+
const path = getBucketPathForKeyHash(root, keyHash);
|
|
988
|
+
startSweepIfDue(sweepNow);
|
|
989
|
+
return await withBucketLock(path, lockOptions, async () => {
|
|
787
990
|
const existing = await readBucket(path);
|
|
788
|
-
|
|
991
|
+
const now = resolveNow();
|
|
992
|
+
if (existing && existing.keyHash !== keyHash) {
|
|
789
993
|
throw new Error(`[@holo-js/security] File rate-limit bucket hash collision detected for stored bucket ${existing.keyHash}.`);
|
|
790
994
|
}
|
|
791
995
|
if (existing && isExpired(existing, now)) {
|
|
@@ -795,9 +999,8 @@ function createFileRateLimitStore(root, options = {}) {
|
|
|
795
999
|
...existing,
|
|
796
1000
|
attempts: existing.attempts + 1
|
|
797
1001
|
} : {
|
|
798
|
-
|
|
799
|
-
keyHash
|
|
800
|
-
prefixHashes: Object.freeze(createBucketPrefixHashes(key)),
|
|
1002
|
+
...hashBucketKey(key, keyHash),
|
|
1003
|
+
keyHash,
|
|
801
1004
|
attempts: 1,
|
|
802
1005
|
expiresAt: new Date(now.getTime() + hitOptions.decaySeconds * 1e3)
|
|
803
1006
|
};
|
|
@@ -810,13 +1013,11 @@ function createFileRateLimitStore(root, options = {}) {
|
|
|
810
1013
|
});
|
|
811
1014
|
},
|
|
812
1015
|
async clear(key) {
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
timeoutMs: lockTimeoutMs
|
|
817
|
-
}, async () => {
|
|
1016
|
+
const keyHash = createBucketHash(key);
|
|
1017
|
+
const path = getBucketPathForKeyHash(root, keyHash);
|
|
1018
|
+
return await withBucketLock(path, lockOptions, async () => {
|
|
818
1019
|
const existing = await readBucket(path);
|
|
819
|
-
if (!existing || existing.keyHash !==
|
|
1020
|
+
if (!existing || existing.keyHash !== keyHash) {
|
|
820
1021
|
return false;
|
|
821
1022
|
}
|
|
822
1023
|
await deleteBucket(path);
|
|
@@ -826,12 +1027,9 @@ function createFileRateLimitStore(root, options = {}) {
|
|
|
826
1027
|
async clearByPrefix(prefix) {
|
|
827
1028
|
let cleared = 0;
|
|
828
1029
|
const now = resolveNow();
|
|
829
|
-
const
|
|
830
|
-
for (const path of
|
|
831
|
-
const removed = await withBucketLock(path, {
|
|
832
|
-
retryDelayMs: lockRetryDelayMs,
|
|
833
|
-
timeoutMs: lockTimeoutMs
|
|
834
|
-
}, async () => {
|
|
1030
|
+
const prefixHash = createBucketHash(prefix);
|
|
1031
|
+
for await (const path of bucketFilePaths(bucketsRoot)) {
|
|
1032
|
+
const removed = await withBucketLock(path, lockOptions, async () => {
|
|
835
1033
|
const bucket = await readBucket(path);
|
|
836
1034
|
if (!bucket) {
|
|
837
1035
|
return false;
|
|
@@ -840,7 +1038,7 @@ function createFileRateLimitStore(root, options = {}) {
|
|
|
840
1038
|
await deleteBucket(path);
|
|
841
1039
|
return false;
|
|
842
1040
|
}
|
|
843
|
-
if (!bucket.prefixHashes.includes(
|
|
1041
|
+
if (!bucket.prefixHashes.includes(prefixHash)) {
|
|
844
1042
|
return false;
|
|
845
1043
|
}
|
|
846
1044
|
await deleteBucket(path);
|
|
@@ -853,13 +1051,9 @@ function createFileRateLimitStore(root, options = {}) {
|
|
|
853
1051
|
return cleared;
|
|
854
1052
|
},
|
|
855
1053
|
async clearAll() {
|
|
856
|
-
const paths = await listBucketPaths(root);
|
|
857
1054
|
let cleared = 0;
|
|
858
|
-
for (const path of
|
|
859
|
-
const removed = await withBucketLock(path, {
|
|
860
|
-
retryDelayMs: lockRetryDelayMs,
|
|
861
|
-
timeoutMs: lockTimeoutMs
|
|
862
|
-
}, async () => {
|
|
1055
|
+
for await (const path of bucketFilePaths(bucketsRoot)) {
|
|
1056
|
+
const removed = await withBucketLock(path, lockOptions, async () => {
|
|
863
1057
|
const bucket = await readBucket(path);
|
|
864
1058
|
if (!bucket) {
|
|
865
1059
|
return false;
|
|
@@ -872,29 +1066,19 @@ function createFileRateLimitStore(root, options = {}) {
|
|
|
872
1066
|
}
|
|
873
1067
|
}
|
|
874
1068
|
return cleared;
|
|
1069
|
+
},
|
|
1070
|
+
async close() {
|
|
1071
|
+
closed = true;
|
|
1072
|
+
await inFlight;
|
|
1073
|
+
await closeCursor();
|
|
875
1074
|
}
|
|
876
1075
|
};
|
|
1076
|
+
return store;
|
|
877
1077
|
}
|
|
878
|
-
var fileRateLimitDriverInternals = {
|
|
879
|
-
createBucketHash,
|
|
880
|
-
createSnapshot,
|
|
881
|
-
deleteBucket,
|
|
882
|
-
deserializeBucket,
|
|
883
|
-
getBucketPath,
|
|
884
|
-
isExpired,
|
|
885
|
-
listBucketPaths,
|
|
886
|
-
readBucket,
|
|
887
|
-
serializeBucket,
|
|
888
|
-
sleep,
|
|
889
|
-
getBucketLockPath,
|
|
890
|
-
acquireBucketLock,
|
|
891
|
-
reclaimStaleBucketLock,
|
|
892
|
-
removeOwnedBucketLock,
|
|
893
|
-
withBucketLock,
|
|
894
|
-
writeBucket
|
|
895
|
-
};
|
|
896
1078
|
|
|
897
1079
|
// src/drivers/memory.ts
|
|
1080
|
+
import { reportError as reportError2 } from "@holo-js/kernel/error-reporting";
|
|
1081
|
+
var DEFAULT_MEMORY_MAX_BUCKETS = 1e4;
|
|
898
1082
|
function createSnapshot2(key, bucket, maxAttempts) {
|
|
899
1083
|
const expiresAt = new Date(bucket.expiresAt.getTime());
|
|
900
1084
|
return Object.freeze({
|
|
@@ -912,27 +1096,41 @@ function isExpired2(bucket, now) {
|
|
|
912
1096
|
function createMemoryRateLimitStore(options = {}) {
|
|
913
1097
|
const buckets = /* @__PURE__ */ new Map();
|
|
914
1098
|
const resolveNow = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
915
|
-
const maxBuckets = options.maxBuckets;
|
|
916
|
-
if (
|
|
1099
|
+
const maxBuckets = options.maxBuckets ?? DEFAULT_MEMORY_MAX_BUCKETS;
|
|
1100
|
+
if (!Number.isInteger(maxBuckets) || maxBuckets < 1) {
|
|
917
1101
|
throw new TypeError("[@holo-js/security] Memory rate-limit store maxBuckets must be an integer greater than or equal to 1.");
|
|
918
1102
|
}
|
|
919
1103
|
const pruneIntervalMs = typeof options.pruneIntervalMs === "number" ? options.pruneIntervalMs : 6e4;
|
|
920
1104
|
if (!Number.isInteger(pruneIntervalMs) || pruneIntervalMs < 1) {
|
|
921
1105
|
throw new TypeError("[@holo-js/security] Memory rate-limit store pruneIntervalMs must be an integer greater than or equal to 1.");
|
|
922
1106
|
}
|
|
923
|
-
|
|
924
|
-
|
|
1107
|
+
let nextExpiryAt = Number.POSITIVE_INFINITY;
|
|
1108
|
+
const refreshNextExpiry = () => {
|
|
1109
|
+
nextExpiryAt = Number.POSITIVE_INFINITY;
|
|
1110
|
+
for (const bucket of buckets.values()) {
|
|
1111
|
+
nextExpiryAt = Math.min(nextExpiryAt, bucket.expiresAt.getTime());
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
const pruneExpiredBuckets = (now) => {
|
|
925
1115
|
for (const [key, bucket] of buckets) {
|
|
926
1116
|
if (isExpired2(bucket, now)) {
|
|
927
1117
|
buckets.delete(key);
|
|
928
1118
|
}
|
|
929
1119
|
}
|
|
1120
|
+
refreshNextExpiry();
|
|
930
1121
|
};
|
|
931
|
-
const pruneTimer = setInterval(pruneExpiredBuckets, pruneIntervalMs);
|
|
1122
|
+
const pruneTimer = setInterval(() => pruneExpiredBuckets(resolveNow()), pruneIntervalMs);
|
|
932
1123
|
pruneTimer.unref?.();
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1124
|
+
let capacityReportedFor;
|
|
1125
|
+
const reportCapacity = (freesAt) => {
|
|
1126
|
+
if (capacityReportedFor === freesAt) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
capacityReportedFor = freesAt;
|
|
1130
|
+
reportError2(
|
|
1131
|
+
new Error(`[@holo-js/security] Memory rate-limit store is at capacity (${maxBuckets} keys). New keys are limited until buckets expire. Use the redis or file rate-limit driver for high-cardinality limiters.`),
|
|
1132
|
+
{ capability: "security", operation: "rate-limit memory store at capacity" }
|
|
1133
|
+
);
|
|
936
1134
|
};
|
|
937
1135
|
return {
|
|
938
1136
|
async hit(key, hitOptions) {
|
|
@@ -950,15 +1148,29 @@ function createMemoryRateLimitStore(options = {}) {
|
|
|
950
1148
|
attempts: 1,
|
|
951
1149
|
expiresAt: new Date(now.getTime() + hitOptions.decaySeconds * 1e3)
|
|
952
1150
|
};
|
|
953
|
-
if (!active &&
|
|
954
|
-
|
|
955
|
-
|
|
1151
|
+
if (!active && buckets.size >= maxBuckets) {
|
|
1152
|
+
if (now.getTime() >= nextExpiryAt) {
|
|
1153
|
+
pruneExpiredBuckets(now);
|
|
1154
|
+
}
|
|
1155
|
+
if (buckets.size >= maxBuckets) {
|
|
1156
|
+
const freesAt = nextExpiryAt;
|
|
1157
|
+
reportCapacity(freesAt);
|
|
1158
|
+
return Object.freeze({
|
|
1159
|
+
limited: true,
|
|
1160
|
+
snapshot: createSnapshot2(key, {
|
|
1161
|
+
key,
|
|
1162
|
+
attempts: hitOptions.maxAttempts + 1,
|
|
1163
|
+
expiresAt: new Date(freesAt)
|
|
1164
|
+
}, hitOptions.maxAttempts),
|
|
1165
|
+
retryAfterSeconds: Math.max(0, Math.ceil((freesAt - now.getTime()) / 1e3))
|
|
1166
|
+
});
|
|
956
1167
|
}
|
|
957
1168
|
}
|
|
958
1169
|
if (active) {
|
|
959
1170
|
buckets.delete(key);
|
|
960
1171
|
}
|
|
961
1172
|
buckets.set(key, bucket);
|
|
1173
|
+
nextExpiryAt = Math.min(nextExpiryAt, bucket.expiresAt.getTime());
|
|
962
1174
|
return Object.freeze({
|
|
963
1175
|
limited: bucket.attempts > hitOptions.maxAttempts,
|
|
964
1176
|
snapshot: createSnapshot2(key, bucket, hitOptions.maxAttempts),
|
|
@@ -966,7 +1178,9 @@ function createMemoryRateLimitStore(options = {}) {
|
|
|
966
1178
|
});
|
|
967
1179
|
},
|
|
968
1180
|
async clear(key) {
|
|
969
|
-
|
|
1181
|
+
const deleted = buckets.delete(key);
|
|
1182
|
+
refreshNextExpiry();
|
|
1183
|
+
return deleted;
|
|
970
1184
|
},
|
|
971
1185
|
async clearByPrefix(prefix) {
|
|
972
1186
|
let cleared = 0;
|
|
@@ -977,11 +1191,13 @@ function createMemoryRateLimitStore(options = {}) {
|
|
|
977
1191
|
buckets.delete(key);
|
|
978
1192
|
cleared += 1;
|
|
979
1193
|
}
|
|
1194
|
+
refreshNextExpiry();
|
|
980
1195
|
return cleared;
|
|
981
1196
|
},
|
|
982
1197
|
async clearAll() {
|
|
983
1198
|
const cleared = buckets.size;
|
|
984
1199
|
buckets.clear();
|
|
1200
|
+
refreshNextExpiry();
|
|
985
1201
|
return cleared;
|
|
986
1202
|
},
|
|
987
1203
|
async close() {
|
|
@@ -1087,7 +1303,7 @@ var securityStoreInternals = {
|
|
|
1087
1303
|
};
|
|
1088
1304
|
|
|
1089
1305
|
// src/signed-tokens.ts
|
|
1090
|
-
import { createHmac
|
|
1306
|
+
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1091
1307
|
var MAX_SIGNED_TOKEN_DEPTH = 32;
|
|
1092
1308
|
var MAX_SIGNED_TOKEN_BODY_BYTES = 64 * 1024;
|
|
1093
1309
|
var MAX_SIGNED_TOKEN_CHARACTERS = Math.ceil(MAX_SIGNED_TOKEN_BODY_BYTES * 4 / 3) + 44;
|
|
@@ -1139,7 +1355,7 @@ function assertPayload(payload) {
|
|
|
1139
1355
|
}
|
|
1140
1356
|
}
|
|
1141
1357
|
function signature(body, purpose) {
|
|
1142
|
-
return
|
|
1358
|
+
return createHmac("sha256", signingKey()).update("holo-signed-token-v1\0").update(purpose).update("\0").update(body).digest();
|
|
1143
1359
|
}
|
|
1144
1360
|
function decodeEnvelope(body) {
|
|
1145
1361
|
try {
|
|
@@ -1215,6 +1431,7 @@ export {
|
|
|
1215
1431
|
getSecurityRuntimeBindings,
|
|
1216
1432
|
resetSecurityRuntime,
|
|
1217
1433
|
securityRuntimeInternals,
|
|
1434
|
+
ip,
|
|
1218
1435
|
defaultRateLimitKey,
|
|
1219
1436
|
rateLimit,
|
|
1220
1437
|
clearRateLimit,
|
|
@@ -1223,6 +1440,7 @@ export {
|
|
|
1223
1440
|
token,
|
|
1224
1441
|
field,
|
|
1225
1442
|
input,
|
|
1443
|
+
encryptedToken,
|
|
1226
1444
|
cookie,
|
|
1227
1445
|
verify,
|
|
1228
1446
|
protect,
|
|
@@ -1234,7 +1452,6 @@ export {
|
|
|
1234
1452
|
cors,
|
|
1235
1453
|
corsInternals,
|
|
1236
1454
|
createFileRateLimitStore,
|
|
1237
|
-
fileRateLimitDriverInternals,
|
|
1238
1455
|
createMemoryRateLimitStore,
|
|
1239
1456
|
memoryRateLimitDriverInternals,
|
|
1240
1457
|
createRedisRateLimitStore,
|