universal-api-layer 0.7.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/CHANGELOG.md +82 -0
- package/LICENSE +21 -0
- package/README.md +534 -0
- package/SECURITY.md +9 -0
- package/dist/angular/index.d.ts +62 -0
- package/dist/angular/index.js +142 -0
- package/dist/chunk-DXYWZQW5.js +92 -0
- package/dist/chunk-HDW7BOZI.js +764 -0
- package/dist/chunk-YTM3FV65.js +19 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +205 -0
- package/dist/client-ByBlZaRg.d.ts +169 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +26 -0
- package/dist/react/index.d.ts +16 -0
- package/dist/react/index.js +227 -0
- package/dist/types-CVzD7l4p.d.ts +81 -0
- package/dist/vue/index.d.ts +23 -0
- package/dist/vue/index.js +257 -0
- package/dist/websocket/angular.d.ts +9 -0
- package/dist/websocket/angular.js +15 -0
- package/dist/websocket/index.d.ts +7 -0
- package/dist/websocket/index.js +529 -0
- package/dist/websocket/react.d.ts +8 -0
- package/dist/websocket/react.js +23 -0
- package/dist/websocket/vue.d.ts +9 -0
- package/dist/websocket/vue.js +39 -0
- package/package.json +181 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import {
|
|
2
|
+
abortable,
|
|
3
|
+
invokeCallback,
|
|
4
|
+
validateResponse
|
|
5
|
+
} from "./chunk-DXYWZQW5.js";
|
|
6
|
+
import {
|
|
7
|
+
ApiError
|
|
8
|
+
} from "./chunk-YTM3FV65.js";
|
|
9
|
+
|
|
10
|
+
// src/core/stream.ts
|
|
11
|
+
async function* readStream(response, signal, assert, format, maxSize) {
|
|
12
|
+
assert();
|
|
13
|
+
const reader = response.body?.getReader();
|
|
14
|
+
if (!reader) {
|
|
15
|
+
if (response.status === 204 || response.status === 205) return;
|
|
16
|
+
throw new ApiError(
|
|
17
|
+
"Response has no readable body",
|
|
18
|
+
response.status,
|
|
19
|
+
{},
|
|
20
|
+
void 0,
|
|
21
|
+
"parse"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const cancel = () => {
|
|
25
|
+
void reader.cancel(signal.reason).catch(() => {
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
29
|
+
const decoder = new TextDecoder();
|
|
30
|
+
let line = "", skipLF = false, data = "", hasData = false, event = "", id = "";
|
|
31
|
+
let retry;
|
|
32
|
+
function tooLarge() {
|
|
33
|
+
throw new ApiError(
|
|
34
|
+
"SSE line or event exceeds maxEventSize",
|
|
35
|
+
response.status,
|
|
36
|
+
{},
|
|
37
|
+
void 0,
|
|
38
|
+
"parse"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function finishLine() {
|
|
42
|
+
const value = line;
|
|
43
|
+
line = "";
|
|
44
|
+
if (!value) {
|
|
45
|
+
const result = hasData ? Object.freeze({
|
|
46
|
+
event: event || "message",
|
|
47
|
+
data: data.slice(0, -1),
|
|
48
|
+
id,
|
|
49
|
+
...retry === void 0 ? {} : { retry }
|
|
50
|
+
}) : void 0;
|
|
51
|
+
data = "";
|
|
52
|
+
hasData = false;
|
|
53
|
+
event = "";
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
const colon = value.indexOf(":");
|
|
57
|
+
const field = colon < 0 ? value : value.slice(0, colon);
|
|
58
|
+
let content = colon < 0 ? "" : value.slice(colon + 1);
|
|
59
|
+
if (content.startsWith(" ")) content = content.slice(1);
|
|
60
|
+
if (field === "data") {
|
|
61
|
+
if (data.length + content.length + 1 > maxSize) tooLarge();
|
|
62
|
+
data += content + "\n";
|
|
63
|
+
hasData = true;
|
|
64
|
+
} else if (field === "event") event = content;
|
|
65
|
+
else if (field === "id" && !content.includes("\0")) id = content;
|
|
66
|
+
else if (field === "retry" && /^\d+$/.test(content) && Number.isSafeInteger(Number(content)))
|
|
67
|
+
retry = Number(content);
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
assert();
|
|
72
|
+
if (format === "sse" && response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "text/event-stream")
|
|
73
|
+
throw new ApiError(
|
|
74
|
+
"Expected text/event-stream response",
|
|
75
|
+
response.status,
|
|
76
|
+
{},
|
|
77
|
+
void 0,
|
|
78
|
+
"parse"
|
|
79
|
+
);
|
|
80
|
+
while (true) {
|
|
81
|
+
const part = await abortable(() => reader.read(), signal);
|
|
82
|
+
assert();
|
|
83
|
+
const text = part.done ? decoder.decode() : decoder.decode(part.value, { stream: true });
|
|
84
|
+
if (format === "text") {
|
|
85
|
+
if (text) {
|
|
86
|
+
assert();
|
|
87
|
+
yield text;
|
|
88
|
+
assert();
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
for (const character of text) {
|
|
92
|
+
assert();
|
|
93
|
+
if (skipLF) {
|
|
94
|
+
skipLF = false;
|
|
95
|
+
if (character === "\n") continue;
|
|
96
|
+
}
|
|
97
|
+
if (character === "\r" || character === "\n") {
|
|
98
|
+
skipLF = character === "\r";
|
|
99
|
+
const message = finishLine();
|
|
100
|
+
if (message) {
|
|
101
|
+
assert();
|
|
102
|
+
yield message;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
if (line.length + character.length > maxSize) tooLarge();
|
|
106
|
+
line += character;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (part.done) break;
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
signal.removeEventListener("abort", cancel);
|
|
114
|
+
void reader.cancel().catch(() => {
|
|
115
|
+
});
|
|
116
|
+
reader.releaseLock();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/core/session.ts
|
|
121
|
+
function createSessionState(initial) {
|
|
122
|
+
let token = null;
|
|
123
|
+
let snapshot = Object.freeze({
|
|
124
|
+
userId: null,
|
|
125
|
+
tenantId: null,
|
|
126
|
+
revision: 0
|
|
127
|
+
});
|
|
128
|
+
let tokenVersion = 0;
|
|
129
|
+
const requests = /* @__PURE__ */ new Set();
|
|
130
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
131
|
+
function validate(value) {
|
|
132
|
+
if (value !== null && (typeof value !== "object" || typeof value.userId !== "string" || !value.userId.trim() || value.tenantId != null && typeof value.tenantId !== "string" || value.token != null && (typeof value.token !== "string" || !value.token.trim())))
|
|
133
|
+
throw new ApiError(
|
|
134
|
+
"Session requires a non-empty userId and valid token/tenantId",
|
|
135
|
+
0,
|
|
136
|
+
{},
|
|
137
|
+
void 0,
|
|
138
|
+
"configuration"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (initial !== void 0) validate(initial);
|
|
142
|
+
if (initial) {
|
|
143
|
+
validate(initial);
|
|
144
|
+
token = initial.token ?? null;
|
|
145
|
+
snapshot = Object.freeze({
|
|
146
|
+
userId: initial.userId,
|
|
147
|
+
tenantId: initial.tenantId ?? null,
|
|
148
|
+
revision: 0
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const getSnapshot = () => snapshot;
|
|
152
|
+
const subscribe = (listener) => {
|
|
153
|
+
listeners.add(listener);
|
|
154
|
+
return () => {
|
|
155
|
+
listeners.delete(listener);
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
const assert = (revision) => {
|
|
159
|
+
if (revision !== snapshot.revision)
|
|
160
|
+
throw new ApiError(
|
|
161
|
+
"Authentication session changed",
|
|
162
|
+
0,
|
|
163
|
+
{},
|
|
164
|
+
void 0,
|
|
165
|
+
"cancelled"
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
function set(value, reason = new ApiError(
|
|
169
|
+
"Authentication session changed",
|
|
170
|
+
0,
|
|
171
|
+
{},
|
|
172
|
+
void 0,
|
|
173
|
+
"cancelled"
|
|
174
|
+
)) {
|
|
175
|
+
validate(value);
|
|
176
|
+
const userId = value?.userId ?? null, tenantId = value?.tenantId ?? null;
|
|
177
|
+
const changed = value === null || userId !== snapshot.userId || tenantId !== snapshot.tenantId;
|
|
178
|
+
if (token !== (value?.token ?? null)) tokenVersion++;
|
|
179
|
+
token = value?.token ?? null;
|
|
180
|
+
if (!changed) return;
|
|
181
|
+
snapshot = Object.freeze({
|
|
182
|
+
userId,
|
|
183
|
+
tenantId,
|
|
184
|
+
revision: snapshot.revision + 1
|
|
185
|
+
});
|
|
186
|
+
for (const controller of [...requests]) controller.abort(reason);
|
|
187
|
+
const errors = [];
|
|
188
|
+
for (const listener of [...listeners]) {
|
|
189
|
+
try {
|
|
190
|
+
listener();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
errors.push(error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (errors.length)
|
|
196
|
+
throw new ApiError(
|
|
197
|
+
"Session subscriber failed",
|
|
198
|
+
0,
|
|
199
|
+
{},
|
|
200
|
+
void 0,
|
|
201
|
+
"callback",
|
|
202
|
+
new AggregateError(errors)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
getSnapshot,
|
|
207
|
+
validate,
|
|
208
|
+
subscribe,
|
|
209
|
+
assert,
|
|
210
|
+
set,
|
|
211
|
+
requests,
|
|
212
|
+
getToken: () => token,
|
|
213
|
+
getTokenVersion: () => tokenVersion,
|
|
214
|
+
updateToken(value, revision, version) {
|
|
215
|
+
assert(revision);
|
|
216
|
+
if (version === tokenVersion) {
|
|
217
|
+
token = value;
|
|
218
|
+
tokenVersion++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/core/helpers.ts
|
|
225
|
+
function laravelErrors(data) {
|
|
226
|
+
if (!data || typeof data !== "object" || !("errors" in data) || !data.errors || typeof data.errors !== "object")
|
|
227
|
+
return {};
|
|
228
|
+
return Object.fromEntries(
|
|
229
|
+
Object.entries(data.errors).map(([key, value]) => [
|
|
230
|
+
key,
|
|
231
|
+
Array.isArray(value) ? value.map(String) : [String(value)]
|
|
232
|
+
])
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function queryIdentity(client, key, url, params, headers, scope) {
|
|
236
|
+
return client.queryKey(key, url, params, headers, scope);
|
|
237
|
+
}
|
|
238
|
+
async function runMutation(client, config, variables) {
|
|
239
|
+
const revision = client.getSessionSnapshot().revision;
|
|
240
|
+
const url = invokeCallback(
|
|
241
|
+
() => typeof config.url === "function" ? config.url(variables) : config.url
|
|
242
|
+
);
|
|
243
|
+
const body = invokeCallback(
|
|
244
|
+
() => config.mapPayload ? config.mapPayload(variables) : variables
|
|
245
|
+
);
|
|
246
|
+
client.assertSession(revision);
|
|
247
|
+
const response = await client.request(url, {
|
|
248
|
+
method: config.method ?? "POST",
|
|
249
|
+
schema: config.schema,
|
|
250
|
+
requiresAuth: config.requiresAuth,
|
|
251
|
+
authRetry: config.authRetry,
|
|
252
|
+
body,
|
|
253
|
+
headers: config.headers,
|
|
254
|
+
signal: config.signal,
|
|
255
|
+
timeoutMs: config.timeoutMs
|
|
256
|
+
});
|
|
257
|
+
client.assertSession(revision);
|
|
258
|
+
const selected = config.select ? invokeCallback(() => config.select(response)) : response;
|
|
259
|
+
client.assertSession(revision);
|
|
260
|
+
return selected;
|
|
261
|
+
}
|
|
262
|
+
function defaultQueryRetry(failureCount, error) {
|
|
263
|
+
return failureCount < 3 && (error.code === "network" || error.code === "timeout" || error.code === "http" && error.status >= 500);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/core/client.ts
|
|
267
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
268
|
+
var representationHeaders = [
|
|
269
|
+
"accept",
|
|
270
|
+
"accept-language",
|
|
271
|
+
"content-type"
|
|
272
|
+
];
|
|
273
|
+
function createApiClient(config = {}) {
|
|
274
|
+
const options = { ...config };
|
|
275
|
+
if (options.getToken && (options.session !== void 0 || options.refreshToken))
|
|
276
|
+
throw new ApiError(
|
|
277
|
+
"Use session/setSession instead of getToken with managed authentication or token refresh",
|
|
278
|
+
0,
|
|
279
|
+
{},
|
|
280
|
+
void 0,
|
|
281
|
+
"configuration"
|
|
282
|
+
);
|
|
283
|
+
let managedAuth = options.session !== void 0;
|
|
284
|
+
const session = createSessionState(options.session);
|
|
285
|
+
let refreshJob;
|
|
286
|
+
function setSession(value) {
|
|
287
|
+
session.validate(value);
|
|
288
|
+
managedAuth = true;
|
|
289
|
+
const oldVersion = session.getTokenVersion();
|
|
290
|
+
session.set(value);
|
|
291
|
+
if (refreshJob && (refreshJob.revision !== session.getSnapshot().revision || oldVersion !== session.getTokenVersion()))
|
|
292
|
+
refreshJob.controller.abort(
|
|
293
|
+
new ApiError(
|
|
294
|
+
"Session credentials changed",
|
|
295
|
+
0,
|
|
296
|
+
{},
|
|
297
|
+
void 0,
|
|
298
|
+
"cancelled"
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const logout = () => setSession(null);
|
|
303
|
+
async function refresh(revision, version, signal) {
|
|
304
|
+
session.assert(revision);
|
|
305
|
+
if (session.getTokenVersion() !== version) return;
|
|
306
|
+
if (!refreshJob || refreshJob.revision !== revision || refreshJob.version !== version) {
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
const timeout = options.refreshTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
309
|
+
if (!Number.isFinite(timeout) || timeout < 0)
|
|
310
|
+
throw new ApiError(
|
|
311
|
+
"refreshTimeoutMs must be finite and non-negative",
|
|
312
|
+
0,
|
|
313
|
+
{},
|
|
314
|
+
void 0,
|
|
315
|
+
"configuration"
|
|
316
|
+
);
|
|
317
|
+
const timer = timeout ? setTimeout(
|
|
318
|
+
() => controller.abort(
|
|
319
|
+
new ApiError(
|
|
320
|
+
"Token refresh timed out",
|
|
321
|
+
0,
|
|
322
|
+
{},
|
|
323
|
+
void 0,
|
|
324
|
+
"auth"
|
|
325
|
+
)
|
|
326
|
+
),
|
|
327
|
+
timeout
|
|
328
|
+
) : void 0;
|
|
329
|
+
const job = { revision, version, controller, promise: Promise.resolve() };
|
|
330
|
+
session.requests.add(controller);
|
|
331
|
+
job.promise = abortable(
|
|
332
|
+
() => options.refreshToken(controller.signal, session.getSnapshot()),
|
|
333
|
+
controller.signal
|
|
334
|
+
).then((token) => {
|
|
335
|
+
session.assert(revision);
|
|
336
|
+
if (session.getTokenVersion() !== version) return;
|
|
337
|
+
if (typeof token !== "string" || !token.trim())
|
|
338
|
+
throw new ApiError(
|
|
339
|
+
"Refresh did not return an access token",
|
|
340
|
+
401,
|
|
341
|
+
{},
|
|
342
|
+
void 0,
|
|
343
|
+
"auth"
|
|
344
|
+
);
|
|
345
|
+
session.updateToken(token, revision, version);
|
|
346
|
+
}).catch((cause) => {
|
|
347
|
+
session.assert(revision);
|
|
348
|
+
if (session.getTokenVersion() !== version) return;
|
|
349
|
+
const error = new ApiError(
|
|
350
|
+
"Token refresh failed",
|
|
351
|
+
401,
|
|
352
|
+
{},
|
|
353
|
+
void 0,
|
|
354
|
+
"auth",
|
|
355
|
+
cause
|
|
356
|
+
);
|
|
357
|
+
session.set(null, error);
|
|
358
|
+
throw error;
|
|
359
|
+
}).finally(() => {
|
|
360
|
+
clearTimeout(timer);
|
|
361
|
+
session.requests.delete(controller);
|
|
362
|
+
if (refreshJob === job) refreshJob = void 0;
|
|
363
|
+
});
|
|
364
|
+
void job.promise.catch(() => {
|
|
365
|
+
});
|
|
366
|
+
refreshJob = job;
|
|
367
|
+
}
|
|
368
|
+
const pending = refreshJob.promise;
|
|
369
|
+
await abortable(() => pending, signal);
|
|
370
|
+
}
|
|
371
|
+
const defaultHeaders = new Headers(config.headers);
|
|
372
|
+
const cacheScope = config.cacheScope ?? globalThis.crypto.randomUUID?.() ?? Array.from(
|
|
373
|
+
globalThis.crypto.getRandomValues(new Uint8Array(16)),
|
|
374
|
+
(value) => value.toString(16).padStart(2, "0")
|
|
375
|
+
).join("");
|
|
376
|
+
const baseUrl = config.baseUrl ?? "";
|
|
377
|
+
const browserOrigin = typeof location !== "undefined" && /^https?:$/.test(location.protocol) ? location.origin : void 0;
|
|
378
|
+
const parseUrl = (value, base) => {
|
|
379
|
+
const parsed = new URL(value, base);
|
|
380
|
+
if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password)
|
|
381
|
+
throw new ApiError(
|
|
382
|
+
"Only HTTP(S) URLs without embedded credentials are supported",
|
|
383
|
+
0,
|
|
384
|
+
{},
|
|
385
|
+
void 0,
|
|
386
|
+
"configuration"
|
|
387
|
+
);
|
|
388
|
+
return parsed;
|
|
389
|
+
};
|
|
390
|
+
if (/\\|^\/\//.test(baseUrl) || /^[a-z][a-z\d+.-]*:/i.test(baseUrl) && !/^https?:\/\//i.test(baseUrl))
|
|
391
|
+
throw new ApiError("Invalid baseUrl", 0, {}, void 0, "configuration");
|
|
392
|
+
const baseOrigin = /^https?:\/\//i.test(baseUrl) ? parseUrl(baseUrl).origin : browserOrigin;
|
|
393
|
+
const allowedOrigins = new Set(
|
|
394
|
+
(config.allowedOrigins ?? []).map((origin) => parseUrl(origin).origin)
|
|
395
|
+
);
|
|
396
|
+
if (baseOrigin) allowedOrigins.add(baseOrigin);
|
|
397
|
+
function targetUrl(url, params) {
|
|
398
|
+
if (/\\|^\/\//.test(url) || /^[a-z][a-z\d+.-]*:/i.test(url) && !/^https?:\/\//i.test(url))
|
|
399
|
+
throw new ApiError(
|
|
400
|
+
"Invalid request URL",
|
|
401
|
+
0,
|
|
402
|
+
{},
|
|
403
|
+
void 0,
|
|
404
|
+
"configuration"
|
|
405
|
+
);
|
|
406
|
+
let path = /^https?:\/\//i.test(url) ? url : `${baseUrl.replace(/\/$/, "")}/${url.replace(/^\//, "")}`;
|
|
407
|
+
if (/^https?:\/\//i.test(path)) {
|
|
408
|
+
const parsed = parseUrl(path);
|
|
409
|
+
if (!allowedOrigins.has(parsed.origin))
|
|
410
|
+
throw new ApiError(
|
|
411
|
+
"Request origin is not allowed. Add its exact origin to allowedOrigins to opt in to credential forwarding.",
|
|
412
|
+
0,
|
|
413
|
+
{},
|
|
414
|
+
void 0,
|
|
415
|
+
"configuration"
|
|
416
|
+
);
|
|
417
|
+
path = parsed.href;
|
|
418
|
+
}
|
|
419
|
+
const query = new URLSearchParams();
|
|
420
|
+
for (const [key, value] of Object.entries(params ?? {}))
|
|
421
|
+
if (value != null)
|
|
422
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
423
|
+
query.append(key, String(item));
|
|
424
|
+
const hashIndex = path.indexOf("#");
|
|
425
|
+
const beforeHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
|
|
426
|
+
const hash = hashIndex === -1 ? "" : path.slice(hashIndex);
|
|
427
|
+
const encoded = query.toString();
|
|
428
|
+
return beforeHash + (encoded ? (beforeHash.includes("?") ? "&" : "?") + encoded : "") + hash;
|
|
429
|
+
}
|
|
430
|
+
function queryKey(key, url, params, headers, scope) {
|
|
431
|
+
const merged = new Headers(defaultHeaders);
|
|
432
|
+
new Headers(headers).forEach((value, name) => merged.set(name, value));
|
|
433
|
+
const vary = representationHeaders.flatMap(
|
|
434
|
+
(name) => merged.has(name) ? [[name, merged.get(name)]] : []
|
|
435
|
+
);
|
|
436
|
+
return [
|
|
437
|
+
...key,
|
|
438
|
+
{
|
|
439
|
+
universalApiLayer: 1,
|
|
440
|
+
scope: cacheScope,
|
|
441
|
+
session: session.getSnapshot(),
|
|
442
|
+
account: scope,
|
|
443
|
+
url,
|
|
444
|
+
params,
|
|
445
|
+
headers: vary
|
|
446
|
+
}
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
async function request(url, request2 = {}) {
|
|
450
|
+
for await (const context of transport(url, request2)) {
|
|
451
|
+
const { response, signal, assert } = context;
|
|
452
|
+
try {
|
|
453
|
+
const data = await readData(response, signal);
|
|
454
|
+
const schema = request2.schema === false ? void 0 : request2.schema ?? options.schema;
|
|
455
|
+
const validated = schema ? await abortable(
|
|
456
|
+
() => validateResponse(schema, data, response.status),
|
|
457
|
+
signal
|
|
458
|
+
) : data;
|
|
459
|
+
assert();
|
|
460
|
+
if (request2.responseMode === "full")
|
|
461
|
+
return Object.freeze({
|
|
462
|
+
data: validated,
|
|
463
|
+
headers: new Headers(response.headers),
|
|
464
|
+
status: response.status,
|
|
465
|
+
statusText: response.statusText,
|
|
466
|
+
url: response.url
|
|
467
|
+
});
|
|
468
|
+
return validated;
|
|
469
|
+
} catch (cause) {
|
|
470
|
+
throw requestError(cause, signal);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
throw new ApiError("Missing response", 0, {}, void 0, "network");
|
|
474
|
+
}
|
|
475
|
+
function requestError(cause, signal) {
|
|
476
|
+
if (signal.aborted) return signal.reason;
|
|
477
|
+
if (cause instanceof ApiError) return cause;
|
|
478
|
+
return new ApiError(
|
|
479
|
+
"Network request failed",
|
|
480
|
+
0,
|
|
481
|
+
{},
|
|
482
|
+
void 0,
|
|
483
|
+
"network",
|
|
484
|
+
cause
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
async function readData(response, signal) {
|
|
488
|
+
const raw = await abortable(() => response.text(), signal);
|
|
489
|
+
let data = raw === "" ? null : raw;
|
|
490
|
+
if (raw && response.headers.get("content-type")?.toLowerCase().includes("json")) {
|
|
491
|
+
try {
|
|
492
|
+
data = JSON.parse(raw);
|
|
493
|
+
} catch (cause) {
|
|
494
|
+
if (response.ok)
|
|
495
|
+
throw new ApiError(
|
|
496
|
+
"Invalid JSON response",
|
|
497
|
+
response.status,
|
|
498
|
+
{},
|
|
499
|
+
void 0,
|
|
500
|
+
"parse",
|
|
501
|
+
cause
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return data;
|
|
506
|
+
}
|
|
507
|
+
async function* stream(url, request2 = {}) {
|
|
508
|
+
if (request2.format !== void 0 && request2.format !== "text" && request2.format !== "sse")
|
|
509
|
+
throw new ApiError(
|
|
510
|
+
"Stream format must be text or sse",
|
|
511
|
+
0,
|
|
512
|
+
{},
|
|
513
|
+
void 0,
|
|
514
|
+
"configuration"
|
|
515
|
+
);
|
|
516
|
+
const maxEventSize = request2.maxEventSize ?? 1048576;
|
|
517
|
+
if (!Number.isSafeInteger(maxEventSize) || maxEventSize <= 0)
|
|
518
|
+
throw new ApiError(
|
|
519
|
+
"maxEventSize must be a positive safe integer",
|
|
520
|
+
0,
|
|
521
|
+
{},
|
|
522
|
+
void 0,
|
|
523
|
+
"configuration"
|
|
524
|
+
);
|
|
525
|
+
const headers = new Headers(request2.headers);
|
|
526
|
+
if (request2.format === "sse" && !headers.has("Accept"))
|
|
527
|
+
headers.set("Accept", "text/event-stream");
|
|
528
|
+
for await (const { response, signal, assert } of transport(
|
|
529
|
+
url,
|
|
530
|
+
{ ...request2, headers },
|
|
531
|
+
true
|
|
532
|
+
)) {
|
|
533
|
+
try {
|
|
534
|
+
yield* readStream(
|
|
535
|
+
response,
|
|
536
|
+
signal,
|
|
537
|
+
assert,
|
|
538
|
+
request2.format ?? "text",
|
|
539
|
+
maxEventSize
|
|
540
|
+
);
|
|
541
|
+
} catch (cause) {
|
|
542
|
+
throw requestError(cause, signal);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function* transport(url, request2 = {}, streaming = false) {
|
|
547
|
+
if (request2.responseMode !== void 0 && request2.responseMode !== "data" && request2.responseMode !== "full")
|
|
548
|
+
throw new ApiError(
|
|
549
|
+
"responseMode must be data or full",
|
|
550
|
+
0,
|
|
551
|
+
{},
|
|
552
|
+
void 0,
|
|
553
|
+
"configuration"
|
|
554
|
+
);
|
|
555
|
+
const timeout = request2.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
556
|
+
const revision = session.getSnapshot().revision;
|
|
557
|
+
if (request2.requiresAuth && !session.getSnapshot().userId)
|
|
558
|
+
throw new ApiError(
|
|
559
|
+
"An authenticated session is required",
|
|
560
|
+
401,
|
|
561
|
+
{},
|
|
562
|
+
void 0,
|
|
563
|
+
"auth"
|
|
564
|
+
);
|
|
565
|
+
if (timeout !== void 0 && (!Number.isFinite(timeout) || timeout < 0))
|
|
566
|
+
throw new ApiError(
|
|
567
|
+
"timeoutMs must be a finite non-negative number",
|
|
568
|
+
0,
|
|
569
|
+
{},
|
|
570
|
+
void 0,
|
|
571
|
+
"configuration"
|
|
572
|
+
);
|
|
573
|
+
let target;
|
|
574
|
+
try {
|
|
575
|
+
target = targetUrl(url, request2.params);
|
|
576
|
+
} catch (cause) {
|
|
577
|
+
if (cause instanceof ApiError) throw cause;
|
|
578
|
+
throw new ApiError(
|
|
579
|
+
"Invalid request configuration",
|
|
580
|
+
0,
|
|
581
|
+
{},
|
|
582
|
+
void 0,
|
|
583
|
+
"configuration",
|
|
584
|
+
cause
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const controller = new AbortController();
|
|
588
|
+
session.requests.add(controller);
|
|
589
|
+
const abort = () => controller.abort(
|
|
590
|
+
new ApiError(
|
|
591
|
+
"Request cancelled",
|
|
592
|
+
0,
|
|
593
|
+
{},
|
|
594
|
+
void 0,
|
|
595
|
+
"cancelled",
|
|
596
|
+
request2.signal?.reason
|
|
597
|
+
)
|
|
598
|
+
);
|
|
599
|
+
if (request2.signal?.aborted) abort();
|
|
600
|
+
else request2.signal?.addEventListener("abort", abort, { once: true });
|
|
601
|
+
const timer = timeout ? setTimeout(
|
|
602
|
+
() => controller.abort(
|
|
603
|
+
new ApiError("Request timed out", 0, {}, void 0, "timeout")
|
|
604
|
+
),
|
|
605
|
+
timeout
|
|
606
|
+
) : void 0;
|
|
607
|
+
const cleanup = () => {
|
|
608
|
+
clearTimeout(timer);
|
|
609
|
+
session.requests.delete(controller);
|
|
610
|
+
request2.signal?.removeEventListener("abort", abort);
|
|
611
|
+
controller.signal.removeEventListener("abort", cleanup);
|
|
612
|
+
};
|
|
613
|
+
controller.signal.addEventListener("abort", cleanup, { once: true });
|
|
614
|
+
try {
|
|
615
|
+
controller.signal.throwIfAborted();
|
|
616
|
+
const headers = new Headers(defaultHeaders);
|
|
617
|
+
new Headers(request2.headers).forEach(
|
|
618
|
+
(value, name) => headers.set(name, value)
|
|
619
|
+
);
|
|
620
|
+
let token;
|
|
621
|
+
try {
|
|
622
|
+
token = await abortable(
|
|
623
|
+
() => managedAuth ? session.getToken() : options.getToken?.(controller.signal),
|
|
624
|
+
controller.signal
|
|
625
|
+
);
|
|
626
|
+
} catch (cause) {
|
|
627
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
628
|
+
throw new ApiError(
|
|
629
|
+
"Authentication token lookup failed",
|
|
630
|
+
0,
|
|
631
|
+
{},
|
|
632
|
+
void 0,
|
|
633
|
+
"auth",
|
|
634
|
+
cause
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
638
|
+
let body;
|
|
639
|
+
if (request2.body !== void 0) {
|
|
640
|
+
if (typeof request2.body === "string" || request2.body instanceof FormData || request2.body instanceof Blob || request2.body instanceof URLSearchParams || request2.body instanceof ArrayBuffer || ArrayBuffer.isView(request2.body))
|
|
641
|
+
body = request2.body;
|
|
642
|
+
else {
|
|
643
|
+
body = invokeCallback(() => JSON.stringify(request2.body));
|
|
644
|
+
if (!headers.has("Content-Type"))
|
|
645
|
+
headers.set("Content-Type", "application/json");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const send = async () => {
|
|
649
|
+
session.assert(revision);
|
|
650
|
+
if (managedAuth) {
|
|
651
|
+
headers.delete("Authorization");
|
|
652
|
+
const currentToken = session.getToken();
|
|
653
|
+
if (currentToken)
|
|
654
|
+
headers.set("Authorization", `Bearer ${currentToken}`);
|
|
655
|
+
}
|
|
656
|
+
const version2 = session.getTokenVersion();
|
|
657
|
+
const response2 = await abortable(
|
|
658
|
+
() => (options.fetch ?? globalThis.fetch)(target, {
|
|
659
|
+
method: request2.method ?? "GET",
|
|
660
|
+
headers,
|
|
661
|
+
body,
|
|
662
|
+
signal: controller.signal,
|
|
663
|
+
credentials: options.credentials,
|
|
664
|
+
redirect: "error",
|
|
665
|
+
cache: streaming || managedAuth || headers.has("Authorization") ? "no-store" : "default"
|
|
666
|
+
}),
|
|
667
|
+
controller.signal
|
|
668
|
+
);
|
|
669
|
+
session.assert(revision);
|
|
670
|
+
return { response: response2, version: version2 };
|
|
671
|
+
};
|
|
672
|
+
const pendingRefresh = refreshJob?.revision === revision ? refreshJob.promise : void 0;
|
|
673
|
+
if (request2.authRetry !== false && pendingRefresh)
|
|
674
|
+
await abortable(() => pendingRefresh, controller.signal);
|
|
675
|
+
let { response, version } = await send();
|
|
676
|
+
if (response.status === 401 && request2.authRetry !== false && options.refreshToken && session.getSnapshot().userId) {
|
|
677
|
+
void response.body?.cancel().catch(() => {
|
|
678
|
+
});
|
|
679
|
+
await refresh(revision, version, controller.signal);
|
|
680
|
+
({ response, version } = await send());
|
|
681
|
+
if (response.status === 401) {
|
|
682
|
+
const error = new ApiError(
|
|
683
|
+
"Refreshed credentials were rejected",
|
|
684
|
+
401,
|
|
685
|
+
{},
|
|
686
|
+
void 0,
|
|
687
|
+
"auth"
|
|
688
|
+
);
|
|
689
|
+
session.set(null, error);
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (!response.ok) {
|
|
694
|
+
const data = await readData(response, controller.signal);
|
|
695
|
+
throw new ApiError(
|
|
696
|
+
typeof data === "object" && data && "message" in data ? String(data.message) : response.statusText || "Request failed",
|
|
697
|
+
response.status,
|
|
698
|
+
invokeCallback(
|
|
699
|
+
() => (options.normalizeErrors ?? laravelErrors)(data)
|
|
700
|
+
),
|
|
701
|
+
data
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
yield {
|
|
705
|
+
response,
|
|
706
|
+
signal: controller.signal,
|
|
707
|
+
assert: () => {
|
|
708
|
+
controller.signal.throwIfAborted();
|
|
709
|
+
session.assert(revision);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
} catch (cause) {
|
|
713
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
714
|
+
if (cause instanceof ApiError) throw cause;
|
|
715
|
+
throw new ApiError(
|
|
716
|
+
"Network request failed",
|
|
717
|
+
0,
|
|
718
|
+
{},
|
|
719
|
+
void 0,
|
|
720
|
+
"network",
|
|
721
|
+
cause
|
|
722
|
+
);
|
|
723
|
+
} finally {
|
|
724
|
+
controller.abort(
|
|
725
|
+
new ApiError("Request closed", 0, {}, void 0, "cancelled")
|
|
726
|
+
);
|
|
727
|
+
cleanup();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return Object.freeze({
|
|
731
|
+
cacheScope,
|
|
732
|
+
queryKey,
|
|
733
|
+
setSession,
|
|
734
|
+
logout,
|
|
735
|
+
getSessionSnapshot: session.getSnapshot,
|
|
736
|
+
subscribeSession: session.subscribe,
|
|
737
|
+
assertSession: session.assert,
|
|
738
|
+
async withSession(operation, revision = session.getSnapshot().revision) {
|
|
739
|
+
session.assert(revision);
|
|
740
|
+
const controller = new AbortController();
|
|
741
|
+
session.requests.add(controller);
|
|
742
|
+
try {
|
|
743
|
+
return await abortable(operation, controller.signal);
|
|
744
|
+
} finally {
|
|
745
|
+
session.requests.delete(controller);
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
ownsQuery(key) {
|
|
749
|
+
const value = key.at(-1);
|
|
750
|
+
return !!value && typeof value === "object" && "universalApiLayer" in value && value.universalApiLayer === 1 && "scope" in value && value.scope === cacheScope;
|
|
751
|
+
},
|
|
752
|
+
request,
|
|
753
|
+
stream
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export {
|
|
758
|
+
laravelErrors,
|
|
759
|
+
queryIdentity,
|
|
760
|
+
runMutation,
|
|
761
|
+
defaultQueryRetry,
|
|
762
|
+
DEFAULT_TIMEOUT_MS,
|
|
763
|
+
createApiClient
|
|
764
|
+
};
|