@spfn/core 0.2.0-beta.6 → 0.2.0-beta.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +183 -1281
  3. package/dist/authz/index.d.ts +34 -0
  4. package/dist/authz/index.js +415 -0
  5. package/dist/authz/index.js.map +1 -0
  6. package/dist/{boss-DI1r4kTS.d.ts → boss-gXhgctn6.d.ts} +40 -0
  7. package/dist/cache/index.js +42 -30
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/codegen/index.d.ts +55 -8
  10. package/dist/codegen/index.js +183 -7
  11. package/dist/codegen/index.js.map +1 -1
  12. package/dist/config/index.d.ts +585 -6
  13. package/dist/config/index.js +116 -5
  14. package/dist/config/index.js.map +1 -1
  15. package/dist/db/index.d.ts +379 -75
  16. package/dist/db/index.js +625 -113
  17. package/dist/db/index.js.map +1 -1
  18. package/dist/define-middleware-DuXD8Hvu.d.ts +167 -0
  19. package/dist/env/index.d.ts +26 -2
  20. package/dist/env/index.js +15 -5
  21. package/dist/env/index.js.map +1 -1
  22. package/dist/env/loader.d.ts +26 -19
  23. package/dist/env/loader.js +32 -25
  24. package/dist/env/loader.js.map +1 -1
  25. package/dist/errors/index.d.ts +10 -0
  26. package/dist/errors/index.js +20 -2
  27. package/dist/errors/index.js.map +1 -1
  28. package/dist/event/index.d.ts +33 -3
  29. package/dist/event/index.js +24 -3
  30. package/dist/event/index.js.map +1 -1
  31. package/dist/event/sse/client.d.ts +42 -3
  32. package/dist/event/sse/client.js +128 -45
  33. package/dist/event/sse/client.js.map +1 -1
  34. package/dist/event/sse/index.d.ts +12 -5
  35. package/dist/event/sse/index.js +271 -32
  36. package/dist/event/sse/index.js.map +1 -1
  37. package/dist/event/ws/client.d.ts +59 -0
  38. package/dist/event/ws/client.js +273 -0
  39. package/dist/event/ws/client.js.map +1 -0
  40. package/dist/event/ws/index.d.ts +94 -0
  41. package/dist/event/ws/index.js +272 -0
  42. package/dist/event/ws/index.js.map +1 -0
  43. package/dist/job/index.d.ts +2 -2
  44. package/dist/job/index.js +155 -42
  45. package/dist/job/index.js.map +1 -1
  46. package/dist/logger/index.d.ts +5 -0
  47. package/dist/logger/index.js +14 -0
  48. package/dist/logger/index.js.map +1 -1
  49. package/dist/middleware/index.d.ts +243 -2
  50. package/dist/middleware/index.js +1323 -13
  51. package/dist/middleware/index.js.map +1 -1
  52. package/dist/nextjs/index.d.ts +2 -2
  53. package/dist/nextjs/index.js +77 -31
  54. package/dist/nextjs/index.js.map +1 -1
  55. package/dist/nextjs/server.d.ts +53 -23
  56. package/dist/nextjs/server.js +197 -66
  57. package/dist/nextjs/server.js.map +1 -1
  58. package/dist/route/index.d.ts +138 -146
  59. package/dist/route/index.js +238 -22
  60. package/dist/route/index.js.map +1 -1
  61. package/dist/security/index.d.ts +83 -0
  62. package/dist/security/index.js +173 -0
  63. package/dist/security/index.js.map +1 -0
  64. package/dist/server/index.d.ts +458 -17
  65. package/dist/server/index.js +1756 -277
  66. package/dist/server/index.js.map +1 -1
  67. package/dist/{router-Di7ENoah.d.ts → token-manager-jKD_EsSE.d.ts} +121 -1
  68. package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
  69. package/dist/types-BFB72jbM.d.ts +282 -0
  70. package/dist/types-DVjf37yO.d.ts +205 -0
  71. package/docs/file-upload.md +717 -0
  72. package/package.json +237 -208
  73. package/dist/types-B-e_f2dQ.d.ts +0 -121
@@ -1,10 +1,76 @@
1
1
  import { SerializableError } from '@spfn/core/errors';
2
2
  import { logger } from '@spfn/core/logger';
3
3
  import { env } from '@spfn/core/config';
4
- import { randomBytes } from 'crypto';
4
+ import { randomBytes, createHmac, createHash, timingSafeEqual } from 'crypto';
5
+ import { format } from 'util';
5
6
 
6
7
  // src/middleware/error-handler.ts
7
8
  var errorLogger = logger.child("@spfn/core:error-handler");
9
+ function extractCauseMessage(err) {
10
+ const cause = err.cause;
11
+ if (!cause) {
12
+ return void 0;
13
+ }
14
+ if (cause instanceof Error) {
15
+ return cause.message;
16
+ }
17
+ if (typeof cause === "string") {
18
+ return cause;
19
+ }
20
+ return String(cause);
21
+ }
22
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "x-api-key", "x-auth-token"]);
23
+ function extractHeaders(c) {
24
+ const headers = {};
25
+ c.req.raw.headers.forEach((value, key) => {
26
+ headers[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "***" : value;
27
+ });
28
+ return headers;
29
+ }
30
+ var SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set([
31
+ "token",
32
+ "access_token",
33
+ "refresh_token",
34
+ "id_token",
35
+ "code",
36
+ "secret",
37
+ "client_secret",
38
+ "password",
39
+ "passwd",
40
+ "pwd",
41
+ "api_key",
42
+ "apikey",
43
+ "key",
44
+ "signature",
45
+ "sig",
46
+ "session",
47
+ "sessionid",
48
+ "auth",
49
+ "authorization"
50
+ ]);
51
+ function extractQuery(c) {
52
+ const query = c.req.query();
53
+ const masked = {};
54
+ for (const [key, value] of Object.entries(query)) {
55
+ masked[key] = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()) ? "***" : value;
56
+ }
57
+ return masked;
58
+ }
59
+ function buildOnErrorContext(c, statusCode) {
60
+ const auth = c.get("auth");
61
+ return {
62
+ statusCode,
63
+ path: c.req.path,
64
+ method: c.req.method,
65
+ requestId: c.get("requestId"),
66
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
67
+ userId: auth?.userId,
68
+ request: {
69
+ headers: extractHeaders(c),
70
+ query: extractQuery(c)
71
+ }
72
+ };
73
+ }
8
74
  function logError(err, logData, includeStack) {
9
75
  const logLevel = logData.statusCode >= 500 ? "error" : "warn";
10
76
  if (includeStack) {
@@ -19,20 +85,35 @@ function isSerializableError(err) {
19
85
  function ErrorHandler(options = {}) {
20
86
  const {
21
87
  includeStack = env.NODE_ENV !== "production",
22
- enableLogging = true
88
+ enableLogging = true,
89
+ onError
23
90
  } = options;
24
91
  return (err, c) => {
92
+ const path = c.req.path;
93
+ const method = c.req.method;
94
+ const causeMessage = extractCauseMessage(err);
25
95
  if (isSerializableError(err)) {
26
96
  const { statusCode: statusCode2 } = err;
27
97
  if (enableLogging) {
28
98
  logError(err, {
29
99
  type: err.constructor.name,
30
100
  message: err.message,
101
+ cause: causeMessage,
31
102
  statusCode: statusCode2,
32
- path: c.req.path,
33
- method: c.req.method
103
+ path,
104
+ method
34
105
  }, includeStack);
35
106
  }
107
+ if (onError) {
108
+ const ctx = buildOnErrorContext(c, statusCode2);
109
+ Promise.resolve(onError(err, ctx)).catch((e) => errorLogger.warn("onError callback failed", e));
110
+ }
111
+ if (err.internal === true && !includeStack) {
112
+ return c.json(
113
+ { __type: err.constructor.name, message: "Internal server error" },
114
+ statusCode2
115
+ );
116
+ }
36
117
  const serialized = err.toJSON();
37
118
  if (includeStack && err.stack) {
38
119
  serialized.stack = err.stack;
@@ -45,21 +126,1068 @@ function ErrorHandler(options = {}) {
45
126
  logError(err, {
46
127
  type: err.name || "Error",
47
128
  message: err.message,
129
+ cause: causeMessage,
48
130
  statusCode,
49
- path: c.req.path,
50
- method: c.req.method
131
+ path,
132
+ method
51
133
  }, includeStack);
52
134
  }
135
+ if (onError) {
136
+ const ctx = buildOnErrorContext(c, statusCode);
137
+ Promise.resolve(onError(err, ctx)).catch((e) => errorLogger.warn("onError callback failed", e));
138
+ }
53
139
  const response = {
54
140
  __type: "Error",
55
- message: err.message || "Internal Server Error"
141
+ message: includeStack ? err.message || "Internal Server Error" : "Internal Server Error"
56
142
  };
143
+ if (causeMessage && includeStack) {
144
+ response.cause = causeMessage;
145
+ }
57
146
  if (includeStack && err.stack) {
58
147
  response.stack = err.stack;
59
148
  }
60
149
  return c.json(response, statusCode);
61
150
  };
62
151
  }
152
+ var PROXY_SIGNATURE_HEADER = "x-spfn-proxy-signature";
153
+ var PROXY_TIMESTAMP_HEADER = "x-spfn-proxy-timestamp";
154
+ var PROXY_NONCE_HEADER = "x-spfn-proxy-nonce";
155
+ var PROXY_KEY_ID_HEADER = "x-spfn-proxy-key-id";
156
+ var PROXY_CLIENT_IP_HEADER = "x-spfn-proxy-client-ip";
157
+ var DEFAULT_KEY_ID = "default";
158
+ function parseProxyKey(raw) {
159
+ const idx = raw.indexOf(":");
160
+ if (idx === -1) {
161
+ return { keyId: DEFAULT_KEY_ID, secret: raw };
162
+ }
163
+ return { keyId: raw.slice(0, idx) || DEFAULT_KEY_ID, secret: raw.slice(idx + 1) };
164
+ }
165
+ function parseProxyKeySet(raws) {
166
+ const keys = [];
167
+ const seen = /* @__PURE__ */ new Set();
168
+ for (const raw of raws) {
169
+ if (!raw) {
170
+ continue;
171
+ }
172
+ for (const part of raw.split(",")) {
173
+ const trimmed = part.trim();
174
+ if (!trimmed) {
175
+ continue;
176
+ }
177
+ const key = parseProxyKey(trimmed);
178
+ if (!key.secret || seen.has(key.keyId)) {
179
+ continue;
180
+ }
181
+ seen.add(key.keyId);
182
+ keys.push(key);
183
+ }
184
+ }
185
+ return keys;
186
+ }
187
+ function buildCanonicalString(parts) {
188
+ return [parts.method.toUpperCase(), parts.path, parts.query, parts.timestamp, parts.nonce, parts.bodyHash].join("\n");
189
+ }
190
+ function hashBody(body) {
191
+ if (!body || body.length === 0) {
192
+ return "";
193
+ }
194
+ return typeof body === "string" ? createHash("sha256").update(body, "utf8").digest("hex") : createHash("sha256").update(body).digest("hex");
195
+ }
196
+ function computeSignature(secret, parts) {
197
+ return createHmac("sha256", secret).update(buildCanonicalString(parts)).digest("hex");
198
+ }
199
+ function safeEqualHex(a, b) {
200
+ const bufA = Buffer.from(a, "hex");
201
+ const bufB = Buffer.from(b, "hex");
202
+ if (bufA.length === 0 || bufA.length !== bufB.length) {
203
+ return false;
204
+ }
205
+ return timingSafeEqual(bufA, bufB);
206
+ }
207
+ function verifyProxyRequest(input) {
208
+ const { signature, timestamp, nonce, keyId } = input;
209
+ if (!signature || !timestamp || !nonce || !keyId) {
210
+ return { valid: false, reason: "missing-headers" };
211
+ }
212
+ const ts = Number(timestamp);
213
+ if (!Number.isFinite(ts)) {
214
+ return { valid: false, reason: "bad-timestamp" };
215
+ }
216
+ const now = input.now ?? Date.now();
217
+ const windowMs = input.windowMs ?? 3e4;
218
+ if (Math.abs(now - ts) > windowMs) {
219
+ return { valid: false, reason: "stale-timestamp" };
220
+ }
221
+ const key = input.keys.find((k) => k.keyId === keyId);
222
+ if (!key) {
223
+ return { valid: false, reason: "unknown-key" };
224
+ }
225
+ const expected = computeSignature(key.secret, {
226
+ method: input.method,
227
+ path: input.path,
228
+ query: input.query ?? "",
229
+ timestamp,
230
+ nonce,
231
+ bodyHash: hashBody(input.body)
232
+ });
233
+ if (!safeEqualHex(signature, expected)) {
234
+ return { valid: false, reason: "signature-mismatch" };
235
+ }
236
+ return { valid: true, nonce, keyId };
237
+ }
238
+
239
+ // src/route/define-middleware.ts
240
+ function defineMiddleware(name, handlerOrFactory, options) {
241
+ const skips = options?.skips;
242
+ if (typeof handlerOrFactory === "function") {
243
+ const paramCount = handlerOrFactory.length;
244
+ if (paramCount === 2) {
245
+ return {
246
+ name,
247
+ handler: handlerOrFactory,
248
+ _name: name,
249
+ ...skips
250
+ };
251
+ } else {
252
+ const factory = handlerOrFactory;
253
+ const wrapper = (...args) => factory(...args);
254
+ Object.defineProperty(wrapper, "name", {
255
+ value: name,
256
+ writable: false,
257
+ enumerable: false,
258
+ configurable: true
259
+ });
260
+ Object.defineProperty(wrapper, "_name", {
261
+ value: name,
262
+ writable: false,
263
+ enumerable: false,
264
+ configurable: true
265
+ });
266
+ return wrapper;
267
+ }
268
+ }
269
+ return {
270
+ name,
271
+ handler: handlerOrFactory,
272
+ _name: name,
273
+ ...skips
274
+ };
275
+ }
276
+ function defineMiddlewareFactory(name, factory) {
277
+ const wrapper = (...args) => factory(...args);
278
+ Object.defineProperty(wrapper, "name", {
279
+ value: name,
280
+ writable: false,
281
+ enumerable: false,
282
+ configurable: true
283
+ });
284
+ Object.defineProperty(wrapper, "_name", {
285
+ value: name,
286
+ writable: false,
287
+ enumerable: false,
288
+ configurable: true
289
+ });
290
+ return wrapper;
291
+ }
292
+ logger.child("@spfn/core:cache");
293
+ logger.child("@spfn/core:cache");
294
+ var CACHE_KEY = Symbol.for("@spfn/core:cache");
295
+ var state = globalThis[CACHE_KEY] ??= {
296
+ write: void 0,
297
+ read: void 0,
298
+ disabled: false
299
+ };
300
+ function getCache() {
301
+ return state.write;
302
+ }
303
+ function isCacheDisabled() {
304
+ return state.disabled;
305
+ }
306
+
307
+ // src/errors/error-registry.ts
308
+ var ErrorRegistry = class _ErrorRegistry {
309
+ errors = /* @__PURE__ */ new Map();
310
+ constructor(initialErrors) {
311
+ if (initialErrors) {
312
+ for (const input of initialErrors) {
313
+ if (input instanceof _ErrorRegistry) {
314
+ this.concat(input);
315
+ } else if (Array.isArray(input)) {
316
+ this.append(input);
317
+ } else {
318
+ this.append(input);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ /**
324
+ * Append error class(es) to the registry
325
+ *
326
+ * @param input - Error constructor or array of constructors
327
+ * @returns This registry for chaining
328
+ */
329
+ append(input) {
330
+ if (Array.isArray(input)) {
331
+ for (const ErrorClass of input) {
332
+ this.errors.set(ErrorClass.name, ErrorClass);
333
+ }
334
+ } else {
335
+ this.errors.set(input.name, input);
336
+ }
337
+ return this;
338
+ }
339
+ /**
340
+ * Concatenate another ErrorRegistry into this one
341
+ *
342
+ * @param registry - Another ErrorRegistry to merge
343
+ * @returns This registry for chaining
344
+ */
345
+ concat(registry) {
346
+ for (const [name, ErrorClass] of registry.errors) {
347
+ this.errors.set(name, ErrorClass);
348
+ }
349
+ return this;
350
+ }
351
+ /**
352
+ * Check if error type is registered
353
+ *
354
+ * @param name - Error class name
355
+ */
356
+ has(name) {
357
+ return this.errors.has(name);
358
+ }
359
+ /**
360
+ * Deserialize error from JSON data
361
+ *
362
+ * @param data - Serialized error data with __type field
363
+ * @returns Deserialized error instance
364
+ * @throws Error if error type is not registered
365
+ */
366
+ deserialize(data) {
367
+ const ErrorClass = this.errors.get(data.__type);
368
+ if (!ErrorClass) {
369
+ throw new Error(`Unknown error type: ${data.__type}`);
370
+ }
371
+ return new ErrorClass(data);
372
+ }
373
+ /**
374
+ * Try to deserialize error, return null if type unknown
375
+ *
376
+ * @param data - Serialized error data
377
+ * @returns Deserialized error or null
378
+ */
379
+ tryDeserialize(data) {
380
+ if (!data.__type || !this.has(data.__type)) {
381
+ return null;
382
+ }
383
+ try {
384
+ return this.deserialize(data);
385
+ } catch {
386
+ return null;
387
+ }
388
+ }
389
+ /**
390
+ * Get all registered error types
391
+ */
392
+ getRegisteredTypes() {
393
+ return Array.from(this.errors.keys());
394
+ }
395
+ };
396
+
397
+ // src/errors/serializable-error.ts
398
+ var SerializableError2 = class extends Error {
399
+ /**
400
+ * Serialize error to JSON-compatible object
401
+ *
402
+ * Automatically includes:
403
+ * - __type: Constructor name for deserialization
404
+ * - message: Error message
405
+ * - All public instance properties (except name, stack)
406
+ */
407
+ toJSON() {
408
+ const json = {
409
+ __type: this.constructor.name,
410
+ message: this.message
411
+ };
412
+ for (const key of Object.keys(this)) {
413
+ if (key !== "name" && key !== "message" && key !== "stack" && key !== "statusCode") {
414
+ json[key] = this[key];
415
+ }
416
+ }
417
+ return json;
418
+ }
419
+ };
420
+
421
+ // src/errors/http-errors.ts
422
+ var HttpError = class extends SerializableError2 {
423
+ statusCode;
424
+ details;
425
+ constructor(data) {
426
+ super(data.message);
427
+ this.name = "HttpError";
428
+ this.statusCode = data.statusCode;
429
+ if (data.details) {
430
+ this.details = data.details;
431
+ }
432
+ Error.captureStackTrace(this, this.constructor);
433
+ }
434
+ };
435
+ var BadRequestError = class extends HttpError {
436
+ constructor(data = {}) {
437
+ super({
438
+ message: data.message || "Bad request",
439
+ statusCode: 400,
440
+ details: data.details
441
+ });
442
+ this.name = "BadRequestError";
443
+ }
444
+ };
445
+ var ValidationError = class extends HttpError {
446
+ fields;
447
+ constructor(data) {
448
+ super({
449
+ message: data.message,
450
+ statusCode: 400,
451
+ details: data.details
452
+ });
453
+ this.name = "ValidationError";
454
+ if (data.fields) {
455
+ this.fields = data.fields;
456
+ }
457
+ }
458
+ };
459
+ var UnauthorizedError = class extends HttpError {
460
+ constructor(data = {}) {
461
+ super({
462
+ message: data.message || "Authentication required",
463
+ statusCode: 401,
464
+ details: data.details
465
+ });
466
+ this.name = "UnauthorizedError";
467
+ }
468
+ };
469
+ var ForbiddenError = class extends HttpError {
470
+ constructor(data = {}) {
471
+ super({
472
+ message: data.message || "Access forbidden",
473
+ statusCode: 403,
474
+ details: data.details
475
+ });
476
+ this.name = "ForbiddenError";
477
+ }
478
+ };
479
+ var NotFoundError = class extends HttpError {
480
+ resource;
481
+ constructor(data = {}) {
482
+ super({
483
+ message: data.message || "Resource not found",
484
+ statusCode: 404,
485
+ details: data.details
486
+ });
487
+ this.name = "NotFoundError";
488
+ if (data.resource) {
489
+ this.resource = data.resource;
490
+ }
491
+ }
492
+ };
493
+ var ConflictError = class extends HttpError {
494
+ constructor(data = {}) {
495
+ super({
496
+ message: data.message || "Resource conflict",
497
+ statusCode: 409,
498
+ details: data.details
499
+ });
500
+ this.name = "ConflictError";
501
+ }
502
+ };
503
+ var GoneError = class extends HttpError {
504
+ resource;
505
+ constructor(data = {}) {
506
+ super({
507
+ message: data.message || "Resource permanently deleted",
508
+ statusCode: 410,
509
+ details: data.details
510
+ });
511
+ this.name = "GoneError";
512
+ if (data.resource) {
513
+ this.resource = data.resource;
514
+ }
515
+ }
516
+ };
517
+ var TooManyRequestsError = class extends HttpError {
518
+ retryAfter;
519
+ constructor(data = {}) {
520
+ super({
521
+ message: data.message || "Too many requests",
522
+ statusCode: 429,
523
+ details: data.details
524
+ });
525
+ this.name = "TooManyRequestsError";
526
+ if (data.retryAfter) {
527
+ this.retryAfter = data.retryAfter;
528
+ }
529
+ }
530
+ };
531
+ var InternalServerError = class extends HttpError {
532
+ constructor(data = {}) {
533
+ super({
534
+ message: data.message || "Internal server error",
535
+ statusCode: 500,
536
+ details: data.details
537
+ });
538
+ this.name = "InternalServerError";
539
+ }
540
+ };
541
+ var UnsupportedMediaTypeError = class extends HttpError {
542
+ mediaType;
543
+ supportedTypes;
544
+ constructor(data = {}) {
545
+ super({
546
+ message: data.message || "Unsupported media type",
547
+ statusCode: 415,
548
+ details: data.details
549
+ });
550
+ this.name = "UnsupportedMediaTypeError";
551
+ if (data.mediaType) {
552
+ this.mediaType = data.mediaType;
553
+ }
554
+ if (data.supportedTypes) {
555
+ this.supportedTypes = data.supportedTypes;
556
+ }
557
+ }
558
+ };
559
+ var UnprocessableEntityError = class extends HttpError {
560
+ constructor(data = {}) {
561
+ super({
562
+ message: data.message || "Unprocessable entity",
563
+ statusCode: 422,
564
+ details: data.details
565
+ });
566
+ this.name = "UnprocessableEntityError";
567
+ }
568
+ };
569
+ var ServiceUnavailableError = class extends HttpError {
570
+ retryAfter;
571
+ constructor(data = {}) {
572
+ super({
573
+ message: data.message || "Service unavailable",
574
+ statusCode: 503,
575
+ details: data.details
576
+ });
577
+ this.name = "ServiceUnavailableError";
578
+ if (data.retryAfter) {
579
+ this.retryAfter = data.retryAfter;
580
+ }
581
+ }
582
+ };
583
+
584
+ // src/errors/database-errors.ts
585
+ var DatabaseError = class extends SerializableError2 {
586
+ statusCode;
587
+ details;
588
+ constructor(data) {
589
+ super(data.message);
590
+ this.name = "DatabaseError";
591
+ this.statusCode = data.statusCode ?? 500;
592
+ this.details = data.details;
593
+ Error.captureStackTrace(this, this.constructor);
594
+ }
595
+ /**
596
+ * Whether this error's message/details are derived from the raw database
597
+ * driver (SQL text, table/column/constraint names, parameter values) and
598
+ * therefore must NOT be exposed to clients in production. Defined as a
599
+ * prototype getter so it is never serialized into the response by toJSON().
600
+ * Subclasses with a safe, constructed message override this to `false`.
601
+ */
602
+ get internal() {
603
+ return true;
604
+ }
605
+ };
606
+ var ConnectionError = class extends DatabaseError {
607
+ constructor(data) {
608
+ super({ message: data.message, statusCode: 503, details: data.details });
609
+ this.name = "ConnectionError";
610
+ }
611
+ };
612
+ var QueryError = class extends DatabaseError {
613
+ constructor(data) {
614
+ super({
615
+ message: data.message,
616
+ statusCode: data.statusCode ?? 500,
617
+ details: data.details
618
+ });
619
+ this.name = "QueryError";
620
+ }
621
+ };
622
+ var EntityNotFoundError = class extends QueryError {
623
+ resource;
624
+ id;
625
+ constructor(data) {
626
+ super({
627
+ message: `${data.resource} with id ${data.id} not found`,
628
+ statusCode: 404,
629
+ details: { resource: data.resource, id: data.id }
630
+ });
631
+ this.name = "EntityNotFoundError";
632
+ this.resource = data.resource;
633
+ this.id = data.id;
634
+ }
635
+ // Message/details are constructed from the resource + id, not driver text
636
+ get internal() {
637
+ return false;
638
+ }
639
+ };
640
+ var ConstraintViolationError = class extends QueryError {
641
+ constructor(data) {
642
+ super({ message: data.message, statusCode: 400, details: data.details });
643
+ this.name = "ConstraintViolationError";
644
+ }
645
+ };
646
+ var TransactionError = class extends DatabaseError {
647
+ constructor(data) {
648
+ super({
649
+ message: data.message,
650
+ statusCode: data.statusCode ?? 500,
651
+ details: data.details
652
+ });
653
+ this.name = "TransactionError";
654
+ }
655
+ };
656
+ var DeadlockError = class extends TransactionError {
657
+ constructor(data) {
658
+ super({ message: data.message, statusCode: 409, details: data.details });
659
+ this.name = "DeadlockError";
660
+ }
661
+ };
662
+ var DuplicateEntryError = class extends QueryError {
663
+ field;
664
+ value;
665
+ constructor(data) {
666
+ super({
667
+ message: `${data.field} already exists`,
668
+ statusCode: 409,
669
+ details: { field: data.field }
670
+ });
671
+ this.name = "DuplicateEntryError";
672
+ this.field = data.field;
673
+ this.value = data.value;
674
+ }
675
+ // Field-only message/details — safe to surface to the client (not internal)
676
+ get internal() {
677
+ return false;
678
+ }
679
+ };
680
+
681
+ // src/errors/index.ts
682
+ var errorRegistry = new ErrorRegistry();
683
+ errorRegistry.append([
684
+ HttpError,
685
+ BadRequestError,
686
+ ValidationError,
687
+ UnauthorizedError,
688
+ ForbiddenError,
689
+ NotFoundError,
690
+ ConflictError,
691
+ GoneError,
692
+ TooManyRequestsError,
693
+ UnsupportedMediaTypeError,
694
+ UnprocessableEntityError,
695
+ InternalServerError,
696
+ ServiceUnavailableError
697
+ ]);
698
+ errorRegistry.append([
699
+ DatabaseError,
700
+ ConnectionError,
701
+ QueryError,
702
+ EntityNotFoundError,
703
+ ConstraintViolationError,
704
+ TransactionError,
705
+ DeadlockError,
706
+ DuplicateEntryError
707
+ ]);
708
+
709
+ // src/logger/types.ts
710
+ var LOG_LEVEL_PRIORITY = {
711
+ debug: 0,
712
+ info: 1,
713
+ warn: 2,
714
+ error: 3,
715
+ fatal: 4
716
+ };
717
+
718
+ // src/logger/formatters.ts
719
+ var SENSITIVE_KEYS = [
720
+ "password",
721
+ "passwd",
722
+ "pwd",
723
+ "secret",
724
+ "token",
725
+ "apikey",
726
+ "api_key",
727
+ "accesstoken",
728
+ "access_token",
729
+ "refreshtoken",
730
+ "refresh_token",
731
+ "authorization",
732
+ "auth",
733
+ "cookie",
734
+ "session",
735
+ "sessionid",
736
+ "session_id",
737
+ "privatekey",
738
+ "private_key",
739
+ "creditcard",
740
+ "credit_card",
741
+ "cardnumber",
742
+ "card_number",
743
+ "cvv",
744
+ "ssn",
745
+ "pin"
746
+ ];
747
+ var MASKED_VALUE = "***MASKED***";
748
+ function isSensitiveKey(key) {
749
+ const lowerKey = key.toLowerCase();
750
+ return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
751
+ }
752
+ function maskSensitiveData(data, seen = /* @__PURE__ */ new WeakSet()) {
753
+ if (data === null || data === void 0) {
754
+ return data;
755
+ }
756
+ if (typeof data !== "object") {
757
+ return data;
758
+ }
759
+ if (seen.has(data)) {
760
+ return "[Circular]";
761
+ }
762
+ seen.add(data);
763
+ if (Array.isArray(data)) {
764
+ return data.map((item) => maskSensitiveData(item, seen));
765
+ }
766
+ const masked = {};
767
+ for (const [key, value] of Object.entries(data)) {
768
+ if (isSensitiveKey(key)) {
769
+ masked[key] = MASKED_VALUE;
770
+ } else if (typeof value === "object" && value !== null) {
771
+ masked[key] = maskSensitiveData(value, seen);
772
+ } else {
773
+ masked[key] = value;
774
+ }
775
+ }
776
+ return masked;
777
+ }
778
+ var COLORS = {
779
+ reset: "\x1B[0m",
780
+ bright: "\x1B[1m",
781
+ dim: "\x1B[2m",
782
+ // 로그 레벨 컬러
783
+ debug: "\x1B[36m",
784
+ // cyan
785
+ info: "\x1B[32m",
786
+ // green
787
+ warn: "\x1B[33m",
788
+ // yellow
789
+ error: "\x1B[31m",
790
+ // red
791
+ fatal: "\x1B[35m",
792
+ // magenta
793
+ // 추가 컬러
794
+ gray: "\x1B[90m"
795
+ };
796
+ function formatTimestampHuman(date) {
797
+ const year = date.getFullYear();
798
+ const month = String(date.getMonth() + 1).padStart(2, "0");
799
+ const day = String(date.getDate()).padStart(2, "0");
800
+ const hours = String(date.getHours()).padStart(2, "0");
801
+ const minutes = String(date.getMinutes()).padStart(2, "0");
802
+ const seconds = String(date.getSeconds()).padStart(2, "0");
803
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
804
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
805
+ }
806
+ function formatError(error) {
807
+ const lines = [];
808
+ lines.push(`${error.name}: ${error.message}`);
809
+ if (error.stack) {
810
+ const stackLines = error.stack.split("\n").slice(1);
811
+ lines.push(...stackLines);
812
+ }
813
+ if (error.cause instanceof Error) {
814
+ lines.push(`Caused by: ${formatError(error.cause)}`);
815
+ } else if (error.cause !== void 0) {
816
+ lines.push(`Caused by: ${String(error.cause)}`);
817
+ }
818
+ return lines.join("\n");
819
+ }
820
+ function formatConsole(metadata, colorize = true) {
821
+ const parts = [];
822
+ const timestamp = formatTimestampHuman(metadata.timestamp);
823
+ if (colorize) {
824
+ parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
825
+ } else {
826
+ parts.push(`[${timestamp}]`);
827
+ }
828
+ const pid = process.pid;
829
+ if (colorize) {
830
+ parts.push(`${COLORS.dim}[pid=${pid}]${COLORS.reset}`);
831
+ } else {
832
+ parts.push(`[pid=${pid}]`);
833
+ }
834
+ if (metadata.module) {
835
+ if (colorize) {
836
+ parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
837
+ } else {
838
+ parts.push(`[module=${metadata.module}]`);
839
+ }
840
+ }
841
+ if (metadata.context && Object.keys(metadata.context).length > 0) {
842
+ Object.entries(metadata.context).forEach(([key, value]) => {
843
+ let valueStr;
844
+ if (typeof value === "string") {
845
+ valueStr = value;
846
+ } else if (typeof value === "object" && value !== null) {
847
+ try {
848
+ valueStr = JSON.stringify(value);
849
+ } catch (error) {
850
+ valueStr = "[circular]";
851
+ }
852
+ } else {
853
+ valueStr = String(value);
854
+ }
855
+ if (colorize) {
856
+ parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
857
+ } else {
858
+ parts.push(`[${key}=${valueStr}]`);
859
+ }
860
+ });
861
+ }
862
+ const levelStr = metadata.level.toUpperCase();
863
+ if (colorize) {
864
+ const color = COLORS[metadata.level];
865
+ parts.push(`${color}(${levelStr})${COLORS.reset}:`);
866
+ } else {
867
+ parts.push(`(${levelStr}):`);
868
+ }
869
+ if (colorize) {
870
+ parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
871
+ } else {
872
+ parts.push(metadata.message);
873
+ }
874
+ let output = parts.join(" ");
875
+ if (metadata.error) {
876
+ output += "\n" + formatError(metadata.error);
877
+ }
878
+ return output;
879
+ }
880
+
881
+ // src/logger/logger.ts
882
+ var FORMAT_PATTERN = /%[sdifjoOc%]/;
883
+ var Logger = class _Logger {
884
+ config;
885
+ module;
886
+ constructor(config) {
887
+ this.config = config;
888
+ this.module = config.module;
889
+ }
890
+ /**
891
+ * Convert unknown error to Error object
892
+ */
893
+ toError(error) {
894
+ if (error instanceof Error) return error;
895
+ if (typeof error === "string") return new Error(error);
896
+ if (typeof error === "object" && error !== null) {
897
+ return new Error(JSON.stringify(error));
898
+ }
899
+ return new Error(String(error));
900
+ }
901
+ /**
902
+ * Check if value is a context object (not an error)
903
+ */
904
+ isContext(value) {
905
+ if (typeof value !== "object" || value === null) return false;
906
+ if (value instanceof Error) return false;
907
+ const hasStack = "stack" in value && typeof value.stack === "string";
908
+ if (hasStack) {
909
+ return false;
910
+ }
911
+ return true;
912
+ }
913
+ /**
914
+ * Get current log level
915
+ */
916
+ get level() {
917
+ return this.config.level;
918
+ }
919
+ /**
920
+ * Create child logger (per module)
921
+ */
922
+ child(module) {
923
+ return new _Logger({
924
+ ...this.config,
925
+ module
926
+ });
927
+ }
928
+ /**
929
+ * Common log method with error/context detection
930
+ */
931
+ logWithLevel(level, message, errorOrContext, context) {
932
+ if (errorOrContext !== void 0 && FORMAT_PATTERN.test(message)) {
933
+ this.log(level, format(message, errorOrContext), void 0, context);
934
+ return;
935
+ }
936
+ if (errorOrContext instanceof Error) {
937
+ this.log(level, message, errorOrContext, context);
938
+ } else if (errorOrContext !== void 0 && typeof errorOrContext === "object" && !this.isContext(errorOrContext)) {
939
+ this.log(level, message, this.toError(errorOrContext), context);
940
+ } else if (typeof errorOrContext === "string" || typeof errorOrContext === "number" || typeof errorOrContext === "boolean") {
941
+ this.log(level, message, this.toError(errorOrContext), context);
942
+ } else {
943
+ this.log(level, message, void 0, errorOrContext);
944
+ }
945
+ }
946
+ debug(message, errorOrContext, context) {
947
+ this.logWithLevel("debug", message, errorOrContext, context);
948
+ }
949
+ info(message, errorOrContext, context) {
950
+ this.logWithLevel("info", message, errorOrContext, context);
951
+ }
952
+ warn(message, errorOrContext, context) {
953
+ this.logWithLevel("warn", message, errorOrContext, context);
954
+ }
955
+ error(message, errorOrContext, context) {
956
+ this.logWithLevel("error", message, errorOrContext, context);
957
+ }
958
+ fatal(message, errorOrContext, context) {
959
+ this.logWithLevel("fatal", message, errorOrContext, context);
960
+ }
961
+ /**
962
+ * Log processing (internal)
963
+ */
964
+ log(level, message, error, context) {
965
+ if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
966
+ return;
967
+ }
968
+ const metadata = {
969
+ timestamp: /* @__PURE__ */ new Date(),
970
+ level,
971
+ message,
972
+ module: this.module,
973
+ error,
974
+ // Mask sensitive information in context to prevent credential leaks
975
+ context: context ? maskSensitiveData(context) : void 0
976
+ };
977
+ this.processTransports(metadata);
978
+ }
979
+ /**
980
+ * Process Transports
981
+ */
982
+ processTransports(metadata) {
983
+ const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
984
+ Promise.all(promises).catch((error) => {
985
+ const errorMessage = error instanceof Error ? error.message : String(error);
986
+ process.stderr.write(`[Logger] Transport error: ${errorMessage}
987
+ `);
988
+ });
989
+ }
990
+ /**
991
+ * Transport log (error-safe)
992
+ */
993
+ async safeTransportLog(transport, metadata) {
994
+ try {
995
+ await transport.log(metadata);
996
+ } catch (error) {
997
+ const errorMessage = error instanceof Error ? error.message : String(error);
998
+ process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
999
+ `);
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Close all Transports
1004
+ */
1005
+ async close() {
1006
+ const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
1007
+ await Promise.all(closePromises);
1008
+ }
1009
+ };
1010
+
1011
+ // src/logger/transports/console.ts
1012
+ var ConsoleTransport = class {
1013
+ name = "console";
1014
+ level;
1015
+ enabled;
1016
+ colorize;
1017
+ constructor(config) {
1018
+ this.level = config.level;
1019
+ this.enabled = config.enabled;
1020
+ this.colorize = config.colorize ?? true;
1021
+ }
1022
+ async log(metadata) {
1023
+ if (!this.enabled) {
1024
+ return;
1025
+ }
1026
+ if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
1027
+ return;
1028
+ }
1029
+ const message = formatConsole(metadata, this.colorize);
1030
+ if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
1031
+ console.error(message);
1032
+ } else {
1033
+ console.log(message);
1034
+ }
1035
+ }
1036
+ };
1037
+
1038
+ // src/logger/config.ts
1039
+ function getConsoleConfig() {
1040
+ const isProduction = process.env.NODE_ENV === "production";
1041
+ return {
1042
+ level: "debug",
1043
+ enabled: true,
1044
+ colorize: !isProduction
1045
+ // Dev: colored output, Production: plain text
1046
+ };
1047
+ }
1048
+ function validateEnvironment() {
1049
+ const nodeEnv = process.env.NODE_ENV;
1050
+ if (!nodeEnv) {
1051
+ process.stderr.write(
1052
+ "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
1053
+ );
1054
+ }
1055
+ }
1056
+ function validateConfig() {
1057
+ validateEnvironment();
1058
+ }
1059
+
1060
+ // src/logger/factory.ts
1061
+ function initializeTransports() {
1062
+ const transports = [];
1063
+ const consoleConfig = getConsoleConfig();
1064
+ transports.push(new ConsoleTransport(consoleConfig));
1065
+ return transports;
1066
+ }
1067
+ function getLogLevel() {
1068
+ const envLevel = process.env.SPFN_LOG_LEVEL || process.env.NEXT_PUBLIC_SPFN_LOG_LEVEL || "info";
1069
+ if (envLevel in LOG_LEVEL_PRIORITY) {
1070
+ return envLevel;
1071
+ }
1072
+ process.stderr.write(
1073
+ `[Logger] Invalid log level "${envLevel}", defaulting to "info"
1074
+ `
1075
+ );
1076
+ return "info";
1077
+ }
1078
+ function initializeLogger() {
1079
+ validateConfig();
1080
+ return new Logger({
1081
+ level: getLogLevel(),
1082
+ transports: initializeTransports()
1083
+ });
1084
+ }
1085
+ var logger4 = initializeLogger();
1086
+
1087
+ // src/middleware/rate-limit.ts
1088
+ var rateLimitLogger = logger4.child("@spfn/core:rate-limit");
1089
+ var FIXED_WINDOW_LUA = `
1090
+ local count = redis.call('INCR', KEYS[1])
1091
+ if count == 1 then
1092
+ redis.call('PEXPIRE', KEYS[1], ARGV[1])
1093
+ end
1094
+ return { count, redis.call('PTTL', KEYS[1]) }
1095
+ `;
1096
+ function getClientIp(c) {
1097
+ const clientType = c.get("clientType");
1098
+ if (clientType && clientType !== "untrusted") {
1099
+ const forwarded = c.req.header(PROXY_CLIENT_IP_HEADER);
1100
+ if (forwarded) {
1101
+ return forwarded;
1102
+ }
1103
+ }
1104
+ const forwardedFor = c.req.header("x-forwarded-for");
1105
+ return forwardedFor?.split(",")[0]?.trim() || c.req.header("x-real-ip") || socketRemoteAddress(c) || "unknown";
1106
+ }
1107
+ function socketRemoteAddress(c) {
1108
+ const env4 = c.env;
1109
+ const bindings = env4?.server ?? env4;
1110
+ return bindings?.incoming?.socket?.remoteAddress;
1111
+ }
1112
+ var rateLimit = defineMiddlewareFactory(
1113
+ "rateLimit",
1114
+ (options) => {
1115
+ const { limit, windowMs, scope, by, failClosed = false, message } = options;
1116
+ return async (c, next) => {
1117
+ const cache = getCache();
1118
+ if (!cache || isCacheDisabled()) {
1119
+ if (failClosed) {
1120
+ throw new TooManyRequestsError({ message: message || "Rate limiter unavailable" });
1121
+ }
1122
+ rateLimitLogger.warn("Cache unavailable \u2014 rate limit not enforced (fail-open)", {
1123
+ path: c.req.path
1124
+ });
1125
+ return next();
1126
+ }
1127
+ const dimensions = (by ? await by(c) : [getClientIp(c)]).filter((d) => Boolean(d));
1128
+ const ns = scope || `${c.req.method} ${c.req.routePath || c.req.path}`;
1129
+ for (const dimension of dimensions) {
1130
+ const key = typeof dimension === "string" ? dimension : dimension.key;
1131
+ if (!key) {
1132
+ continue;
1133
+ }
1134
+ const dimLimit = typeof dimension === "string" ? limit : dimension.limit ?? limit;
1135
+ const [count, pttl] = await cache.eval(
1136
+ FIXED_WINDOW_LUA,
1137
+ 1,
1138
+ `ratelimit:${ns}:${key}`,
1139
+ String(windowMs)
1140
+ );
1141
+ if (count > dimLimit) {
1142
+ const retryAfter = Math.max(1, Math.ceil((pttl > 0 ? pttl : windowMs) / 1e3));
1143
+ c.header("Retry-After", String(retryAfter));
1144
+ throw new TooManyRequestsError({
1145
+ message: message || "Too many requests, please try again later",
1146
+ retryAfter
1147
+ });
1148
+ }
1149
+ }
1150
+ return next();
1151
+ };
1152
+ }
1153
+ );
1154
+ var policyRegistry = /* @__PURE__ */ new Map();
1155
+ var policyFailClosedDefault = false;
1156
+ function setRateLimitFailClosedDefault(failClosed) {
1157
+ policyFailClosedDefault = failClosed;
1158
+ }
1159
+ function setRateLimitPolicies(policies) {
1160
+ policyRegistry.clear();
1161
+ if (!policies) {
1162
+ return;
1163
+ }
1164
+ for (const [name, options] of Object.entries(policies)) {
1165
+ policyRegistry.set(name, options);
1166
+ }
1167
+ }
1168
+ function getRateLimitPolicy(name) {
1169
+ return policyRegistry.get(name);
1170
+ }
1171
+ function rateLimitPolicy(name, fallback) {
1172
+ let resolved;
1173
+ const handler = (c, next) => {
1174
+ if (!resolved) {
1175
+ const configured = getRateLimitPolicy(name);
1176
+ const merged = configured ? { ...fallback, ...configured } : { ...fallback };
1177
+ if (merged.scope === void 0) {
1178
+ merged.scope = name;
1179
+ }
1180
+ if (merged.failClosed === void 0) {
1181
+ merged.failClosed = policyFailClosedDefault;
1182
+ }
1183
+ resolved = rateLimit(merged);
1184
+ }
1185
+ return resolved(c, next);
1186
+ };
1187
+ return defineMiddleware(`rateLimit:${name}`, handler);
1188
+ }
1189
+
1190
+ // src/middleware/request-logger.ts
63
1191
  var DEFAULT_CONFIG = {
64
1192
  excludePaths: ["/health", "/ping", "/favicon.ico"],
65
1193
  sensitiveFields: ["password", "token", "apiKey", "secret", "authorization"],
@@ -70,7 +1198,7 @@ function generateRequestId() {
70
1198
  const randomPart = randomBytes(6).toString("hex");
71
1199
  return `req_${timestamp}_${randomPart}`;
72
1200
  }
73
- function maskSensitiveData(obj, sensitiveFields, seen = /* @__PURE__ */ new WeakSet()) {
1201
+ function maskSensitiveData2(obj, sensitiveFields, seen = /* @__PURE__ */ new WeakSet()) {
74
1202
  if (!obj || typeof obj !== "object") return obj;
75
1203
  if (seen.has(obj)) return "[Circular]";
76
1204
  seen.add(obj);
@@ -81,7 +1209,7 @@ function maskSensitiveData(obj, sensitiveFields, seen = /* @__PURE__ */ new Weak
81
1209
  if (lowerFields.some((field) => lowerKey.includes(field))) {
82
1210
  masked[key] = "***MASKED***";
83
1211
  } else if (typeof masked[key] === "object" && masked[key] !== null) {
84
- masked[key] = maskSensitiveData(masked[key], sensitiveFields, seen);
1212
+ masked[key] = maskSensitiveData2(masked[key], sensitiveFields, seen);
85
1213
  }
86
1214
  }
87
1215
  return masked;
@@ -101,8 +1229,7 @@ function RequestLogger(options) {
101
1229
  c.set("requestId", requestId);
102
1230
  const method = c.req.method;
103
1231
  const userAgent = c.req.header("user-agent");
104
- const forwardedFor = c.req.header("x-forwarded-for");
105
- const ip = forwardedFor?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
1232
+ const ip = getClientIp(c);
106
1233
  const startTime = Date.now();
107
1234
  apiLogger.info("Request received", {
108
1235
  requestId,
@@ -134,7 +1261,7 @@ function RequestLogger(options) {
134
1261
  if (["POST", "PUT", "PATCH"].includes(method)) {
135
1262
  try {
136
1263
  const requestBody = await c.req.json();
137
- logData.request = maskSensitiveData(requestBody, cfg.sensitiveFields);
1264
+ logData.request = maskSensitiveData2(requestBody, cfg.sensitiveFields);
138
1265
  } catch {
139
1266
  }
140
1267
  }
@@ -153,7 +1280,190 @@ function RequestLogger(options) {
153
1280
  }
154
1281
  };
155
1282
  }
1283
+ var guardLogger = logger.child("@spfn/core:proxy-guard");
1284
+ var BODY_OVERSIZE = Symbol("proxy-guard:body-oversize");
1285
+ var BODY_READ_ERROR = Symbol("proxy-guard:body-read-error");
1286
+ async function readRawBody(c, maxBytes) {
1287
+ const method = c.req.method;
1288
+ if (method === "GET" || method === "HEAD") {
1289
+ return void 0;
1290
+ }
1291
+ const contentType = c.req.header("content-type") || "";
1292
+ if (contentType.includes("multipart/form-data")) {
1293
+ return void 0;
1294
+ }
1295
+ const stream = c.req.raw.clone().body;
1296
+ if (!stream) {
1297
+ return void 0;
1298
+ }
1299
+ const reader = stream.getReader();
1300
+ const chunks = [];
1301
+ let total = 0;
1302
+ try {
1303
+ for (; ; ) {
1304
+ const { done, value } = await reader.read();
1305
+ if (done) {
1306
+ break;
1307
+ }
1308
+ total += value.byteLength;
1309
+ if (maxBytes !== void 0 && total > maxBytes) {
1310
+ void reader.cancel().catch(() => void 0);
1311
+ return BODY_OVERSIZE;
1312
+ }
1313
+ chunks.push(value);
1314
+ }
1315
+ } catch {
1316
+ guardLogger.debug("Request body read failed (client abort?)");
1317
+ return BODY_READ_ERROR;
1318
+ }
1319
+ return Buffer.concat(chunks);
1320
+ }
1321
+ function isOriginAllowed(c, allowedOrigins) {
1322
+ if (!allowedOrigins || allowedOrigins.length === 0) {
1323
+ return true;
1324
+ }
1325
+ const origin = c.req.header("origin");
1326
+ if (!origin) {
1327
+ return true;
1328
+ }
1329
+ return allowedOrigins.includes(origin);
1330
+ }
1331
+ function createProxyGuard(config = {}) {
1332
+ const mode = config.mode ?? "off";
1333
+ const windowMs = config.windowMs ?? 3e4;
1334
+ const allowedOrigins = config.allowedOrigins;
1335
+ const nonceStore = config.nonceStore;
1336
+ const nonceFailClosed = config.nonceFailClosed ?? false;
1337
+ const skipPaths = new Set(config.skipPaths ?? []);
1338
+ const maxBodyBytes = config.maxBodyBytes;
1339
+ const activeRaw = config.secret ?? env.SPFN_PROXY_SECRET;
1340
+ const previousRaw = config.previousSecrets ?? env.SPFN_PROXY_SECRET_PREVIOUS;
1341
+ const keys = parseProxyKeySet([activeRaw, previousRaw]);
1342
+ if (mode === "strict" && keys.length === 0) {
1343
+ throw new Error(
1344
+ '[proxy-guard] mode "strict" requires a proxy key but none is configured (SPFN_PROXY_SECRET is empty/unset). Refusing to start with the guard open \u2014 set the secret, or use mode "tag" / "off".'
1345
+ );
1346
+ }
1347
+ if (activeRaw && previousRaw) {
1348
+ const activeId = parseProxyKey(activeRaw).keyId;
1349
+ const collides = previousRaw.split(",").some((p) => p.trim() && parseProxyKey(p.trim()).keyId === activeId);
1350
+ if (collides) {
1351
+ guardLogger.warn(
1352
+ 'Previous proxy key shares keyId with the active key (likely bare secrets without a "keyId:" prefix) \u2014 grace key ignored, rotation will drop in-flight requests. Use "<keyId>:<secret>" on both keys.',
1353
+ { keyId: activeId }
1354
+ );
1355
+ }
1356
+ }
1357
+ return async (c, next) => {
1358
+ if (mode === "off" || skipPaths.has(c.req.path)) {
1359
+ return next();
1360
+ }
1361
+ if (c.req.method === "OPTIONS" && c.req.header("access-control-request-method")) {
1362
+ c.set("clientType", "untrusted");
1363
+ return next();
1364
+ }
1365
+ if (!isOriginAllowed(c, allowedOrigins)) {
1366
+ return reject(c, mode, next, "origin-not-allowed");
1367
+ }
1368
+ if (keys.length === 0) {
1369
+ c.set("clientType", "untrusted");
1370
+ return next();
1371
+ }
1372
+ const signature = c.req.header(PROXY_SIGNATURE_HEADER);
1373
+ const timestamp = c.req.header(PROXY_TIMESTAMP_HEADER);
1374
+ const nonce = c.req.header(PROXY_NONCE_HEADER);
1375
+ const keyId = c.req.header(PROXY_KEY_ID_HEADER);
1376
+ if (!signature || !timestamp || !nonce || !keyId) {
1377
+ return reject(c, mode, next, "missing-headers");
1378
+ }
1379
+ const body = await readRawBody(c, maxBodyBytes);
1380
+ if (body === BODY_OVERSIZE) {
1381
+ if (mode === "strict") {
1382
+ return c.json({ error: "Payload Too Large" }, 413);
1383
+ }
1384
+ c.set("clientType", "untrusted");
1385
+ return next();
1386
+ }
1387
+ if (body === BODY_READ_ERROR) {
1388
+ return reject(c, mode, next, "body-read-error");
1389
+ }
1390
+ const url = new URL(c.req.url);
1391
+ const result = verifyProxyRequest({
1392
+ keys,
1393
+ method: c.req.method,
1394
+ path: url.pathname,
1395
+ query: url.search,
1396
+ body,
1397
+ signature,
1398
+ timestamp,
1399
+ nonce,
1400
+ keyId,
1401
+ windowMs
1402
+ });
1403
+ if (!result.valid) {
1404
+ return reject(c, mode, next, result.reason);
1405
+ }
1406
+ if (nonceStore && result.nonce) {
1407
+ try {
1408
+ const fresh = await nonceStore.checkAndSet(result.nonce, windowMs * 2);
1409
+ if (!fresh) {
1410
+ return reject(c, mode, next, "nonce-replay");
1411
+ }
1412
+ } catch (err) {
1413
+ if (nonceFailClosed) {
1414
+ guardLogger.warn("Nonce store unavailable \u2014 rejecting (fail-closed)", {
1415
+ error: err.message
1416
+ });
1417
+ return reject(c, mode, next, "nonce-store-unavailable");
1418
+ }
1419
+ guardLogger.warn("Nonce store unavailable \u2014 falling back to timestamp window", {
1420
+ error: err.message
1421
+ });
1422
+ }
1423
+ }
1424
+ c.set("clientType", "web");
1425
+ return next();
1426
+ };
1427
+ }
1428
+ function createCacheNonceStore(cache, prefix = "spfn:proxy-nonce:") {
1429
+ return {
1430
+ async checkAndSet(nonce, ttlMs) {
1431
+ const res = await cache.set(`${prefix}${nonce}`, "1", "PX", Math.ceil(ttlMs), "NX");
1432
+ return res === "OK";
1433
+ }
1434
+ };
1435
+ }
1436
+ function createInMemoryNonceStore() {
1437
+ const seen = /* @__PURE__ */ new Map();
1438
+ let lastSweep = 0;
1439
+ return {
1440
+ async checkAndSet(nonce, ttlMs) {
1441
+ const now = Date.now();
1442
+ if (now - lastSweep > 6e4) {
1443
+ for (const [n, exp] of seen) {
1444
+ if (exp <= now) seen.delete(n);
1445
+ }
1446
+ lastSweep = now;
1447
+ }
1448
+ const existing = seen.get(nonce);
1449
+ if (existing !== void 0 && existing > now) {
1450
+ return false;
1451
+ }
1452
+ seen.set(nonce, now + ttlMs);
1453
+ return true;
1454
+ }
1455
+ };
1456
+ }
1457
+ function reject(c, mode, next, reason) {
1458
+ if (mode === "strict") {
1459
+ guardLogger.warn("Rejected unverified request", { reason, path: c.req.path, method: c.req.method });
1460
+ return c.json({ error: "Forbidden", message: "Request origin could not be verified" }, 403);
1461
+ }
1462
+ guardLogger.debug("Unverified request (would reject in strict mode)", { reason, path: c.req.path });
1463
+ c.set("clientType", "untrusted");
1464
+ return next();
1465
+ }
156
1466
 
157
- export { ErrorHandler, RequestLogger, maskSensitiveData };
1467
+ export { ErrorHandler, RequestLogger, createCacheNonceStore, createInMemoryNonceStore, createProxyGuard, getClientIp, getRateLimitPolicy, maskSensitiveData2 as maskSensitiveData, rateLimit, rateLimitPolicy, setRateLimitFailClosedDefault, setRateLimitPolicies };
158
1468
  //# sourceMappingURL=index.js.map
159
1469
  //# sourceMappingURL=index.js.map