stitchkit 0.72.5 → 0.74.0
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/dist/application/diagnostic-journal-contract.d.ts +24 -0
- package/dist/application/diagnostic-journal-contract.d.ts.map +1 -1
- package/dist/application/diagnostic-journal-lock.d.ts +20 -1
- package/dist/application/diagnostic-journal-lock.d.ts.map +1 -1
- package/dist/application/diagnostic-journal-storage.d.ts +1 -0
- package/dist/application/diagnostic-journal-storage.d.ts.map +1 -1
- package/dist/application/diagnostic-journal.d.ts.map +1 -1
- package/dist/application.d.ts +1 -1
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +93 -17
- package/dist/browser/client-multipart.d.ts.map +1 -1
- package/dist/browser/client-url.d.ts.map +1 -1
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +15 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/{index-6d32zk83.js → index-58jzmnn4.js} +91 -64
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +584 -547
- package/dist/remote.js +1 -1
- package/dist/testing.js +1 -1
- package/llms-full.txt +247 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -86,617 +86,633 @@ function cancellationError(cause) {
|
|
|
86
86
|
return new RequestCancellationError(cause);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// src/browser/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
// src/browser/http.ts
|
|
90
|
+
import ky, {
|
|
91
|
+
isHTTPError,
|
|
92
|
+
isNetworkError,
|
|
93
|
+
isTimeoutError
|
|
94
|
+
} from "ky";
|
|
95
|
+
|
|
96
|
+
// src/internal/random-hex.ts
|
|
97
|
+
function randomHex(bytes) {
|
|
98
|
+
const arr = new Uint8Array(bytes);
|
|
99
|
+
crypto.getRandomValues(arr);
|
|
100
|
+
let hex = "";
|
|
101
|
+
for (const byte of arr)
|
|
102
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
103
|
+
return hex;
|
|
95
104
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
|
|
106
|
+
// src/observability/trace.ts
|
|
107
|
+
var TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(.*)$/i;
|
|
108
|
+
function createTraceContext() {
|
|
109
|
+
return { traceId: randomHex(16), spanId: randomHex(8) };
|
|
99
110
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
function parseTraceparent(header) {
|
|
112
|
+
if (!header)
|
|
113
|
+
return null;
|
|
114
|
+
const match = TRACEPARENT_RE.exec(header.trim());
|
|
115
|
+
if (!match?.[1] || !match[2] || !match[3] || !match[4] || match[5] === undefined)
|
|
116
|
+
return null;
|
|
117
|
+
const version = match[1].toLowerCase();
|
|
118
|
+
if (version === "ff")
|
|
119
|
+
return null;
|
|
120
|
+
const suffix = match[5];
|
|
121
|
+
if (version === "00" ? suffix !== "" : suffix !== "" && !/^(?:-[0-9a-f]{2,})+$/i.test(suffix)) {
|
|
122
|
+
return null;
|
|
105
123
|
}
|
|
124
|
+
const traceId = match[2].toLowerCase();
|
|
125
|
+
const parentSpanId = match[3].toLowerCase();
|
|
126
|
+
const traceFlags = match[4].toLowerCase();
|
|
127
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId))
|
|
128
|
+
return null;
|
|
129
|
+
return { traceId, spanId: randomHex(8), parentSpanId, traceFlags };
|
|
106
130
|
}
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
const fileFields = new Set(Object.keys(descriptor.files));
|
|
110
|
-
for (const [field, policy] of Object.entries(descriptor.files)) {
|
|
111
|
-
const value = values[field];
|
|
112
|
-
if (value === undefined) {
|
|
113
|
-
if (policy.required !== false)
|
|
114
|
-
throw new Error(`Missing multipart file field: ${field}`);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (policy.multiple === true) {
|
|
118
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
119
|
-
throw new Error(`Multipart file field "${field}" must be a non-empty array`);
|
|
120
|
-
}
|
|
121
|
-
for (const file of value) {
|
|
122
|
-
if (!isMultipartFile(file)) {
|
|
123
|
-
throw new Error(`Invalid multipart file field: ${field}`);
|
|
124
|
-
}
|
|
125
|
-
appendMultipartFile(formData, field, file);
|
|
126
|
-
}
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (!isMultipartFile(value))
|
|
130
|
-
throw new Error(`Invalid multipart file field: ${field}`);
|
|
131
|
-
appendMultipartFile(formData, field, value);
|
|
132
|
-
}
|
|
133
|
-
appendFormFields(formData, values, fileFields);
|
|
134
|
-
return formData;
|
|
131
|
+
function formatTraceparent(ctx) {
|
|
132
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${ctx.traceFlags ?? "01"}`;
|
|
135
133
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
function childSpan(parent) {
|
|
135
|
+
return {
|
|
136
|
+
traceId: parent.traceId,
|
|
137
|
+
spanId: randomHex(8),
|
|
138
|
+
parentSpanId: parent.spanId,
|
|
139
|
+
...parent.tracestate !== undefined && { tracestate: parent.tracestate },
|
|
140
|
+
...parent.baggage !== undefined && { baggage: parent.baggage },
|
|
141
|
+
...parent.traceFlags !== undefined && { traceFlags: parent.traceFlags }
|
|
142
|
+
};
|
|
140
143
|
}
|
|
144
|
+
var encoder = new TextEncoder;
|
|
141
145
|
|
|
142
|
-
// src/browser/
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
// src/browser/request-id.ts
|
|
147
|
+
var REQUEST_ID_HEADER = "x-request-id";
|
|
148
|
+
function responseTraceId(response) {
|
|
149
|
+
return response?.headers.get(REQUEST_ID_HEADER) ?? undefined;
|
|
145
150
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
continue;
|
|
152
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
153
|
-
params[key] = value;
|
|
154
|
-
hasParams = true;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
158
|
-
throw new Error(`${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
|
|
159
|
-
}
|
|
160
|
-
return hasParams ? params : undefined;
|
|
151
|
+
|
|
152
|
+
// src/browser/http.ts
|
|
153
|
+
var API_ERROR_BRAND = Symbol.for("stitchkit.ApiError");
|
|
154
|
+
function messageForCode(code, message) {
|
|
155
|
+
return message !== undefined && message.length > 0 ? message : `${code} (no message supplied)`;
|
|
161
156
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
|
|
158
|
+
class ApiError extends Error {
|
|
159
|
+
code;
|
|
160
|
+
status;
|
|
161
|
+
details;
|
|
162
|
+
hint;
|
|
163
|
+
traceId;
|
|
164
|
+
constructor(code, status = 0, details, message, hint, traceId, options) {
|
|
165
|
+
super(messageForCode(code, message), options);
|
|
166
|
+
this.code = code;
|
|
167
|
+
this.status = status;
|
|
168
|
+
this.details = details;
|
|
169
|
+
this.hint = hint;
|
|
170
|
+
this.traceId = traceId;
|
|
171
|
+
this.name = "ApiError";
|
|
172
|
+
Object.defineProperty(this, API_ERROR_BRAND, { value: true });
|
|
166
173
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
function resolvePathPrefix(config, args) {
|
|
170
|
-
if (!config?.pathPrefix)
|
|
171
|
-
return "";
|
|
172
|
-
if (typeof config.pathPrefix === "string")
|
|
173
|
-
return config.pathPrefix;
|
|
174
|
-
const keys = config.stripPrefixKeys ?? [];
|
|
175
|
-
if (!hasStringKeys(args, keys)) {
|
|
176
|
-
const missing = keys.find((key) => typeof args[key] !== "string");
|
|
177
|
-
throw new Error(`Missing path prefix key: ${missing}`);
|
|
174
|
+
static is(error) {
|
|
175
|
+
return typeof error === "object" && error !== null && API_ERROR_BRAND in error;
|
|
178
176
|
}
|
|
179
|
-
return config.pathPrefix(args);
|
|
180
177
|
}
|
|
181
|
-
function
|
|
182
|
-
return
|
|
178
|
+
function refuseLocally(path, message) {
|
|
179
|
+
return new ApiError("VALIDATION_ERROR", 0, { issues: [{ path, code: "invalid_type", message }] }, message);
|
|
183
180
|
}
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
return
|
|
187
|
-
} catch {
|
|
188
|
-
return value;
|
|
181
|
+
function shouldRetryBunNetworkError(error) {
|
|
182
|
+
if (ApiError.is(error) || isHTTPError(error) || isNetworkError(error) || isTimeoutError(error) || error instanceof RequestCancellationError || error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) {
|
|
183
|
+
return;
|
|
189
184
|
}
|
|
185
|
+
if (!(error instanceof Error))
|
|
186
|
+
return;
|
|
187
|
+
const code = Object.getOwnPropertyDescriptor(error, "code");
|
|
188
|
+
return code && "value" in code && code.value === "ConnectionRefused" ? true : undefined;
|
|
190
189
|
}
|
|
191
|
-
function
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
190
|
+
function parseApiErrorBody(body) {
|
|
191
|
+
if (!isRecord(body) || !isRecord(body.error))
|
|
192
|
+
return null;
|
|
193
|
+
const error = body.error;
|
|
194
|
+
if (typeof error.code !== "string")
|
|
195
|
+
return null;
|
|
196
|
+
return {
|
|
197
|
+
code: error.code,
|
|
198
|
+
message: typeof error.message === "string" ? error.message : undefined,
|
|
199
|
+
details: error.details,
|
|
200
|
+
hint: typeof error.hint === "string" ? error.hint : undefined
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function createRetryAwareFetch(transportFetch, unix) {
|
|
204
|
+
const runtimeFetch = transportFetch;
|
|
205
|
+
let attempt = 0;
|
|
206
|
+
return (input, init) => {
|
|
207
|
+
attempt += 1;
|
|
208
|
+
if (unix === undefined && attempt === 1) {
|
|
209
|
+
return runtimeFetch(input, init);
|
|
209
210
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (pattern.startsWith(":"))
|
|
216
|
-
continue;
|
|
217
|
-
let source = escapeRegex(pattern);
|
|
218
|
-
for (const marker of markers) {
|
|
219
|
-
source = source.replaceAll(escapeRegex(marker), "[^/]+");
|
|
220
|
-
}
|
|
221
|
-
if (!new RegExp(`^${source}$`).test(actual))
|
|
222
|
-
return false;
|
|
211
|
+
if (!(input instanceof Request)) {
|
|
212
|
+
if (unix === undefined)
|
|
213
|
+
return runtimeFetch(input, init);
|
|
214
|
+
const unixInit = { ...init, unix };
|
|
215
|
+
return runtimeFetch(input, unixInit);
|
|
223
216
|
}
|
|
224
|
-
|
|
217
|
+
const streamedBody = input.body ? { body: input.body, duplex: "half" } : {};
|
|
218
|
+
const materialized = {
|
|
219
|
+
...init,
|
|
220
|
+
...unix !== undefined && { unix },
|
|
221
|
+
method: input.method,
|
|
222
|
+
headers: input.headers,
|
|
223
|
+
...streamedBody,
|
|
224
|
+
cache: input.cache,
|
|
225
|
+
credentials: input.credentials,
|
|
226
|
+
integrity: input.integrity,
|
|
227
|
+
keepalive: input.keepalive,
|
|
228
|
+
mode: input.mode,
|
|
229
|
+
redirect: input.redirect,
|
|
230
|
+
referrer: input.referrer,
|
|
231
|
+
referrerPolicy: input.referrerPolicy,
|
|
232
|
+
signal: input.signal
|
|
233
|
+
};
|
|
234
|
+
return runtimeFetch(input.url, materialized);
|
|
225
235
|
};
|
|
226
236
|
}
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
function fillPathParams(path, args) {
|
|
236
|
-
const wildcard = parseTrailingWildcard(path);
|
|
237
|
-
let filled = path.replace(/:(\w+)/g, (_, key) => {
|
|
238
|
-
const value = args[key];
|
|
239
|
-
if (value === undefined || value === null) {
|
|
240
|
-
throw new Error(`Missing path param: ${key}`);
|
|
237
|
+
function createHttpClient(config) {
|
|
238
|
+
if (config.fetch && config.unix) {
|
|
239
|
+
throw new TypeError("HttpClientConfig.fetch and unix are mutually exclusive");
|
|
240
|
+
}
|
|
241
|
+
if (config.unix !== undefined) {
|
|
242
|
+
if (!config.unix.startsWith("/") || config.unix.includes("\x00")) {
|
|
243
|
+
throw new TypeError("HttpClientConfig.unix must be an absolute Unix socket path");
|
|
241
244
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (!wildcard)
|
|
245
|
-
return filled;
|
|
246
|
-
const wildcardValue = args[wildcard.name];
|
|
247
|
-
if (wildcardValue === undefined || wildcardValue === null) {
|
|
248
|
-
throw new Error(`Missing path param: ${wildcard.name}`);
|
|
249
|
-
}
|
|
250
|
-
const remainder = String(wildcardValue).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
251
|
-
filled = `${filled.slice(0, -(wildcard.name.length + 1))}${remainder}`;
|
|
252
|
-
return filled;
|
|
253
|
-
}
|
|
254
|
-
function stripConsumedArgs(args, path, scopeKeys) {
|
|
255
|
-
const consumed = new Set(scopeKeys);
|
|
256
|
-
for (const name of extractParamNames(path))
|
|
257
|
-
consumed.add(name);
|
|
258
|
-
const remaining = {};
|
|
259
|
-
for (const [key, value] of Object.entries(args)) {
|
|
260
|
-
if (!consumed.has(key) && value !== undefined)
|
|
261
|
-
remaining[key] = value;
|
|
262
|
-
}
|
|
263
|
-
return remaining;
|
|
264
|
-
}
|
|
265
|
-
function appendQuery(relativeUrl, params) {
|
|
266
|
-
if (!params)
|
|
267
|
-
return relativeUrl;
|
|
268
|
-
const search = new URLSearchParams;
|
|
269
|
-
for (const [key, value] of Object.entries(params)) {
|
|
270
|
-
if (Array.isArray(value)) {
|
|
271
|
-
for (const item of value)
|
|
272
|
-
search.append(key, String(item));
|
|
273
|
-
} else {
|
|
274
|
-
search.set(key, String(value));
|
|
245
|
+
if (typeof Reflect.get(globalThis, "Bun") !== "object") {
|
|
246
|
+
throw new TypeError("HttpClientConfig.unix requires Bun; on Bun or Node use createUnixClientTransport().fetch for an explicit portable transport");
|
|
275
247
|
}
|
|
276
248
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
relativeUrl = relativeUrl.slice(0, -1);
|
|
289
|
-
const remainingArgs = stripConsumedArgs(args, endpoint.path, config?.stripPrefixKeys ?? []);
|
|
290
|
-
if (inputIsQuery(endpoint.method)) {
|
|
291
|
-
relativeUrl = appendQuery(relativeUrl, collectQueryParams(remainingArgs, endpoint));
|
|
292
|
-
}
|
|
293
|
-
return { relativeUrl, remainingArgs };
|
|
294
|
-
}
|
|
295
|
-
function joinClientBaseUrl(baseUrl, relativeUrl) {
|
|
296
|
-
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
297
|
-
const path = relativeUrl.startsWith("/") ? relativeUrl : `/${relativeUrl}`;
|
|
298
|
-
return `${base}${path}`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// src/internal/bounded-lines.ts
|
|
302
|
-
var DEFAULT_STREAM_LINE_BYTES = 1024 * 1024;
|
|
303
|
-
function lineLimit(value) {
|
|
304
|
-
const resolved = value ?? DEFAULT_STREAM_LINE_BYTES;
|
|
305
|
-
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
306
|
-
throw new TypeError("maxLineBytes must be a positive safe integer");
|
|
249
|
+
let ssrCookies = null;
|
|
250
|
+
let isLoggedOut = false;
|
|
251
|
+
const listeners = new Set;
|
|
252
|
+
const suppressUnauthorizedFor = config.suppressUnauthorizedFor ?? [];
|
|
253
|
+
const parseError = config.parseError ?? parseApiErrorBody;
|
|
254
|
+
function emit(event) {
|
|
255
|
+
for (const fn of listeners) {
|
|
256
|
+
try {
|
|
257
|
+
fn(event);
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
307
260
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
261
|
+
const client = ky.create({
|
|
262
|
+
prefix: config.baseUrl,
|
|
263
|
+
credentials: config.credentials ?? "include",
|
|
264
|
+
timeout: false,
|
|
265
|
+
retry: {
|
|
266
|
+
limit: config.retry?.limit ?? 2,
|
|
267
|
+
methods: config.retry?.methods ?? ["get"],
|
|
268
|
+
statusCodes: config.retry?.statusCodes ?? [],
|
|
269
|
+
shouldRetry: ({ error }) => shouldRetryBunNetworkError(error)
|
|
270
|
+
},
|
|
271
|
+
hooks: {
|
|
272
|
+
beforeRequest: [
|
|
273
|
+
({ request: request2 }) => {
|
|
274
|
+
if (ssrCookies) {
|
|
275
|
+
request2.headers.set("Cookie", ssrCookies);
|
|
276
|
+
}
|
|
277
|
+
const extra = typeof config.headers === "function" ? config.headers() : config.headers;
|
|
278
|
+
if (extra) {
|
|
279
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
280
|
+
request2.headers.set(key, value);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (config.trace && !request2.headers.has("traceparent")) {
|
|
284
|
+
request2.headers.set("traceparent", formatTraceparent(createTraceContext()));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
],
|
|
288
|
+
afterResponse: [
|
|
289
|
+
async ({ request: request2, response }) => {
|
|
290
|
+
if (response.status === 401) {
|
|
291
|
+
const url = new URL(request2.url).pathname;
|
|
292
|
+
if (!isLoggedOut && !suppressUnauthorizedFor.some((matches) => matches(url))) {
|
|
293
|
+
isLoggedOut = true;
|
|
294
|
+
emit({ type: "unauthorized" });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
const body = await response.clone().json().catch(() => null);
|
|
299
|
+
if (body) {
|
|
300
|
+
const parsed = parseError(body);
|
|
301
|
+
if (parsed) {
|
|
302
|
+
throw new ApiError(parsed.code, response.status, parsed.details, parsed.message, parsed.hint, responseTraceId(response));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
async function request(method, url, data, options = {}) {
|
|
311
|
+
const cancellation = createRequestCancellation(options.signal, options.timeout ?? config.timeout ?? 30000);
|
|
312
|
+
const kyOptions = {
|
|
313
|
+
fetch: createRetryAwareFetch(config.fetch ?? globalThis.fetch.bind(globalThis), config.unix),
|
|
314
|
+
timeout: false,
|
|
315
|
+
signal: cancellation.signal
|
|
316
|
+
};
|
|
317
|
+
if (options.params) {
|
|
318
|
+
const searchParams = new URLSearchParams;
|
|
319
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
320
|
+
if (value === undefined)
|
|
333
321
|
continue;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
for (const item of value)
|
|
324
|
+
searchParams.append(key, String(item));
|
|
325
|
+
} else {
|
|
326
|
+
searchParams.set(key, String(value));
|
|
337
327
|
}
|
|
338
|
-
yield decoder.decode(lineBytes);
|
|
339
|
-
pending = new Uint8Array;
|
|
340
|
-
start = index + 1;
|
|
341
328
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
throw new RangeError(`Stream line exceeds the ${limit} byte limit`);
|
|
329
|
+
if (searchParams.size > 0) {
|
|
330
|
+
kyOptions.searchParams = searchParams;
|
|
345
331
|
}
|
|
346
332
|
}
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
333
|
+
if (data instanceof FormData) {
|
|
334
|
+
kyOptions.body = data;
|
|
335
|
+
} else if (data !== undefined) {
|
|
336
|
+
kyOptions.json = data;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
return await cancellation.run(async () => {
|
|
340
|
+
if (options.responseType === "blob") {
|
|
341
|
+
return transportResult(await client[method](url, kyOptions).blob());
|
|
342
|
+
}
|
|
343
|
+
if (options.responseType === "response") {
|
|
344
|
+
return transportResult(await client[method](url, kyOptions));
|
|
345
|
+
}
|
|
346
|
+
const response = await client[method](url, kyOptions);
|
|
347
|
+
if (options.responseType === "void") {
|
|
348
|
+
const text = await response.text();
|
|
349
|
+
if (text.length > 0) {
|
|
350
|
+
throw new Error("Server returned data for an endpoint with no output contract");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (options.responseType === "void" || response.status === 204 || response.headers.get("content-length") === "0") {
|
|
354
|
+
return transportResult(undefined);
|
|
355
|
+
}
|
|
356
|
+
return await response.json();
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error instanceof RequestCancellationError) {
|
|
360
|
+
throw new ApiError(error.cause === "caller" ? "REQUEST_ABORTED" : "REQUEST_TIMEOUT", 0, undefined, error.message);
|
|
350
361
|
}
|
|
351
|
-
|
|
362
|
+
if (ApiError.is(error))
|
|
363
|
+
throw error;
|
|
364
|
+
emit({ type: "network_error" });
|
|
365
|
+
const response = isHTTPError(error) ? error.response : undefined;
|
|
366
|
+
const status = response?.status ?? 0;
|
|
367
|
+
const msg = error instanceof Error ? error.message : undefined;
|
|
368
|
+
throw new ApiError("UNKNOWN_ERROR", status, msg ? { message: msg } : undefined, msg, undefined, responseTraceId(response), { cause: error });
|
|
352
369
|
}
|
|
353
|
-
} finally {
|
|
354
|
-
await reader.cancel().catch(() => {
|
|
355
|
-
return;
|
|
356
|
-
});
|
|
357
|
-
reader.releaseLock();
|
|
358
370
|
}
|
|
371
|
+
return {
|
|
372
|
+
baseUrl: config.baseUrl,
|
|
373
|
+
get: (url, options) => request("get", url, undefined, options),
|
|
374
|
+
head: (url, options) => request("head", url, undefined, options),
|
|
375
|
+
post: (url, data, options) => request("post", url, data, options),
|
|
376
|
+
put: (url, data, options) => request("put", url, data, options),
|
|
377
|
+
patch: (url, data, options) => request("patch", url, data, options),
|
|
378
|
+
delete: (url, options) => request("delete", url, undefined, options),
|
|
379
|
+
setServerContext(cookies) {
|
|
380
|
+
ssrCookies = cookies;
|
|
381
|
+
},
|
|
382
|
+
subscribe(listener) {
|
|
383
|
+
listeners.add(listener);
|
|
384
|
+
return () => listeners.delete(listener);
|
|
385
|
+
},
|
|
386
|
+
logout() {
|
|
387
|
+
isLoggedOut = true;
|
|
388
|
+
emit({ type: "logout" });
|
|
389
|
+
},
|
|
390
|
+
resetLogoutState() {
|
|
391
|
+
isLoggedOut = false;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
359
394
|
}
|
|
360
395
|
|
|
361
|
-
// src/
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return path.length > 0 ? path.map(String).join(".") : "(root)";
|
|
396
|
+
// src/browser/client-multipart.ts
|
|
397
|
+
function isFileDescriptor(value) {
|
|
398
|
+
return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
|
|
365
399
|
}
|
|
366
|
-
function
|
|
367
|
-
return
|
|
368
|
-
path: issuePath(issue.path),
|
|
369
|
-
code: issue.code,
|
|
370
|
-
message: issue.message
|
|
371
|
-
}));
|
|
400
|
+
function isMultipartFile(value) {
|
|
401
|
+
return value instanceof Blob || isFileDescriptor(value);
|
|
372
402
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
403
|
+
function appendMultipartFile(form, field, file) {
|
|
404
|
+
const sink = form;
|
|
405
|
+
sink.append(field, file);
|
|
406
|
+
}
|
|
407
|
+
function appendFormFields(formData, values, skipKeys) {
|
|
408
|
+
for (const [key, value] of Object.entries(values)) {
|
|
409
|
+
if (skipKeys.has(key) || value === undefined || value === null)
|
|
410
|
+
continue;
|
|
411
|
+
formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function buildMultipartForm(descriptor, values) {
|
|
415
|
+
const formData = new FormData;
|
|
416
|
+
const fileFields = new Set(Object.keys(descriptor.files));
|
|
417
|
+
for (const [field, policy] of Object.entries(descriptor.files)) {
|
|
418
|
+
const value = values[field];
|
|
419
|
+
if (value === undefined) {
|
|
420
|
+
if (policy.required !== false) {
|
|
421
|
+
throw refuseLocally(field, `Missing multipart file field: ${field}`);
|
|
391
422
|
}
|
|
423
|
+
continue;
|
|
392
424
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
425
|
+
if (policy.multiple === true) {
|
|
426
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
427
|
+
throw refuseLocally(field, `Multipart file field "${field}" must be a non-empty array`);
|
|
428
|
+
}
|
|
429
|
+
for (const file of value) {
|
|
430
|
+
if (!isMultipartFile(file)) {
|
|
431
|
+
throw refuseLocally(field, `Invalid multipart file field: ${field}`);
|
|
432
|
+
}
|
|
433
|
+
appendMultipartFile(formData, field, file);
|
|
434
|
+
}
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (!isMultipartFile(value)) {
|
|
438
|
+
throw refuseLocally(field, `Invalid multipart file field: ${field}`);
|
|
439
|
+
}
|
|
440
|
+
appendMultipartFile(formData, field, value);
|
|
398
441
|
}
|
|
442
|
+
appendFormFields(formData, values, fileFields);
|
|
443
|
+
return formData;
|
|
399
444
|
}
|
|
400
445
|
|
|
401
|
-
// src/
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
isNetworkError,
|
|
405
|
-
isTimeoutError
|
|
406
|
-
} from "ky";
|
|
407
|
-
|
|
408
|
-
// src/internal/random-hex.ts
|
|
409
|
-
function randomHex(bytes) {
|
|
410
|
-
const arr = new Uint8Array(bytes);
|
|
411
|
-
crypto.getRandomValues(arr);
|
|
412
|
-
let hex = "";
|
|
413
|
-
for (const byte of arr)
|
|
414
|
-
hex += byte.toString(16).padStart(2, "0");
|
|
415
|
-
return hex;
|
|
446
|
+
// src/internal/http-input.ts
|
|
447
|
+
function inputIsQuery(method) {
|
|
448
|
+
return method === "GET" || method === "DELETE";
|
|
416
449
|
}
|
|
417
450
|
|
|
418
|
-
// src/
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return { traceId: randomHex(16), spanId: randomHex(8) };
|
|
451
|
+
// src/browser/client-url.ts
|
|
452
|
+
function isParamArray(value) {
|
|
453
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number");
|
|
422
454
|
}
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
455
|
+
function collectQueryParams(args, endpoint) {
|
|
456
|
+
const params = {};
|
|
457
|
+
let hasParams = false;
|
|
458
|
+
for (const [key, value] of Object.entries(args)) {
|
|
459
|
+
if (value === undefined || value === null)
|
|
460
|
+
continue;
|
|
461
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
462
|
+
params[key] = value;
|
|
463
|
+
hasParams = true;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
467
|
+
throw refuseLocally(key, `${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
|
|
435
468
|
}
|
|
436
|
-
|
|
437
|
-
const parentSpanId = match[3].toLowerCase();
|
|
438
|
-
const traceFlags = match[4].toLowerCase();
|
|
439
|
-
if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId))
|
|
440
|
-
return null;
|
|
441
|
-
return { traceId, spanId: randomHex(8), parentSpanId, traceFlags };
|
|
469
|
+
return hasParams ? params : undefined;
|
|
442
470
|
}
|
|
443
|
-
function
|
|
444
|
-
|
|
471
|
+
function hasStringKeys(args, keys) {
|
|
472
|
+
for (const key of keys) {
|
|
473
|
+
if (typeof args[key] !== "string")
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
return true;
|
|
445
477
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
478
|
+
function resolvePathPrefix(config, args) {
|
|
479
|
+
if (!config?.pathPrefix)
|
|
480
|
+
return "";
|
|
481
|
+
if (typeof config.pathPrefix === "string")
|
|
482
|
+
return config.pathPrefix;
|
|
483
|
+
const keys = config.stripPrefixKeys ?? [];
|
|
484
|
+
if (!hasStringKeys(args, keys)) {
|
|
485
|
+
const missing = keys.find((key) => typeof args[key] !== "string");
|
|
486
|
+
throw refuseLocally(String(missing), `Missing path prefix key: ${missing}`);
|
|
487
|
+
}
|
|
488
|
+
return config.pathPrefix(args);
|
|
455
489
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
// src/browser/request-id.ts
|
|
459
|
-
var REQUEST_ID_HEADER = "x-request-id";
|
|
460
|
-
function responseTraceId(response) {
|
|
461
|
-
return response?.headers.get(REQUEST_ID_HEADER) ?? undefined;
|
|
490
|
+
function escapeRegex(value) {
|
|
491
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
462
492
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
code;
|
|
469
|
-
status;
|
|
470
|
-
details;
|
|
471
|
-
hint;
|
|
472
|
-
traceId;
|
|
473
|
-
constructor(code, status = 0, details, message, hint, traceId, options) {
|
|
474
|
-
super(message ?? `API Error: ${code}`, options);
|
|
475
|
-
this.code = code;
|
|
476
|
-
this.status = status;
|
|
477
|
-
this.details = details;
|
|
478
|
-
this.hint = hint;
|
|
479
|
-
this.traceId = traceId;
|
|
480
|
-
this.name = "ApiError";
|
|
481
|
-
Object.defineProperty(this, API_ERROR_BRAND, { value: true });
|
|
482
|
-
}
|
|
483
|
-
static is(error) {
|
|
484
|
-
return typeof error === "object" && error !== null && API_ERROR_BRAND in error;
|
|
493
|
+
function decodePathSegment(value) {
|
|
494
|
+
try {
|
|
495
|
+
return decodeURIComponent(value);
|
|
496
|
+
} catch {
|
|
497
|
+
return value;
|
|
485
498
|
}
|
|
486
499
|
}
|
|
487
|
-
function
|
|
488
|
-
if (
|
|
489
|
-
|
|
500
|
+
function createClientRouteMatcher(endpoint, contractPrefix, config) {
|
|
501
|
+
if (typeof config?.pathPrefix === "function" && (!config.stripPrefixKeys || config.stripPrefixKeys.length === 0)) {
|
|
502
|
+
throw new Error("Dynamic pathPrefix matchers require stripPrefixKeys");
|
|
490
503
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
details: error.details,
|
|
506
|
-
hint: typeof error.hint === "string" ? error.hint : undefined
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
function createRetryAwareFetch(transportFetch, unix) {
|
|
510
|
-
const runtimeFetch = transportFetch;
|
|
511
|
-
let attempt = 0;
|
|
512
|
-
return (input, init) => {
|
|
513
|
-
attempt += 1;
|
|
514
|
-
if (unix === undefined && attempt === 1) {
|
|
515
|
-
return runtimeFetch(input, init);
|
|
504
|
+
const markerByKey = {};
|
|
505
|
+
for (const [index, key] of (config?.stripPrefixKeys ?? []).entries()) {
|
|
506
|
+
markerByKey[key] = `__stitch_scope_${index}__`;
|
|
507
|
+
}
|
|
508
|
+
const pathPrefix = resolvePathPrefix(config, markerByKey);
|
|
509
|
+
const route = [pathPrefix, contractPrefix, endpoint.path === "/" ? "" : endpoint.path].filter(Boolean).join("/");
|
|
510
|
+
const patternSegments = route.split("/").filter(Boolean);
|
|
511
|
+
const wildcard = patternSegments.at(-1)?.startsWith("*") === true;
|
|
512
|
+
const fixedCount = wildcard ? patternSegments.length - 1 : patternSegments.length;
|
|
513
|
+
const markers = Object.values(markerByKey);
|
|
514
|
+
return (pathname) => {
|
|
515
|
+
const actualSegments = pathname.split("/").filter(Boolean).map(decodePathSegment);
|
|
516
|
+
if (wildcard ? actualSegments.length < fixedCount : actualSegments.length !== fixedCount) {
|
|
517
|
+
return false;
|
|
516
518
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
519
|
+
for (let index = 0;index < fixedCount; index += 1) {
|
|
520
|
+
const pattern = patternSegments[index];
|
|
521
|
+
const actual = actualSegments[index];
|
|
522
|
+
if (!pattern || actual === undefined)
|
|
523
|
+
return false;
|
|
524
|
+
if (pattern.startsWith(":"))
|
|
525
|
+
continue;
|
|
526
|
+
let source = escapeRegex(pattern);
|
|
527
|
+
for (const marker of markers) {
|
|
528
|
+
source = source.replaceAll(escapeRegex(marker), "[^/]+");
|
|
529
|
+
}
|
|
530
|
+
if (!new RegExp(`^${source}$`).test(actual))
|
|
531
|
+
return false;
|
|
522
532
|
}
|
|
523
|
-
|
|
524
|
-
const materialized = {
|
|
525
|
-
...init,
|
|
526
|
-
...unix !== undefined && { unix },
|
|
527
|
-
method: input.method,
|
|
528
|
-
headers: input.headers,
|
|
529
|
-
...streamedBody,
|
|
530
|
-
cache: input.cache,
|
|
531
|
-
credentials: input.credentials,
|
|
532
|
-
integrity: input.integrity,
|
|
533
|
-
keepalive: input.keepalive,
|
|
534
|
-
mode: input.mode,
|
|
535
|
-
redirect: input.redirect,
|
|
536
|
-
referrer: input.referrer,
|
|
537
|
-
referrerPolicy: input.referrerPolicy,
|
|
538
|
-
signal: input.signal
|
|
539
|
-
};
|
|
540
|
-
return runtimeFetch(input.url, materialized);
|
|
533
|
+
return true;
|
|
541
534
|
};
|
|
542
535
|
}
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
|
|
536
|
+
function extractParamNames(path) {
|
|
537
|
+
const matches = path.match(/:(\w+)/g);
|
|
538
|
+
const names = matches ? matches.map((match) => match.slice(1)) : [];
|
|
539
|
+
const wildcard = parseTrailingWildcard(path);
|
|
540
|
+
if (wildcard)
|
|
541
|
+
names.push(wildcard.name);
|
|
542
|
+
return names;
|
|
543
|
+
}
|
|
544
|
+
function fillPathParams(path, args) {
|
|
545
|
+
const wildcard = parseTrailingWildcard(path);
|
|
546
|
+
let filled = path.replace(/:(\w+)/g, (_, key) => {
|
|
547
|
+
const value = args[key];
|
|
548
|
+
if (value === undefined || value === null) {
|
|
549
|
+
throw refuseLocally(String(key), `Missing path param: ${key}`);
|
|
550
550
|
}
|
|
551
|
-
|
|
552
|
-
|
|
551
|
+
return encodeURIComponent(String(value));
|
|
552
|
+
});
|
|
553
|
+
if (!wildcard)
|
|
554
|
+
return filled;
|
|
555
|
+
const wildcardValue = args[wildcard.name];
|
|
556
|
+
if (wildcardValue === undefined || wildcardValue === null) {
|
|
557
|
+
throw refuseLocally(wildcard.name, `Missing path param: ${wildcard.name}`);
|
|
558
|
+
}
|
|
559
|
+
const remainder = String(wildcardValue).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
560
|
+
filled = `${filled.slice(0, -(wildcard.name.length + 1))}${remainder}`;
|
|
561
|
+
return filled;
|
|
562
|
+
}
|
|
563
|
+
function stripConsumedArgs(args, path, scopeKeys) {
|
|
564
|
+
const consumed = new Set(scopeKeys);
|
|
565
|
+
for (const name of extractParamNames(path))
|
|
566
|
+
consumed.add(name);
|
|
567
|
+
const remaining = {};
|
|
568
|
+
for (const [key, value] of Object.entries(args)) {
|
|
569
|
+
if (!consumed.has(key) && value !== undefined)
|
|
570
|
+
remaining[key] = value;
|
|
571
|
+
}
|
|
572
|
+
return remaining;
|
|
573
|
+
}
|
|
574
|
+
function appendQuery(relativeUrl, params) {
|
|
575
|
+
if (!params)
|
|
576
|
+
return relativeUrl;
|
|
577
|
+
const search = new URLSearchParams;
|
|
578
|
+
for (const [key, value] of Object.entries(params)) {
|
|
579
|
+
if (Array.isArray(value)) {
|
|
580
|
+
for (const item of value)
|
|
581
|
+
search.append(key, String(item));
|
|
582
|
+
} else {
|
|
583
|
+
search.set(key, String(value));
|
|
553
584
|
}
|
|
554
585
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
586
|
+
return search.size > 0 ? `${relativeUrl}?${search}` : relativeUrl;
|
|
587
|
+
}
|
|
588
|
+
function planClientRequest(endpoint, contractPrefix, args, config) {
|
|
589
|
+
let pathPrefix = resolvePathPrefix(config, args);
|
|
590
|
+
if (pathPrefix && !pathPrefix.endsWith("/"))
|
|
591
|
+
pathPrefix += "/";
|
|
592
|
+
if (pathPrefix.startsWith("/"))
|
|
593
|
+
pathPrefix = pathPrefix.slice(1);
|
|
594
|
+
const endpointPath = endpoint.path === "/" ? "" : endpoint.path;
|
|
595
|
+
let relativeUrl = fillPathParams(`${pathPrefix}${contractPrefix}${endpointPath}`, args);
|
|
596
|
+
if (relativeUrl.endsWith("/"))
|
|
597
|
+
relativeUrl = relativeUrl.slice(0, -1);
|
|
598
|
+
const remainingArgs = stripConsumedArgs(args, endpoint.path, config?.stripPrefixKeys ?? []);
|
|
599
|
+
if (inputIsQuery(endpoint.method)) {
|
|
600
|
+
relativeUrl = appendQuery(relativeUrl, collectQueryParams(remainingArgs, endpoint));
|
|
566
601
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (parsed) {
|
|
608
|
-
throw new ApiError(parsed.code, response.status, parsed.details, parsed.message, parsed.hint, responseTraceId(response));
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
]
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
async function request(method, url, data, options = {}) {
|
|
617
|
-
const cancellation = createRequestCancellation(options.signal, options.timeout ?? config.timeout ?? 30000);
|
|
618
|
-
const kyOptions = {
|
|
619
|
-
fetch: createRetryAwareFetch(config.fetch ?? globalThis.fetch.bind(globalThis), config.unix),
|
|
620
|
-
timeout: false,
|
|
621
|
-
signal: cancellation.signal
|
|
622
|
-
};
|
|
623
|
-
if (options.params) {
|
|
624
|
-
const searchParams = new URLSearchParams;
|
|
625
|
-
for (const [key, value] of Object.entries(options.params)) {
|
|
626
|
-
if (value === undefined)
|
|
602
|
+
return { relativeUrl, remainingArgs };
|
|
603
|
+
}
|
|
604
|
+
function joinClientBaseUrl(baseUrl, relativeUrl) {
|
|
605
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
606
|
+
const path = relativeUrl.startsWith("/") ? relativeUrl : `/${relativeUrl}`;
|
|
607
|
+
return `${base}${path}`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/internal/bounded-lines.ts
|
|
611
|
+
var DEFAULT_STREAM_LINE_BYTES = 1024 * 1024;
|
|
612
|
+
function lineLimit(value) {
|
|
613
|
+
const resolved = value ?? DEFAULT_STREAM_LINE_BYTES;
|
|
614
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
615
|
+
throw new TypeError("maxLineBytes must be a positive safe integer");
|
|
616
|
+
}
|
|
617
|
+
return resolved;
|
|
618
|
+
}
|
|
619
|
+
function joinBytes(left, right) {
|
|
620
|
+
if (left.byteLength === 0)
|
|
621
|
+
return right.slice();
|
|
622
|
+
const result = new Uint8Array(left.byteLength + right.byteLength);
|
|
623
|
+
result.set(left);
|
|
624
|
+
result.set(right, left.byteLength);
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
async function* readBoundedUtf8Lines(response, maxLineBytes, requireFinalNewline = false) {
|
|
628
|
+
const reader = response.body?.getReader();
|
|
629
|
+
if (!reader)
|
|
630
|
+
return;
|
|
631
|
+
const limit = lineLimit(maxLineBytes);
|
|
632
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
633
|
+
let pending = new Uint8Array;
|
|
634
|
+
try {
|
|
635
|
+
for (;; ) {
|
|
636
|
+
const chunk = await reader.read();
|
|
637
|
+
if (chunk.done)
|
|
638
|
+
break;
|
|
639
|
+
let start = 0;
|
|
640
|
+
for (let index = 0;index < chunk.value.byteLength; index += 1) {
|
|
641
|
+
if (chunk.value[index] !== 10)
|
|
627
642
|
continue;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
} else {
|
|
632
|
-
searchParams.set(key, String(value));
|
|
643
|
+
const lineBytes = joinBytes(pending, chunk.value.slice(start, index));
|
|
644
|
+
if (lineBytes.byteLength > limit) {
|
|
645
|
+
throw new RangeError(`Stream line exceeds the ${limit} byte limit`);
|
|
633
646
|
}
|
|
647
|
+
yield decoder.decode(lineBytes);
|
|
648
|
+
pending = new Uint8Array;
|
|
649
|
+
start = index + 1;
|
|
634
650
|
}
|
|
635
|
-
|
|
636
|
-
|
|
651
|
+
pending = joinBytes(pending, chunk.value.slice(start));
|
|
652
|
+
if (pending.byteLength > limit) {
|
|
653
|
+
throw new RangeError(`Stream line exceeds the ${limit} byte limit`);
|
|
637
654
|
}
|
|
638
655
|
}
|
|
639
|
-
if (
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
kyOptions.json = data;
|
|
643
|
-
}
|
|
644
|
-
try {
|
|
645
|
-
return await cancellation.run(async () => {
|
|
646
|
-
if (options.responseType === "blob") {
|
|
647
|
-
return transportResult(await client[method](url, kyOptions).blob());
|
|
648
|
-
}
|
|
649
|
-
if (options.responseType === "response") {
|
|
650
|
-
return transportResult(await client[method](url, kyOptions));
|
|
651
|
-
}
|
|
652
|
-
const response = await client[method](url, kyOptions);
|
|
653
|
-
if (options.responseType === "void") {
|
|
654
|
-
const text = await response.text();
|
|
655
|
-
if (text.length > 0) {
|
|
656
|
-
throw new Error("Server returned data for an endpoint with no output contract");
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
if (options.responseType === "void" || response.status === 204 || response.headers.get("content-length") === "0") {
|
|
660
|
-
return transportResult(undefined);
|
|
661
|
-
}
|
|
662
|
-
return await response.json();
|
|
663
|
-
});
|
|
664
|
-
} catch (error) {
|
|
665
|
-
if (error instanceof RequestCancellationError) {
|
|
666
|
-
throw new ApiError(error.cause === "caller" ? "REQUEST_ABORTED" : "REQUEST_TIMEOUT", 0, undefined, error.message);
|
|
656
|
+
if (pending.byteLength > 0) {
|
|
657
|
+
if (requireFinalNewline) {
|
|
658
|
+
throw new SyntaxError("Stream ended with an unterminated final line");
|
|
667
659
|
}
|
|
668
|
-
|
|
669
|
-
throw error;
|
|
670
|
-
emit({ type: "network_error" });
|
|
671
|
-
const response = isHTTPError(error) ? error.response : undefined;
|
|
672
|
-
const status = response?.status ?? 0;
|
|
673
|
-
const msg = error instanceof Error ? error.message : undefined;
|
|
674
|
-
throw new ApiError("UNKNOWN_ERROR", status, msg ? { message: msg } : undefined, undefined, undefined, responseTraceId(response));
|
|
660
|
+
yield decoder.decode(pending);
|
|
675
661
|
}
|
|
662
|
+
} finally {
|
|
663
|
+
await reader.cancel().catch(() => {
|
|
664
|
+
return;
|
|
665
|
+
});
|
|
666
|
+
reader.releaseLock();
|
|
676
667
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/internal/errors.ts
|
|
671
|
+
import { z } from "zod";
|
|
672
|
+
function issuePath(path) {
|
|
673
|
+
return path.length > 0 ? path.map(String).join(".") : "(root)";
|
|
674
|
+
}
|
|
675
|
+
function formatZodError(error) {
|
|
676
|
+
const issues = error.issues.slice(0, 5);
|
|
677
|
+
const lines = issues.map((issue) => `${issuePath(issue.path)}: ${issue.message}`);
|
|
678
|
+
const suffix = error.issues.length > 5 ? `
|
|
679
|
+
...and ${error.issues.length - 5} more issues` : "";
|
|
680
|
+
return lines.join(`
|
|
681
|
+
`) + suffix;
|
|
682
|
+
}
|
|
683
|
+
function zodIssues(error) {
|
|
684
|
+
return error.issues.map((issue) => ({
|
|
685
|
+
path: issuePath(issue.path),
|
|
686
|
+
code: issue.code,
|
|
687
|
+
message: issue.message
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/server/stream.ts
|
|
692
|
+
async function* parseSSE(response, options) {
|
|
693
|
+
try {
|
|
694
|
+
for await (const rawLine of readBoundedUtf8Lines(response, options?.maxLineBytes ?? DEFAULT_STREAM_LINE_BYTES)) {
|
|
695
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
696
|
+
if (!line.startsWith("data:"))
|
|
697
|
+
continue;
|
|
698
|
+
const data = line.slice(5).replace(/^ /, "");
|
|
699
|
+
if (data === "[DONE]")
|
|
700
|
+
return;
|
|
701
|
+
try {
|
|
702
|
+
yield JSON.parse(data);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
705
|
+
if (!options?.onParseError)
|
|
706
|
+
throw failure;
|
|
707
|
+
options.onParseError(data, failure);
|
|
708
|
+
}
|
|
698
709
|
}
|
|
699
|
-
}
|
|
710
|
+
} catch (error) {
|
|
711
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
712
|
+
if (!options?.onParseError)
|
|
713
|
+
throw failure;
|
|
714
|
+
options.onParseError("", failure);
|
|
715
|
+
}
|
|
700
716
|
}
|
|
701
717
|
// src/browser/stream.ts
|
|
702
718
|
function parseFailure(raw, error, onParseError) {
|
|
@@ -925,16 +941,35 @@ function setClientMethod(target, key, method) {
|
|
|
925
941
|
function createEndpointMethod(endpoint, execute, contractConfig) {
|
|
926
942
|
const hasScopedArguments = (contractConfig?.stripPrefixKeys?.length ?? 0) > 0;
|
|
927
943
|
if (endpointHasArguments(endpoint) || hasScopedArguments) {
|
|
928
|
-
const method2 = (requestArgs) => execute
|
|
944
|
+
const method2 = (requestArgs) => settle(execute, readClientRequestArgs(requestArgs), undefined);
|
|
929
945
|
return Object.assign(method2, {
|
|
930
|
-
withOptions: (
|
|
946
|
+
withOptions: (...args) => {
|
|
947
|
+
refuseExtraWithOptionsArguments(endpoint, args.length, 2);
|
|
948
|
+
return settle(execute, readClientRequestArgs(args[0]), readClientRequestOptions(args[1]));
|
|
949
|
+
}
|
|
931
950
|
});
|
|
932
951
|
}
|
|
933
|
-
const method = () => execute
|
|
952
|
+
const method = () => settle(execute, {}, undefined);
|
|
934
953
|
return Object.assign(method, {
|
|
935
|
-
withOptions: (
|
|
954
|
+
withOptions: (...args) => {
|
|
955
|
+
refuseExtraWithOptionsArguments(endpoint, args.length, 1);
|
|
956
|
+
return settle(execute, {}, readClientRequestOptions(args[0]));
|
|
957
|
+
}
|
|
936
958
|
});
|
|
937
959
|
}
|
|
960
|
+
function settle(execute, requestArgs, options) {
|
|
961
|
+
try {
|
|
962
|
+
return execute(requestArgs, options);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
return Promise.reject(error);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function refuseExtraWithOptionsArguments(endpoint, received, expected) {
|
|
968
|
+
if (received <= expected)
|
|
969
|
+
return;
|
|
970
|
+
const shape = expected === 1 ? "withOptions(options)" : "withOptions(args, options)";
|
|
971
|
+
throw new TypeError(`${endpoint.method} ${endpoint.path}: this endpoint declares ${expected === 1 ? "no input" : "an input"}, so its method is ${shape} — it received ${received} arguments. ` + "An extra argument here is dropped, and a request options object in the dropped position " + "sends the request without them: an abort signal placed there never reaches the server.");
|
|
972
|
+
}
|
|
938
973
|
function createHttpExecutor(endpoint, prefix, client, config) {
|
|
939
974
|
const httpMethod = endpoint.method.toLowerCase();
|
|
940
975
|
return (requestArgs, options) => {
|
|
@@ -2638,6 +2673,7 @@ var RealtimeRejectReasonSchema = z7.enum([
|
|
|
2638
2673
|
]);
|
|
2639
2674
|
var RealtimeRejectFaultSchema = z7.enum(["peer", "local"]);
|
|
2640
2675
|
export {
|
|
2676
|
+
zodIssues,
|
|
2641
2677
|
unauthorized,
|
|
2642
2678
|
resumableIterator,
|
|
2643
2679
|
rateLimited,
|
|
@@ -2647,6 +2683,7 @@ export {
|
|
|
2647
2683
|
paginatedSchema,
|
|
2648
2684
|
notFound,
|
|
2649
2685
|
isStitchErrorCode,
|
|
2686
|
+
formatZodError,
|
|
2650
2687
|
formatTraceparent,
|
|
2651
2688
|
forbidden,
|
|
2652
2689
|
encodeCursor,
|