@spfn/core 0.2.0-beta.9 → 0.3.0-beta.10

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 (119) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +693 -279
  3. package/dist/app-config/index.d.ts +96 -0
  4. package/dist/app-config/index.js +58 -0
  5. package/dist/app-config/index.js.map +1 -0
  6. package/dist/authz/index.d.ts +34 -0
  7. package/dist/authz/index.js +430 -0
  8. package/dist/authz/index.js.map +1 -0
  9. package/dist/boss-BiDCbbAQ.d.ts +337 -0
  10. package/dist/cache/index.js +42 -30
  11. package/dist/cache/index.js.map +1 -1
  12. package/dist/codegen/index.d.ts +121 -13
  13. package/dist/codegen/index.js +212 -15
  14. package/dist/codegen/index.js.map +1 -1
  15. package/dist/config/index.d.ts +576 -30
  16. package/dist/config/index.js +136 -17
  17. package/dist/config/index.js.map +1 -1
  18. package/dist/contract/index.d.ts +221 -0
  19. package/dist/contract/index.js +558 -0
  20. package/dist/contract/index.js.map +1 -0
  21. package/dist/db/index.d.ts +704 -94
  22. package/dist/db/index.js +1105 -260
  23. package/dist/db/index.js.map +1 -1
  24. package/dist/define-middleware-CVKgqo8S.d.ts +167 -0
  25. package/dist/env/index.d.ts +26 -2
  26. package/dist/env/index.js +15 -5
  27. package/dist/env/index.js.map +1 -1
  28. package/dist/env/loader.d.ts +26 -19
  29. package/dist/env/loader.js +55 -25
  30. package/dist/env/loader.js.map +1 -1
  31. package/dist/errors/index.d.ts +10 -0
  32. package/dist/errors/index.js +40 -7
  33. package/dist/errors/index.js.map +1 -1
  34. package/dist/event/index.d.ts +33 -3
  35. package/dist/event/index.js +24 -3
  36. package/dist/event/index.js.map +1 -1
  37. package/dist/event/sse/client.d.ts +55 -3
  38. package/dist/event/sse/client.js +164 -45
  39. package/dist/event/sse/client.js.map +1 -1
  40. package/dist/event/sse/index.d.ts +12 -5
  41. package/dist/event/sse/index.js +280 -32
  42. package/dist/event/sse/index.js.map +1 -1
  43. package/dist/event/ws/client.d.ts +59 -0
  44. package/dist/event/ws/client.js +273 -0
  45. package/dist/event/ws/client.js.map +1 -0
  46. package/dist/event/ws/index.d.ts +94 -0
  47. package/dist/event/ws/index.js +272 -0
  48. package/dist/event/ws/index.js.map +1 -0
  49. package/dist/job/index.d.ts +48 -3
  50. package/dist/job/index.js +265 -74
  51. package/dist/job/index.js.map +1 -1
  52. package/dist/logger/index.d.ts +5 -0
  53. package/dist/logger/index.js +14 -12
  54. package/dist/logger/index.js.map +1 -1
  55. package/dist/middleware/index.d.ts +352 -10
  56. package/dist/middleware/index.js +1079 -13
  57. package/dist/middleware/index.js.map +1 -1
  58. package/dist/nextjs/index.d.ts +39 -3
  59. package/dist/nextjs/index.js +82 -29
  60. package/dist/nextjs/index.js.map +1 -1
  61. package/dist/nextjs/server.d.ts +69 -52
  62. package/dist/nextjs/server.js +140 -60
  63. package/dist/nextjs/server.js.map +1 -1
  64. package/dist/ops/index.d.ts +208 -0
  65. package/dist/ops/index.js +800 -0
  66. package/dist/ops/index.js.map +1 -0
  67. package/dist/route/index.d.ts +10 -694
  68. package/dist/route/index.js +112 -23
  69. package/dist/route/index.js.map +1 -1
  70. package/dist/route-builder-2ani2jEI.d.ts +553 -0
  71. package/dist/router-DJdpwuB6.d.ts +127 -0
  72. package/dist/security/index.d.ts +83 -0
  73. package/dist/security/index.js +173 -0
  74. package/dist/security/index.js.map +1 -0
  75. package/dist/server/index.d.ts +619 -31
  76. package/dist/server/index.js +2583 -360
  77. package/dist/server/index.js.map +1 -1
  78. package/dist/token-manager-BT5EnUAR.d.ts +278 -0
  79. package/dist/types-2AbaW4Ie.d.ts +205 -0
  80. package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
  81. package/dist/types-ClQVomgV.d.ts +115 -0
  82. package/dist/types-ZQODsBft.d.ts +282 -0
  83. package/docs/file-upload.md +195 -333
  84. package/package.json +250 -208
  85. package/src/cache/README.md +330 -0
  86. package/src/codegen/README.md +516 -0
  87. package/src/config/README.md +326 -0
  88. package/src/contract/README.md +326 -0
  89. package/src/db/README.md +589 -0
  90. package/src/db/manager/README.md +500 -0
  91. package/src/db/schema/README.md +354 -0
  92. package/src/db/transaction/README.md +822 -0
  93. package/src/env/README.md +651 -0
  94. package/src/errors/README.md +429 -0
  95. package/src/event/README.md +741 -0
  96. package/src/job/README.md +613 -0
  97. package/src/logger/README.md +321 -0
  98. package/src/middleware/README.md +634 -0
  99. package/src/nextjs/README.md +608 -0
  100. package/src/route/README.md +738 -0
  101. package/src/security/README.md +100 -0
  102. package/src/server/README.md +704 -0
  103. package/dist/boss-DI1r4kTS.d.ts +0 -244
  104. package/dist/router-Di7ENoah.d.ts +0 -151
  105. package/dist/types-B-e_f2dQ.d.ts +0 -121
  106. package/docs/cache.md +0 -133
  107. package/docs/codegen.md +0 -74
  108. package/docs/database.md +0 -346
  109. package/docs/entity.md +0 -539
  110. package/docs/env.md +0 -477
  111. package/docs/errors.md +0 -319
  112. package/docs/event.md +0 -116
  113. package/docs/job.md +0 -131
  114. package/docs/logger.md +0 -108
  115. package/docs/middleware.md +0 -337
  116. package/docs/nextjs.md +0 -241
  117. package/docs/repository.md +0 -496
  118. package/docs/route.md +0 -497
  119. package/docs/server.md +0 -307
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { Router, HttpMethod } from '@spfn/core/route';
2
+ import { HttpMethod } from '@spfn/core/route';
3
3
  import { InterceptorRule as InterceptorRule$1 } from '@spfn/core/nextjs/server';
4
- import { f as SetCookie } from '../types-BOPTApC2.js';
4
+ import { S as SetCookie } from '../types-9oszaJqp.js';
5
5
  import '@sinclair/typebox';
6
6
  import '@spfn/core/errors';
7
7
 
@@ -44,9 +44,11 @@ interface TypedProxyConfig {
44
44
  */
45
45
  debug?: boolean;
46
46
  /**
47
- * Request timeout in milliseconds
47
+ * RPC proxy request timeout in milliseconds
48
+ * AbortController timeout - cancels proxied request if backend doesn't respond in time
49
+ * Should be shorter than FETCH_HEADERS_TIMEOUT to ensure meaningful 504 response
48
50
  *
49
- * @default 30000
51
+ * @default env.RPC_PROXY_TIMEOUT (120000)
50
52
  */
51
53
  timeout?: number;
52
54
  /**
@@ -115,22 +117,17 @@ interface TypedProxyConfig {
115
117
  * Next.js API Route handler that resolves routeName to method/path
116
118
  * and forwards requests to SPFN backend.
117
119
  *
118
- * @example Using routeMap (recommended - no server code loaded)
120
+ * @example
119
121
  * ```typescript
120
122
  * // app/api/rpc/[routeName]/route.ts
123
+ * import { createRpcProxy } from '@spfn/core/nextjs/server';
124
+ * import { authRouteMap } from '@spfn/auth';
125
+ * import { eventRouteMap } from '@spfn/core/event';
121
126
  * import { routeMap } from '@/generated/route-map';
122
- * import { createRpcProxy } from '@spfn/core/nextjs/proxy';
123
- *
124
- * export const { GET, POST } = createRpcProxy({ routeMap });
125
- * ```
126
- *
127
- * @example Using router (legacy - loads full server code)
128
- * ```typescript
129
- * // app/api/rpc/[routeName]/route.ts
130
- * import { appRouter } from '@/server/router';
131
- * import { createRpcProxy } from '@spfn/core/nextjs/proxy';
132
127
  *
133
- * export const { GET, POST } = createRpcProxy({ router: appRouter });
128
+ * export const { GET, POST } = createRpcProxy({
129
+ * routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },
130
+ * });
134
131
  * ```
135
132
  */
136
133
 
@@ -146,54 +143,37 @@ interface RouteMapEntry {
146
143
  */
147
144
  type RouteMap = Record<string, RouteMapEntry>;
148
145
  /**
149
- * Base config for RPC proxy
150
- */
151
- interface RpcProxyBaseConfig extends Omit<TypedProxyConfig, 'onRequest' | 'onResponse'> {
152
- }
153
- /**
154
- * Config using routeMap (recommended)
155
- *
156
- * Uses generated route map file - no server code loaded in Next.js process.
146
+ * RPC proxy configuration
157
147
  */
158
- interface RpcProxyRouteMapConfig extends RpcProxyBaseConfig {
148
+ interface RpcProxyConfig extends Omit<TypedProxyConfig, 'onRequest' | 'onResponse'> {
159
149
  /**
160
- * Generated route map containing routeName → {method, path} mappings
150
+ * Route map containing routeName → {method, path} mappings
151
+ *
152
+ * Merge generated route map with package route maps (auth, events, etc.)
161
153
  *
162
154
  * @example
163
155
  * ```typescript
156
+ * import { authRouteMap } from '@spfn/auth';
157
+ * import { eventRouteMap } from '@spfn/core/event';
164
158
  * import { routeMap } from '@/generated/route-map';
165
159
  *
166
- * export const { GET, POST } = createRpcProxy({ routeMap });
160
+ * export const { GET, POST } = createRpcProxy({
161
+ * routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },
162
+ * });
167
163
  * ```
168
164
  */
169
165
  routeMap: RouteMap;
170
- router?: never;
171
- }
172
- /**
173
- * Config using router (legacy)
174
- *
175
- * @deprecated Use routeMap instead to avoid loading server code in Next.js process
176
- */
177
- interface RpcProxyRouterConfig<TRouter extends Router<any>> extends RpcProxyBaseConfig {
178
166
  /**
179
- * The router containing all route definitions
167
+ * Shared secret for signing proxy→backend requests (HMAC-SHA256).
180
168
  *
181
- * @deprecated Use routeMap instead - router imports all server code
169
+ * When set, every forwarded request carries an HMAC signature the backend's
170
+ * proxy-guard middleware verifies, letting it reject direct-to-backend calls
171
+ * that bypass this proxy. Set the SAME value here and on the backend.
182
172
  *
183
- * @example
184
- * ```typescript
185
- * export const { GET, POST } = createRpcProxy({
186
- * router: appRouter,
187
- * });
188
- * ```
173
+ * @default process.env.SPFN_PROXY_SECRET (undefined → signing disabled)
189
174
  */
190
- router: TRouter;
191
- routeMap?: never;
175
+ proxySecret?: string;
192
176
  }
193
- /**
194
- * Combined config type
195
- */
196
- type RpcProxyConfig<TRouter extends Router<any> = Router<any>> = RpcProxyRouteMapConfig | RpcProxyRouterConfig<TRouter>;
197
177
  /**
198
178
  * Create RPC proxy handler for Next.js API Route
199
179
  *
@@ -201,10 +181,10 @@ type RpcProxyConfig<TRouter extends Router<any> = Router<any>> = RpcProxyRouteMa
201
181
  * - GET /api/rpc/{routeName}?input={...}
202
182
  * - POST /api/rpc/{routeName} with body
203
183
  *
204
- * Resolves routeName to actual HTTP method and path from the router or routeMap,
184
+ * Resolves routeName to actual HTTP method and path from routeMap,
205
185
  * then forwards to SPFN backend.
206
186
  */
207
- declare function createRpcProxy<TRouter extends Router<any>>(config: RpcProxyConfig<TRouter>): {
187
+ declare function createRpcProxy(config: RpcProxyConfig): {
208
188
  GET: (req: NextRequest, context: {
209
189
  params: Promise<{
210
190
  routeName?: string;
@@ -262,6 +242,28 @@ interface InterceptorRule {
262
242
  */
263
243
  response?: ResponseInterceptor;
264
244
  }
245
+ /**
246
+ * Response an interceptor refuses a request with
247
+ */
248
+ interface ProxyAbort {
249
+ /** HTTP status returned to the caller */
250
+ status: number;
251
+ /** JSON body returned to the caller */
252
+ body: unknown;
253
+ /**
254
+ * Cookies to set on the refusal itself
255
+ *
256
+ * A refusal skips the backend and every response interceptor, so this is the
257
+ * only way a refused request can hand the browser anything. The CSRF check in
258
+ * @spfn/auth uses it to reissue the readable token cookie on the 403 it emits,
259
+ * which is what makes "retry and it works" true rather than aspirational: the
260
+ * client that was refused now holds the value the next attempt needs.
261
+ *
262
+ * Set nothing an unauthenticated caller could not already have — a refusal is
263
+ * reachable by whoever sent the request.
264
+ */
265
+ setCookies?: SetCookie[];
266
+ }
265
267
  /**
266
268
  * Request Interceptor Context
267
269
  *
@@ -300,6 +302,17 @@ interface RequestInterceptorContext {
300
302
  * Metadata for sharing data between interceptors
301
303
  */
302
304
  metadata: Record<string, any>;
305
+ /**
306
+ * Refuse the request without calling the backend
307
+ *
308
+ * An interceptor that sets this stops the proxy before the fetch: the status
309
+ * and body are returned to the caller as-is and no response interceptor runs.
310
+ * A refusal therefore sets only the cookies the refusing interceptor put on
311
+ * `abort.setCookies` itself. Used by the CSRF check in @spfn/auth, the only
312
+ * layer that knows a request was authenticated from the session cookie rather
313
+ * than from a caller-supplied bearer token.
314
+ */
315
+ abort?: ProxyAbort;
303
316
  }
304
317
  /**
305
318
  * Response Interceptor Context
@@ -332,6 +345,10 @@ interface ResponseInterceptorContext {
332
345
  headers: Headers;
333
346
  body: any;
334
347
  };
348
+ /**
349
+ * Cookies from original request (read-only)
350
+ */
351
+ cookies: Map<string, string>;
335
352
  /**
336
353
  * Cookies to set in response
337
354
  *
@@ -627,4 +644,4 @@ declare const interceptorRegistry: InterceptorRegistry;
627
644
  */
628
645
  declare function registerInterceptors(packageName: string, interceptors: InterceptorRule[]): void;
629
646
 
630
- export { type InterceptorRule, type ProxyConfig, type RequestInterceptor, type RequestInterceptorContext, type ResponseInterceptor, type ResponseInterceptorContext, type RpcProxyConfig, createRpcProxy, executeRequestInterceptors, executeResponseInterceptors, filterMatchingInterceptors, interceptorRegistry, matchMethod, matchPath, registerInterceptors };
647
+ export { type InterceptorRule, type ProxyAbort, type ProxyConfig, type RequestInterceptor, type RequestInterceptorContext, type ResponseInterceptor, type ResponseInterceptorContext, type RpcProxyConfig, createRpcProxy, executeRequestInterceptors, executeResponseInterceptors, filterMatchingInterceptors, interceptorRegistry, matchMethod, matchPath, registerInterceptors };
@@ -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) {
@@ -188,6 +257,17 @@ function registerInterceptors(packageName, interceptors) {
188
257
  }
189
258
 
190
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
+ }
191
271
  function buildProxyHeaders(sourceHeaders, defaultHeaders) {
192
272
  const headers = new Headers();
193
273
  const headersToForward2 = [
@@ -196,7 +276,10 @@ function buildProxyHeaders(sourceHeaders, defaultHeaders) {
196
276
  "cookie",
197
277
  "user-agent",
198
278
  "accept",
199
- "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"
200
283
  ];
201
284
  for (const header of headersToForward2) {
202
285
  const value = sourceHeaders instanceof Headers ? sourceHeaders.get(header) : sourceHeaders[header];
@@ -204,6 +287,11 @@ function buildProxyHeaders(sourceHeaders, defaultHeaders) {
204
287
  headers.set(header, value);
205
288
  }
206
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
+ }
207
295
  for (const [key, value] of Object.entries(defaultHeaders)) {
208
296
  headers.set(key, value);
209
297
  }
@@ -305,7 +393,7 @@ function buildRequestContext(path, method, headers, body, searchParams, cookiesM
305
393
  metadata: {}
306
394
  };
307
395
  }
308
- function buildResponseContext(path, method, requestHeaders, requestBody, response, responseBody, requestMetadata) {
396
+ function buildResponseContext(path, method, requestHeaders, requestBody, response, responseBody, requestMetadata, cookies) {
309
397
  return {
310
398
  path: `/${path}`,
311
399
  method,
@@ -320,6 +408,7 @@ function buildResponseContext(path, method, requestHeaders, requestBody, respons
320
408
  headers: response.headers,
321
409
  body: responseBody
322
410
  },
411
+ cookies,
323
412
  setCookies: [],
324
413
  metadata: requestMetadata
325
414
  };
@@ -327,71 +416,34 @@ function buildResponseContext(path, method, requestHeaders, requestBody, respons
327
416
 
328
417
  // src/nextjs/proxy/rpc.ts
329
418
  var rpcLogger = logger.child("@spfn/core:rpc-proxy");
330
- function isRouteDef(value) {
331
- return value !== null && typeof value === "object" && "handler" in value && "method" in value && "path" in value;
332
- }
333
- function isRouter(value) {
334
- return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
335
- }
336
- function getRouteByPath(router, routePath) {
337
- const parts = routePath.split(".");
338
- let current = router.routes;
339
- for (const part of parts) {
340
- if (!current || typeof current !== "object") {
341
- return null;
342
- }
343
- const next = current[part];
344
- if (isRouter(next)) {
345
- current = next.routes;
346
- } else if (isRouteDef(next)) {
347
- return next;
348
- } else {
349
- current = next;
350
- }
351
- }
352
- if (isRouteDef(current)) {
353
- return current;
354
- }
355
- return null;
356
- }
357
419
  function createRpcProxy(config) {
358
420
  const {
359
421
  apiUrl = env.SPFN_API_URL || "http://localhost:8790",
360
422
  debug = env.NODE_ENV === "development",
361
- timeout = 3e4,
423
+ timeout = env.RPC_PROXY_TIMEOUT,
362
424
  headers: defaultHeaders = {},
363
425
  interceptors,
364
426
  autoDiscoverInterceptors = true,
365
- disableAutoInterceptors
427
+ disableAutoInterceptors,
428
+ proxySecret = env.SPFN_PROXY_SECRET,
429
+ routeMap
366
430
  } = config;
367
- const useRouteMap = "routeMap" in config && config.routeMap !== void 0;
368
- const routeMap = useRouteMap ? config.routeMap : null;
369
- const router = !useRouteMap && "router" in config ? config.router : null;
370
- const packageRouters = router?._packageRouters || [];
371
- function resolveRoute(routeName) {
372
- if (routeMap) {
373
- const entry = routeMap[routeName];
374
- if (entry) {
375
- return { method: entry.method, path: entry.path };
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
+ );
376
439
  }
377
- return null;
440
+ } catch {
378
441
  }
379
- if (router) {
380
- let routeDef = getRouteByPath(router, routeName);
381
- if (!routeDef && packageRouters.length > 0) {
382
- for (const pkgRouter of packageRouters) {
383
- routeDef = getRouteByPath(pkgRouter, routeName);
384
- if (routeDef) {
385
- if (debug) {
386
- rpcLogger.debug(`Route "${routeName}" found in package router`);
387
- }
388
- break;
389
- }
390
- }
391
- }
392
- if (routeDef && routeDef.method && routeDef.path) {
393
- return { method: routeDef.method, path: routeDef.path };
394
- }
442
+ }
443
+ function resolveRoute(routeName) {
444
+ const entry = routeMap[routeName];
445
+ if (entry) {
446
+ return { method: entry.method, path: entry.path };
395
447
  }
396
448
  return null;
397
449
  }
@@ -515,7 +567,7 @@ function createRpcProxy(config) {
515
567
  const allInterceptors = collectInterceptors(autoDiscoverInterceptors, disableAutoInterceptors, interceptors, interceptorRegistry);
516
568
  const matchingInterceptors = filterMatchingInterceptors(allInterceptors, resolvedPath, targetMethod);
517
569
  if (debug && matchingInterceptors.length > 0) {
518
- rpcLogger.debug(`\u{1F3AF} Found ${matchingInterceptors.length} matching interceptors for ${targetMethod} ${resolvedPath}`);
570
+ rpcLogger.debug(`Found ${matchingInterceptors.length} matching interceptors for ${targetMethod} ${resolvedPath}`);
519
571
  }
520
572
  const cookiesMap = parseCookiesFromNextRequest(request);
521
573
  const requestCtx = buildRequestContext(
@@ -539,6 +591,33 @@ function createRpcProxy(config) {
539
591
  fetchOptions.body = JSON.stringify(requestCtx.body);
540
592
  }
541
593
  }
594
+ if (requestCtx.abort) {
595
+ rpcLogger.warn("Request refused by an interceptor", {
596
+ routeName,
597
+ status: requestCtx.abort.status
598
+ });
599
+ const abortResponse = NextResponse.json(
600
+ requestCtx.abort.body,
601
+ { status: requestCtx.abort.status }
602
+ );
603
+ for (const cookie of requestCtx.abort.setCookies ?? []) {
604
+ abortResponse.headers.append("Set-Cookie", buildSetCookieHeader(cookie));
605
+ }
606
+ return abortResponse;
607
+ }
608
+ if (proxyKey) {
609
+ const signedBody = typeof fetchOptions.body === "string" ? fetchOptions.body : void 0;
610
+ const signatureHeaders = signProxyRequest({
611
+ key: proxyKey,
612
+ method: targetMethod,
613
+ path: resolvedPath,
614
+ query: queryString,
615
+ body: signedBody
616
+ });
617
+ for (const [headerName, value] of Object.entries(signatureHeaders)) {
618
+ headers.set(headerName, value);
619
+ }
620
+ }
542
621
  const controller = new AbortController();
543
622
  const timeoutId = setTimeout(() => controller.abort(), timeout);
544
623
  try {
@@ -555,7 +634,8 @@ function createRpcProxy(config) {
555
634
  inputBody,
556
635
  response,
557
636
  body,
558
- requestCtx.metadata
637
+ requestCtx.metadata,
638
+ requestCtx.cookies
559
639
  );
560
640
  const responseInterceptorsToRun = matchingInterceptors.map((r) => r.response).filter((i) => !!i);
561
641
  if (responseInterceptorsToRun.length > 0) {
@@ -582,7 +662,7 @@ function createRpcProxy(config) {
582
662
  const setCookieHeader = buildSetCookieHeader(cookie);
583
663
  nextResponse.headers.append("Set-Cookie", setCookieHeader);
584
664
  if (debug) {
585
- rpcLogger.debug("\u{1F36A} Set-Cookie header added", {
665
+ rpcLogger.debug("Set-Cookie header added", {
586
666
  name: cookie.name
587
667
  });
588
668
  }