@oxchannels/sdk 1.0.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/LICENSE +7 -0
- package/QUICKSTART.md +604 -0
- package/README.md +193 -0
- package/contracts/openapi-agent-bootstrap-contract-v2.json +1266 -0
- package/contracts/openapi-dashboard-contract-v2.json +98996 -0
- package/contracts/openapi-partner-contract-v2.json +14808 -0
- package/contracts/openapi-public-contract-v2.json +13489 -0
- package/dist/client.d.ts +83 -0
- package/dist/client.js +661 -0
- package/dist/generated/agent-bootstrap-contract-v2-schema.d.ts +398 -0
- package/dist/generated/agent-bootstrap-contract-v2-schema.js +1 -0
- package/dist/generated/client.d.ts +1 -0
- package/dist/generated/client.js +2 -0
- package/dist/generated/contracts/api-errors.d.ts +83 -0
- package/dist/generated/contracts/api-errors.js +296 -0
- package/dist/generated/contracts/json-data.d.ts +16 -0
- package/dist/generated/contracts/json-data.js +308 -0
- package/dist/generated/dashboard-contract-v2-schema.d.ts +45429 -0
- package/dist/generated/dashboard-contract-v2-schema.js +1 -0
- package/dist/generated/partner-contract-v2-schema.d.ts +6458 -0
- package/dist/generated/partner-contract-v2-schema.js +1 -0
- package/dist/generated/public-contract-v2-schema.d.ts +4112 -0
- package/dist/generated/public-contract-v2-schema.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -0
- package/generated/provenance.json +87 -0
- package/package.json +55 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import createClient from 'openapi-fetch';
|
|
2
|
+
import { PARITY_CONTRACT_SET_VERSION, parseApiErrorEnvelopeV2, } from './generated/contracts/api-errors.js';
|
|
3
|
+
const TrustedHeaders = globalThis.Headers;
|
|
4
|
+
const TrustedRequest = globalThis.Request;
|
|
5
|
+
const TrustedUrl = globalThis.URL;
|
|
6
|
+
const trustedReflectApply = Reflect.apply;
|
|
7
|
+
const trustedHeadersDelete = TrustedHeaders.prototype.delete;
|
|
8
|
+
const trustedHeadersSet = TrustedHeaders.prototype.set;
|
|
9
|
+
function captureIntrinsicGetter(prototype, property) {
|
|
10
|
+
const getter = Object.getOwnPropertyDescriptor(prototype, property)?.get;
|
|
11
|
+
if (getter === undefined)
|
|
12
|
+
throw new TypeError(`FETCH_INTRINSIC_MISSING:${property}`);
|
|
13
|
+
return (receiver) => trustedReflectApply(getter, receiver, []);
|
|
14
|
+
}
|
|
15
|
+
const trustedRequestCredentials = captureIntrinsicGetter(TrustedRequest.prototype, 'credentials');
|
|
16
|
+
const trustedRequestHeaders = captureIntrinsicGetter(TrustedRequest.prototype, 'headers');
|
|
17
|
+
const trustedRequestMethod = captureIntrinsicGetter(TrustedRequest.prototype, 'method');
|
|
18
|
+
const trustedRequestRedirect = captureIntrinsicGetter(TrustedRequest.prototype, 'redirect');
|
|
19
|
+
const trustedRequestSignal = captureIntrinsicGetter(TrustedRequest.prototype, 'signal');
|
|
20
|
+
const trustedRequestUrl = captureIntrinsicGetter(TrustedRequest.prototype, 'url');
|
|
21
|
+
const trustedUrlHash = captureIntrinsicGetter(TrustedUrl.prototype, 'hash');
|
|
22
|
+
const trustedUrlHostname = captureIntrinsicGetter(TrustedUrl.prototype, 'hostname');
|
|
23
|
+
const trustedUrlOrigin = captureIntrinsicGetter(TrustedUrl.prototype, 'origin');
|
|
24
|
+
const trustedUrlPassword = captureIntrinsicGetter(TrustedUrl.prototype, 'password');
|
|
25
|
+
const trustedUrlPathname = captureIntrinsicGetter(TrustedUrl.prototype, 'pathname');
|
|
26
|
+
const trustedUrlProtocol = captureIntrinsicGetter(TrustedUrl.prototype, 'protocol');
|
|
27
|
+
const trustedUrlSearch = captureIntrinsicGetter(TrustedUrl.prototype, 'search');
|
|
28
|
+
const trustedUrlUsername = captureIntrinsicGetter(TrustedUrl.prototype, 'username');
|
|
29
|
+
export const OXCHANNELS_CONTRACT_SET_VERSION = PARITY_CONTRACT_SET_VERSION;
|
|
30
|
+
export class OxChannelsApiError extends Error {
|
|
31
|
+
status;
|
|
32
|
+
code;
|
|
33
|
+
traceId;
|
|
34
|
+
constructor(status, code, traceId) {
|
|
35
|
+
super(`OxChannels API request failed (${code})`);
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.traceId = traceId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class OxChannelsApiErrorV2 extends OxChannelsApiError {
|
|
42
|
+
schemaVersion = PARITY_CONTRACT_SET_VERSION;
|
|
43
|
+
envelope;
|
|
44
|
+
apiMessage;
|
|
45
|
+
correlationId;
|
|
46
|
+
retryable;
|
|
47
|
+
category;
|
|
48
|
+
effectEvidence;
|
|
49
|
+
retryAfterSeconds;
|
|
50
|
+
violations;
|
|
51
|
+
constructor(status, envelope) {
|
|
52
|
+
super(status, envelope.code, envelope.traceId);
|
|
53
|
+
this.name = 'OxChannelsApiErrorV2';
|
|
54
|
+
this.envelope = envelope;
|
|
55
|
+
this.apiMessage = envelope.message;
|
|
56
|
+
this.correlationId = envelope.correlationId;
|
|
57
|
+
this.retryable = envelope.retryable;
|
|
58
|
+
this.category = envelope.category;
|
|
59
|
+
this.effectEvidence = envelope.effectEvidence;
|
|
60
|
+
if (envelope.retryAfterSeconds !== undefined) {
|
|
61
|
+
this.retryAfterSeconds = envelope.retryAfterSeconds;
|
|
62
|
+
}
|
|
63
|
+
if (envelope.violations !== undefined)
|
|
64
|
+
this.violations = envelope.violations;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export class OxChannelsApiProtocolError extends Error {
|
|
68
|
+
status;
|
|
69
|
+
code = 'ERROR_ENVELOPE_INVALID';
|
|
70
|
+
constructor(status) {
|
|
71
|
+
super('OxChannels API returned a response outside the negotiated contract');
|
|
72
|
+
this.status = status;
|
|
73
|
+
this.name = 'OxChannelsApiProtocolError';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const MAX_ERROR_BODY_BYTES = 65_536;
|
|
77
|
+
const MAX_ERROR_BODY_CHUNKS = 1_024;
|
|
78
|
+
function cancelResponseBody(response) {
|
|
79
|
+
try {
|
|
80
|
+
void response.body?.cancel().catch(() => undefined);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Cancellation is best-effort and must never delay the protocol failure.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function cancelReader(reader) {
|
|
87
|
+
try {
|
|
88
|
+
void reader.cancel().catch(() => undefined);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Cancellation is best-effort and must never delay the protocol failure.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function readBoundedResponseBody(response) {
|
|
95
|
+
if (response.body === null)
|
|
96
|
+
throw new TypeError('ERROR_ENVELOPE_INVALID');
|
|
97
|
+
const reader = response.body.getReader();
|
|
98
|
+
const chunks = [];
|
|
99
|
+
let totalBytes = 0;
|
|
100
|
+
let chunkCount = 0;
|
|
101
|
+
let canceled = false;
|
|
102
|
+
try {
|
|
103
|
+
while (true) {
|
|
104
|
+
const result = await reader.read();
|
|
105
|
+
if (result.done)
|
|
106
|
+
break;
|
|
107
|
+
const chunk = result.value;
|
|
108
|
+
chunkCount += 1;
|
|
109
|
+
totalBytes += chunk.byteLength;
|
|
110
|
+
if (chunkCount > MAX_ERROR_BODY_CHUNKS || totalBytes > MAX_ERROR_BODY_BYTES) {
|
|
111
|
+
canceled = true;
|
|
112
|
+
cancelReader(reader);
|
|
113
|
+
throw new TypeError('ERROR_ENVELOPE_INVALID');
|
|
114
|
+
}
|
|
115
|
+
chunks.push(chunk);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!canceled)
|
|
120
|
+
cancelReader(reader);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
reader.releaseLock();
|
|
125
|
+
}
|
|
126
|
+
const bytes = new Uint8Array(totalBytes);
|
|
127
|
+
let offset = 0;
|
|
128
|
+
for (const chunk of chunks) {
|
|
129
|
+
bytes.set(chunk, offset);
|
|
130
|
+
offset += chunk.byteLength;
|
|
131
|
+
}
|
|
132
|
+
return bytes;
|
|
133
|
+
}
|
|
134
|
+
async function readBoundedErrorJson(response) {
|
|
135
|
+
const mediaType = response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase();
|
|
136
|
+
if (mediaType !== 'application/json') {
|
|
137
|
+
cancelResponseBody(response);
|
|
138
|
+
throw new TypeError('ERROR_ENVELOPE_INVALID');
|
|
139
|
+
}
|
|
140
|
+
const declaredLength = response.headers.get('content-length');
|
|
141
|
+
if (declaredLength !== null &&
|
|
142
|
+
(!/^\d+$/.test(declaredLength) || Number(declaredLength) > MAX_ERROR_BODY_BYTES)) {
|
|
143
|
+
cancelResponseBody(response);
|
|
144
|
+
throw new TypeError('ERROR_ENVELOPE_INVALID');
|
|
145
|
+
}
|
|
146
|
+
const bytes = await readBoundedResponseBody(response);
|
|
147
|
+
let value;
|
|
148
|
+
try {
|
|
149
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
throw new TypeError('ERROR_ENVELOPE_INVALID');
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
async function readV2ErrorEnvelope(response) {
|
|
157
|
+
const envelope = parseApiErrorEnvelopeV2(await readBoundedErrorJson(response));
|
|
158
|
+
const expectedCodes = response.status === 401
|
|
159
|
+
? ['AUTHENTICATION_REQUIRED']
|
|
160
|
+
: response.status === 403
|
|
161
|
+
? ['FORBIDDEN']
|
|
162
|
+
: response.status === 404
|
|
163
|
+
? ['NOT_FOUND']
|
|
164
|
+
: response.status === 409
|
|
165
|
+
? ['CONFLICT', 'IDEMPOTENCY_CONFLICT']
|
|
166
|
+
: response.status === 429
|
|
167
|
+
? ['RATE_LIMITED']
|
|
168
|
+
: response.status >= 500 && response.status <= 599
|
|
169
|
+
? ['INTERNAL_SAFE_FAILURE']
|
|
170
|
+
: response.status >= 400 && response.status <= 499
|
|
171
|
+
? ['INVALID_REQUEST']
|
|
172
|
+
: [];
|
|
173
|
+
if (!expectedCodes.includes(envelope.code)) {
|
|
174
|
+
throw new TypeError('ERROR_ENVELOPE_STATUS_CODE_MISMATCH');
|
|
175
|
+
}
|
|
176
|
+
return envelope;
|
|
177
|
+
}
|
|
178
|
+
function contractV2ErrorMiddleware(onUnauthorized) {
|
|
179
|
+
return {
|
|
180
|
+
async onResponse({ response }) {
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
let envelope;
|
|
183
|
+
try {
|
|
184
|
+
envelope = await readV2ErrorEnvelope(response);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
throw new OxChannelsApiProtocolError(response.status);
|
|
188
|
+
}
|
|
189
|
+
if (response.status === 401 && envelope.code === 'AUTHENTICATION_REQUIRED') {
|
|
190
|
+
onUnauthorized?.();
|
|
191
|
+
}
|
|
192
|
+
throw new OxChannelsApiErrorV2(response.status, envelope);
|
|
193
|
+
}
|
|
194
|
+
return response;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function requiredHeaderValue(value, label, minimum, maximum) {
|
|
199
|
+
if (typeof value !== 'string' ||
|
|
200
|
+
value.length < minimum ||
|
|
201
|
+
value.length > maximum ||
|
|
202
|
+
value !== value.trim() ||
|
|
203
|
+
/[\r\n\0]/.test(value)) {
|
|
204
|
+
throw new TypeError(`${label}_INVALID`);
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function optionalCorrelationId(value) {
|
|
209
|
+
if (value === undefined)
|
|
210
|
+
return undefined;
|
|
211
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{8,64}$/.test(value)) {
|
|
212
|
+
throw new TypeError('CORRELATION_ID_INVALID');
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
function combineAbortSignals(signals) {
|
|
217
|
+
const uniqueSignals = [...new Set(signals)];
|
|
218
|
+
if (typeof AbortSignal.any === 'function') {
|
|
219
|
+
return { signal: AbortSignal.any(uniqueSignals), cleanup: () => undefined };
|
|
220
|
+
}
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const listeners = [];
|
|
223
|
+
const cleanup = () => {
|
|
224
|
+
for (const { signal, listener } of listeners)
|
|
225
|
+
signal.removeEventListener('abort', listener);
|
|
226
|
+
listeners.length = 0;
|
|
227
|
+
};
|
|
228
|
+
const forwardAbort = (signal) => {
|
|
229
|
+
if (!controller.signal.aborted)
|
|
230
|
+
controller.abort(signal.reason);
|
|
231
|
+
cleanup();
|
|
232
|
+
};
|
|
233
|
+
for (const signal of uniqueSignals) {
|
|
234
|
+
if (signal.aborted) {
|
|
235
|
+
forwardAbort(signal);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
const listener = () => forwardAbort(signal);
|
|
239
|
+
listeners.push({ signal, listener });
|
|
240
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
241
|
+
}
|
|
242
|
+
return { signal: controller.signal, cleanup };
|
|
243
|
+
}
|
|
244
|
+
function requestAbortSignal(input, init) {
|
|
245
|
+
if (init?.signal !== undefined && init.signal !== null)
|
|
246
|
+
return init.signal;
|
|
247
|
+
try {
|
|
248
|
+
return trustedRequestSignal(input);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function copyResponseMetadata(source, target) {
|
|
255
|
+
for (const property of ['url', 'redirected', 'type']) {
|
|
256
|
+
try {
|
|
257
|
+
Object.defineProperty(target, property, {
|
|
258
|
+
configurable: true,
|
|
259
|
+
enumerable: false,
|
|
260
|
+
value: source[property],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Status, headers and body remain standards-compliant if a runtime rejects metadata shadowing.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function responseWithAbortLifecycle(response, signal, cleanup) {
|
|
269
|
+
if (response.body === null) {
|
|
270
|
+
cleanup();
|
|
271
|
+
return response;
|
|
272
|
+
}
|
|
273
|
+
const reader = response.body.getReader();
|
|
274
|
+
let controller;
|
|
275
|
+
let finished = false;
|
|
276
|
+
const finish = () => {
|
|
277
|
+
if (finished)
|
|
278
|
+
return false;
|
|
279
|
+
finished = true;
|
|
280
|
+
signal.removeEventListener('abort', abort);
|
|
281
|
+
cleanup();
|
|
282
|
+
return true;
|
|
283
|
+
};
|
|
284
|
+
function abort() {
|
|
285
|
+
if (!finish())
|
|
286
|
+
return;
|
|
287
|
+
const reason = signal.reason ?? new DOMException('Request aborted', 'AbortError');
|
|
288
|
+
try {
|
|
289
|
+
controller?.error(reason);
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
try {
|
|
293
|
+
void reader
|
|
294
|
+
.cancel(reason)
|
|
295
|
+
.catch(() => undefined)
|
|
296
|
+
.finally(() => reader.releaseLock());
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Cancellation is best-effort after the abort has reached the consumer.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const body = new ReadableStream({
|
|
304
|
+
start(streamController) {
|
|
305
|
+
controller = streamController;
|
|
306
|
+
if (signal.aborted)
|
|
307
|
+
abort();
|
|
308
|
+
else
|
|
309
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
310
|
+
},
|
|
311
|
+
async pull(streamController) {
|
|
312
|
+
try {
|
|
313
|
+
const result = await reader.read();
|
|
314
|
+
if (finished)
|
|
315
|
+
return;
|
|
316
|
+
if (result.done) {
|
|
317
|
+
reader.releaseLock();
|
|
318
|
+
finish();
|
|
319
|
+
streamController.close();
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
streamController.enqueue(result.value);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (finish()) {
|
|
327
|
+
reader.releaseLock();
|
|
328
|
+
streamController.error(error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
async cancel(reason) {
|
|
333
|
+
if (!finish())
|
|
334
|
+
return;
|
|
335
|
+
try {
|
|
336
|
+
await reader.cancel(reason);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// The consumer cancellation is already authoritative.
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
reader.releaseLock();
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
const wrapped = new Response(body, {
|
|
347
|
+
headers: response.headers,
|
|
348
|
+
status: response.status,
|
|
349
|
+
statusText: response.statusText,
|
|
350
|
+
});
|
|
351
|
+
copyResponseMetadata(response, wrapped);
|
|
352
|
+
return wrapped;
|
|
353
|
+
}
|
|
354
|
+
function boundedFetch(fetchImplementation, timeoutMs = 15_000) {
|
|
355
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000) {
|
|
356
|
+
throw new TypeError('TIMEOUT_MS_INVALID');
|
|
357
|
+
}
|
|
358
|
+
const fetchWithTimeout = (input, init) => {
|
|
359
|
+
const timeoutController = new AbortController();
|
|
360
|
+
const timeoutId = setTimeout(() => timeoutController.abort(new DOMException('Request timed out', 'TimeoutError')), timeoutMs);
|
|
361
|
+
const callerSignal = requestAbortSignal(input, init);
|
|
362
|
+
const combined = combineAbortSignals(callerSignal === undefined
|
|
363
|
+
? [timeoutController.signal]
|
|
364
|
+
: [callerSignal, timeoutController.signal]);
|
|
365
|
+
const cleanup = () => {
|
|
366
|
+
clearTimeout(timeoutId);
|
|
367
|
+
combined.cleanup();
|
|
368
|
+
};
|
|
369
|
+
try {
|
|
370
|
+
return Promise.resolve(fetchImplementation(input, { ...init, signal: combined.signal })).then((response) => responseWithAbortLifecycle(response, combined.signal, cleanup), (error) => {
|
|
371
|
+
cleanup();
|
|
372
|
+
throw error;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
cleanup();
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
return fetchWithTimeout;
|
|
381
|
+
}
|
|
382
|
+
function isLoopbackHostname(hostname) {
|
|
383
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
384
|
+
}
|
|
385
|
+
function canonicalOpenApiFetchBaseUrl(baseUrl) {
|
|
386
|
+
if (baseUrl === '' || baseUrl === '/')
|
|
387
|
+
return '/';
|
|
388
|
+
return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
389
|
+
}
|
|
390
|
+
function validateClientBaseUrl(baseUrl, allowInsecureLoopbackForDevelopment, allowBrowserRelative, policyName) {
|
|
391
|
+
const errorCode = `${policyName}_BASE_URL_INVALID`;
|
|
392
|
+
if (typeof baseUrl !== 'string' || baseUrl !== baseUrl.trim() || /[\r\n\0\\]/.test(baseUrl)) {
|
|
393
|
+
throw new TypeError(errorCode);
|
|
394
|
+
}
|
|
395
|
+
if (!baseUrl.startsWith('https://') && !baseUrl.startsWith('http://')) {
|
|
396
|
+
if (!allowBrowserRelative ||
|
|
397
|
+
!baseUrl.startsWith('/') ||
|
|
398
|
+
baseUrl.startsWith('//') ||
|
|
399
|
+
baseUrl.includes('?') ||
|
|
400
|
+
baseUrl.includes('#')) {
|
|
401
|
+
throw new TypeError(errorCode);
|
|
402
|
+
}
|
|
403
|
+
let browserOrigin;
|
|
404
|
+
if (typeof globalThis.location?.origin === 'string') {
|
|
405
|
+
try {
|
|
406
|
+
browserOrigin = new TrustedUrl(globalThis.location.origin);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
throw new TypeError(errorCode);
|
|
410
|
+
}
|
|
411
|
+
if (trustedUrlUsername(browserOrigin) !== '' ||
|
|
412
|
+
trustedUrlPassword(browserOrigin) !== '' ||
|
|
413
|
+
trustedUrlPathname(browserOrigin) !== '/' ||
|
|
414
|
+
trustedUrlSearch(browserOrigin) !== '' ||
|
|
415
|
+
trustedUrlHash(browserOrigin) !== '' ||
|
|
416
|
+
(trustedUrlProtocol(browserOrigin) !== 'https:' &&
|
|
417
|
+
!(trustedUrlProtocol(browserOrigin) === 'http:' &&
|
|
418
|
+
allowInsecureLoopbackForDevelopment &&
|
|
419
|
+
isLoopbackHostname(trustedUrlHostname(browserOrigin))))) {
|
|
420
|
+
throw new TypeError(errorCode);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
canonicalBaseUrl: canonicalOpenApiFetchBaseUrl(baseUrl),
|
|
425
|
+
...(browserOrigin === undefined ? {} : { origin: trustedUrlOrigin(browserOrigin) }),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
let parsed;
|
|
429
|
+
try {
|
|
430
|
+
parsed = new TrustedUrl(baseUrl);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
throw new TypeError(errorCode);
|
|
434
|
+
}
|
|
435
|
+
if (trustedUrlUsername(parsed) !== '' ||
|
|
436
|
+
trustedUrlPassword(parsed) !== '' ||
|
|
437
|
+
trustedUrlSearch(parsed) !== '' ||
|
|
438
|
+
trustedUrlHash(parsed) !== '' ||
|
|
439
|
+
(trustedUrlProtocol(parsed) !== 'https:' &&
|
|
440
|
+
!(trustedUrlProtocol(parsed) === 'http:' &&
|
|
441
|
+
allowInsecureLoopbackForDevelopment &&
|
|
442
|
+
isLoopbackHostname(trustedUrlHostname(parsed))))) {
|
|
443
|
+
throw new TypeError(errorCode);
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
canonicalBaseUrl: canonicalOpenApiFetchBaseUrl(baseUrl),
|
|
447
|
+
origin: trustedUrlOrigin(parsed),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function assertTrustedRequest(request, baseUrl, expectedCredentials, policyName) {
|
|
451
|
+
if (trustedRequestRedirect(request) !== 'error') {
|
|
452
|
+
throw new TypeError(`${policyName}_REDIRECT_POLICY_INVALID`);
|
|
453
|
+
}
|
|
454
|
+
if (trustedRequestCredentials(request) !== expectedCredentials) {
|
|
455
|
+
throw new TypeError(`${policyName}_CREDENTIALS_POLICY_INVALID`);
|
|
456
|
+
}
|
|
457
|
+
let requestUrl;
|
|
458
|
+
try {
|
|
459
|
+
requestUrl = new TrustedUrl(trustedRequestUrl(request));
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
throw new TypeError(`${policyName}_REQUEST_URL_INVALID`);
|
|
463
|
+
}
|
|
464
|
+
if (baseUrl.origin === undefined ||
|
|
465
|
+
trustedUrlOrigin(requestUrl) !== baseUrl.origin ||
|
|
466
|
+
trustedUrlUsername(requestUrl) !== '' ||
|
|
467
|
+
trustedUrlPassword(requestUrl) !== '') {
|
|
468
|
+
throw new TypeError(`${policyName}_REQUEST_URL_INVALID`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function createTrustedFetch(options) {
|
|
472
|
+
const downstream = options.timeoutMs === undefined
|
|
473
|
+
? options.fetch
|
|
474
|
+
: boundedFetch(options.fetch, options.timeoutMs);
|
|
475
|
+
return async (input) => {
|
|
476
|
+
const trustedRequest = new TrustedRequest(input);
|
|
477
|
+
assertTrustedRequest(trustedRequest, options.baseUrl, options.credentials, options.policyName);
|
|
478
|
+
await options.decorate(trustedRequest);
|
|
479
|
+
return downstream(trustedRequest);
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function setTrustedHeader(request, name, value) {
|
|
483
|
+
trustedReflectApply(trustedHeadersSet, trustedRequestHeaders(request), [name, value]);
|
|
484
|
+
}
|
|
485
|
+
function deleteTrustedHeader(request, name) {
|
|
486
|
+
trustedReflectApply(trustedHeadersDelete, trustedRequestHeaders(request), [name]);
|
|
487
|
+
}
|
|
488
|
+
const PROVIDER_BINDING_ROTATION_PATH = /(?:^|\/)v1\/workspaces\/([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\/provider-apps\/([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\/bindings$/iu;
|
|
489
|
+
const PROVIDER_BINDING_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
490
|
+
function decorateProviderBindingRotationFence(request, supplier) {
|
|
491
|
+
deleteTrustedHeader(request, 'x-oxchannels-expected-binding-id');
|
|
492
|
+
if (supplier === undefined || trustedRequestMethod(request) !== 'POST')
|
|
493
|
+
return;
|
|
494
|
+
const requestUrl = new TrustedUrl(trustedRequestUrl(request));
|
|
495
|
+
const match = PROVIDER_BINDING_ROTATION_PATH.exec(trustedUrlPathname(requestUrl));
|
|
496
|
+
if (match === null)
|
|
497
|
+
return;
|
|
498
|
+
const workspaceId = match[1];
|
|
499
|
+
const registrationId = match[2];
|
|
500
|
+
if (workspaceId === undefined || registrationId === undefined) {
|
|
501
|
+
throw new TypeError('PROVIDER_BINDING_ROTATION_PATH_INVALID');
|
|
502
|
+
}
|
|
503
|
+
let expectedBindingId;
|
|
504
|
+
try {
|
|
505
|
+
expectedBindingId = supplier({
|
|
506
|
+
workspaceId: workspaceId.toLowerCase(),
|
|
507
|
+
registrationId: registrationId.toLowerCase(),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
throw new TypeError('PROVIDER_BINDING_ROTATION_FENCE_UNAVAILABLE');
|
|
512
|
+
}
|
|
513
|
+
if (expectedBindingId === undefined)
|
|
514
|
+
return;
|
|
515
|
+
if (typeof expectedBindingId !== 'string' ||
|
|
516
|
+
!PROVIDER_BINDING_ID_PATTERN.test(expectedBindingId)) {
|
|
517
|
+
throw new TypeError('PROVIDER_BINDING_ROTATION_FENCE_INVALID');
|
|
518
|
+
}
|
|
519
|
+
setTrustedHeader(request, 'x-oxchannels-expected-binding-id', expectedBindingId.toLowerCase());
|
|
520
|
+
}
|
|
521
|
+
function trustBoundaryMiddleware(baseUrl, expectedCredentials, trustedFetch, policyName) {
|
|
522
|
+
return {
|
|
523
|
+
onRequest({ request, options }) {
|
|
524
|
+
assertTrustedRequest(request, baseUrl, expectedCredentials, policyName);
|
|
525
|
+
if (canonicalOpenApiFetchBaseUrl(options.baseUrl) !== baseUrl.canonicalBaseUrl) {
|
|
526
|
+
throw new TypeError(`${policyName}_REQUEST_URL_INVALID`);
|
|
527
|
+
}
|
|
528
|
+
if (options.fetch !== trustedFetch) {
|
|
529
|
+
throw new TypeError(`${policyName}_FETCH_POLICY_INVALID`);
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
export function createPublicClient(options = {}) {
|
|
535
|
+
const publicBaseUrl = validateClientBaseUrl(options.baseUrl ?? '/', options.allowInsecureLoopbackForDevelopment === true, true, 'PUBLIC');
|
|
536
|
+
const trustedFetch = createTrustedFetch({
|
|
537
|
+
baseUrl: publicBaseUrl,
|
|
538
|
+
credentials: 'omit',
|
|
539
|
+
policyName: 'PUBLIC',
|
|
540
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
541
|
+
timeoutMs: options.timeoutMs ?? 15_000,
|
|
542
|
+
decorate(request) {
|
|
543
|
+
setTrustedHeader(request, 'OxChannels-Contract-Set', OXCHANNELS_CONTRACT_SET_VERSION);
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
const client = createClient({
|
|
547
|
+
baseUrl: publicBaseUrl.canonicalBaseUrl,
|
|
548
|
+
credentials: 'omit',
|
|
549
|
+
redirect: 'error',
|
|
550
|
+
fetch: trustedFetch,
|
|
551
|
+
});
|
|
552
|
+
client.use(trustBoundaryMiddleware(publicBaseUrl, 'omit', trustedFetch, 'PUBLIC'));
|
|
553
|
+
client.use(contractV2ErrorMiddleware(options.onUnauthorized));
|
|
554
|
+
return client;
|
|
555
|
+
}
|
|
556
|
+
export function createDashboardV2Client(options) {
|
|
557
|
+
const dashboardBaseUrl = validateClientBaseUrl(options.baseUrl ?? '/', options.allowInsecureLoopbackForDevelopment === true, true, 'DASHBOARD');
|
|
558
|
+
const trustedFetch = createTrustedFetch({
|
|
559
|
+
baseUrl: dashboardBaseUrl,
|
|
560
|
+
credentials: 'include',
|
|
561
|
+
policyName: 'DASHBOARD',
|
|
562
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
563
|
+
timeoutMs: options.timeoutMs ?? 15_000,
|
|
564
|
+
decorate(request) {
|
|
565
|
+
setTrustedHeader(request, 'OxChannels-Contract-Set', OXCHANNELS_CONTRACT_SET_VERSION);
|
|
566
|
+
decorateProviderBindingRotationFence(request, options.expectedProviderBindingId);
|
|
567
|
+
const method = trustedRequestMethod(request);
|
|
568
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
569
|
+
let untrustedCsrf;
|
|
570
|
+
try {
|
|
571
|
+
untrustedCsrf = options.csrfToken();
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
throw new TypeError('CSRF_TOKEN_UNAVAILABLE');
|
|
575
|
+
}
|
|
576
|
+
setTrustedHeader(request, 'x-csrf-token', requiredHeaderValue(untrustedCsrf, 'CSRF_TOKEN', 16, 4_096));
|
|
577
|
+
}
|
|
578
|
+
if (options.requestId !== undefined) {
|
|
579
|
+
setTrustedHeader(request, 'x-request-id', options.requestId());
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
const client = createClient({
|
|
584
|
+
baseUrl: dashboardBaseUrl.canonicalBaseUrl,
|
|
585
|
+
credentials: 'include',
|
|
586
|
+
redirect: 'error',
|
|
587
|
+
fetch: trustedFetch,
|
|
588
|
+
});
|
|
589
|
+
client.use(trustBoundaryMiddleware(dashboardBaseUrl, 'include', trustedFetch, 'DASHBOARD'));
|
|
590
|
+
client.use(contractV2ErrorMiddleware(options.onUnauthorized));
|
|
591
|
+
return client;
|
|
592
|
+
}
|
|
593
|
+
export function createPartnerClient(options) {
|
|
594
|
+
const partnerBaseUrl = validateClientBaseUrl(options.baseUrl, options.allowInsecureLoopbackForDevelopment === true, false, 'PARTNER');
|
|
595
|
+
const trustedFetch = createTrustedFetch({
|
|
596
|
+
baseUrl: partnerBaseUrl,
|
|
597
|
+
credentials: 'omit',
|
|
598
|
+
policyName: 'PARTNER',
|
|
599
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
600
|
+
timeoutMs: options.timeoutMs ?? 15_000,
|
|
601
|
+
decorate(request) {
|
|
602
|
+
setTrustedHeader(request, 'OxChannels-Contract-Set', OXCHANNELS_CONTRACT_SET_VERSION);
|
|
603
|
+
const readServiceKey = options.serviceKey ?? options.bearer;
|
|
604
|
+
if (readServiceKey === undefined)
|
|
605
|
+
throw new TypeError('SERVICE_KEY_REQUIRED');
|
|
606
|
+
let untrustedServiceKey;
|
|
607
|
+
try {
|
|
608
|
+
untrustedServiceKey = readServiceKey();
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
throw new TypeError('SERVICE_KEY_UNAVAILABLE');
|
|
612
|
+
}
|
|
613
|
+
const serviceKey = requiredHeaderValue(untrustedServiceKey, 'SERVICE_KEY', 1, 8_192);
|
|
614
|
+
setTrustedHeader(request, 'authorization', `Bearer ${serviceKey}`);
|
|
615
|
+
let untrustedCorrelationId;
|
|
616
|
+
try {
|
|
617
|
+
untrustedCorrelationId = options.correlationId?.();
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
throw new TypeError('CORRELATION_ID_UNAVAILABLE');
|
|
621
|
+
}
|
|
622
|
+
const correlationId = optionalCorrelationId(untrustedCorrelationId);
|
|
623
|
+
if (correlationId !== undefined) {
|
|
624
|
+
setTrustedHeader(request, 'x-correlation-id', correlationId);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
const client = createClient({
|
|
629
|
+
baseUrl: partnerBaseUrl.canonicalBaseUrl,
|
|
630
|
+
credentials: 'omit',
|
|
631
|
+
redirect: 'error',
|
|
632
|
+
fetch: trustedFetch,
|
|
633
|
+
});
|
|
634
|
+
client.use(trustBoundaryMiddleware(partnerBaseUrl, 'omit', trustedFetch, 'PARTNER'));
|
|
635
|
+
client.use(contractV2ErrorMiddleware());
|
|
636
|
+
return client;
|
|
637
|
+
}
|
|
638
|
+
export function createAgentBootstrapClient(options) {
|
|
639
|
+
const agentBaseUrl = validateClientBaseUrl(options.baseUrl, options.allowInsecureLoopbackForDevelopment === true, false, 'AGENT_BOOTSTRAP');
|
|
640
|
+
const trustedFetch = createTrustedFetch({
|
|
641
|
+
baseUrl: agentBaseUrl,
|
|
642
|
+
credentials: 'omit',
|
|
643
|
+
policyName: 'AGENT_BOOTSTRAP',
|
|
644
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
645
|
+
timeoutMs: options.timeoutMs ?? 15_000,
|
|
646
|
+
decorate(request) {
|
|
647
|
+
setTrustedHeader(request, 'OxChannels-Contract-Set', OXCHANNELS_CONTRACT_SET_VERSION);
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
const client = createClient({
|
|
651
|
+
baseUrl: agentBaseUrl.canonicalBaseUrl,
|
|
652
|
+
credentials: 'omit',
|
|
653
|
+
redirect: 'error',
|
|
654
|
+
fetch: trustedFetch,
|
|
655
|
+
});
|
|
656
|
+
client.use(trustBoundaryMiddleware(agentBaseUrl, 'omit', trustedFetch, 'AGENT_BOOTSTRAP'));
|
|
657
|
+
client.use(contractV2ErrorMiddleware());
|
|
658
|
+
return client;
|
|
659
|
+
}
|
|
660
|
+
export const createDashboardContractClient = createDashboardV2Client;
|
|
661
|
+
export const createOxStudioClient = createPartnerClient;
|