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

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 +181 -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 +270 -4
  16. package/dist/db/index.js +404 -60
  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 +450 -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 +235 -208
  73. package/dist/types-B-e_f2dQ.d.ts +0 -121
@@ -1,8 +1,77 @@
1
1
  import { NextResponse } from 'next/server';
2
2
  import { env } from '@spfn/core/config';
3
3
  import { logger } from '@spfn/core/logger';
4
+ import { randomBytes, createHmac, createHash } from 'crypto';
4
5
 
5
6
  // src/nextjs/proxy/rpc.ts
7
+ var PROXY_SIGNATURE_HEADER = "x-spfn-proxy-signature";
8
+ var PROXY_TIMESTAMP_HEADER = "x-spfn-proxy-timestamp";
9
+ var PROXY_NONCE_HEADER = "x-spfn-proxy-nonce";
10
+ var PROXY_KEY_ID_HEADER = "x-spfn-proxy-key-id";
11
+ var PROXY_CLIENT_IP_HEADER = "x-spfn-proxy-client-ip";
12
+ var DEFAULT_KEY_ID = "default";
13
+ function parseProxyKey(raw) {
14
+ const idx = raw.indexOf(":");
15
+ if (idx === -1) {
16
+ return { keyId: DEFAULT_KEY_ID, secret: raw };
17
+ }
18
+ return { keyId: raw.slice(0, idx) || DEFAULT_KEY_ID, secret: raw.slice(idx + 1) };
19
+ }
20
+ function parseProxyKeySet(raws) {
21
+ const keys = [];
22
+ const seen = /* @__PURE__ */ new Set();
23
+ for (const raw of raws) {
24
+ if (!raw) {
25
+ continue;
26
+ }
27
+ for (const part of raw.split(",")) {
28
+ const trimmed = part.trim();
29
+ if (!trimmed) {
30
+ continue;
31
+ }
32
+ const key = parseProxyKey(trimmed);
33
+ if (!key.secret || seen.has(key.keyId)) {
34
+ continue;
35
+ }
36
+ seen.add(key.keyId);
37
+ keys.push(key);
38
+ }
39
+ }
40
+ return keys;
41
+ }
42
+ function buildCanonicalString(parts) {
43
+ return [parts.method.toUpperCase(), parts.path, parts.query, parts.timestamp, parts.nonce, parts.bodyHash].join("\n");
44
+ }
45
+ function hashBody(body) {
46
+ if (!body || body.length === 0) {
47
+ return "";
48
+ }
49
+ return typeof body === "string" ? createHash("sha256").update(body, "utf8").digest("hex") : createHash("sha256").update(body).digest("hex");
50
+ }
51
+ function computeSignature(secret, parts) {
52
+ return createHmac("sha256", secret).update(buildCanonicalString(parts)).digest("hex");
53
+ }
54
+ function generateNonce() {
55
+ return randomBytes(16).toString("hex");
56
+ }
57
+ function signProxyRequest(input) {
58
+ const timestamp = input.timestamp ?? String(Date.now());
59
+ const nonce = input.nonce ?? generateNonce();
60
+ const signature = computeSignature(input.key.secret, {
61
+ method: input.method,
62
+ path: input.path,
63
+ query: input.query ?? "",
64
+ timestamp,
65
+ nonce,
66
+ bodyHash: hashBody(input.body)
67
+ });
68
+ return {
69
+ [PROXY_SIGNATURE_HEADER]: signature,
70
+ [PROXY_TIMESTAMP_HEADER]: timestamp,
71
+ [PROXY_NONCE_HEADER]: nonce,
72
+ [PROXY_KEY_ID_HEADER]: input.key.keyId
73
+ };
74
+ }
6
75
 
7
76
  // src/nextjs/shared.ts
8
77
  function buildUrlWithParams(path, params) {
@@ -27,6 +96,9 @@ function buildQueryString(query) {
27
96
  return `?${searchParams.toString()}`;
28
97
  }
29
98
  async function parseResponseBody(response) {
99
+ if (response.status === 204) {
100
+ return null;
101
+ }
30
102
  const contentType = response.headers.get("content-type");
31
103
  if (contentType?.includes("application/json")) {
32
104
  const text = await response.text();
@@ -185,6 +257,17 @@ function registerInterceptors(packageName, interceptors) {
185
257
  }
186
258
 
187
259
  // src/nextjs/proxy/helpers.ts
260
+ function clientIpFromForwardedChain(xff, trustedHops) {
261
+ if (!xff) return void 0;
262
+ const parts = xff.split(",").map((s) => s.trim()).filter(Boolean);
263
+ if (parts.length === 0) return void 0;
264
+ const idx = parts.length - Math.max(1, trustedHops);
265
+ return parts[idx] ?? parts[0];
266
+ }
267
+ function trustedProxyHops() {
268
+ const raw = Number.parseInt(process.env.TRUSTED_PROXY_HOPS ?? "", 10);
269
+ return Number.isFinite(raw) && raw > 0 ? raw : 1;
270
+ }
188
271
  function buildProxyHeaders(sourceHeaders, defaultHeaders) {
189
272
  const headers = new Headers();
190
273
  const headersToForward2 = [
@@ -193,7 +276,10 @@ function buildProxyHeaders(sourceHeaders, defaultHeaders) {
193
276
  "cookie",
194
277
  "user-agent",
195
278
  "accept",
196
- "accept-language"
279
+ "accept-language",
280
+ // Forwarded so the backend proxy-guard can enforce its origin allowlist on
281
+ // the real browser Origin; without this allowedOrigins would be a no-op.
282
+ "origin"
197
283
  ];
198
284
  for (const header of headersToForward2) {
199
285
  const value = sourceHeaders instanceof Headers ? sourceHeaders.get(header) : sourceHeaders[header];
@@ -201,6 +287,11 @@ function buildProxyHeaders(sourceHeaders, defaultHeaders) {
201
287
  headers.set(header, value);
202
288
  }
203
289
  }
290
+ const xff = sourceHeaders instanceof Headers ? sourceHeaders.get("x-forwarded-for") : sourceHeaders["x-forwarded-for"];
291
+ const clientIp = clientIpFromForwardedChain(xff, trustedProxyHops());
292
+ if (clientIp) {
293
+ headers.set(PROXY_CLIENT_IP_HEADER, clientIp);
294
+ }
204
295
  for (const [key, value] of Object.entries(defaultHeaders)) {
205
296
  headers.set(key, value);
206
297
  }
@@ -302,7 +393,7 @@ function buildRequestContext(path, method, headers, body, searchParams, cookiesM
302
393
  metadata: {}
303
394
  };
304
395
  }
305
- function buildResponseContext(path, method, requestHeaders, requestBody, response, responseBody, requestMetadata) {
396
+ function buildResponseContext(path, method, requestHeaders, requestBody, response, responseBody, requestMetadata, cookies) {
306
397
  return {
307
398
  path: `/${path}`,
308
399
  method,
@@ -317,6 +408,7 @@ function buildResponseContext(path, method, requestHeaders, requestBody, respons
317
408
  headers: response.headers,
318
409
  body: responseBody
319
410
  },
411
+ cookies,
320
412
  setCookies: [],
321
413
  metadata: requestMetadata
322
414
  };
@@ -324,45 +416,37 @@ function buildResponseContext(path, method, requestHeaders, requestBody, respons
324
416
 
325
417
  // src/nextjs/proxy/rpc.ts
326
418
  var rpcLogger = logger.child("@spfn/core:rpc-proxy");
327
- function isRouteDef(value) {
328
- return value !== null && typeof value === "object" && "handler" in value && "method" in value && "path" in value;
329
- }
330
- function isRouter(value) {
331
- return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
332
- }
333
- function getRouteByPath(router, routePath) {
334
- const parts = routePath.split(".");
335
- let current = router.routes;
336
- for (const part of parts) {
337
- if (!current || typeof current !== "object") {
338
- return null;
339
- }
340
- const next = current[part];
341
- if (isRouter(next)) {
342
- current = next.routes;
343
- } else if (isRouteDef(next)) {
344
- return next;
345
- } else {
346
- current = next;
347
- }
348
- }
349
- if (isRouteDef(current)) {
350
- return current;
351
- }
352
- return null;
353
- }
354
419
  function createRpcProxy(config) {
355
420
  const {
356
- router,
357
421
  apiUrl = env.SPFN_API_URL || "http://localhost:8790",
358
422
  debug = env.NODE_ENV === "development",
359
- timeout = 3e4,
423
+ timeout = env.RPC_PROXY_TIMEOUT,
360
424
  headers: defaultHeaders = {},
361
425
  interceptors,
362
426
  autoDiscoverInterceptors = true,
363
- disableAutoInterceptors
427
+ disableAutoInterceptors,
428
+ proxySecret = env.SPFN_PROXY_SECRET,
429
+ routeMap
364
430
  } = config;
365
- const packageRouters = router._packageRouters || [];
431
+ const proxyKey = proxySecret ? parseProxyKeySet([proxySecret])[0] ?? null : null;
432
+ if (proxyKey) {
433
+ try {
434
+ const basePath = new URL(apiUrl).pathname;
435
+ if (basePath !== "/" && basePath !== "") {
436
+ rpcLogger.warn(
437
+ `SPFN_API_URL has a base path ("${basePath}"). proxy-guard signs route-relative paths, so the backend must receive paths WITHOUT this prefix (e.g. ingress strip) or strict mode will reject all signed requests with 403.`
438
+ );
439
+ }
440
+ } catch {
441
+ }
442
+ }
443
+ function resolveRoute(routeName) {
444
+ const entry = routeMap[routeName];
445
+ if (entry) {
446
+ return { method: entry.method, path: entry.path };
447
+ }
448
+ return null;
449
+ }
366
450
  async function handleRpc(request, context) {
367
451
  const startTime = Date.now();
368
452
  const params = await context.params;
@@ -375,6 +459,7 @@ function createRpcProxy(config) {
375
459
  );
376
460
  }
377
461
  let input = {};
462
+ let rawFormData = null;
378
463
  if (request.method === "GET") {
379
464
  const inputParam = request.nextUrl.searchParams.get("input");
380
465
  if (inputParam) {
@@ -388,45 +473,63 @@ function createRpcProxy(config) {
388
473
  }
389
474
  }
390
475
  } else {
391
- try {
392
- input = await request.json();
393
- } catch {
394
- return NextResponse.json(
395
- buildErrorResponse("Bad Request", "Invalid JSON body", debug),
396
- { status: 400 }
397
- );
398
- }
399
- }
400
- let routeDef = getRouteByPath(router, routeName);
401
- if (!routeDef && packageRouters.length > 0) {
402
- for (const pkgRouter of packageRouters) {
403
- routeDef = getRouteByPath(pkgRouter, routeName);
404
- if (routeDef) {
405
- if (debug) {
406
- rpcLogger.debug(`Route "${routeName}" found in package router`);
476
+ const contentType = request.headers.get("content-type") || "";
477
+ if (contentType.includes("multipart/form-data")) {
478
+ try {
479
+ rawFormData = await request.formData();
480
+ const metadataStr = rawFormData.get("__metadata");
481
+ if (metadataStr && typeof metadataStr === "string") {
482
+ const metadata = JSON.parse(metadataStr);
483
+ input.params = metadata.params;
484
+ input.query = metadata.query;
485
+ input.headers = metadata.headers;
486
+ input.cookies = metadata.cookies;
407
487
  }
408
- break;
488
+ input.formData = {};
489
+ rawFormData.forEach((value, key) => {
490
+ if (key === "__metadata") return;
491
+ const existing = input.formData[key];
492
+ if (existing !== void 0) {
493
+ if (Array.isArray(existing)) {
494
+ existing.push(value);
495
+ } else {
496
+ input.formData[key] = [existing, value];
497
+ }
498
+ } else {
499
+ input.formData[key] = value;
500
+ }
501
+ });
502
+ } catch (error) {
503
+ return NextResponse.json(
504
+ buildErrorResponse("Bad Request", "Invalid form data", debug),
505
+ { status: 400 }
506
+ );
507
+ }
508
+ } else {
509
+ try {
510
+ input = await request.json();
511
+ } catch {
512
+ return NextResponse.json(
513
+ buildErrorResponse("Bad Request", "Invalid JSON body", debug),
514
+ { status: 400 }
515
+ );
409
516
  }
410
517
  }
411
518
  }
412
- if (!routeDef) {
519
+ const routeInfo = resolveRoute(routeName);
520
+ if (!routeInfo) {
413
521
  rpcLogger.warn(`Route not found: ${routeName}`);
414
522
  return NextResponse.json(
415
- buildErrorResponse("Not Found", `Route "${routeName}" not found in router`, debug),
523
+ buildErrorResponse("Not Found", `Route "${routeName}" not found`, debug),
416
524
  { status: 404 }
417
525
  );
418
526
  }
419
- const { method: targetMethod, path: targetPath } = routeDef;
420
- if (!targetMethod || !targetPath) {
421
- rpcLogger.warn(`Route "${routeName}" is missing method or path`);
422
- return NextResponse.json(
423
- buildErrorResponse("Internal Error", `Route "${routeName}" is misconfigured`, debug),
424
- { status: 500 }
425
- );
426
- }
527
+ const { method: targetMethod, path: targetPath } = routeInfo;
427
528
  const inputParams = input.params || {};
428
529
  const inputQuery = input.query || {};
429
530
  const inputBody = input.body;
531
+ const inputFormData = input.formData;
532
+ const hasFormData = rawFormData !== null && inputFormData && Object.keys(inputFormData).length > 0;
430
533
  const resolvedPath = buildUrlWithParams(targetPath, inputParams);
431
534
  const queryString = buildQueryString(inputQuery);
432
535
  const targetUrl = `${apiUrl}${resolvedPath}${queryString}`;
@@ -436,21 +539,35 @@ function createRpcProxy(config) {
436
539
  targetMethod,
437
540
  targetPath: resolvedPath,
438
541
  targetUrl,
439
- hasBody: !!inputBody
542
+ hasBody: !!inputBody,
543
+ hasFormData
440
544
  });
441
545
  }
442
546
  const headers = buildProxyHeaders(request.headers, defaultHeaders);
547
+ if (hasFormData) {
548
+ headers.delete("content-type");
549
+ }
443
550
  const fetchOptions = {
444
551
  method: targetMethod,
445
552
  headers
446
553
  };
447
- if (["POST", "PUT", "PATCH"].includes(targetMethod) && inputBody) {
448
- fetchOptions.body = JSON.stringify(inputBody);
554
+ if (["POST", "PUT", "PATCH"].includes(targetMethod)) {
555
+ if (hasFormData && rawFormData) {
556
+ const forwardFormData = new FormData();
557
+ rawFormData.forEach((value, key) => {
558
+ if (key !== "__metadata") {
559
+ forwardFormData.append(key, value);
560
+ }
561
+ });
562
+ fetchOptions.body = forwardFormData;
563
+ } else if (inputBody) {
564
+ fetchOptions.body = JSON.stringify(inputBody);
565
+ }
449
566
  }
450
567
  const allInterceptors = collectInterceptors(autoDiscoverInterceptors, disableAutoInterceptors, interceptors, interceptorRegistry);
451
568
  const matchingInterceptors = filterMatchingInterceptors(allInterceptors, resolvedPath, targetMethod);
452
569
  if (debug && matchingInterceptors.length > 0) {
453
- rpcLogger.debug(`\u{1F3AF} Found ${matchingInterceptors.length} matching interceptors for ${targetMethod} ${resolvedPath}`);
570
+ rpcLogger.debug(`Found ${matchingInterceptors.length} matching interceptors for ${targetMethod} ${resolvedPath}`);
454
571
  }
455
572
  const cookiesMap = parseCookiesFromNextRequest(request);
456
573
  const requestCtx = buildRequestContext(
@@ -474,6 +591,19 @@ function createRpcProxy(config) {
474
591
  fetchOptions.body = JSON.stringify(requestCtx.body);
475
592
  }
476
593
  }
594
+ if (proxyKey) {
595
+ const signedBody = typeof fetchOptions.body === "string" ? fetchOptions.body : void 0;
596
+ const signatureHeaders = signProxyRequest({
597
+ key: proxyKey,
598
+ method: targetMethod,
599
+ path: resolvedPath,
600
+ query: queryString,
601
+ body: signedBody
602
+ });
603
+ for (const [headerName, value] of Object.entries(signatureHeaders)) {
604
+ headers.set(headerName, value);
605
+ }
606
+ }
477
607
  const controller = new AbortController();
478
608
  const timeoutId = setTimeout(() => controller.abort(), timeout);
479
609
  try {
@@ -490,7 +620,8 @@ function createRpcProxy(config) {
490
620
  inputBody,
491
621
  response,
492
622
  body,
493
- requestCtx.metadata
623
+ requestCtx.metadata,
624
+ requestCtx.cookies
494
625
  );
495
626
  const responseInterceptorsToRun = matchingInterceptors.map((r) => r.response).filter((i) => !!i);
496
627
  if (responseInterceptorsToRun.length > 0) {
@@ -517,7 +648,7 @@ function createRpcProxy(config) {
517
648
  const setCookieHeader = buildSetCookieHeader(cookie);
518
649
  nextResponse.headers.append("Set-Cookie", setCookieHeader);
519
650
  if (debug) {
520
- rpcLogger.debug("\u{1F36A} Set-Cookie header added", {
651
+ rpcLogger.debug("Set-Cookie header added", {
521
652
  name: cookie.name
522
653
  });
523
654
  }