@spfn/core 0.3.0-beta.3 → 0.3.0-beta.31
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/README.md +304 -12
- package/dist/app-config/index.d.ts +39 -1
- package/dist/app-config/index.js.map +1 -1
- package/dist/authz/index.js +47 -391
- package/dist/authz/index.js.map +1 -1
- package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
- package/dist/codegen/index.d.ts +157 -16
- package/dist/codegen/index.js +772 -166
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js +4 -4
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +39 -16
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- package/dist/db/index.d.ts +230 -35
- package/dist/db/index.js +316 -160
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +6 -442
- package/dist/env/index.js +13 -16
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.js +24 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/error-CIZlYJlL.d.ts +157 -0
- package/dist/errors/index.d.ts +18 -1
- package/dist/errors/index.js +47 -391
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +5 -5
- package/dist/event/index.js +202 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +18 -4
- package/dist/event/sse/client.js +94 -18
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +18 -5
- package/dist/event/sse/index.js +452 -114
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +3 -2
- package/dist/event/ws/index.d.ts +4 -3
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +131 -52
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js +0 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.js +52 -397
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +75 -14
- package/dist/nextjs/index.js +200 -84
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +46 -8
- package/dist/nextjs/server.js +15 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +61 -42
- package/dist/ops/index.js +648 -97
- package/dist/ops/index.js.map +1 -1
- package/dist/registry-C2KQCh0m.d.ts +473 -0
- package/dist/route/index.d.ts +5 -2
- package/dist/route/index.js +107 -28
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +7 -1
- package/dist/{router-Cy7rAfmj.d.ts → route-builder-DRW-MSCJ.d.ts} +51 -126
- package/dist/router-C57y-vz1.d.ts +184 -0
- package/dist/router-CX5uVjmU.d.ts +406 -0
- package/dist/server/index.d.ts +130 -7
- package/dist/server/index.js +1174 -164
- package/dist/server/index.js.map +1 -1
- package/dist/state/client.d.ts +206 -0
- package/dist/state/client.js +678 -0
- package/dist/state/client.js.map +1 -0
- package/dist/state/index.d.ts +443 -0
- package/dist/state/index.js +644 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/react.d.ts +49 -0
- package/dist/state/react.js +50 -0
- package/dist/state/react.js.map +1 -0
- package/dist/surface/index.d.ts +115 -0
- package/dist/surface/index.js +1013 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-CfYVhIQ9.d.ts → types-5Rl11_Nb.d.ts} +43 -4
- package/dist/types-CKH0D3Dy.d.ts +441 -0
- package/dist/{types-ZQODsBft.d.ts → types-CurMab2Q.d.ts} +52 -2
- package/dist/{types-2AbaW4Ie.d.ts → types-DncqXfWN.d.ts} +4 -3
- package/docs/file-upload.md +195 -333
- package/package.json +41 -11
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +721 -0
- package/src/config/README.md +333 -0
- package/src/contract/README.md +442 -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 +845 -0
- package/src/env/README.md +787 -0
- package/src/errors/README.md +445 -0
- package/src/event/README.md +994 -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 +756 -0
- package/src/route/README.md +763 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/src/state/README.md +806 -0
- package/dist/token-manager-BT5EnUAR.d.ts +0 -278
- package/dist/types-9oszaJqp.d.ts +0 -250
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router, RouteDef } from '@spfn/core/route';
|
|
2
|
-
import { R as RequestInterceptor, a as ResponseInterceptor, I as InferRouteInput, b as InferRouteOutput, A as ApiConfig } from '../types-
|
|
3
|
-
export { C as CallOptions,
|
|
2
|
+
import { R as RequestInterceptor, a as ResponseInterceptor, I as InferRouteInput, b as InferRouteOutput, E as ExtractRoutes, A as ApiConfig } from '../types-CKH0D3Dy.js';
|
|
3
|
+
export { c as ApiError, d as ApiOutcome, C as CallOptions, e as CookieOptions, f as RouterInput, g as RouterOutput, S as SetCookie, h as StructuredInput } from '../types-CKH0D3Dy.js';
|
|
4
4
|
import '@sinclair/typebox';
|
|
5
5
|
import '@spfn/core/errors';
|
|
6
6
|
|
|
@@ -57,6 +57,11 @@ type RouteClient<TRoute extends RouteDef<any, any>> = {
|
|
|
57
57
|
tags?: string[];
|
|
58
58
|
};
|
|
59
59
|
}): RouteClient<TRoute>;
|
|
60
|
+
/**
|
|
61
|
+
* Set this call's timeout in milliseconds, overriding the client-wide one.
|
|
62
|
+
* Throws a `TypeError` unless `ms` is finite, `> 0` and `<= 2147483647`.
|
|
63
|
+
*/
|
|
64
|
+
timeout(ms: number): RouteClient<TRoute>;
|
|
60
65
|
/**
|
|
61
66
|
* Set request interceptor
|
|
62
67
|
*/
|
|
@@ -80,11 +85,42 @@ type RouteClient<TRoute extends RouteDef<any, any>> = {
|
|
|
80
85
|
call: HasAnyRequiredFields<InferRouteInput<TRoute>> extends true ? (input: CleanStructuredInput<InferRouteInput<TRoute>>) => Promise<InferRouteOutput<TRoute>> : (input?: CleanStructuredInput<InferRouteInput<TRoute>>) => Promise<InferRouteOutput<TRoute>>;
|
|
81
86
|
};
|
|
82
87
|
/**
|
|
83
|
-
*
|
|
88
|
+
* One property per route in a flattened route record.
|
|
89
|
+
*
|
|
90
|
+
* A slot the flattening could not resolve to a route definition is `never`, as it was
|
|
91
|
+
* before nesting was flattened — a record whose values are neither routes nor routers
|
|
92
|
+
* names nothing callable.
|
|
84
93
|
*/
|
|
85
|
-
type
|
|
86
|
-
[K in keyof
|
|
94
|
+
type RouteClients<TRoutes> = {
|
|
95
|
+
[K in keyof TRoutes]: TRoutes[K] extends RouteDef<any, any, any> ? RouteClient<TRoutes[K]> : never;
|
|
87
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Typed client for entire router
|
|
99
|
+
*
|
|
100
|
+
* One flat namespace, holding the same route names the server registers. A route
|
|
101
|
+
* declared inside a nested `defineRouter` is reached by its own key — `api.getThing`,
|
|
102
|
+
* never `api.group.getThing` — because that is the name `registerRoutes` mounts it
|
|
103
|
+
* under, the name the RPC proxy resolves, and the name `RouterOutput` takes. The group
|
|
104
|
+
* key is not a route and naming it is a compile error.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const appRouter = defineRouter({
|
|
109
|
+
* getRoot,
|
|
110
|
+
* users: defineRouter({ getUser }),
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* const api = createApi<typeof appRouter>();
|
|
114
|
+
*
|
|
115
|
+
* await api.getUser.call({ params: { id: '1' } }); // not api.users.getUser
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* A name two branches both declare is left out of the namespace rather than resolved to
|
|
119
|
+
* one of them — the same collision `RouterOutput` refuses. Routes mounted with
|
|
120
|
+
* `.packages()` are deliberately absent: a package publishes its own route map and its
|
|
121
|
+
* own client (`authApi`, `cmsApi`), and that is what calls them.
|
|
122
|
+
*/
|
|
123
|
+
type Client<TRouter extends Router<any>> = RouteClients<ExtractRoutes<TRouter>>;
|
|
88
124
|
|
|
89
125
|
/**
|
|
90
126
|
* Create type-safe RPC client
|
|
@@ -107,14 +143,39 @@ type Client<TRouter extends Router<any>> = {
|
|
|
107
143
|
declare function createApi<TRouter extends Router<any>>(config?: ApiConfig): Client<TRouter>;
|
|
108
144
|
|
|
109
145
|
/**
|
|
110
|
-
*
|
|
146
|
+
* CSRF token transport for the RPC client
|
|
147
|
+
*
|
|
148
|
+
* The Next.js proxy sets a readable (non-HttpOnly) cookie carrying an HMAC of
|
|
149
|
+
* the session's key id; this module mirrors it into a request header. The proxy
|
|
150
|
+
* never compares cookie against header — it recomputes the value from the
|
|
151
|
+
* session — so the cookie is transport only and reading it here is not a trust
|
|
152
|
+
* decision. See the CSRF section of the @spfn/auth README.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* Header the readable CSRF cookie is mirrored into.
|
|
156
|
+
*
|
|
157
|
+
* Must match CSRF_HEADER in @spfn/auth. It is duplicated rather than imported
|
|
158
|
+
* because @spfn/core cannot depend on @spfn/auth.
|
|
159
|
+
*/
|
|
160
|
+
declare const CSRF_HEADER = "x-spfn-csrf";
|
|
161
|
+
/**
|
|
162
|
+
* Collect the CSRF token candidates from cookie entries.
|
|
163
|
+
*
|
|
164
|
+
* Takes entries rather than a map because a name can legitimately appear twice:
|
|
165
|
+
* a sibling subdomain that tosses `spfn_csrf` for the parent domain leaves the
|
|
166
|
+
* browser holding two cookies of that name, and folding them into a map would
|
|
167
|
+
* throw away whichever the tossed one displaced — locking the user out of every
|
|
168
|
+
* mutation. Sending both keeps the real one in play, and the tossed one is inert
|
|
169
|
+
* because the proxy recomputes what it expects.
|
|
170
|
+
*
|
|
171
|
+
* @returns Header value (comma-separated), or undefined when no cookie exists
|
|
172
|
+
*/
|
|
173
|
+
declare function csrfHeaderValue(cookies: Iterable<[string, string]>): string | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* Read the browser cookie jar as entries, duplicates preserved.
|
|
176
|
+
*
|
|
177
|
+
* Empty outside a browser (SSR reads the jar it forwards instead).
|
|
111
178
|
*/
|
|
112
|
-
declare
|
|
113
|
-
readonly status: number;
|
|
114
|
-
readonly url: string;
|
|
115
|
-
readonly response?: unknown | undefined;
|
|
116
|
-
readonly errorType?: "http" | "network" | "timeout" | undefined;
|
|
117
|
-
constructor(message: string, status: number, url: string, response?: unknown | undefined, errorType?: "http" | "network" | "timeout" | undefined);
|
|
118
|
-
}
|
|
179
|
+
declare function documentCookieEntries(): Array<[string, string]>;
|
|
119
180
|
|
|
120
|
-
export { ApiConfig,
|
|
181
|
+
export { ApiConfig, CSRF_HEADER, type Client, InferRouteInput, InferRouteOutput, RequestInterceptor, ResponseInterceptor, type RouteClient, createApi, csrfHeaderValue, documentCookieEntries };
|
package/dist/nextjs/index.js
CHANGED
|
@@ -69,8 +69,8 @@ function logThrowingDeserializedError(logger3, error) {
|
|
|
69
69
|
|
|
70
70
|
// src/nextjs/client/errors.ts
|
|
71
71
|
var ApiError = class extends Error {
|
|
72
|
-
constructor(message, status, url, response, errorType) {
|
|
73
|
-
super(message);
|
|
72
|
+
constructor(message, status, url, response, errorType, options) {
|
|
73
|
+
super(message, options);
|
|
74
74
|
this.status = status;
|
|
75
75
|
this.url = url;
|
|
76
76
|
this.response = response;
|
|
@@ -103,7 +103,7 @@ async function autoDetectServerCookies() {
|
|
|
103
103
|
return {};
|
|
104
104
|
}
|
|
105
105
|
try {
|
|
106
|
-
const { cookies } = await import('next/headers');
|
|
106
|
+
const { cookies } = await import('next/headers.js');
|
|
107
107
|
const cookieStore = await cookies();
|
|
108
108
|
const allCookies = cookieStore.getAll();
|
|
109
109
|
const result = Object.fromEntries(
|
|
@@ -123,55 +123,90 @@ async function autoDetectServerCookies() {
|
|
|
123
123
|
return {};
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
function openAbortScope(callerSignal, timeout) {
|
|
127
127
|
const controller = new AbortController();
|
|
128
|
-
|
|
128
|
+
let cause;
|
|
129
|
+
const abort = (source, reason) => {
|
|
130
|
+
cause ??= source;
|
|
131
|
+
controller.abort(reason);
|
|
132
|
+
};
|
|
133
|
+
const onCallerAbort = () => abort("caller", callerSignal?.reason);
|
|
134
|
+
const timeoutId = setTimeout(() => abort("timeout"), timeout);
|
|
135
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
136
|
+
return {
|
|
137
|
+
signal: controller.signal,
|
|
138
|
+
cause: () => cause,
|
|
139
|
+
stopTimer: () => clearTimeout(timeoutId),
|
|
140
|
+
close: () => {
|
|
141
|
+
clearTimeout(timeoutId);
|
|
142
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function fetchInAbortScope(url, init, timeout, customFetch = fetch) {
|
|
147
|
+
const callerSignal = init.signal ?? void 0;
|
|
148
|
+
if (callerSignal?.aborted) {
|
|
149
|
+
return { ok: false, error: callerSignal.reason, cause: "caller" };
|
|
150
|
+
}
|
|
151
|
+
const scope = openAbortScope(callerSignal, timeout);
|
|
129
152
|
try {
|
|
130
|
-
const response = await customFetch(url, {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
const response = await customFetch(url, { ...init, signal: scope.signal });
|
|
154
|
+
scope.stopTimer();
|
|
155
|
+
const body = await parseResponseBody(response);
|
|
156
|
+
if (scope.cause() === "caller") {
|
|
157
|
+
return { ok: false, error: callerSignal?.reason, cause: "caller" };
|
|
158
|
+
}
|
|
159
|
+
return { ok: true, response, body };
|
|
136
160
|
} catch (error) {
|
|
137
|
-
|
|
138
|
-
|
|
161
|
+
const cause = scope.cause();
|
|
162
|
+
return { ok: false, cause, error: cause === "caller" ? callerSignal?.reason : error };
|
|
163
|
+
} finally {
|
|
164
|
+
scope.close();
|
|
139
165
|
}
|
|
140
166
|
}
|
|
141
|
-
|
|
167
|
+
function errorFromResponse(response, body, fullUrl, errorRegistry, debug, logger3) {
|
|
142
168
|
if (debug) {
|
|
143
169
|
logErrorResponse(logger3, response.status, body);
|
|
144
170
|
}
|
|
145
|
-
|
|
146
|
-
if (
|
|
171
|
+
const deserializedError = deserializeRegisteredError(body, errorRegistry, debug, logger3);
|
|
172
|
+
if (deserializedError) {
|
|
147
173
|
if (debug) {
|
|
148
|
-
|
|
174
|
+
logThrowingDeserializedError(logger3, deserializedError);
|
|
149
175
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
176
|
+
return deserializedError;
|
|
177
|
+
}
|
|
178
|
+
return fallbackApiError(response, body, fullUrl, logger3);
|
|
179
|
+
}
|
|
180
|
+
function deserializeRegisteredError(body, errorRegistry, debug, logger3) {
|
|
181
|
+
if (!errorRegistry || !body || typeof body !== "object" || !("__type" in body)) {
|
|
182
|
+
if (debug) {
|
|
183
|
+
logErrorDeserializationSkipped(logger3, errorRegistry, body);
|
|
159
184
|
}
|
|
160
|
-
|
|
161
|
-
logErrorDeserializationSkipped(logger3, errorRegistry, body);
|
|
185
|
+
return null;
|
|
162
186
|
}
|
|
163
|
-
if (
|
|
187
|
+
if (debug) {
|
|
188
|
+
logErrorDeserializationAttempt(logger3, body.__type, errorRegistry.getRegisteredTypes());
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const deserializedError = errorRegistry.deserialize(body);
|
|
164
192
|
if (debug) {
|
|
165
|
-
|
|
193
|
+
logErrorDeserializationSuccess(logger3, deserializedError);
|
|
194
|
+
}
|
|
195
|
+
return deserializedError;
|
|
196
|
+
} catch (deserializeError) {
|
|
197
|
+
if (debug) {
|
|
198
|
+
logErrorDeserializationFailure(logger3, deserializeError);
|
|
166
199
|
}
|
|
167
|
-
|
|
200
|
+
return null;
|
|
168
201
|
}
|
|
202
|
+
}
|
|
203
|
+
function fallbackApiError(response, body, fullUrl, logger3) {
|
|
169
204
|
if (response.status === 404 && process.env.NODE_ENV !== "production") {
|
|
170
205
|
logger3.warn(
|
|
171
206
|
"\n\u26A0\uFE0F 404 Not Found\n\nCheck the following:\n 1. Routes are registered in server.config.ts:\n \u2192 defineServerConfig().routes(appRouter)\n 2. Delete .spfn cache if you recently added new routes:\n \u2192 rm -rf .spfn\n"
|
|
172
207
|
);
|
|
173
208
|
}
|
|
174
|
-
|
|
209
|
+
return new ApiError(
|
|
175
210
|
body?.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
176
211
|
response.status,
|
|
177
212
|
fullUrl,
|
|
@@ -180,7 +215,40 @@ async function handleErrorResponse(response, body, fullUrl, errorRegistry, debug
|
|
|
180
215
|
);
|
|
181
216
|
}
|
|
182
217
|
|
|
218
|
+
// src/nextjs/client/csrf.ts
|
|
219
|
+
var CSRF_HEADER = "x-spfn-csrf";
|
|
220
|
+
var CSRF_COOKIE_PATTERN = /^spfn_csrf(_\d+)?$/;
|
|
221
|
+
var HEADER_SAFE_VALUE = /^[!-~]+$/;
|
|
222
|
+
var MAX_CANDIDATES = 32;
|
|
223
|
+
function selectCandidates(values) {
|
|
224
|
+
const unique = Array.from(new Set(values));
|
|
225
|
+
if (unique.length <= MAX_CANDIDATES) {
|
|
226
|
+
return unique;
|
|
227
|
+
}
|
|
228
|
+
const half = MAX_CANDIDATES / 2;
|
|
229
|
+
return [...unique.slice(0, half), ...unique.slice(-half)];
|
|
230
|
+
}
|
|
231
|
+
function csrfHeaderValue(cookies) {
|
|
232
|
+
const values = Array.from(cookies).filter(([name, value]) => CSRF_COOKIE_PATTERN.test(name) && HEADER_SAFE_VALUE.test(value) && !value.includes(",")).map(([, value]) => value);
|
|
233
|
+
const candidates = selectCandidates(values);
|
|
234
|
+
return candidates.length > 0 ? candidates.join(",") : void 0;
|
|
235
|
+
}
|
|
236
|
+
function documentCookieEntries() {
|
|
237
|
+
if (typeof document === "undefined" || !document.cookie) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const entries = [];
|
|
241
|
+
for (const pair of document.cookie.split(";")) {
|
|
242
|
+
const separator = pair.indexOf("=");
|
|
243
|
+
if (separator > 0) {
|
|
244
|
+
entries.push([pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return entries;
|
|
248
|
+
}
|
|
249
|
+
|
|
183
250
|
// src/nextjs/client/builder.ts
|
|
251
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
184
252
|
var RouteCallBuilder = class _RouteCallBuilder {
|
|
185
253
|
constructor(executor, routeName) {
|
|
186
254
|
this.executor = executor;
|
|
@@ -189,6 +257,7 @@ var RouteCallBuilder = class _RouteCallBuilder {
|
|
|
189
257
|
_headers;
|
|
190
258
|
_cookies;
|
|
191
259
|
_fetchOptions;
|
|
260
|
+
_timeout;
|
|
192
261
|
_onRequest;
|
|
193
262
|
_onResponse;
|
|
194
263
|
/**
|
|
@@ -202,6 +271,7 @@ var RouteCallBuilder = class _RouteCallBuilder {
|
|
|
202
271
|
builder._headers = this._headers;
|
|
203
272
|
builder._cookies = this._cookies;
|
|
204
273
|
builder._fetchOptions = this._fetchOptions;
|
|
274
|
+
builder._timeout = this._timeout;
|
|
205
275
|
builder._onRequest = this._onRequest;
|
|
206
276
|
builder._onResponse = this._onResponse;
|
|
207
277
|
return builder;
|
|
@@ -230,6 +300,20 @@ var RouteCallBuilder = class _RouteCallBuilder {
|
|
|
230
300
|
builder._fetchOptions = { ...this._fetchOptions, ...options };
|
|
231
301
|
return builder;
|
|
232
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Set this call's timeout in milliseconds, overriding the client-wide one
|
|
305
|
+
*
|
|
306
|
+
* Throws a `TypeError`, before any request is made, unless `ms` is a finite number
|
|
307
|
+
* greater than 0 and at most 2147483647 (the largest delay `setTimeout` honours).
|
|
308
|
+
*/
|
|
309
|
+
timeout(ms) {
|
|
310
|
+
if (!Number.isFinite(ms) || ms <= 0 || ms > MAX_TIMEOUT_MS) {
|
|
311
|
+
throw new TypeError(`timeout must be a finite number of milliseconds in (0, ${MAX_TIMEOUT_MS}], got ${String(ms)}`);
|
|
312
|
+
}
|
|
313
|
+
const builder = this.clone();
|
|
314
|
+
builder._timeout = ms;
|
|
315
|
+
return builder;
|
|
316
|
+
}
|
|
233
317
|
/**
|
|
234
318
|
* Set request interceptor
|
|
235
319
|
*/
|
|
@@ -265,6 +349,9 @@ var RouteCallBuilder = class _RouteCallBuilder {
|
|
|
265
349
|
if (this._fetchOptions) {
|
|
266
350
|
options.fetchOptions = this._fetchOptions;
|
|
267
351
|
}
|
|
352
|
+
if (this._timeout !== void 0) {
|
|
353
|
+
options.timeout = this._timeout;
|
|
354
|
+
}
|
|
268
355
|
if (this._onRequest) {
|
|
269
356
|
options.onRequest = this._onRequest;
|
|
270
357
|
}
|
|
@@ -277,6 +364,53 @@ var RouteCallBuilder = class _RouteCallBuilder {
|
|
|
277
364
|
|
|
278
365
|
// src/nextjs/client/core.ts
|
|
279
366
|
var apiLogger = logger.child("@spfn/core:api-client");
|
|
367
|
+
function transportOutcome(errorType, message, status, url, options) {
|
|
368
|
+
return { ok: false, errorType, status, error: new ApiError(message, status, url, void 0, errorType, options) };
|
|
369
|
+
}
|
|
370
|
+
function transportError(failure, site, timeout) {
|
|
371
|
+
if (failure.cause === "timeout") {
|
|
372
|
+
apiLogger.error("Request timeout", { ...site, timeout });
|
|
373
|
+
return transportOutcome("timeout", `Request timeout after ${timeout}ms`, 408, site.url);
|
|
374
|
+
}
|
|
375
|
+
if (failure.cause === "caller") {
|
|
376
|
+
apiLogger.debug("Request aborted by caller", site);
|
|
377
|
+
return transportOutcome("aborted", "Request aborted", 0, site.url, { cause: failure.error });
|
|
378
|
+
}
|
|
379
|
+
const { error } = failure;
|
|
380
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
381
|
+
apiLogger.error("Request timeout", { ...site, timeout });
|
|
382
|
+
return transportOutcome("timeout", `Request timeout after ${timeout}ms`, 408, site.url);
|
|
383
|
+
}
|
|
384
|
+
const errorMessage = error instanceof Error ? error.message : "Network error";
|
|
385
|
+
apiLogger.error("Network error", {
|
|
386
|
+
...site,
|
|
387
|
+
error: errorMessage,
|
|
388
|
+
errorName: error instanceof Error ? error.name : "unknown"
|
|
389
|
+
});
|
|
390
|
+
return transportOutcome("network", errorMessage, 0, site.url);
|
|
391
|
+
}
|
|
392
|
+
function notifyOutcome(onOutcome, outcome) {
|
|
393
|
+
if (!onOutcome) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const returned = onOutcome(outcome);
|
|
398
|
+
if (isThenable(returned)) {
|
|
399
|
+
Promise.resolve(returned).catch(logObserverFailure);
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
logObserverFailure(error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function isThenable(value) {
|
|
406
|
+
return typeof value?.then === "function";
|
|
407
|
+
}
|
|
408
|
+
function logObserverFailure(error) {
|
|
409
|
+
apiLogger.error("onOutcome observer failed; the call result is unchanged", {
|
|
410
|
+
errorName: error instanceof Error ? error.name : typeof error,
|
|
411
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
412
|
+
});
|
|
413
|
+
}
|
|
280
414
|
function createApi(config = {}) {
|
|
281
415
|
const {
|
|
282
416
|
baseUrl = "/api/rpc",
|
|
@@ -285,6 +419,7 @@ function createApi(config = {}) {
|
|
|
285
419
|
fetch: customFetch = fetch,
|
|
286
420
|
onRequest: globalOnRequest,
|
|
287
421
|
onResponse: globalOnResponse,
|
|
422
|
+
onOutcome,
|
|
288
423
|
errorRegistry: errorRegistryConfig,
|
|
289
424
|
debug = false
|
|
290
425
|
} = config;
|
|
@@ -299,7 +434,7 @@ function createApi(config = {}) {
|
|
|
299
434
|
let appUrl = env.SPFN_APP_URL || "";
|
|
300
435
|
if (!appUrl && typeof window === "undefined") {
|
|
301
436
|
try {
|
|
302
|
-
const { headers: headers2 } = await import('next/headers');
|
|
437
|
+
const { headers: headers2 } = await import('next/headers.js');
|
|
303
438
|
const headersList = await headers2();
|
|
304
439
|
const host = headersList.get("host");
|
|
305
440
|
const protocol = headersList.get("x-forwarded-proto") || "http";
|
|
@@ -335,6 +470,13 @@ function createApi(config = {}) {
|
|
|
335
470
|
if (Object.keys(cookiesToSend).length > 0) {
|
|
336
471
|
headers["Cookie"] = buildCookieHeader(cookiesToSend);
|
|
337
472
|
}
|
|
473
|
+
const csrfToken = csrfHeaderValue(
|
|
474
|
+
typeof window === "undefined" ? Object.entries(cookiesToSend) : documentCookieEntries()
|
|
475
|
+
);
|
|
476
|
+
const csrfSetByCaller = Object.keys(headers).some((name) => name.toLowerCase() === CSRF_HEADER);
|
|
477
|
+
if (csrfToken && !csrfSetByCaller) {
|
|
478
|
+
headers[CSRF_HEADER] = csrfToken;
|
|
479
|
+
}
|
|
338
480
|
if (debug && Object.keys(autoDetectedCookies).length > 0) {
|
|
339
481
|
const cookieArray = Object.entries(autoDetectedCookies).map(([name, value]) => ({ name, value }));
|
|
340
482
|
logCookieAutoDetection(apiLogger, cookieArray);
|
|
@@ -385,59 +527,33 @@ function createApi(config = {}) {
|
|
|
385
527
|
if (debug) {
|
|
386
528
|
logRequest(apiLogger, routeName, method, fullUrl, !!init.body);
|
|
387
529
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
apiLogger.error("Request timeout", {
|
|
409
|
-
route: routeName,
|
|
410
|
-
method,
|
|
411
|
-
url: fullUrl,
|
|
412
|
-
timeout
|
|
413
|
-
});
|
|
414
|
-
throw new ApiError(
|
|
415
|
-
`Request timeout after ${timeout}ms`,
|
|
416
|
-
408,
|
|
417
|
-
fullUrl,
|
|
418
|
-
void 0,
|
|
419
|
-
"timeout"
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
const errorMessage = error instanceof Error ? error.message : "Network error";
|
|
423
|
-
apiLogger.error("Network error", {
|
|
424
|
-
route: routeName,
|
|
425
|
-
method,
|
|
426
|
-
url: fullUrl,
|
|
427
|
-
error: errorMessage,
|
|
428
|
-
errorName: error instanceof Error ? error.name : "unknown"
|
|
429
|
-
});
|
|
430
|
-
throw new ApiError(
|
|
431
|
-
errorMessage,
|
|
432
|
-
0,
|
|
433
|
-
fullUrl,
|
|
434
|
-
void 0,
|
|
435
|
-
"network"
|
|
436
|
-
);
|
|
530
|
+
const appliedTimeout = options.timeout ?? timeout;
|
|
531
|
+
const fetched = await fetchInAbortScope(fullUrl, init, appliedTimeout, customFetch);
|
|
532
|
+
if (!fetched.ok) {
|
|
533
|
+
const outcome = transportError(fetched, { route: routeName, method, url: fullUrl }, appliedTimeout);
|
|
534
|
+
notifyOutcome(onOutcome, outcome);
|
|
535
|
+
throw outcome.error;
|
|
536
|
+
}
|
|
537
|
+
let { response, body } = fetched;
|
|
538
|
+
if (globalOnResponse) {
|
|
539
|
+
const result = await globalOnResponse(response, body);
|
|
540
|
+
response = result.response;
|
|
541
|
+
body = result.body;
|
|
542
|
+
}
|
|
543
|
+
if (options.onResponse) {
|
|
544
|
+
const result = await options.onResponse(response, body);
|
|
545
|
+
response = result.response;
|
|
546
|
+
body = result.body;
|
|
547
|
+
}
|
|
548
|
+
if (debug) {
|
|
549
|
+
logResponse(apiLogger, routeName, response.status, !!body);
|
|
437
550
|
}
|
|
438
551
|
if (!response.ok) {
|
|
439
|
-
|
|
552
|
+
const error = errorFromResponse(response, body, fullUrl, errorRegistry$1, debug, apiLogger);
|
|
553
|
+
notifyOutcome(onOutcome, { ok: false, errorType: "http", status: response.status, error });
|
|
554
|
+
throw error;
|
|
440
555
|
}
|
|
556
|
+
notifyOutcome(onOutcome, { ok: true, response, body });
|
|
441
557
|
return body;
|
|
442
558
|
}
|
|
443
559
|
function buildProxy(prefix = "") {
|
|
@@ -457,6 +573,6 @@ function createApi(config = {}) {
|
|
|
457
573
|
return buildProxy();
|
|
458
574
|
}
|
|
459
575
|
|
|
460
|
-
export { ApiError, createApi };
|
|
576
|
+
export { ApiError, CSRF_HEADER, createApi, csrfHeaderValue, documentCookieEntries };
|
|
461
577
|
//# sourceMappingURL=index.js.map
|
|
462
578
|
//# sourceMappingURL=index.js.map
|