@spfn/core 0.3.0-beta.3 → 0.3.0-beta.30

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 (104) hide show
  1. package/README.md +301 -10
  2. package/dist/app-config/index.d.ts +39 -1
  3. package/dist/app-config/index.js.map +1 -1
  4. package/dist/authz/index.js +47 -391
  5. package/dist/authz/index.js.map +1 -1
  6. package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
  7. package/dist/codegen/index.d.ts +157 -16
  8. package/dist/codegen/index.js +772 -166
  9. package/dist/codegen/index.js.map +1 -1
  10. package/dist/config/index.js +4 -4
  11. package/dist/config/index.js.map +1 -1
  12. package/dist/contract/index.d.ts +39 -16
  13. package/dist/contract/index.js +147 -23
  14. package/dist/contract/index.js.map +1 -1
  15. package/dist/db/index.d.ts +230 -35
  16. package/dist/db/index.js +316 -160
  17. package/dist/db/index.js.map +1 -1
  18. package/dist/env/index.d.ts +6 -442
  19. package/dist/env/index.js +13 -16
  20. package/dist/env/index.js.map +1 -1
  21. package/dist/env/loader.js +24 -1
  22. package/dist/env/loader.js.map +1 -1
  23. package/dist/error-CIZlYJlL.d.ts +157 -0
  24. package/dist/errors/index.d.ts +18 -1
  25. package/dist/errors/index.js +47 -391
  26. package/dist/errors/index.js.map +1 -1
  27. package/dist/event/index.d.ts +5 -5
  28. package/dist/event/index.js +202 -3
  29. package/dist/event/index.js.map +1 -1
  30. package/dist/event/sse/client.d.ts +18 -4
  31. package/dist/event/sse/client.js +94 -18
  32. package/dist/event/sse/client.js.map +1 -1
  33. package/dist/event/sse/index.d.ts +18 -5
  34. package/dist/event/sse/index.js +452 -114
  35. package/dist/event/sse/index.js.map +1 -1
  36. package/dist/event/ws/client.d.ts +3 -2
  37. package/dist/event/ws/index.d.ts +4 -3
  38. package/dist/job/index.d.ts +48 -3
  39. package/dist/job/index.js +131 -52
  40. package/dist/job/index.js.map +1 -1
  41. package/dist/logger/index.js +0 -12
  42. package/dist/logger/index.js.map +1 -1
  43. package/dist/middleware/index.js +52 -397
  44. package/dist/middleware/index.js.map +1 -1
  45. package/dist/nextjs/index.d.ts +75 -14
  46. package/dist/nextjs/index.js +200 -84
  47. package/dist/nextjs/index.js.map +1 -1
  48. package/dist/nextjs/server.d.ts +46 -8
  49. package/dist/nextjs/server.js +15 -1
  50. package/dist/nextjs/server.js.map +1 -1
  51. package/dist/ops/index.d.ts +61 -42
  52. package/dist/ops/index.js +648 -97
  53. package/dist/ops/index.js.map +1 -1
  54. package/dist/registry-C2KQCh0m.d.ts +473 -0
  55. package/dist/route/index.d.ts +5 -2
  56. package/dist/route/index.js +107 -28
  57. package/dist/route/index.js.map +1 -1
  58. package/dist/route/types.d.ts +7 -1
  59. package/dist/{router-Cy7rAfmj.d.ts → route-builder-DRW-MSCJ.d.ts} +51 -126
  60. package/dist/router-C57y-vz1.d.ts +184 -0
  61. package/dist/router-CX5uVjmU.d.ts +406 -0
  62. package/dist/server/index.d.ts +130 -7
  63. package/dist/server/index.js +1174 -164
  64. package/dist/server/index.js.map +1 -1
  65. package/dist/state/client.d.ts +206 -0
  66. package/dist/state/client.js +678 -0
  67. package/dist/state/client.js.map +1 -0
  68. package/dist/state/index.d.ts +443 -0
  69. package/dist/state/index.js +644 -0
  70. package/dist/state/index.js.map +1 -0
  71. package/dist/state/react.d.ts +49 -0
  72. package/dist/state/react.js +50 -0
  73. package/dist/state/react.js.map +1 -0
  74. package/dist/surface/index.d.ts +115 -0
  75. package/dist/surface/index.js +1013 -0
  76. package/dist/surface/index.js.map +1 -0
  77. package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
  78. package/dist/{types-CfYVhIQ9.d.ts → types-5Rl11_Nb.d.ts} +43 -4
  79. package/dist/types-CKH0D3Dy.d.ts +441 -0
  80. package/dist/{types-ZQODsBft.d.ts → types-CurMab2Q.d.ts} +52 -2
  81. package/dist/{types-2AbaW4Ie.d.ts → types-DncqXfWN.d.ts} +4 -3
  82. package/docs/file-upload.md +195 -333
  83. package/package.json +39 -9
  84. package/src/cache/README.md +330 -0
  85. package/src/codegen/README.md +721 -0
  86. package/src/config/README.md +333 -0
  87. package/src/contract/README.md +442 -0
  88. package/src/db/README.md +589 -0
  89. package/src/db/manager/README.md +500 -0
  90. package/src/db/schema/README.md +354 -0
  91. package/src/db/transaction/README.md +845 -0
  92. package/src/env/README.md +787 -0
  93. package/src/errors/README.md +445 -0
  94. package/src/event/README.md +994 -0
  95. package/src/job/README.md +613 -0
  96. package/src/logger/README.md +321 -0
  97. package/src/middleware/README.md +634 -0
  98. package/src/nextjs/README.md +756 -0
  99. package/src/route/README.md +763 -0
  100. package/src/security/README.md +100 -0
  101. package/src/server/README.md +704 -0
  102. package/src/state/README.md +806 -0
  103. package/dist/token-manager-BT5EnUAR.d.ts +0 -278
  104. package/dist/types-9oszaJqp.d.ts +0 -250
@@ -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-9oszaJqp.js';
3
- export { C as CallOptions, c as CookieOptions, d as RouterInput, e as RouterOutput, S as SetCookie, f as StructuredInput } from '../types-9oszaJqp.js';
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
- * Typed client for entire router
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 Client<TRouter extends Router<any>> = {
86
- [K in keyof TRouter['routes']]: TRouter['routes'][K] extends RouteDef<any, any, any> ? RouteClient<TRouter['routes'][K]> : TRouter['routes'][K] extends Router<any> ? Client<TRouter['routes'][K]> : never;
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
- * Typed client error
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 class ApiError extends Error {
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, ApiError, type Client, InferRouteInput, InferRouteOutput, RequestInterceptor, ResponseInterceptor, type RouteClient, createApi };
181
+ export { ApiConfig, CSRF_HEADER, type Client, InferRouteInput, InferRouteOutput, RequestInterceptor, ResponseInterceptor, type RouteClient, createApi, csrfHeaderValue, documentCookieEntries };
@@ -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
- async function executeFetchWithTimeout(url, init, timeout, customFetch = fetch) {
126
+ function openAbortScope(callerSignal, timeout) {
127
127
  const controller = new AbortController();
128
- const timeoutId = setTimeout(() => controller.abort(), timeout);
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
- ...init,
132
- signal: controller.signal
133
- });
134
- clearTimeout(timeoutId);
135
- return response;
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
- clearTimeout(timeoutId);
138
- throw error;
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
- async function handleErrorResponse(response, body, fullUrl, errorRegistry, debug, logger3) {
167
+ function errorFromResponse(response, body, fullUrl, errorRegistry, debug, logger3) {
142
168
  if (debug) {
143
169
  logErrorResponse(logger3, response.status, body);
144
170
  }
145
- let deserializedError = null;
146
- if (errorRegistry && body && typeof body === "object" && "__type" in body) {
171
+ const deserializedError = deserializeRegisteredError(body, errorRegistry, debug, logger3);
172
+ if (deserializedError) {
147
173
  if (debug) {
148
- logErrorDeserializationAttempt(logger3, body.__type, errorRegistry.getRegisteredTypes());
174
+ logThrowingDeserializedError(logger3, deserializedError);
149
175
  }
150
- try {
151
- deserializedError = errorRegistry.deserialize(body);
152
- if (debug) {
153
- logErrorDeserializationSuccess(logger3, deserializedError);
154
- }
155
- } catch (deserializeError) {
156
- if (debug) {
157
- logErrorDeserializationFailure(logger3, deserializeError);
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
- } else if (debug) {
161
- logErrorDeserializationSkipped(logger3, errorRegistry, body);
185
+ return null;
162
186
  }
163
- if (deserializedError) {
187
+ if (debug) {
188
+ logErrorDeserializationAttempt(logger3, body.__type, errorRegistry.getRegisteredTypes());
189
+ }
190
+ try {
191
+ const deserializedError = errorRegistry.deserialize(body);
164
192
  if (debug) {
165
- logThrowingDeserializedError(logger3, deserializedError);
193
+ logErrorDeserializationSuccess(logger3, deserializedError);
194
+ }
195
+ return deserializedError;
196
+ } catch (deserializeError) {
197
+ if (debug) {
198
+ logErrorDeserializationFailure(logger3, deserializeError);
166
199
  }
167
- throw deserializedError;
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
- throw new ApiError(
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
- let response;
389
- let body;
390
- try {
391
- response = await executeFetchWithTimeout(fullUrl, init, timeout, customFetch);
392
- body = await parseResponseBody(response);
393
- if (globalOnResponse) {
394
- const result = await globalOnResponse(response, body);
395
- response = result.response;
396
- body = result.body;
397
- }
398
- if (options.onResponse) {
399
- const result = await options.onResponse(response, body);
400
- response = result.response;
401
- body = result.body;
402
- }
403
- if (debug) {
404
- logResponse(apiLogger, routeName, response.status, !!body);
405
- }
406
- } catch (error) {
407
- if (error instanceof Error && error.name === "AbortError") {
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
- await handleErrorResponse(response, body, fullUrl, errorRegistry$1, debug, apiLogger);
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