@holo-js/security 0.1.3 → 0.1.5

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/index.mjs CHANGED
@@ -1,3 +1,39 @@
1
+ import {
2
+ SecurityRuntimeNotConfiguredError,
3
+ apply,
4
+ clearRateLimit,
5
+ configureSecurityRuntime,
6
+ cookie,
7
+ cors,
8
+ corsInternals,
9
+ createFileRateLimitStore,
10
+ createMemoryRateLimitStore,
11
+ createRateLimitStoreFromConfig,
12
+ createRedisRateLimitStore,
13
+ csrf,
14
+ csrfInternals,
15
+ defaultRateLimitKey,
16
+ defineSecurityConfig,
17
+ field,
18
+ fileRateLimitDriverInternals,
19
+ getSecurityRuntime,
20
+ getSecurityRuntimeBindings,
21
+ headers,
22
+ input,
23
+ isSecureRequest,
24
+ memoryRateLimitDriverInternals,
25
+ preflight,
26
+ protect,
27
+ rateLimit,
28
+ rateLimitInternals,
29
+ redisRateLimitDriverInternals,
30
+ resetSecurityRuntime,
31
+ securityRuntimeInternals,
32
+ securityStoreInternals,
33
+ src_default,
34
+ token,
35
+ verify
36
+ } from "./chunk-Q3A7RJ67.mjs";
1
37
  import {
2
38
  SecurityCsrfError,
3
39
  SecurityRateLimitError,
@@ -9,880 +45,21 @@ import {
9
45
  ip,
10
46
  limit,
11
47
  securityInternals
12
- } from "./chunk-3J5QRTPZ.mjs";
13
-
14
- // src/csrf.ts
15
- import { createHmac, randomBytes, timingSafeEqual } from "crypto";
16
-
17
- // src/runtime.ts
18
- import { normalizeSecurityConfig } from "@holo-js/config";
19
- function getSecurityRuntimeState() {
20
- const runtime = globalThis;
21
- runtime.__holoSecurityRuntime__ ??= {};
22
- return runtime.__holoSecurityRuntime__;
23
- }
24
- var SecurityRuntimeNotConfiguredError = class extends Error {
25
- constructor() {
26
- super("[@holo-js/security] Security runtime is not configured yet.");
27
- }
28
- };
29
- function configureSecurityRuntime(bindings) {
30
- getSecurityRuntimeState().bindings = bindings ? Object.freeze({
31
- config: normalizeSecurityConfig(bindings.config),
32
- rateLimitStore: bindings.rateLimitStore,
33
- csrfSigningKey: bindings.csrfSigningKey,
34
- defaultKeyResolver: bindings.defaultKeyResolver
35
- }) : void 0;
36
- }
37
- function getSecurityRuntime() {
38
- const bindings = getSecurityRuntimeState().bindings;
39
- if (!bindings) {
40
- throw new SecurityRuntimeNotConfiguredError();
41
- }
42
- return bindings;
43
- }
44
- function getSecurityRuntimeBindings() {
45
- return getSecurityRuntimeState().bindings;
46
- }
47
- function resetSecurityRuntime() {
48
- getSecurityRuntimeState().bindings = void 0;
49
- }
50
- var securityRuntimeInternals = {
51
- getSecurityRuntimeState
52
- };
53
-
54
- // src/rate-limit.ts
55
- function encodeBucketPart(value) {
56
- return encodeURIComponent(value);
57
- }
58
- function createLimiterPrefix(limiter) {
59
- return `limiter:${encodeBucketPart(limiter)}|`;
60
- }
61
- function createBucketKey(limiter, key) {
62
- return `${createLimiterPrefix(limiter)}${encodeBucketPart(key)}`;
63
- }
64
- function getRateLimitStore() {
65
- const store = getSecurityRuntime().rateLimitStore;
66
- if (!store) {
67
- throw new Error("[@holo-js/security] Rate-limit store is not configured yet.");
68
- }
69
- return store;
70
- }
71
- function resolveLimiterConfig(name) {
72
- const limiter = getSecurityRuntime().config.rateLimit.limiters[name];
73
- if (!limiter) {
74
- throw new Error(`[@holo-js/security] Rate limiter "${name}" is not defined in config/security.ts.`);
75
- }
76
- return limiter;
77
- }
78
- function normalizeResolvedLimiterKey(value, label) {
79
- if (typeof value === "string" && value.length > 0) {
80
- return value;
81
- }
82
- if (typeof value === "number" && Number.isFinite(value)) {
83
- return String(value);
84
- }
85
- throw new TypeError(`[@holo-js/security] ${label} must resolve a non-empty string key.`);
86
- }
87
- function shouldTrustProxyHeaders() {
88
- const trustedProxy = typeof process !== "undefined" ? process.env.HOLO_SECURITY_TRUST_PROXY?.trim().toLowerCase() : void 0;
89
- return trustedProxy === "1" || trustedProxy === "true" || trustedProxy === "yes" || trustedProxy === "on";
90
- }
91
- async function defaultRateLimitKey(request) {
92
- const runtimeDefaultKey = await getSecurityRuntime().defaultKeyResolver?.(request);
93
- if (typeof runtimeDefaultKey !== "undefined" && runtimeDefaultKey !== null) {
94
- return normalizeResolvedLimiterKey(runtimeDefaultKey, "Default rate limiter key resolver");
95
- }
96
- return `ip:${ip(request, shouldTrustProxyHeaders())}`;
97
- }
98
- async function resolveLimiterKey(name, limiter, options) {
99
- if (typeof options.key === "string" && options.key.length > 0) {
100
- return options.key;
101
- }
102
- if (limiter.key) {
103
- if (!options.request) {
104
- throw new TypeError(`[@holo-js/security] Rate limiter "${name}" requires a request when using its configured key resolver.`);
105
- }
106
- const resolved = await limiter.key({
107
- request: options.request,
108
- values: options.values
109
- });
110
- return normalizeResolvedLimiterKey(resolved, `Rate limiter "${name}"`);
111
- }
112
- if (options.request) {
113
- return await defaultRateLimitKey(options.request);
114
- }
115
- throw new TypeError(`[@holo-js/security] Rate limiter "${name}" requires either an explicit key or a request for the default key resolver.`);
116
- }
117
- async function rateLimit(name, options) {
118
- const limiter = resolveLimiterConfig(name);
119
- const resolvedKey = await resolveLimiterKey(name, limiter, options);
120
- const bucketKey = createBucketKey(name, resolvedKey);
121
- const result = await getRateLimitStore().hit(bucketKey, {
122
- maxAttempts: limiter.maxAttempts,
123
- decaySeconds: limiter.decaySeconds
124
- });
125
- const snapshot = Object.freeze({
126
- limiter: name,
127
- key: resolvedKey,
128
- attempts: result.snapshot.attempts,
129
- maxAttempts: limiter.maxAttempts,
130
- remainingAttempts: Math.max(0, limiter.maxAttempts - result.snapshot.attempts),
131
- expiresAt: result.snapshot.expiresAt
132
- });
133
- const normalizedResult = Object.freeze({
134
- limited: result.limited,
135
- snapshot,
136
- retryAfterSeconds: result.retryAfterSeconds
137
- });
138
- if (normalizedResult.limited) {
139
- throw new SecurityRateLimitError(void 0, {
140
- retryAfterSeconds: normalizedResult.retryAfterSeconds,
141
- snapshot
142
- });
143
- }
144
- return normalizedResult;
145
- }
146
- async function clearRateLimit(options) {
147
- const store = getRateLimitStore();
148
- if (options.all && (options.limiter || options.key)) {
149
- throw new TypeError("[@holo-js/security] clearRateLimit(...) must use either { all: true } or a scoped limiter/key pair, not both.");
150
- }
151
- if (options.all) {
152
- return await store.clearAll();
153
- }
154
- if (!options.limiter) {
155
- throw new TypeError("[@holo-js/security] clearRateLimit(...) requires a limiter name unless { all: true } is used.");
156
- }
157
- if (typeof options.key === "string" && options.key.length > 0) {
158
- return await store.clear(createBucketKey(options.limiter, options.key));
159
- }
160
- return await store.clearByPrefix(createLimiterPrefix(options.limiter));
161
- }
162
- var rateLimitInternals = {
163
- createBucketKey,
164
- createLimiterPrefix,
165
- encodeBucketPart,
166
- defaultRateLimitKey,
167
- getRateLimitStore,
168
- normalizeResolvedLimiterKey,
169
- resolveLimiterConfig,
170
- resolveLimiterKey
171
- };
172
-
173
- // src/csrf.ts
174
- var generatedTokenCache = /* @__PURE__ */ new WeakMap();
175
- function parseCookieHeader(header) {
176
- if (!header) {
177
- return Object.freeze({});
178
- }
179
- const decodeCookiePart = (value) => {
180
- try {
181
- return decodeURIComponent(value);
182
- } catch {
183
- return void 0;
184
- }
185
- };
186
- const entries = header.split(";").map((segment) => segment.trim()).filter(Boolean).map((segment) => {
187
- const separator = segment.indexOf("=");
188
- if (separator <= 0) {
189
- return void 0;
190
- }
191
- const key = decodeCookiePart(segment.slice(0, separator));
192
- const value = decodeCookiePart(segment.slice(separator + 1));
193
- if (!key || typeof value === "undefined") {
194
- return void 0;
195
- }
196
- return [key, value];
197
- }).filter((entry) => !!entry);
198
- return Object.freeze(Object.fromEntries(entries));
199
- }
200
- function serializeCookie(name, value, options = {}) {
201
- const attributes = [
202
- `${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
203
- "Path=/",
204
- "SameSite=Lax"
205
- ];
206
- if (options.secure) {
207
- attributes.push("Secure");
208
- }
209
- return attributes.join("; ");
210
- }
211
- function isSafeMethod(method) {
212
- const normalized = method.trim().toUpperCase();
213
- return normalized === "GET" || normalized === "HEAD" || normalized === "OPTIONS" || normalized === "TRACE";
214
- }
215
- function escapeRegex(value) {
216
- return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
217
- }
218
- function matchesPathPattern(pathname, pattern) {
219
- const source = `^${escapeRegex(pattern).replaceAll("*", ".*")}$`;
220
- return new RegExp(source).test(pathname);
221
- }
222
- function isExcludedPath(request) {
223
- const { except } = getSecurityRuntime().config.csrf;
224
- const pathname = new URL(request.url).pathname;
225
- return except.some((pattern) => matchesPathPattern(pathname, pattern));
226
- }
227
- function isSecureRequest(request) {
228
- return new URL(request.url).protocol === "https:";
229
- }
230
- function createCsrfToken() {
231
- return randomBytes(32).toString("base64url");
232
- }
233
- function getCsrfSigningKey() {
234
- const configured = getSecurityRuntime().csrfSigningKey?.trim();
235
- if (configured) {
236
- return configured;
237
- }
238
- throw new Error("[@holo-js/security] CSRF signing key is not configured.");
239
- }
240
- function signCsrfNonce(nonce) {
241
- return createHmac("sha256", getCsrfSigningKey()).update(nonce).digest("base64url");
242
- }
243
- function encodeCsrfToken(nonce) {
244
- return `${nonce}.${signCsrfNonce(nonce)}`;
245
- }
246
- function decodeCsrfToken(token2) {
247
- const separator = token2.indexOf(".");
248
- if (separator <= 0 || separator === token2.length - 1) {
249
- return null;
250
- }
251
- return Object.freeze({
252
- nonce: token2.slice(0, separator),
253
- signature: token2.slice(separator + 1)
254
- });
255
- }
256
- function isValidSignedCsrfToken(token2) {
257
- const decoded = decodeCsrfToken(token2);
258
- if (!decoded) {
259
- return false;
260
- }
261
- const expected = Buffer.from(signCsrfNonce(decoded.nonce));
262
- const received = Buffer.from(decoded.signature);
263
- if (expected.length !== received.length) {
264
- return false;
265
- }
266
- return timingSafeEqual(expected, received);
267
- }
268
- function getCookieToken(request) {
269
- const { cookie: cookie2 } = getSecurityRuntime().config.csrf;
270
- return parseCookieHeader(request.headers.get("cookie"))[cookie2];
271
- }
272
- async function readFormToken(request) {
273
- const { field: field2 } = getSecurityRuntime().config.csrf;
274
- try {
275
- const formData = await request.clone().formData();
276
- const value = formData.get(field2);
277
- return typeof value === "string" ? value : void 0;
278
- } catch {
279
- return void 0;
280
- }
281
- }
282
- async function getRequestToken(request) {
283
- const { header } = getSecurityRuntime().config.csrf;
284
- const headerToken = request.headers.get(header)?.trim();
285
- if (headerToken) {
286
- return headerToken;
287
- }
288
- return await readFormToken(request);
289
- }
290
- async function verifyRequest(request, options) {
291
- if (isSafeMethod(request.method)) {
292
- return;
293
- }
294
- if (options.allowExcludedPath && isExcludedPath(request)) {
295
- return;
296
- }
297
- const cookieToken = getCookieToken(request);
298
- const requestToken = await getRequestToken(request);
299
- if (!cookieToken || !requestToken || cookieToken !== requestToken || !isValidSignedCsrfToken(cookieToken)) {
300
- throw new SecurityCsrfError();
301
- }
302
- }
303
- function resolveShouldProtect(request, options = {}) {
304
- if (options.csrf === false) {
305
- return false;
306
- }
307
- if (isSafeMethod(request.method)) {
308
- return false;
309
- }
310
- if (options.csrf === true) {
311
- return true;
312
- }
313
- const { enabled } = getSecurityRuntime().config.csrf;
314
- if (!enabled) {
315
- return false;
316
- }
317
- if (isExcludedPath(request)) {
318
- return false;
319
- }
320
- return true;
321
- }
322
- async function token(request) {
323
- const cookieToken = getCookieToken(request);
324
- if (cookieToken && isValidSignedCsrfToken(cookieToken)) {
325
- return cookieToken;
326
- }
327
- const cached = generatedTokenCache.get(request);
328
- if (cached) {
329
- return cached;
330
- }
331
- const created = encodeCsrfToken(createCsrfToken());
332
- generatedTokenCache.set(request, created);
333
- return created;
334
- }
335
- async function field(request) {
336
- const config = getSecurityRuntime().config.csrf;
337
- return Object.freeze({
338
- name: config.field,
339
- value: await token(request)
340
- });
341
- }
342
- async function cookie(request, explicitToken) {
343
- const config = getSecurityRuntime().config.csrf;
344
- const value = explicitToken ? isValidSignedCsrfToken(explicitToken) ? explicitToken : encodeCsrfToken(explicitToken) : await token(request);
345
- return serializeCookie(config.cookie, value, {
346
- secure: isSecureRequest(request)
347
- });
348
- }
349
- async function verify(request) {
350
- await verifyRequest(request, { allowExcludedPath: true });
351
- }
352
- async function protect(request, options = {}) {
353
- if (!resolveShouldProtect(request, options)) {
354
- if (typeof options.throttle !== "string") {
355
- return;
356
- }
357
- } else {
358
- await verifyRequest(request, {
359
- allowExcludedPath: options.csrf !== true
360
- });
361
- }
362
- if (typeof options.throttle === "string") {
363
- await rateLimit(options.throttle, { request });
364
- }
365
- }
366
- var csrf = Object.freeze({
367
- token,
368
- field,
369
- cookie,
370
- verify
371
- });
372
- var csrfInternals = {
373
- createCsrfToken,
374
- generatedTokenCache,
375
- getCookieToken,
376
- getRequestToken,
377
- isExcludedPath,
378
- isSafeMethod,
379
- matchesPathPattern,
380
- parseCookieHeader,
381
- serializeCookie,
382
- decodeCsrfToken,
383
- encodeCsrfToken,
384
- getCsrfSigningKey,
385
- isValidSignedCsrfToken
386
- };
387
-
388
- // src/store.ts
389
- import { resolve } from "path";
390
- import { normalizeSecurityConfig as normalizeSecurityConfig2 } from "@holo-js/config";
391
-
392
- // src/drivers/file.ts
393
- import { createHash, randomUUID } from "crypto";
394
- import { mkdir, readFile, readdir, rename, rm, stat, utimes, writeFile } from "fs/promises";
395
- import { dirname, join } from "path";
396
- function createBucketHash(key) {
397
- return createHash("sha256").update(key).digest("hex");
398
- }
399
- function createBucketNamespace(key) {
400
- const separatorIndex = key.indexOf("|");
401
- return separatorIndex >= 0 ? key.slice(0, separatorIndex + 1) : key;
402
- }
403
- function createBucketPrefixHashes(key) {
404
- const prefixHashes = [];
405
- for (let index = 1; index <= key.length; index += 1) {
406
- prefixHashes.push(createBucketHash(key.slice(0, index)));
407
- }
408
- return prefixHashes;
409
- }
410
- function getBucketPath(root, key) {
411
- const hash = createBucketHash(key);
412
- return join(root, hash.slice(0, 2), hash.slice(2, 4), `${hash}.json`);
413
- }
414
- function serializeBucket(bucket) {
415
- return JSON.stringify({
416
- namespace: bucket.namespace,
417
- keyHash: bucket.keyHash,
418
- prefixHashes: [...bucket.prefixHashes],
419
- attempts: bucket.attempts,
420
- expiresAt: bucket.expiresAt.toISOString()
421
- });
422
- }
423
- function deserializeBucket(raw) {
424
- const parsed = JSON.parse(raw);
425
- const namespace = typeof parsed.namespace === "string" && parsed.namespace.length > 0 ? parsed.namespace : void 0;
426
- const keyHash = typeof parsed.keyHash === "string" && parsed.keyHash.length > 0 ? parsed.keyHash : void 0;
427
- const prefixHashes = Array.isArray(parsed.prefixHashes) && parsed.prefixHashes.every(
428
- (value) => typeof value === "string" && value.length > 0
429
- ) ? parsed.prefixHashes : void 0;
430
- const { attempts, expiresAt: expiresAtValue } = parsed;
431
- if (typeof namespace !== "string" || namespace.length === 0) {
432
- throw new TypeError("[@holo-js/security] File rate-limit buckets must contain a non-empty string namespace.");
433
- }
434
- if (typeof keyHash !== "string" || keyHash.length === 0) {
435
- throw new TypeError("[@holo-js/security] File rate-limit buckets must contain a non-empty string key hash.");
436
- }
437
- if (!Array.isArray(prefixHashes) || prefixHashes.length === 0) {
438
- throw new TypeError("[@holo-js/security] File rate-limit buckets must contain non-empty prefix hashes.");
439
- }
440
- if (typeof attempts !== "number" || !Number.isInteger(attempts) || attempts < 1) {
441
- throw new TypeError("[@holo-js/security] File rate-limit buckets must contain an integer attempts count greater than 0.");
442
- }
443
- const normalizedAttempts = attempts;
444
- if (typeof expiresAtValue !== "string") {
445
- throw new TypeError("[@holo-js/security] File rate-limit buckets must contain an ISO expiry timestamp.");
446
- }
447
- const expiresAt = new Date(expiresAtValue);
448
- if (Number.isNaN(expiresAt.getTime())) {
449
- throw new TypeError("[@holo-js/security] File rate-limit buckets must contain a valid expiry timestamp.");
450
- }
451
- return Object.freeze({
452
- namespace,
453
- keyHash,
454
- prefixHashes: Object.freeze([...prefixHashes]),
455
- attempts: normalizedAttempts,
456
- expiresAt
457
- });
458
- }
459
- async function readBucket(path) {
460
- const raw = await readFile(path, "utf8").catch((error) => {
461
- if (error.code === "ENOENT") {
462
- return void 0;
463
- }
464
- throw error;
465
- });
466
- return raw ? deserializeBucket(raw) : null;
467
- }
468
- function isExpired(bucket, now) {
469
- return bucket.expiresAt.getTime() <= now.getTime();
470
- }
471
- async function writeBucket(path, bucket) {
472
- await mkdir(dirname(path), { recursive: true });
473
- const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
474
- await writeFile(tempPath, serializeBucket(bucket), "utf8");
475
- await rename(tempPath, path);
476
- }
477
- async function deleteBucket(path) {
478
- await rm(path, { force: true });
479
- }
480
- function getBucketLockPath(path) {
481
- return `${path}.lock`;
482
- }
483
- async function sleep(delayMs) {
484
- await new Promise((resolve2) => {
485
- setTimeout(resolve2, delayMs);
486
- });
487
- }
488
- function startLockHeartbeat(lockPath, timeoutMs) {
489
- const heartbeat = setInterval(() => {
490
- const now = /* @__PURE__ */ new Date();
491
- void utimes(lockPath, now, now).catch(() => {
492
- });
493
- }, Math.max(1, Math.floor(timeoutMs / 2)));
494
- heartbeat.unref?.();
495
- return heartbeat;
496
- }
497
- async function withBucketLock(path, options, operation) {
498
- const lockPath = getBucketLockPath(path);
499
- const deadline = Date.now() + options.timeoutMs;
500
- await mkdir(dirname(lockPath), { recursive: true });
501
- while (true) {
502
- try {
503
- await mkdir(lockPath);
504
- break;
505
- } catch (error) {
506
- const candidate = error;
507
- if (candidate.code !== "EEXIST") {
508
- throw error;
509
- }
510
- const stale = await stat(lockPath).then((stats) => stats.mtimeMs <= Date.now() - options.timeoutMs).catch(() => false);
511
- if (stale) {
512
- await rm(lockPath, { recursive: true, force: true });
513
- continue;
514
- }
515
- if (Date.now() >= deadline) {
516
- throw new Error(`[@holo-js/security] Timed out waiting for file rate-limit lock "${lockPath}".`);
517
- }
518
- await sleep(options.retryDelayMs);
519
- }
520
- }
521
- const heartbeat = startLockHeartbeat(lockPath, options.timeoutMs);
522
- try {
523
- return await operation();
524
- } finally {
525
- clearInterval(heartbeat);
526
- await rm(lockPath, { recursive: true, force: true });
527
- }
528
- }
529
- async function listBucketPaths(root) {
530
- const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
531
- if (error.code === "ENOENT") {
532
- return [];
533
- }
534
- throw error;
535
- });
536
- const nested = await Promise.all(entries.map(async (entry) => {
537
- const entryPath = join(root, entry.name);
538
- if (entry.isDirectory()) {
539
- return await listBucketPaths(entryPath);
540
- }
541
- return entry.name.endsWith(".json") ? [entryPath] : [];
542
- }));
543
- return nested.flat();
544
- }
545
- function createSnapshot(key, bucket, maxAttempts) {
546
- const expiresAt = new Date(bucket.expiresAt.getTime());
547
- return Object.freeze({
548
- limiter: "",
549
- key,
550
- attempts: bucket.attempts,
551
- maxAttempts,
552
- remainingAttempts: Math.max(0, maxAttempts - bucket.attempts),
553
- expiresAt
554
- });
555
- }
556
- function createFileRateLimitStore(root, options = {}) {
557
- const resolveNow = options.now ?? (() => /* @__PURE__ */ new Date());
558
- const lockRetryDelayMs = options.lockRetryDelayMs ?? 5;
559
- const lockTimeoutMs = options.lockTimeoutMs ?? 2e3;
560
- return {
561
- async hit(key, hitOptions) {
562
- const now = resolveNow();
563
- const path = getBucketPath(root, key);
564
- return await withBucketLock(path, {
565
- retryDelayMs: lockRetryDelayMs,
566
- timeoutMs: lockTimeoutMs
567
- }, async () => {
568
- const existing = await readBucket(path);
569
- if (existing && existing.keyHash !== createBucketHash(key)) {
570
- throw new Error(`[@holo-js/security] File rate-limit bucket hash collision detected for stored bucket ${existing.keyHash}.`);
571
- }
572
- if (existing && isExpired(existing, now)) {
573
- await deleteBucket(path);
574
- }
575
- const bucket = existing && !isExpired(existing, now) ? {
576
- ...existing,
577
- attempts: existing.attempts + 1
578
- } : {
579
- namespace: createBucketNamespace(key),
580
- keyHash: createBucketHash(key),
581
- prefixHashes: Object.freeze(createBucketPrefixHashes(key)),
582
- attempts: 1,
583
- expiresAt: new Date(now.getTime() + hitOptions.decaySeconds * 1e3)
584
- };
585
- await writeBucket(path, bucket);
586
- return Object.freeze({
587
- limited: bucket.attempts > hitOptions.maxAttempts,
588
- snapshot: createSnapshot(key, bucket, hitOptions.maxAttempts),
589
- retryAfterSeconds: Math.max(0, Math.ceil((bucket.expiresAt.getTime() - now.getTime()) / 1e3))
590
- });
591
- });
592
- },
593
- async clear(key) {
594
- const path = getBucketPath(root, key);
595
- return await withBucketLock(path, {
596
- retryDelayMs: lockRetryDelayMs,
597
- timeoutMs: lockTimeoutMs
598
- }, async () => {
599
- const existing = await readBucket(path);
600
- if (!existing || existing.keyHash !== createBucketHash(key)) {
601
- return false;
602
- }
603
- await deleteBucket(path);
604
- return true;
605
- });
606
- },
607
- async clearByPrefix(prefix) {
608
- let cleared = 0;
609
- const now = resolveNow();
610
- const paths = await listBucketPaths(root);
611
- for (const path of paths) {
612
- const removed = await withBucketLock(path, {
613
- retryDelayMs: lockRetryDelayMs,
614
- timeoutMs: lockTimeoutMs
615
- }, async () => {
616
- const bucket = await readBucket(path);
617
- if (!bucket) {
618
- return false;
619
- }
620
- if (isExpired(bucket, now)) {
621
- await deleteBucket(path);
622
- return false;
623
- }
624
- if (!bucket.prefixHashes.includes(createBucketHash(prefix))) {
625
- return false;
626
- }
627
- await deleteBucket(path);
628
- return true;
629
- });
630
- if (removed) {
631
- cleared += 1;
632
- }
633
- }
634
- return cleared;
635
- },
636
- async clearAll() {
637
- const paths = await listBucketPaths(root);
638
- let cleared = 0;
639
- for (const path of paths) {
640
- const removed = await withBucketLock(path, {
641
- retryDelayMs: lockRetryDelayMs,
642
- timeoutMs: lockTimeoutMs
643
- }, async () => {
644
- const bucket = await readBucket(path);
645
- if (!bucket) {
646
- return false;
647
- }
648
- await deleteBucket(path);
649
- return true;
650
- });
651
- if (removed) {
652
- cleared += 1;
653
- }
654
- }
655
- return cleared;
656
- }
657
- };
658
- }
659
- var fileRateLimitDriverInternals = {
660
- createBucketHash,
661
- createSnapshot,
662
- deleteBucket,
663
- deserializeBucket,
664
- getBucketPath,
665
- isExpired,
666
- listBucketPaths,
667
- readBucket,
668
- serializeBucket,
669
- sleep,
670
- getBucketLockPath,
671
- withBucketLock,
672
- writeBucket
673
- };
674
-
675
- // src/drivers/memory.ts
676
- function createSnapshot2(key, bucket, maxAttempts) {
677
- const expiresAt = new Date(bucket.expiresAt.getTime());
678
- return Object.freeze({
679
- limiter: "",
680
- key,
681
- attempts: bucket.attempts,
682
- maxAttempts,
683
- remainingAttempts: Math.max(0, maxAttempts - bucket.attempts),
684
- expiresAt
685
- });
686
- }
687
- function isExpired2(bucket, now) {
688
- return bucket.expiresAt.getTime() <= now.getTime();
689
- }
690
- function createMemoryRateLimitStore(options = {}) {
691
- const buckets = /* @__PURE__ */ new Map();
692
- const resolveNow = options.now ?? (() => /* @__PURE__ */ new Date());
693
- const maxBuckets = options.maxBuckets;
694
- if (typeof maxBuckets !== "undefined" && (!Number.isInteger(maxBuckets) || maxBuckets < 1)) {
695
- throw new TypeError("[@holo-js/security] Memory rate-limit store maxBuckets must be an integer greater than or equal to 1.");
696
- }
697
- const pruneIntervalMs = typeof options.pruneIntervalMs === "number" ? options.pruneIntervalMs : 6e4;
698
- if (!Number.isInteger(pruneIntervalMs) || pruneIntervalMs < 1) {
699
- throw new TypeError("[@holo-js/security] Memory rate-limit store pruneIntervalMs must be an integer greater than or equal to 1.");
700
- }
701
- const pruneExpiredBuckets = () => {
702
- const now = resolveNow();
703
- for (const [key, bucket] of buckets) {
704
- if (isExpired2(bucket, now)) {
705
- buckets.delete(key);
706
- }
707
- }
708
- };
709
- const pruneTimer = setInterval(pruneExpiredBuckets, pruneIntervalMs);
710
- pruneTimer.unref?.();
711
- const evictOldestBucket = () => {
712
- const oldestKey = buckets.keys().next().value;
713
- if (oldestKey) {
714
- buckets.delete(oldestKey);
715
- }
716
- };
717
- return {
718
- async hit(key, hitOptions) {
719
- const now = resolveNow();
720
- const existing = buckets.get(key);
721
- if (existing && isExpired2(existing, now)) {
722
- buckets.delete(key);
723
- }
724
- const active = buckets.get(key);
725
- const bucket = active ? {
726
- ...active,
727
- attempts: active.attempts + 1
728
- } : {
729
- key,
730
- attempts: 1,
731
- expiresAt: new Date(now.getTime() + hitOptions.decaySeconds * 1e3)
732
- };
733
- if (!active && typeof maxBuckets === "number") {
734
- while (buckets.size >= maxBuckets) {
735
- evictOldestBucket();
736
- }
737
- }
738
- if (active) {
739
- buckets.delete(key);
740
- }
741
- buckets.set(key, bucket);
742
- return Object.freeze({
743
- limited: bucket.attempts > hitOptions.maxAttempts,
744
- snapshot: createSnapshot2(key, bucket, hitOptions.maxAttempts),
745
- retryAfterSeconds: Math.max(0, Math.ceil((bucket.expiresAt.getTime() - now.getTime()) / 1e3))
746
- });
747
- },
748
- async clear(key) {
749
- return buckets.delete(key);
750
- },
751
- async clearByPrefix(prefix) {
752
- let cleared = 0;
753
- for (const key of buckets.keys()) {
754
- if (!key.startsWith(prefix)) {
755
- continue;
756
- }
757
- buckets.delete(key);
758
- cleared += 1;
759
- }
760
- return cleared;
761
- },
762
- async clearAll() {
763
- const cleared = buckets.size;
764
- buckets.clear();
765
- return cleared;
766
- },
767
- async close() {
768
- clearInterval(pruneTimer);
769
- }
770
- };
771
- }
772
- var memoryRateLimitDriverInternals = {
773
- createSnapshot: createSnapshot2,
774
- isExpired: isExpired2
775
- };
776
-
777
- // src/drivers/redis.ts
778
- function assertNonNegativeInteger(value, label) {
779
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
780
- throw new TypeError(`[@holo-js/security] Redis rate-limit adapter ${label} must be a non-negative integer.`);
781
- }
782
- return value;
783
- }
784
- function createSnapshot3(key, attempts, maxAttempts, expiresAt) {
785
- return Object.freeze({
786
- limiter: "",
787
- key,
788
- attempts,
789
- maxAttempts,
790
- remainingAttempts: Math.max(0, maxAttempts - attempts),
791
- expiresAt
792
- });
793
- }
794
- function createRedisRateLimitStore(adapter, options = {}) {
795
- const resolveNow = options.now ?? (() => /* @__PURE__ */ new Date());
796
- return {
797
- async hit(key, hitOptions) {
798
- const result = await adapter.increment(key, {
799
- decaySeconds: hitOptions.decaySeconds
800
- });
801
- const attempts = assertNonNegativeInteger(result.attempts, "attempts");
802
- const ttlSeconds = assertNonNegativeInteger(result.ttlSeconds, "ttlSeconds");
803
- const now = resolveNow();
804
- const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
805
- return Object.freeze({
806
- limited: attempts > hitOptions.maxAttempts,
807
- snapshot: createSnapshot3(key, attempts, hitOptions.maxAttempts, expiresAt),
808
- retryAfterSeconds: ttlSeconds
809
- });
810
- },
811
- async clear(key) {
812
- const deleted = await adapter.del(key);
813
- return assertNonNegativeInteger(deleted, "del() result") > 0;
814
- },
815
- async clearByPrefix(prefix) {
816
- if (typeof adapter.clearByPrefix !== "function") {
817
- return 0;
818
- }
819
- const cleared = await adapter.clearByPrefix(prefix);
820
- return assertNonNegativeInteger(cleared, "clearByPrefix() result");
821
- },
822
- async clearAll() {
823
- if (typeof adapter.clearAll !== "function") {
824
- return 0;
825
- }
826
- const cleared = await adapter.clearAll();
827
- return assertNonNegativeInteger(cleared, "clearAll() result");
828
- }
829
- };
830
- }
831
- var redisRateLimitDriverInternals = {
832
- assertNonNegativeInteger,
833
- createSnapshot: createSnapshot3
834
- };
835
-
836
- // src/store.ts
837
- function normalizeStoreConfig(config) {
838
- return normalizeSecurityConfig2(config);
839
- }
840
- function createRateLimitStoreFromConfig(config, options = {}) {
841
- const normalized = normalizeStoreConfig(config);
842
- switch (normalized.rateLimit.driver) {
843
- case "memory":
844
- return createMemoryRateLimitStore();
845
- case "file": {
846
- const root = options.projectRoot ? resolve(options.projectRoot, normalized.rateLimit.file.path) : normalized.rateLimit.file.path;
847
- return createFileRateLimitStore(root);
848
- }
849
- case "redis":
850
- if (!options.redisAdapter) {
851
- throw new Error("[@holo-js/security] Redis-backed rate limits require a redis adapter.");
852
- }
853
- return createRedisRateLimitStore(options.redisAdapter);
854
- default:
855
- throw new Error(`[@holo-js/security] Unsupported rate limit driver "${normalized.rateLimit.driver}".`);
856
- }
857
- }
858
- var securityStoreInternals = {
859
- normalizeStoreConfig
860
- };
861
-
862
- // src/index.ts
863
- import { defineSecurityConfig } from "@holo-js/config";
864
- var security = Object.freeze({
865
- configureSecurityRuntime,
866
- getSecurityRuntime,
867
- getSecurityRuntimeBindings,
868
- resetSecurityRuntime,
869
- csrf,
870
- protect,
871
- defaultRateLimitKey,
872
- rateLimit,
873
- clearRateLimit,
874
- limit,
875
- ip
876
- });
877
- var src_default = security;
48
+ } from "./chunk-EWQKJSFA.mjs";
878
49
  export {
879
50
  SecurityCsrfError,
880
51
  SecurityRateLimitError,
881
52
  SecurityRuntimeNotConfiguredError,
53
+ apply as applyCors,
882
54
  clearRateLimit,
883
55
  configureSecurityRuntime,
56
+ cors,
57
+ corsInternals,
58
+ headers as createCorsHeaders,
59
+ preflight as createCorsPreflightResponse,
884
60
  cookie as createCsrfCookie,
885
61
  field as createCsrfField,
62
+ input as createCsrfInput,
886
63
  token as createCsrfToken,
887
64
  createFileRateLimitStore,
888
65
  createFileRateLimitStoreConfig,
@@ -902,6 +79,7 @@ export {
902
79
  getSecurityRuntime,
903
80
  getSecurityRuntimeBindings,
904
81
  ip,
82
+ isSecureRequest,
905
83
  limit,
906
84
  memoryRateLimitDriverInternals,
907
85
  protect,