@peac/middleware-core 0.10.9 → 0.10.11

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 CHANGED
@@ -175,7 +175,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
175
175
 
176
176
  END OF TERMS AND CONDITIONS
177
177
 
178
- Copyright 2025 PEAC Protocol Contributors
178
+ Copyright 2025-2026 PEAC Protocol Contributors
179
179
 
180
180
  Licensed under the Apache License, Version 2.0 (the "License");
181
181
  you may not use this file except in compliance with the License.
package/dist/index.cjs ADDED
@@ -0,0 +1,411 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('@peac/crypto');
4
+ var kernel = require('@peac/kernel');
5
+ var uuidv7 = require('uuidv7');
6
+ var schema = require('@peac/schema');
7
+
8
+ // src/config.ts
9
+ var MAX_PATH_LENGTH = 2048;
10
+ var CONFIG_DEFAULTS = {
11
+ expiresIn: 300,
12
+ transport: "header",
13
+ maxHeaderSize: 4096,
14
+ interactionBinding: "minimal"
15
+ };
16
+ var ConfigError = class extends Error {
17
+ errors;
18
+ constructor(errors) {
19
+ const message = errors.map((e) => `${e.field}: ${e.message}`).join("; ");
20
+ super(`Invalid middleware configuration: ${message}`);
21
+ this.name = "ConfigError";
22
+ this.errors = errors;
23
+ }
24
+ };
25
+ function isValidHttpsUrl(url) {
26
+ try {
27
+ const parsed = new URL(url);
28
+ return parsed.protocol === "https:";
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ function validateSigningKey(key, errors) {
34
+ if (!key || typeof key !== "object") {
35
+ errors.push({ field: "signingKey", message: "must be an object" });
36
+ return;
37
+ }
38
+ const jwk = key;
39
+ if (jwk.kty !== "OKP") {
40
+ errors.push({ field: "signingKey.kty", message: 'must be "OKP" for Ed25519 keys' });
41
+ }
42
+ if (jwk.crv !== "Ed25519") {
43
+ errors.push({ field: "signingKey.crv", message: 'must be "Ed25519"' });
44
+ }
45
+ if (typeof jwk.x !== "string") {
46
+ errors.push({ field: "signingKey.x", message: "must be a base64url string" });
47
+ } else {
48
+ try {
49
+ const xBytes = crypto.base64urlDecode(jwk.x);
50
+ if (xBytes.length !== 32) {
51
+ errors.push({ field: "signingKey.x", message: "must be 32 bytes (base64url encoded)" });
52
+ }
53
+ } catch {
54
+ errors.push({ field: "signingKey.x", message: "must be valid base64url encoding" });
55
+ }
56
+ }
57
+ if (typeof jwk.d !== "string") {
58
+ errors.push({ field: "signingKey.d", message: "must be a base64url string (private key)" });
59
+ } else {
60
+ try {
61
+ const dBytes = crypto.base64urlDecode(jwk.d);
62
+ if (dBytes.length !== 32) {
63
+ errors.push({ field: "signingKey.d", message: "must be 32 bytes (base64url encoded)" });
64
+ }
65
+ } catch {
66
+ errors.push({ field: "signingKey.d", message: "must be valid base64url encoding" });
67
+ }
68
+ }
69
+ }
70
+ function validateConfig(config) {
71
+ const errors = [];
72
+ if (!config.issuer) {
73
+ errors.push({ field: "issuer", message: "is required" });
74
+ } else if (!isValidHttpsUrl(config.issuer)) {
75
+ errors.push({ field: "issuer", message: "must be a valid HTTPS URL" });
76
+ }
77
+ if (!config.signingKey) {
78
+ errors.push({ field: "signingKey", message: "is required" });
79
+ } else {
80
+ validateSigningKey(config.signingKey, errors);
81
+ }
82
+ if (!config.keyId) {
83
+ errors.push({ field: "keyId", message: "is required" });
84
+ } else if (typeof config.keyId !== "string" || config.keyId.length === 0) {
85
+ errors.push({ field: "keyId", message: "must be a non-empty string" });
86
+ }
87
+ if (config.expiresIn !== void 0) {
88
+ if (!Number.isInteger(config.expiresIn) || config.expiresIn <= 0) {
89
+ errors.push({ field: "expiresIn", message: "must be a positive integer" });
90
+ }
91
+ }
92
+ if (config.transport !== void 0) {
93
+ if (!["header", "body", "pointer"].includes(config.transport)) {
94
+ errors.push({ field: "transport", message: 'must be "header", "body", or "pointer"' });
95
+ }
96
+ }
97
+ if (config.maxHeaderSize !== void 0) {
98
+ if (!Number.isInteger(config.maxHeaderSize) || config.maxHeaderSize <= 0) {
99
+ errors.push({ field: "maxHeaderSize", message: "must be a positive integer" });
100
+ }
101
+ }
102
+ if (config.transport === "pointer" && !config.pointerUrlGenerator) {
103
+ errors.push({
104
+ field: "pointerUrlGenerator",
105
+ message: 'is required when transport is "pointer"'
106
+ });
107
+ }
108
+ if (config.interactionBinding !== void 0) {
109
+ if (!["minimal", "off", "full"].includes(config.interactionBinding)) {
110
+ errors.push({ field: "interactionBinding", message: 'must be "minimal", "off", or "full"' });
111
+ }
112
+ }
113
+ if (config.claimsGenerator !== void 0 && typeof config.claimsGenerator !== "function") {
114
+ errors.push({ field: "claimsGenerator", message: "must be a function" });
115
+ }
116
+ if (config.pointerUrlGenerator !== void 0 && typeof config.pointerUrlGenerator !== "function") {
117
+ errors.push({ field: "pointerUrlGenerator", message: "must be a function" });
118
+ }
119
+ if (errors.length > 0) {
120
+ throw new ConfigError(errors);
121
+ }
122
+ }
123
+ async function validateConfigAsync(config) {
124
+ validateConfig(config);
125
+ const jwk = config.signingKey;
126
+ const isValidKeypair = await crypto.validateKeypair(jwk);
127
+ if (!isValidKeypair) {
128
+ throw new ConfigError([
129
+ {
130
+ field: "signingKey",
131
+ message: "keypair inconsistent: private key (d) does not derive to public key (x)"
132
+ }
133
+ ]);
134
+ }
135
+ }
136
+ function applyDefaults(config) {
137
+ return {
138
+ ...config,
139
+ expiresIn: config.expiresIn ?? CONFIG_DEFAULTS.expiresIn,
140
+ transport: config.transport ?? CONFIG_DEFAULTS.transport,
141
+ maxHeaderSize: config.maxHeaderSize ?? CONFIG_DEFAULTS.maxHeaderSize,
142
+ interactionBinding: config.interactionBinding ?? CONFIG_DEFAULTS.interactionBinding
143
+ };
144
+ }
145
+ var MAX_POINTER_HEADER_SIZE = 2048;
146
+ function validatePointerUrl(url) {
147
+ let parsed;
148
+ try {
149
+ parsed = new URL(url);
150
+ } catch {
151
+ throw new Error("Pointer URL is not a valid URL");
152
+ }
153
+ if (parsed.protocol !== "https:") {
154
+ throw new Error("Pointer URL must use HTTPS");
155
+ }
156
+ if (url.includes('"') || url.includes("\\")) {
157
+ throw new Error("Pointer URL contains invalid characters for structured header");
158
+ }
159
+ if (/[\x00-\x1f\x7f]/.test(url)) {
160
+ throw new Error("Pointer URL contains control characters");
161
+ }
162
+ if (url.length > MAX_POINTER_HEADER_SIZE) {
163
+ throw new Error(`Pointer URL exceeds maximum length (${MAX_POINTER_HEADER_SIZE})`);
164
+ }
165
+ }
166
+ function selectTransport(receipt, config = {}) {
167
+ const preferredTransport = config.transport ?? CONFIG_DEFAULTS.transport;
168
+ const maxHeaderSize = config.maxHeaderSize ?? CONFIG_DEFAULTS.maxHeaderSize;
169
+ if (preferredTransport === "pointer") {
170
+ return "pointer";
171
+ }
172
+ if (preferredTransport === "body") {
173
+ return "body";
174
+ }
175
+ const receiptBytes = new TextEncoder().encode(receipt);
176
+ if (receiptBytes.length > maxHeaderSize) {
177
+ return "body";
178
+ }
179
+ return "header";
180
+ }
181
+ function wrapResponse(data, receipt) {
182
+ return {
183
+ data,
184
+ peac_receipt: receipt
185
+ };
186
+ }
187
+ async function buildResponseHeaders(receipt, transport, pointerUrl) {
188
+ switch (transport) {
189
+ case "header":
190
+ return {
191
+ [kernel.HEADERS.receipt]: receipt
192
+ };
193
+ case "pointer": {
194
+ if (!pointerUrl) {
195
+ throw new Error("pointerUrl is required for pointer transport");
196
+ }
197
+ validatePointerUrl(pointerUrl);
198
+ const digestHex = await crypto.sha256Hex(receipt);
199
+ return {
200
+ [kernel.HEADERS.receiptPointer]: `sha256="${digestHex}", url="${pointerUrl}"`
201
+ };
202
+ }
203
+ case "body":
204
+ return {};
205
+ default:
206
+ throw new Error(`Unknown transport profile: ${transport}`);
207
+ }
208
+ }
209
+ async function buildReceiptResult(input) {
210
+ const headers = await buildResponseHeaders(input.receipt, input.transport, input.pointerUrl);
211
+ const result = {
212
+ receipt: input.receipt,
213
+ transport: input.transport,
214
+ headers
215
+ };
216
+ if (input.transport === "body" && input.originalBody !== void 0) {
217
+ result.bodyWrapper = wrapResponse(input.originalBody, input.receipt);
218
+ }
219
+ return result;
220
+ }
221
+ function normalizeHeaders(headers) {
222
+ const normalized = {};
223
+ for (const [key, value] of Object.entries(headers)) {
224
+ normalized[key.toLowerCase()] = value;
225
+ }
226
+ return normalized;
227
+ }
228
+ function getHeader(headers, name) {
229
+ const value = headers[name.toLowerCase()];
230
+ if (value === void 0) return void 0;
231
+ return Array.isArray(value) ? value[0] : value;
232
+ }
233
+ function normalizeIssuer(issuer) {
234
+ let end = issuer.length;
235
+ while (end > 0 && issuer[end - 1] === "/") {
236
+ end--;
237
+ }
238
+ return end === issuer.length ? issuer : issuer.slice(0, end);
239
+ }
240
+ function processPath(path, mode) {
241
+ let processedPath = path;
242
+ if (mode === "minimal") {
243
+ const queryIndex = path.indexOf("?");
244
+ if (queryIndex !== -1) {
245
+ processedPath = path.substring(0, queryIndex);
246
+ }
247
+ }
248
+ if (processedPath.length > MAX_PATH_LENGTH) {
249
+ processedPath = processedPath.substring(0, MAX_PATH_LENGTH);
250
+ }
251
+ return processedPath;
252
+ }
253
+ function extractAudience(normalizedHeaders) {
254
+ const host = getHeader(normalizedHeaders, "host");
255
+ if (host) {
256
+ return `https://${host}`;
257
+ }
258
+ const origin = getHeader(normalizedHeaders, "origin");
259
+ if (origin) {
260
+ return origin;
261
+ }
262
+ return "https://localhost";
263
+ }
264
+ function jwkToPrivateKeyBytes(jwk) {
265
+ return crypto.base64urlDecode(jwk.d);
266
+ }
267
+ async function createReceipt(config, request, response) {
268
+ validateConfig(config);
269
+ const fullConfig = applyDefaults(config);
270
+ const normalizedHeaders = normalizeHeaders(request.headers);
271
+ const rid = uuidv7.uuidv7();
272
+ const iat = Math.floor(Date.now() / 1e3);
273
+ const exp = iat + fullConfig.expiresIn;
274
+ const normalizedIssuer = normalizeIssuer(config.issuer);
275
+ const audience = extractAudience(normalizedHeaders);
276
+ const claims = {
277
+ iss: normalizedIssuer,
278
+ aud: audience,
279
+ iat,
280
+ exp,
281
+ rid
282
+ };
283
+ if (fullConfig.interactionBinding !== "off") {
284
+ const processedPath = processPath(
285
+ request.path,
286
+ fullConfig.interactionBinding
287
+ );
288
+ const interactionBinding = {
289
+ method: request.method.toUpperCase(),
290
+ path: processedPath,
291
+ status: response.statusCode
292
+ };
293
+ claims.ext = {
294
+ [schema.MIDDLEWARE_INTERACTION_KEY]: interactionBinding
295
+ };
296
+ }
297
+ if (config.claimsGenerator) {
298
+ const customClaims = await config.claimsGenerator(request);
299
+ if (customClaims.aud) {
300
+ claims.aud = customClaims.aud;
301
+ }
302
+ if (customClaims.sub) {
303
+ claims.sub = customClaims.sub;
304
+ }
305
+ if (customClaims.ext) {
306
+ claims.ext = { ...claims.ext, ...customClaims.ext };
307
+ }
308
+ }
309
+ const privateKeyBytes = jwkToPrivateKeyBytes(config.signingKey);
310
+ const receipt = await crypto.sign(claims, privateKeyBytes, config.keyId);
311
+ const transport = selectTransport(receipt, fullConfig);
312
+ let pointerUrl;
313
+ if (transport === "pointer" && config.pointerUrlGenerator) {
314
+ pointerUrl = await config.pointerUrlGenerator(receipt);
315
+ }
316
+ return buildReceiptResult({
317
+ receipt,
318
+ transport,
319
+ pointerUrl,
320
+ originalBody: response.body
321
+ });
322
+ }
323
+ async function createReceiptWithClaims(config, claims, responseBody) {
324
+ validateConfig(config);
325
+ const fullConfig = applyDefaults(config);
326
+ const rid = uuidv7.uuidv7();
327
+ const iat = Math.floor(Date.now() / 1e3);
328
+ const exp = iat + fullConfig.expiresIn;
329
+ const normalizedIssuer = normalizeIssuer(config.issuer);
330
+ const receiptClaims = {
331
+ iss: normalizedIssuer,
332
+ aud: claims.aud,
333
+ iat,
334
+ exp,
335
+ rid,
336
+ ...claims.sub && { sub: claims.sub },
337
+ ...claims.ext && { ext: claims.ext }
338
+ };
339
+ const privateKeyBytes = jwkToPrivateKeyBytes(config.signingKey);
340
+ const receipt = await crypto.sign(receiptClaims, privateKeyBytes, config.keyId);
341
+ const transport = selectTransport(receipt, fullConfig);
342
+ let pointerUrl;
343
+ if (transport === "pointer" && config.pointerUrlGenerator) {
344
+ pointerUrl = await config.pointerUrlGenerator(receipt);
345
+ }
346
+ return buildReceiptResult({
347
+ receipt,
348
+ transport,
349
+ pointerUrl,
350
+ originalBody: responseBody
351
+ });
352
+ }
353
+
354
+ // src/rate-limit.ts
355
+ var MemoryRateLimitStore = class {
356
+ store = /* @__PURE__ */ new Map();
357
+ maxKeys;
358
+ constructor(options) {
359
+ this.maxKeys = options?.maxKeys ?? 1e4;
360
+ }
361
+ async increment(key, windowMs) {
362
+ const now = Date.now();
363
+ let entry = this.store.get(key);
364
+ if (!entry || now >= entry.resetAt) {
365
+ entry = { count: 0, resetAt: now + windowMs, lastAccess: now };
366
+ }
367
+ entry.count++;
368
+ entry.lastAccess = now;
369
+ this.store.delete(key);
370
+ this.store.set(key, entry);
371
+ this.evictIfNeeded();
372
+ return { count: entry.count, resetAt: entry.resetAt };
373
+ }
374
+ async reset(key) {
375
+ this.store.delete(key);
376
+ }
377
+ /** Number of tracked keys */
378
+ get size() {
379
+ return this.store.size;
380
+ }
381
+ /** Remove all entries */
382
+ clear() {
383
+ this.store.clear();
384
+ }
385
+ evictIfNeeded() {
386
+ while (this.store.size > this.maxKeys) {
387
+ const oldestKey = this.store.keys().next().value;
388
+ if (oldestKey !== void 0) {
389
+ this.store.delete(oldestKey);
390
+ } else {
391
+ break;
392
+ }
393
+ }
394
+ }
395
+ };
396
+
397
+ exports.CONFIG_DEFAULTS = CONFIG_DEFAULTS;
398
+ exports.ConfigError = ConfigError;
399
+ exports.MAX_PATH_LENGTH = MAX_PATH_LENGTH;
400
+ exports.MemoryRateLimitStore = MemoryRateLimitStore;
401
+ exports.applyDefaults = applyDefaults;
402
+ exports.buildReceiptResult = buildReceiptResult;
403
+ exports.buildResponseHeaders = buildResponseHeaders;
404
+ exports.createReceipt = createReceipt;
405
+ exports.createReceiptWithClaims = createReceiptWithClaims;
406
+ exports.selectTransport = selectTransport;
407
+ exports.validateConfig = validateConfig;
408
+ exports.validateConfigAsync = validateConfigAsync;
409
+ exports.wrapResponse = wrapResponse;
410
+ //# sourceMappingURL=index.cjs.map
411
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/transport.ts","../src/receipt.ts","../src/rate-limit.ts"],"names":["base64urlDecode","validateKeypair","HEADERS","sha256Hex","uuidv7","MIDDLEWARE_INTERACTION_KEY","sign"],"mappings":";;;;;;;;AAcO,IAAM,eAAA,GAAkB;AAKxB,IAAM,eAAA,GAAkB;AAAA,EAC7B,SAAA,EAAW,GAAA;AAAA,EACX,SAAA,EAAW,QAAA;AAAA,EACX,aAAA,EAAe,IAAA;AAAA,EACf,kBAAA,EAAoB;AACtB;AAKO,IAAM,WAAA,GAAN,cAA0B,KAAA,CAAM;AAAA,EAC5B,MAAA;AAAA,EAET,YAAY,MAAA,EAAiC;AAC3C,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,GAAA,CAAI,CAAC,MAAM,CAAA,EAAG,CAAA,CAAE,KAAK,CAAA,EAAA,EAAK,CAAA,CAAE,OAAO,CAAA,CAAE,CAAA,CAAE,KAAK,IAAI,CAAA;AACvE,IAAA,KAAA,CAAM,CAAA,kCAAA,EAAqC,OAAO,CAAA,CAAE,CAAA;AACpD,IAAA,IAAA,CAAK,IAAA,GAAO,aAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;AAKA,SAAS,gBAAgB,GAAA,EAAsB;AAC7C,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,OAAO,OAAO,QAAA,KAAa,QAAA;AAAA,EAC7B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAKA,SAAS,kBAAA,CAAmB,KAAc,MAAA,EAAuC;AAC/E,EAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,EAAU;AACnC,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,YAAA,EAAc,OAAA,EAAS,qBAAqB,CAAA;AACjE,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,GAAA,GAAM,GAAA;AAGZ,EAAA,IAAI,GAAA,CAAI,QAAQ,KAAA,EAAO;AACrB,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,gBAAA,EAAkB,OAAA,EAAS,kCAAkC,CAAA;AAAA,EACpF;AAGA,EAAA,IAAI,GAAA,CAAI,QAAQ,SAAA,EAAW;AACzB,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,gBAAA,EAAkB,OAAA,EAAS,qBAAqB,CAAA;AAAA,EACvE;AAGA,EAAA,IAAI,OAAO,GAAA,CAAI,CAAA,KAAM,QAAA,EAAU;AAC7B,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,OAAA,EAAS,8BAA8B,CAAA;AAAA,EAC9E,CAAA,MAAO;AACL,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAASA,sBAAA,CAAgB,GAAA,CAAI,CAAC,CAAA;AACpC,MAAA,IAAI,MAAA,CAAO,WAAW,EAAA,EAAI;AACxB,QAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,OAAA,EAAS,wCAAwC,CAAA;AAAA,MACxF;AAAA,IACF,CAAA,CAAA,MAAQ;AACN,MAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,OAAA,EAAS,oCAAoC,CAAA;AAAA,IACpF;AAAA,EACF;AAGA,EAAA,IAAI,OAAO,GAAA,CAAI,CAAA,KAAM,QAAA,EAAU;AAC7B,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,OAAA,EAAS,4CAA4C,CAAA;AAAA,EAC5F,CAAA,MAAO;AACL,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAASA,sBAAA,CAAgB,GAAA,CAAI,CAAC,CAAA;AACpC,MAAA,IAAI,MAAA,CAAO,WAAW,EAAA,EAAI;AACxB,QAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,OAAA,EAAS,wCAAwC,CAAA;AAAA,MACxF;AAAA,IACF,CAAA,CAAA,MAAQ;AACN,MAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,cAAA,EAAgB,OAAA,EAAS,oCAAoC,CAAA;AAAA,IACpF;AAAA,EACF;AACF;AAmBO,SAAS,eAAe,MAAA,EAAgC;AAC7D,EAAA,MAAM,SAAkC,EAAC;AAGzC,EAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAClB,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,QAAA,EAAU,OAAA,EAAS,eAAe,CAAA;AAAA,EACzD,CAAA,MAAA,IAAW,CAAC,eAAA,CAAgB,MAAA,CAAO,MAAM,CAAA,EAAG;AAC1C,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,QAAA,EAAU,OAAA,EAAS,6BAA6B,CAAA;AAAA,EACvE;AAGA,EAAA,IAAI,CAAC,OAAO,UAAA,EAAY;AACtB,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,YAAA,EAAc,OAAA,EAAS,eAAe,CAAA;AAAA,EAC7D,CAAA,MAAO;AACL,IAAA,kBAAA,CAAmB,MAAA,CAAO,YAAY,MAAM,CAAA;AAAA,EAC9C;AAGA,EAAA,IAAI,CAAC,OAAO,KAAA,EAAO;AACjB,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,OAAA,EAAS,OAAA,EAAS,eAAe,CAAA;AAAA,EACxD,CAAA,MAAA,IAAW,OAAO,MAAA,CAAO,KAAA,KAAU,YAAY,MAAA,CAAO,KAAA,CAAM,WAAW,CAAA,EAAG;AACxE,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,OAAA,EAAS,OAAA,EAAS,8BAA8B,CAAA;AAAA,EACvE;AAGA,EAAA,IAAI,MAAA,CAAO,cAAc,MAAA,EAAW;AAClC,IAAA,IAAI,CAAC,OAAO,SAAA,CAAU,MAAA,CAAO,SAAS,CAAA,IAAK,MAAA,CAAO,aAAa,CAAA,EAAG;AAChE,MAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,WAAA,EAAa,OAAA,EAAS,8BAA8B,CAAA;AAAA,IAC3E;AAAA,EACF;AAGA,EAAA,IAAI,MAAA,CAAO,cAAc,MAAA,EAAW;AAClC,IAAA,IAAI,CAAC,CAAC,QAAA,EAAU,MAAA,EAAQ,SAAS,CAAA,CAAE,QAAA,CAAS,MAAA,CAAO,SAAS,CAAA,EAAG;AAC7D,MAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,WAAA,EAAa,OAAA,EAAS,0CAA0C,CAAA;AAAA,IACvF;AAAA,EACF;AAGA,EAAA,IAAI,MAAA,CAAO,kBAAkB,MAAA,EAAW;AACtC,IAAA,IAAI,CAAC,OAAO,SAAA,CAAU,MAAA,CAAO,aAAa,CAAA,IAAK,MAAA,CAAO,iBAAiB,CAAA,EAAG;AACxE,MAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,eAAA,EAAiB,OAAA,EAAS,8BAA8B,CAAA;AAAA,IAC/E;AAAA,EACF;AAGA,EAAA,IAAI,MAAA,CAAO,SAAA,KAAc,SAAA,IAAa,CAAC,OAAO,mBAAA,EAAqB;AACjE,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,KAAA,EAAO,qBAAA;AAAA,MACP,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAGA,EAAA,IAAI,MAAA,CAAO,uBAAuB,MAAA,EAAW;AAC3C,IAAA,IAAI,CAAC,CAAC,SAAA,EAAW,KAAA,EAAO,MAAM,CAAA,CAAE,QAAA,CAAS,MAAA,CAAO,kBAAkB,CAAA,EAAG;AACnE,MAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,oBAAA,EAAsB,OAAA,EAAS,uCAAuC,CAAA;AAAA,IAC7F;AAAA,EACF;AAGA,EAAA,IAAI,OAAO,eAAA,KAAoB,MAAA,IAAa,OAAO,MAAA,CAAO,oBAAoB,UAAA,EAAY;AACxF,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,iBAAA,EAAmB,OAAA,EAAS,sBAAsB,CAAA;AAAA,EACzE;AAGA,EAAA,IACE,OAAO,mBAAA,KAAwB,MAAA,IAC/B,OAAO,MAAA,CAAO,wBAAwB,UAAA,EACtC;AACA,IAAA,MAAA,CAAO,KAAK,EAAE,KAAA,EAAO,qBAAA,EAAuB,OAAA,EAAS,sBAAsB,CAAA;AAAA,EAC7E;AAEA,EAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG;AACrB,IAAA,MAAM,IAAI,YAAY,MAAM,CAAA;AAAA,EAC9B;AACF;AAgBA,eAAsB,oBAAoB,MAAA,EAAyC;AAEjF,EAAA,cAAA,CAAe,MAAM,CAAA;AAGrB,EAAA,MAAM,MAAM,MAAA,CAAO,UAAA;AACnB,EAAA,MAAM,cAAA,GAAiB,MAAMC,sBAAA,CAAgB,GAAG,CAAA;AAEhD,EAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB;AAAA,QACE,KAAA,EAAO,YAAA;AAAA,QACP,OAAA,EAAS;AAAA;AACX,KACD,CAAA;AAAA,EACH;AACF;AAKO,SAAS,cACd,MAAA,EAIiB;AACjB,EAAA,OAAO;AAAA,IACL,GAAG,MAAA;AAAA,IACH,SAAA,EAAW,MAAA,CAAO,SAAA,IAAa,eAAA,CAAgB,SAAA;AAAA,IAC/C,SAAA,EAAW,MAAA,CAAO,SAAA,IAAa,eAAA,CAAgB,SAAA;AAAA,IAC/C,aAAA,EAAe,MAAA,CAAO,aAAA,IAAiB,eAAA,CAAgB,aAAA;AAAA,IACvD,kBAAA,EAAoB,MAAA,CAAO,kBAAA,IAAsB,eAAA,CAAgB;AAAA,GACnE;AACF;ACrOA,IAAM,uBAAA,GAA0B,IAAA;AAOhC,SAAS,mBAAmB,GAAA,EAAmB;AAE7C,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAI,IAAI,GAAG,CAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,gCAAgC,CAAA;AAAA,EAClD;AAEA,EAAA,IAAI,MAAA,CAAO,aAAa,QAAA,EAAU;AAChC,IAAA,MAAM,IAAI,MAAM,4BAA4B,CAAA;AAAA,EAC9C;AAIA,EAAA,IAAI,IAAI,QAAA,CAAS,GAAG,KAAK,GAAA,CAAI,QAAA,CAAS,IAAI,CAAA,EAAG;AAC3C,IAAA,MAAM,IAAI,MAAM,+DAA+D,CAAA;AAAA,EACjF;AAGA,EAAA,IAAI,iBAAA,CAAkB,IAAA,CAAK,GAAG,CAAA,EAAG;AAC/B,IAAA,MAAM,IAAI,MAAM,yCAAyC,CAAA;AAAA,EAC3D;AAGA,EAAA,IAAI,GAAA,CAAI,SAAS,uBAAA,EAAyB;AACxC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oCAAA,EAAuC,uBAAuB,CAAA,CAAA,CAAG,CAAA;AAAA,EACnF;AACF;AAqBO,SAAS,eAAA,CACd,OAAA,EACA,MAAA,GAAyE,EAAC,EAC3C;AAC/B,EAAA,MAAM,kBAAA,GAAqB,MAAA,CAAO,SAAA,IAAa,eAAA,CAAgB,SAAA;AAC/D,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,aAAA,IAAiB,eAAA,CAAgB,aAAA;AAG9D,EAAA,IAAI,uBAAuB,SAAA,EAAW;AACpC,IAAA,OAAO,SAAA;AAAA,EACT;AAGA,EAAA,IAAI,uBAAuB,MAAA,EAAQ;AACjC,IAAA,OAAO,MAAA;AAAA,EACT;AAIA,EAAA,MAAM,YAAA,GAAe,IAAI,WAAA,EAAY,CAAE,OAAO,OAAO,CAAA;AACrD,EAAA,IAAI,YAAA,CAAa,SAAS,aAAA,EAAe;AACvC,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,QAAA;AACT;AAqBO,SAAS,YAAA,CAAgB,MAAS,OAAA,EAAoD;AAC3F,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,YAAA,EAAc;AAAA,GAChB;AACF;AAcA,eAAsB,oBAAA,CACpB,OAAA,EACA,SAAA,EACA,UAAA,EACiC;AACjC,EAAA,QAAQ,SAAA;AAAW,IACjB,KAAK,QAAA;AACH,MAAA,OAAO;AAAA,QACL,CAACC,cAAA,CAAQ,OAAO,GAAG;AAAA,OACrB;AAAA,IAEF,KAAK,SAAA,EAAW;AACd,MAAA,IAAI,CAAC,UAAA,EAAY;AACf,QAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAAA,MAChE;AAEA,MAAA,kBAAA,CAAmB,UAAU,CAAA;AAE7B,MAAA,MAAM,SAAA,GAAY,MAAMC,gBAAA,CAAU,OAAO,CAAA;AAEzC,MAAA,OAAO;AAAA,QACL,CAACD,cAAA,CAAQ,cAAc,GAAG,CAAA,QAAA,EAAW,SAAS,WAAW,UAAU,CAAA,CAAA;AAAA,OACrE;AAAA,IACF;AAAA,IAEA,KAAK,MAAA;AAEH,MAAA,OAAO,EAAC;AAAA,IAEV;AACE,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,2BAAA,EAA8B,SAAS,CAAA,CAAE,CAAA;AAAA;AAE/D;AAkBA,eAAsB,mBAAmB,KAAA,EAKtC;AACD,EAAA,MAAM,OAAA,GAAU,MAAM,oBAAA,CAAqB,KAAA,CAAM,SAAS,KAAA,CAAM,SAAA,EAAW,MAAM,UAAU,CAAA;AAE3F,EAAA,MAAM,MAAA,GAKF;AAAA,IACF,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,WAAW,KAAA,CAAM,SAAA;AAAA,IACjB;AAAA,GACF;AAGA,EAAA,IAAI,KAAA,CAAM,SAAA,KAAc,MAAA,IAAU,KAAA,CAAM,iBAAiB,MAAA,EAAW;AAClE,IAAA,MAAA,CAAO,WAAA,GAAc,YAAA,CAAa,KAAA,CAAM,YAAA,EAAc,MAAM,OAAO,CAAA;AAAA,EACrE;AAEA,EAAA,OAAO,MAAA;AACT;AC5IA,SAAS,iBACP,OAAA,EAC+C;AAC/C,EAAA,MAAM,aAA4D,EAAC;AACnE,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,IAAA,UAAA,CAAW,GAAA,CAAI,WAAA,EAAa,CAAA,GAAI,KAAA;AAAA,EAClC;AACA,EAAA,OAAO,UAAA;AACT;AAKA,SAAS,SAAA,CACP,SACA,IAAA,EACoB;AACpB,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,CAAK,WAAA,EAAa,CAAA;AACxC,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,MAAA;AAChC,EAAA,OAAO,MAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,KAAA,CAAM,CAAC,CAAA,GAAI,KAAA;AAC3C;AAOA,SAAS,gBAAgB,MAAA,EAAwB;AAC/C,EAAA,IAAI,MAAM,MAAA,CAAO,MAAA;AACjB,EAAA,OAAO,MAAM,CAAA,IAAK,MAAA,CAAO,GAAA,GAAM,CAAC,MAAM,GAAA,EAAK;AACzC,IAAA,GAAA,EAAA;AAAA,EACF;AACA,EAAA,OAAO,QAAQ,MAAA,CAAO,MAAA,GAAS,SAAS,MAAA,CAAO,KAAA,CAAM,GAAG,GAAG,CAAA;AAC7D;AAYA,SAAS,WAAA,CAAY,MAAc,IAAA,EAAkC;AACnE,EAAA,IAAI,aAAA,GAAgB,IAAA;AAGpB,EAAA,IAAI,SAAS,SAAA,EAAW;AACtB,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAA;AACnC,IAAA,IAAI,eAAe,EAAA,EAAI;AACrB,MAAA,aAAA,GAAgB,IAAA,CAAK,SAAA,CAAU,CAAA,EAAG,UAAU,CAAA;AAAA,IAC9C;AAAA,EACF;AAGA,EAAA,IAAI,aAAA,CAAc,SAAS,eAAA,EAAiB;AAC1C,IAAA,aAAA,GAAgB,aAAA,CAAc,SAAA,CAAU,CAAA,EAAG,eAAe,CAAA;AAAA,EAC5D;AAEA,EAAA,OAAO,aAAA;AACT;AAOA,SAAS,gBAAgB,iBAAA,EAA0E;AAEjG,EAAA,MAAM,IAAA,GAAO,SAAA,CAAU,iBAAA,EAAmB,MAAM,CAAA;AAChD,EAAA,IAAI,IAAA,EAAM;AAER,IAAA,OAAO,WAAW,IAAI,CAAA,CAAA;AAAA,EACxB;AAGA,EAAA,MAAM,MAAA,GAAS,SAAA,CAAU,iBAAA,EAAmB,QAAQ,CAAA;AACpD,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,OAAO,MAAA;AAAA,EACT;AAGA,EAAA,OAAO,mBAAA;AACT;AAKA,SAAS,qBAAqB,GAAA,EAAgC;AAC5D,EAAA,OAAOF,sBAAAA,CAAgB,IAAI,CAAC,CAAA;AAC9B;AAgCA,eAAsB,aAAA,CACpB,MAAA,EACA,OAAA,EACA,QAAA,EACwB;AAExB,EAAA,cAAA,CAAe,MAAM,CAAA;AAGrB,EAAA,MAAM,UAAA,GAAa,cAAc,MAAM,CAAA;AAGvC,EAAA,MAAM,iBAAA,GAAoB,gBAAA,CAAiB,OAAA,CAAQ,OAAO,CAAA;AAG1D,EAAA,MAAM,MAAMI,aAAA,EAAO;AAGnB,EAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AACxC,EAAA,MAAM,GAAA,GAAM,MAAM,UAAA,CAAW,SAAA;AAG7B,EAAA,MAAM,gBAAA,GAAmB,eAAA,CAAgB,MAAA,CAAO,MAAM,CAAA;AACtD,EAAA,MAAM,QAAA,GAAW,gBAAgB,iBAAiB,CAAA;AAGlD,EAAA,MAAM,MAAA,GAAmC;AAAA,IACvC,GAAA,EAAK,gBAAA;AAAA,IACL,GAAA,EAAK,QAAA;AAAA,IACL,GAAA;AAAA,IACA,GAAA;AAAA,IACA;AAAA,GACF;AAGA,EAAA,IAAI,UAAA,CAAW,uBAAuB,KAAA,EAAO;AAC3C,IAAA,MAAM,aAAA,GAAgB,WAAA;AAAA,MACpB,OAAA,CAAQ,IAAA;AAAA,MACR,UAAA,CAAW;AAAA,KACb;AACA,IAAA,MAAM,kBAAA,GAAyC;AAAA,MAC7C,MAAA,EAAQ,OAAA,CAAQ,MAAA,CAAO,WAAA,EAAY;AAAA,MACnC,IAAA,EAAM,aAAA;AAAA,MACN,QAAQ,QAAA,CAAS;AAAA,KACnB;AACA,IAAA,MAAA,CAAO,GAAA,GAAM;AAAA,MACX,CAACC,iCAA0B,GAAG;AAAA,KAChC;AAAA,EACF;AAGA,EAAA,IAAI,OAAO,eAAA,EAAiB;AAC1B,IAAA,MAAM,YAAA,GAAe,MAAM,MAAA,CAAO,eAAA,CAAgB,OAAO,CAAA;AAGzD,IAAA,IAAI,aAAa,GAAA,EAAK;AACpB,MAAA,MAAA,CAAO,MAAM,YAAA,CAAa,GAAA;AAAA,IAC5B;AAGA,IAAA,IAAI,aAAa,GAAA,EAAK;AACpB,MAAA,MAAA,CAAO,MAAM,YAAA,CAAa,GAAA;AAAA,IAC5B;AAGA,IAAA,IAAI,aAAa,GAAA,EAAK;AACpB,MAAA,MAAA,CAAO,MAAM,EAAE,GAAG,OAAO,GAAA,EAAK,GAAG,aAAa,GAAA,EAAI;AAAA,IACpD;AAAA,EACF;AAGA,EAAA,MAAM,eAAA,GAAkB,oBAAA,CAAqB,MAAA,CAAO,UAAU,CAAA;AAC9D,EAAA,MAAM,UAAU,MAAMC,WAAA,CAAK,MAAA,EAAQ,eAAA,EAAiB,OAAO,KAAK,CAAA;AAGhE,EAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,OAAA,EAAS,UAAU,CAAA;AAGrD,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI,SAAA,KAAc,SAAA,IAAa,MAAA,CAAO,mBAAA,EAAqB;AACzD,IAAA,UAAA,GAAa,MAAM,MAAA,CAAO,mBAAA,CAAoB,OAAO,CAAA;AAAA,EACvD;AAGA,EAAA,OAAO,kBAAA,CAAmB;AAAA,IACxB,OAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA,cAAc,QAAA,CAAS;AAAA,GACxB,CAAA;AACH;AAaA,eAAsB,uBAAA,CACpB,MAAA,EACA,MAAA,EACA,YAAA,EACwB;AAExB,EAAA,cAAA,CAAe,MAAM,CAAA;AAGrB,EAAA,MAAM,UAAA,GAAa,cAAc,MAAM,CAAA;AAGvC,EAAA,MAAM,MAAMF,aAAA,EAAO;AAGnB,EAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AACxC,EAAA,MAAM,GAAA,GAAM,MAAM,UAAA,CAAW,SAAA;AAG7B,EAAA,MAAM,gBAAA,GAAmB,eAAA,CAAgB,MAAA,CAAO,MAAM,CAAA;AAGtD,EAAA,MAAM,aAAA,GAA0C;AAAA,IAC9C,GAAA,EAAK,gBAAA;AAAA,IACL,KAAK,MAAA,CAAO,GAAA;AAAA,IACZ,GAAA;AAAA,IACA,GAAA;AAAA,IACA,GAAA;AAAA,IACA,GAAI,MAAA,CAAO,GAAA,IAAO,EAAE,GAAA,EAAK,OAAO,GAAA,EAAI;AAAA,IACpC,GAAI,MAAA,CAAO,GAAA,IAAO,EAAE,GAAA,EAAK,OAAO,GAAA;AAAI,GACtC;AAGA,EAAA,MAAM,eAAA,GAAkB,oBAAA,CAAqB,MAAA,CAAO,UAAU,CAAA;AAC9D,EAAA,MAAM,UAAU,MAAME,WAAA,CAAK,aAAA,EAAe,eAAA,EAAiB,OAAO,KAAK,CAAA;AAGvE,EAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,OAAA,EAAS,UAAU,CAAA;AAGrD,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI,SAAA,KAAc,SAAA,IAAa,MAAA,CAAO,mBAAA,EAAqB;AACzD,IAAA,UAAA,GAAa,MAAM,MAAA,CAAO,mBAAA,CAAoB,OAAO,CAAA;AAAA,EACvD;AAGA,EAAA,OAAO,kBAAA,CAAmB;AAAA,IACxB,OAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA,YAAA,EAAc;AAAA,GACf,CAAA;AACH;;;ACvTO,IAAM,uBAAN,MAAqD;AAAA,EACzC,KAAA,uBAAY,GAAA,EAAyB;AAAA,EACrC,OAAA;AAAA,EAEjB,YAAY,OAAA,EAAuC;AACjD,IAAA,IAAA,CAAK,OAAA,GAAU,SAAS,OAAA,IAAW,GAAA;AAAA,EACrC;AAAA,EAEA,MAAM,SAAA,CAAU,GAAA,EAAa,QAAA,EAA+D;AAC1F,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,IAAI,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAE9B,IAAA,IAAI,CAAC,KAAA,IAAS,GAAA,IAAO,KAAA,CAAM,OAAA,EAAS;AAElC,MAAA,KAAA,GAAQ,EAAE,KAAA,EAAO,CAAA,EAAG,SAAS,GAAA,GAAM,QAAA,EAAU,YAAY,GAAA,EAAI;AAAA,IAC/D;AAEA,IAAA,KAAA,CAAM,KAAA,EAAA;AACN,IAAA,KAAA,CAAM,UAAA,GAAa,GAAA;AAGnB,IAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AACrB,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAGzB,IAAA,IAAA,CAAK,aAAA,EAAc;AAEnB,IAAA,OAAO,EAAE,KAAA,EAAO,KAAA,CAAM,KAAA,EAAO,OAAA,EAAS,MAAM,OAAA,EAAQ;AAAA,EACtD;AAAA,EAEA,MAAM,MAAM,GAAA,EAA4B;AACtC,IAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,EACvB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,KAAA,CAAM,IAAA;AAAA,EACpB;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,MAAM,KAAA,EAAM;AAAA,EACnB;AAAA,EAEQ,aAAA,GAAsB;AAC5B,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,IAAA,CAAK,OAAA,EAAS;AAErC,MAAA,MAAM,YAAY,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK,CAAE,MAAK,CAAE,KAAA;AAC3C,MAAA,IAAI,cAAc,MAAA,EAAW;AAC3B,QAAA,IAAA,CAAK,KAAA,CAAM,OAAO,SAAS,CAAA;AAAA,MAC7B,CAAA,MAAO;AACL,QAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF","file":"index.cjs","sourcesContent":["/**\n * Middleware Configuration Validation\n *\n * Validates middleware configuration before use.\n *\n * @packageDocumentation\n */\n\nimport { base64urlDecode, validateKeypair } from '@peac/crypto';\nimport type { MiddlewareConfig, ConfigValidationError, Ed25519PrivateJwk } from './types.js';\n\n/**\n * Maximum path length for interaction binding (DoS protection)\n */\nexport const MAX_PATH_LENGTH = 2048;\n\n/**\n * Default configuration values\n */\nexport const CONFIG_DEFAULTS = {\n expiresIn: 300,\n transport: 'header' as const,\n maxHeaderSize: 4096,\n interactionBinding: 'minimal' as const,\n} as const;\n\n/**\n * Configuration validation error\n */\nexport class ConfigError extends Error {\n readonly errors: ConfigValidationError[];\n\n constructor(errors: ConfigValidationError[]) {\n const message = errors.map((e) => `${e.field}: ${e.message}`).join('; ');\n super(`Invalid middleware configuration: ${message}`);\n this.name = 'ConfigError';\n this.errors = errors;\n }\n}\n\n/**\n * Validate a URL is HTTPS\n */\nfunction isValidHttpsUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return parsed.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\n/**\n * Validate Ed25519 JWK private key\n */\nfunction validateSigningKey(key: unknown, errors: ConfigValidationError[]): void {\n if (!key || typeof key !== 'object') {\n errors.push({ field: 'signingKey', message: 'must be an object' });\n return;\n }\n\n const jwk = key as Record<string, unknown>;\n\n // Validate key type\n if (jwk.kty !== 'OKP') {\n errors.push({ field: 'signingKey.kty', message: 'must be \"OKP\" for Ed25519 keys' });\n }\n\n // Validate curve\n if (jwk.crv !== 'Ed25519') {\n errors.push({ field: 'signingKey.crv', message: 'must be \"Ed25519\"' });\n }\n\n // Validate public key\n if (typeof jwk.x !== 'string') {\n errors.push({ field: 'signingKey.x', message: 'must be a base64url string' });\n } else {\n try {\n const xBytes = base64urlDecode(jwk.x);\n if (xBytes.length !== 32) {\n errors.push({ field: 'signingKey.x', message: 'must be 32 bytes (base64url encoded)' });\n }\n } catch {\n errors.push({ field: 'signingKey.x', message: 'must be valid base64url encoding' });\n }\n }\n\n // Validate private key\n if (typeof jwk.d !== 'string') {\n errors.push({ field: 'signingKey.d', message: 'must be a base64url string (private key)' });\n } else {\n try {\n const dBytes = base64urlDecode(jwk.d);\n if (dBytes.length !== 32) {\n errors.push({ field: 'signingKey.d', message: 'must be 32 bytes (base64url encoded)' });\n }\n } catch {\n errors.push({ field: 'signingKey.d', message: 'must be valid base64url encoding' });\n }\n }\n}\n\n/**\n * Validate middleware configuration\n *\n * @param config - Configuration to validate\n * @throws ConfigError if configuration is invalid\n *\n * @example\n * ```typescript\n * try {\n * validateConfig(config);\n * } catch (e) {\n * if (e instanceof ConfigError) {\n * console.error('Config errors:', e.errors);\n * }\n * }\n * ```\n */\nexport function validateConfig(config: MiddlewareConfig): void {\n const errors: ConfigValidationError[] = [];\n\n // Validate issuer URL\n if (!config.issuer) {\n errors.push({ field: 'issuer', message: 'is required' });\n } else if (!isValidHttpsUrl(config.issuer)) {\n errors.push({ field: 'issuer', message: 'must be a valid HTTPS URL' });\n }\n\n // Validate signing key\n if (!config.signingKey) {\n errors.push({ field: 'signingKey', message: 'is required' });\n } else {\n validateSigningKey(config.signingKey, errors);\n }\n\n // Validate key ID\n if (!config.keyId) {\n errors.push({ field: 'keyId', message: 'is required' });\n } else if (typeof config.keyId !== 'string' || config.keyId.length === 0) {\n errors.push({ field: 'keyId', message: 'must be a non-empty string' });\n }\n\n // Validate expiresIn (if provided)\n if (config.expiresIn !== undefined) {\n if (!Number.isInteger(config.expiresIn) || config.expiresIn <= 0) {\n errors.push({ field: 'expiresIn', message: 'must be a positive integer' });\n }\n }\n\n // Validate transport (if provided)\n if (config.transport !== undefined) {\n if (!['header', 'body', 'pointer'].includes(config.transport)) {\n errors.push({ field: 'transport', message: 'must be \"header\", \"body\", or \"pointer\"' });\n }\n }\n\n // Validate maxHeaderSize (if provided)\n if (config.maxHeaderSize !== undefined) {\n if (!Number.isInteger(config.maxHeaderSize) || config.maxHeaderSize <= 0) {\n errors.push({ field: 'maxHeaderSize', message: 'must be a positive integer' });\n }\n }\n\n // Validate pointer transport has URL generator\n if (config.transport === 'pointer' && !config.pointerUrlGenerator) {\n errors.push({\n field: 'pointerUrlGenerator',\n message: 'is required when transport is \"pointer\"',\n });\n }\n\n // Validate interactionBinding (if provided)\n if (config.interactionBinding !== undefined) {\n if (!['minimal', 'off', 'full'].includes(config.interactionBinding)) {\n errors.push({ field: 'interactionBinding', message: 'must be \"minimal\", \"off\", or \"full\"' });\n }\n }\n\n // Validate claimsGenerator is a function (if provided)\n if (config.claimsGenerator !== undefined && typeof config.claimsGenerator !== 'function') {\n errors.push({ field: 'claimsGenerator', message: 'must be a function' });\n }\n\n // Validate pointerUrlGenerator is a function (if provided)\n if (\n config.pointerUrlGenerator !== undefined &&\n typeof config.pointerUrlGenerator !== 'function'\n ) {\n errors.push({ field: 'pointerUrlGenerator', message: 'must be a function' });\n }\n\n if (errors.length > 0) {\n throw new ConfigError(errors);\n }\n}\n\n/**\n * Validate middleware configuration with async keypair verification\n *\n * This extends basic validation with cryptographic verification that\n * the private key (d) correctly derives to the public key (x).\n *\n * @param config - Configuration to validate\n * @throws ConfigError if configuration is invalid\n *\n * @example\n * ```typescript\n * await validateConfigAsync(config);\n * ```\n */\nexport async function validateConfigAsync(config: MiddlewareConfig): Promise<void> {\n // First run synchronous validation\n validateConfig(config);\n\n // Then validate keypair consistency (d derives to x)\n const jwk = config.signingKey as Ed25519PrivateJwk;\n const isValidKeypair = await validateKeypair(jwk);\n\n if (!isValidKeypair) {\n throw new ConfigError([\n {\n field: 'signingKey',\n message: 'keypair inconsistent: private key (d) does not derive to public key (x)',\n },\n ]);\n }\n}\n\n/**\n * Apply default values to configuration\n */\nexport function applyDefaults(\n config: MiddlewareConfig\n): Required<\n Pick<MiddlewareConfig, 'expiresIn' | 'transport' | 'maxHeaderSize' | 'interactionBinding'>\n> &\n MiddlewareConfig {\n return {\n ...config,\n expiresIn: config.expiresIn ?? CONFIG_DEFAULTS.expiresIn,\n transport: config.transport ?? CONFIG_DEFAULTS.transport,\n maxHeaderSize: config.maxHeaderSize ?? CONFIG_DEFAULTS.maxHeaderSize,\n interactionBinding: config.interactionBinding ?? CONFIG_DEFAULTS.interactionBinding,\n };\n}\n","/**\n * Transport Profile Selection and Response Wrapping\n *\n * Implements transport profile selection and body wrapping per TRANSPORT-PROFILES.md.\n *\n * @packageDocumentation\n */\n\nimport { HEADERS } from '@peac/kernel';\nimport { sha256Hex } from '@peac/crypto';\nimport { CONFIG_DEFAULTS } from './config.js';\nimport type { MiddlewareConfig } from './types.js';\n\n/**\n * Maximum pointer header size (prevents oversized headers)\n */\nconst MAX_POINTER_HEADER_SIZE = 2048;\n\n/**\n * Validate pointer URL for safety (SSRF prevention and header injection)\n *\n * @throws Error if URL is invalid or unsafe\n */\nfunction validatePointerUrl(url: string): void {\n // Must be absolute HTTPS URL\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error('Pointer URL is not a valid URL');\n }\n\n if (parsed.protocol !== 'https:') {\n throw new Error('Pointer URL must use HTTPS');\n }\n\n // Prevent header injection via URL content\n // RFC 8941 quoted strings cannot contain \" or \\ without escaping\n if (url.includes('\"') || url.includes('\\\\')) {\n throw new Error('Pointer URL contains invalid characters for structured header');\n }\n\n // Control characters are never valid in headers\n if (/[\\x00-\\x1f\\x7f]/.test(url)) {\n throw new Error('Pointer URL contains control characters');\n }\n\n // Sanity check on length to prevent oversized headers\n if (url.length > MAX_POINTER_HEADER_SIZE) {\n throw new Error(`Pointer URL exceeds maximum length (${MAX_POINTER_HEADER_SIZE})`);\n }\n}\n\n/**\n * Calculate receipt size and determine appropriate transport\n *\n * Falls back from header to body if receipt exceeds maxHeaderSize.\n * Pointer transport is only used if explicitly configured (never auto-selected).\n *\n * @param receipt - JWS compact serialization\n * @param config - Transport configuration (optional fields)\n * @returns Selected transport profile\n *\n * @example\n * ```typescript\n * const transport = selectTransport(receipt, { maxHeaderSize: 4096 });\n * // Returns 'header' if receipt fits, 'body' otherwise\n *\n * // Also accepts empty config for defaults\n * const transport = selectTransport(receipt, {});\n * ```\n */\nexport function selectTransport(\n receipt: string,\n config: Partial<Pick<MiddlewareConfig, 'transport' | 'maxHeaderSize'>> = {}\n): 'header' | 'body' | 'pointer' {\n const preferredTransport = config.transport ?? CONFIG_DEFAULTS.transport;\n const maxHeaderSize = config.maxHeaderSize ?? CONFIG_DEFAULTS.maxHeaderSize;\n\n // Pointer is only used when explicitly configured\n if (preferredTransport === 'pointer') {\n return 'pointer';\n }\n\n // Body is used when explicitly configured\n if (preferredTransport === 'body') {\n return 'body';\n }\n\n // For header preference, check size and fallback to body if needed\n // Header profile uses the receipt directly as the header value\n const receiptBytes = new TextEncoder().encode(receipt);\n if (receiptBytes.length > maxHeaderSize) {\n return 'body';\n }\n\n return 'header';\n}\n\n/**\n * Wrap a response body with a PEAC receipt (for body transport profile)\n *\n * Creates a wrapper object containing the original data and the receipt.\n * Per TRANSPORT-PROFILES.md, the body format is:\n * `{ \"data\": <original>, \"peac_receipt\": \"<jws>\" }`\n *\n * @param data - Original response body\n * @param receipt - JWS compact serialization\n * @returns Wrapped response body\n *\n * @example\n * ```typescript\n * const original = { items: [1, 2, 3] };\n * const wrapped = wrapResponse(original, receipt);\n * // { data: { items: [1, 2, 3] }, peac_receipt: \"eyJ...\" }\n * res.json(wrapped);\n * ```\n */\nexport function wrapResponse<T>(data: T, receipt: string): { data: T; peac_receipt: string } {\n return {\n data,\n peac_receipt: receipt,\n };\n}\n\n/**\n * Build response headers for a receipt based on transport profile\n *\n * For header profile: adds PEAC-Receipt header\n * For pointer profile: adds PEAC-Receipt-Pointer header\n * For body profile: returns empty headers (receipt is in body)\n *\n * @param receipt - JWS compact serialization\n * @param transport - Transport profile to use\n * @param pointerUrl - URL for pointer profile (required if transport is 'pointer')\n * @returns Headers to add to response\n */\nexport async function buildResponseHeaders(\n receipt: string,\n transport: 'header' | 'body' | 'pointer',\n pointerUrl?: string\n): Promise<Record<string, string>> {\n switch (transport) {\n case 'header':\n return {\n [HEADERS.receipt]: receipt,\n };\n\n case 'pointer': {\n if (!pointerUrl) {\n throw new Error('pointerUrl is required for pointer transport');\n }\n // Validate pointer URL (SSRF prevention, header injection prevention)\n validatePointerUrl(pointerUrl);\n // Compute SHA-256 digest of receipt (lowercase hex)\n const digestHex = await sha256Hex(receipt);\n // RFC 8941 structured header dictionary format with quoted strings\n return {\n [HEADERS.receiptPointer]: `sha256=\"${digestHex}\", url=\"${pointerUrl}\"`,\n };\n }\n\n case 'body':\n // No headers for body profile\n return {};\n\n default:\n throw new Error(`Unknown transport profile: ${transport}`);\n }\n}\n\n/**\n * Input for building complete receipt result\n */\nexport interface BuildReceiptResultInput {\n receipt: string;\n transport: 'header' | 'body' | 'pointer';\n pointerUrl?: string;\n originalBody?: unknown;\n}\n\n/**\n * Build complete receipt result with headers and optional body wrapper\n *\n * @param input - Receipt and transport information\n * @returns Complete receipt result ready for response\n */\nexport async function buildReceiptResult(input: BuildReceiptResultInput): Promise<{\n receipt: string;\n transport: 'header' | 'body' | 'pointer';\n headers: Record<string, string>;\n bodyWrapper?: { data: unknown; peac_receipt: string };\n}> {\n const headers = await buildResponseHeaders(input.receipt, input.transport, input.pointerUrl);\n\n const result: {\n receipt: string;\n transport: 'header' | 'body' | 'pointer';\n headers: Record<string, string>;\n bodyWrapper?: { data: unknown; peac_receipt: string };\n } = {\n receipt: input.receipt,\n transport: input.transport,\n headers,\n };\n\n // Add body wrapper for body transport\n if (input.transport === 'body' && input.originalBody !== undefined) {\n result.bodyWrapper = wrapResponse(input.originalBody, input.receipt);\n }\n\n return result;\n}\n","/**\n * Receipt Generation\n *\n * Creates PEAC receipts from request/response context.\n *\n * This module produces **attestation receipts** - lightweight signed tokens\n * that attest to API interactions. For full payment receipts with amt/cur/payment\n * fields, use @peac/protocol directly.\n *\n * @packageDocumentation\n */\n\nimport { uuidv7 } from 'uuidv7';\nimport { sign, base64urlDecode } from '@peac/crypto';\nimport { MIDDLEWARE_INTERACTION_KEY } from '@peac/schema';\nimport type {\n MiddlewareConfig,\n RequestContext,\n ResponseContext,\n ReceiptResult,\n ReceiptClaimsInput,\n} from './types.js';\nimport { validateConfig, applyDefaults, MAX_PATH_LENGTH } from './config.js';\nimport { selectTransport, buildReceiptResult } from './transport.js';\n\n/**\n * PEAC Attestation Receipt claims structure\n *\n * This is an attestation receipt format for middleware use - it attests to\n * API interactions without payment fields. For full payment receipts with\n * amt/cur/payment, use @peac/protocol issue() directly.\n *\n * Claims structure:\n * - Core JWT claims: iss, aud, iat, exp\n * - PEAC claims: rid (UUIDv7 receipt ID)\n * - Optional: sub, ext (extensions including interaction binding)\n */\ninterface AttestationReceiptClaims {\n /** Issuer URL (normalized, no trailing slash) */\n iss: string;\n /** Audience URL */\n aud: string;\n /** Issued at (Unix seconds) */\n iat: number;\n /** Expiration (Unix seconds) */\n exp: number;\n /** Receipt ID (UUIDv7) */\n rid: string;\n /** Subject identifier (optional) */\n sub?: string;\n /** Extensions (optional) */\n ext?: Record<string, unknown>;\n}\n\n/**\n * Interaction binding data included in ext by default\n */\ninterface InteractionBinding {\n /** HTTP method */\n method: string;\n /** Request path */\n path: string;\n /** Response status code */\n status: number;\n}\n\n/**\n * Create a lowercase header lookup map for case-insensitive access\n *\n * HTTP headers are case-insensitive per RFC 7230, but different frameworks\n * may provide them with different casing. This normalizes to lowercase.\n */\nfunction normalizeHeaders(\n headers: Record<string, string | string[] | undefined>\n): Record<string, string | string[] | undefined> {\n const normalized: Record<string, string | string[] | undefined> = {};\n for (const [key, value] of Object.entries(headers)) {\n normalized[key.toLowerCase()] = value;\n }\n return normalized;\n}\n\n/**\n * Get a header value (case-insensitive)\n */\nfunction getHeader(\n headers: Record<string, string | string[] | undefined>,\n name: string\n): string | undefined {\n const value = headers[name.toLowerCase()];\n if (value === undefined) return undefined;\n return Array.isArray(value) ? value[0] : value;\n}\n\n/**\n * Normalize issuer URL (remove trailing slashes for consistency)\n *\n * Uses explicit loop instead of regex to avoid ReDoS with quantifiers.\n */\nfunction normalizeIssuer(issuer: string): string {\n let end = issuer.length;\n while (end > 0 && issuer[end - 1] === '/') {\n end--;\n }\n return end === issuer.length ? issuer : issuer.slice(0, end);\n}\n\n/**\n * Process path for interaction binding based on mode\n *\n * - 'minimal': Strip query string, truncate to MAX_PATH_LENGTH\n * - 'full': Keep full path with query string, truncate to MAX_PATH_LENGTH\n *\n * @param path - Original request path (may include query string)\n * @param mode - Interaction binding mode\n * @returns Processed path\n */\nfunction processPath(path: string, mode: 'minimal' | 'full'): string {\n let processedPath = path;\n\n // Strip query string in minimal mode (privacy-safe default)\n if (mode === 'minimal') {\n const queryIndex = path.indexOf('?');\n if (queryIndex !== -1) {\n processedPath = path.substring(0, queryIndex);\n }\n }\n\n // Truncate to maximum length (DoS protection)\n if (processedPath.length > MAX_PATH_LENGTH) {\n processedPath = processedPath.substring(0, MAX_PATH_LENGTH);\n }\n\n return processedPath;\n}\n\n/**\n * Extract audience from request context\n *\n * Derives audience from the request host or origin header (case-insensitive).\n */\nfunction extractAudience(normalizedHeaders: Record<string, string | string[] | undefined>): string {\n // Try to get from Host header (case-insensitive)\n const host = getHeader(normalizedHeaders, 'host');\n if (host) {\n // Assume HTTPS for audience\n return `https://${host}`;\n }\n\n // Fallback to origin header\n const origin = getHeader(normalizedHeaders, 'origin');\n if (origin) {\n return origin;\n }\n\n // Should not happen with proper request context\n return 'https://localhost';\n}\n\n/**\n * Convert JWK private key to raw bytes\n */\nfunction jwkToPrivateKeyBytes(jwk: { d: string }): Uint8Array {\n return base64urlDecode(jwk.d);\n}\n\n/**\n * Create a receipt for a request/response pair\n *\n * This is the main function for middleware receipt generation.\n * It validates configuration, builds claims, signs the receipt,\n * and determines the appropriate transport profile.\n *\n * By default, includes minimal interaction binding (method, path, status)\n * in the `ext[MIDDLEWARE_INTERACTION_KEY]` field for evidentiary value.\n *\n * @param config - Middleware configuration\n * @param request - Request context\n * @param response - Response context\n * @returns Receipt result with JWS, headers, and optional body wrapper\n *\n * @example\n * ```typescript\n * const result = await createReceipt(config, requestCtx, responseCtx);\n *\n * // Add headers to response\n * for (const [key, value] of Object.entries(result.headers)) {\n * res.setHeader(key, value);\n * }\n *\n * // If body transport, use wrapped body\n * if (result.bodyWrapper) {\n * res.json(result.bodyWrapper);\n * }\n * ```\n */\nexport async function createReceipt(\n config: MiddlewareConfig,\n request: RequestContext,\n response: ResponseContext\n): Promise<ReceiptResult> {\n // Validate configuration\n validateConfig(config);\n\n // Apply defaults\n const fullConfig = applyDefaults(config);\n\n // Normalize headers for case-insensitive access\n const normalizedHeaders = normalizeHeaders(request.headers);\n\n // Generate receipt ID (UUIDv7 for time-ordering)\n const rid = uuidv7();\n\n // Get current timestamp\n const iat = Math.floor(Date.now() / 1000);\n const exp = iat + fullConfig.expiresIn;\n\n // Normalize issuer and extract audience\n const normalizedIssuer = normalizeIssuer(config.issuer);\n const audience = extractAudience(normalizedHeaders);\n\n // Build base claims\n const claims: AttestationReceiptClaims = {\n iss: normalizedIssuer,\n aud: audience,\n iat,\n exp,\n rid,\n };\n\n // Add interaction binding unless disabled\n if (fullConfig.interactionBinding !== 'off') {\n const processedPath = processPath(\n request.path,\n fullConfig.interactionBinding as 'minimal' | 'full'\n );\n const interactionBinding: InteractionBinding = {\n method: request.method.toUpperCase(),\n path: processedPath,\n status: response.statusCode,\n };\n claims.ext = {\n [MIDDLEWARE_INTERACTION_KEY]: interactionBinding,\n };\n }\n\n // Apply custom claims if generator is provided\n if (config.claimsGenerator) {\n const customClaims = await config.claimsGenerator(request);\n\n // Override audience if provided\n if (customClaims.aud) {\n claims.aud = customClaims.aud;\n }\n\n // Add subject if provided\n if (customClaims.sub) {\n claims.sub = customClaims.sub;\n }\n\n // Merge extensions (custom claims override defaults)\n if (customClaims.ext) {\n claims.ext = { ...claims.ext, ...customClaims.ext };\n }\n }\n\n // Sign the receipt\n const privateKeyBytes = jwkToPrivateKeyBytes(config.signingKey);\n const receipt = await sign(claims, privateKeyBytes, config.keyId);\n\n // Determine transport profile\n const transport = selectTransport(receipt, fullConfig);\n\n // Generate pointer URL if needed\n let pointerUrl: string | undefined;\n if (transport === 'pointer' && config.pointerUrlGenerator) {\n pointerUrl = await config.pointerUrlGenerator(receipt);\n }\n\n // Build complete result\n return buildReceiptResult({\n receipt,\n transport,\n pointerUrl,\n originalBody: response.body,\n });\n}\n\n/**\n * Create a receipt with explicit claims (bypasses context extraction)\n *\n * Use this when you have explicit claims and don't need context extraction.\n * Does NOT include automatic interaction binding.\n *\n * @param config - Middleware configuration\n * @param claims - Explicit claims to include\n * @param responseBody - Optional response body for body transport\n * @returns Receipt result\n */\nexport async function createReceiptWithClaims(\n config: MiddlewareConfig,\n claims: ReceiptClaimsInput & { aud: string },\n responseBody?: unknown\n): Promise<ReceiptResult> {\n // Validate configuration\n validateConfig(config);\n\n // Apply defaults\n const fullConfig = applyDefaults(config);\n\n // Generate receipt ID\n const rid = uuidv7();\n\n // Get current timestamp\n const iat = Math.floor(Date.now() / 1000);\n const exp = iat + fullConfig.expiresIn;\n\n // Normalize issuer\n const normalizedIssuer = normalizeIssuer(config.issuer);\n\n // Build claims\n const receiptClaims: AttestationReceiptClaims = {\n iss: normalizedIssuer,\n aud: claims.aud,\n iat,\n exp,\n rid,\n ...(claims.sub && { sub: claims.sub }),\n ...(claims.ext && { ext: claims.ext }),\n };\n\n // Sign the receipt\n const privateKeyBytes = jwkToPrivateKeyBytes(config.signingKey);\n const receipt = await sign(receiptClaims, privateKeyBytes, config.keyId);\n\n // Determine transport profile\n const transport = selectTransport(receipt, fullConfig);\n\n // Generate pointer URL if needed\n let pointerUrl: string | undefined;\n if (transport === 'pointer' && config.pointerUrlGenerator) {\n pointerUrl = await config.pointerUrlGenerator(receipt);\n }\n\n // Build complete result\n return buildReceiptResult({\n receipt,\n transport,\n pointerUrl,\n originalBody: responseBody,\n });\n}\n","/**\n * Rate-limit store interface and bounded in-memory implementation.\n *\n * Provides a pluggable rate-limit store abstraction. The default\n * MemoryRateLimitStore uses LRU eviction to bound memory usage.\n *\n * For production multi-instance deployments, implement RateLimitStore\n * backed by Redis or similar shared storage.\n */\n\n/**\n * Pluggable rate-limit store interface.\n *\n * Increment returns the current count and window reset time.\n * Implementations must handle window expiry and cleanup.\n */\nexport interface RateLimitStore {\n increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }>;\n reset(key: string): Promise<void>;\n}\n\ninterface MemoryEntry {\n count: number;\n resetAt: number;\n /** Last access timestamp for LRU eviction */\n lastAccess: number;\n}\n\nexport interface MemoryRateLimitStoreOptions {\n /** Maximum number of tracked keys before LRU eviction (default: 10000) */\n maxKeys?: number;\n}\n\n/**\n * Bounded in-memory rate-limit store with LRU eviction.\n *\n * - Expired windows are lazily cleaned on access\n * - When maxKeys is exceeded, the least-recently-accessed entry is evicted\n * - Suitable for single-instance deployments (state lost on restart)\n */\nexport class MemoryRateLimitStore implements RateLimitStore {\n private readonly store = new Map<string, MemoryEntry>();\n private readonly maxKeys: number;\n\n constructor(options?: MemoryRateLimitStoreOptions) {\n this.maxKeys = options?.maxKeys ?? 10_000;\n }\n\n async increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }> {\n const now = Date.now();\n let entry = this.store.get(key);\n\n if (!entry || now >= entry.resetAt) {\n // Expired or new -- start fresh window\n entry = { count: 0, resetAt: now + windowMs, lastAccess: now };\n }\n\n entry.count++;\n entry.lastAccess = now;\n\n // Re-set to maintain Map insertion order (most recent last)\n this.store.delete(key);\n this.store.set(key, entry);\n\n // Evict oldest entries if over capacity\n this.evictIfNeeded();\n\n return { count: entry.count, resetAt: entry.resetAt };\n }\n\n async reset(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n /** Number of tracked keys */\n get size(): number {\n return this.store.size;\n }\n\n /** Remove all entries */\n clear(): void {\n this.store.clear();\n }\n\n private evictIfNeeded(): void {\n while (this.store.size > this.maxKeys) {\n // Map iterates in insertion order -- first key is oldest\n const oldestKey = this.store.keys().next().value;\n if (oldestKey !== undefined) {\n this.store.delete(oldestKey);\n } else {\n break;\n }\n }\n }\n}\n"]}