@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.
- package/LICENSE +1 -1
- package/README.md +693 -279
- package/dist/app-config/index.d.ts +96 -0
- package/dist/app-config/index.js +58 -0
- package/dist/app-config/index.js.map +1 -0
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +430 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/boss-BiDCbbAQ.d.ts +337 -0
- package/dist/cache/index.js +42 -30
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +121 -13
- package/dist/codegen/index.js +212 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +576 -30
- package/dist/config/index.js +136 -17
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +221 -0
- package/dist/contract/index.js +558 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/db/index.d.ts +704 -94
- package/dist/db/index.js +1105 -260
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-CVKgqo8S.d.ts +167 -0
- package/dist/env/index.d.ts +26 -2
- package/dist/env/index.js +15 -5
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +55 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +40 -7
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +24 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +55 -3
- package/dist/event/sse/client.js +164 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +280 -32
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +272 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +265 -74
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +352 -10
- package/dist/middleware/index.js +1079 -13
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +39 -3
- package/dist/nextjs/index.js +82 -29
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +69 -52
- package/dist/nextjs/server.js +140 -60
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +208 -0
- package/dist/ops/index.js +800 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/route/index.d.ts +10 -694
- package/dist/route/index.js +112 -23
- package/dist/route/index.js.map +1 -1
- package/dist/route-builder-2ani2jEI.d.ts +553 -0
- package/dist/router-DJdpwuB6.d.ts +127 -0
- package/dist/security/index.d.ts +83 -0
- package/dist/security/index.js +173 -0
- package/dist/security/index.js.map +1 -0
- package/dist/server/index.d.ts +619 -31
- package/dist/server/index.js +2583 -360
- package/dist/server/index.js.map +1 -1
- package/dist/token-manager-BT5EnUAR.d.ts +278 -0
- package/dist/types-2AbaW4Ie.d.ts +205 -0
- package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
- package/dist/types-ClQVomgV.d.ts +115 -0
- package/dist/types-ZQODsBft.d.ts +282 -0
- package/docs/file-upload.md +195 -333
- package/package.json +250 -208
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +354 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +741 -0
- package/src/job/README.md +613 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/dist/boss-DI1r4kTS.d.ts +0 -244
- package/dist/router-Di7ENoah.d.ts +0 -151
- package/dist/types-B-e_f2dQ.d.ts +0 -121
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -346
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -477
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -116
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -241
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- package/docs/server.md +0 -307
package/dist/nextjs/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
2
|
+
import { HttpMethod } from '@spfn/core/route';
|
|
3
3
|
import { InterceptorRule as InterceptorRule$1 } from '@spfn/core/nextjs/server';
|
|
4
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
*
|
|
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
|
|
148
|
+
interface RpcProxyConfig extends Omit<TypedProxyConfig, 'onRequest' | 'onResponse'> {
|
|
159
149
|
/**
|
|
160
|
-
*
|
|
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({
|
|
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
|
-
*
|
|
167
|
+
* Shared secret for signing proxy→backend requests (HMAC-SHA256).
|
|
180
168
|
*
|
|
181
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
|
184
|
+
* Resolves routeName to actual HTTP method and path from routeMap,
|
|
205
185
|
* then forwards to SPFN backend.
|
|
206
186
|
*/
|
|
207
|
-
declare function createRpcProxy
|
|
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 };
|
package/dist/nextjs/server.js
CHANGED
|
@@ -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 =
|
|
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
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
440
|
+
} catch {
|
|
378
441
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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(
|
|
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("
|
|
665
|
+
rpcLogger.debug("Set-Cookie header added", {
|
|
586
666
|
name: cookie.name
|
|
587
667
|
});
|
|
588
668
|
}
|