@foir/sdk 0.10.0 → 0.12.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 +353 -2
- package/README.md +10 -2
- package/dist/app/client.d.ts +371 -0
- package/dist/app/contract/index.d.ts +23 -0
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +1465 -0
- package/dist/app/contract/proto/apps/v1/discovery_pb.d.ts +678 -0
- package/dist/app/dispatch.d.ts +64 -0
- package/dist/app/errors.d.ts +50 -0
- package/dist/app/index.d.ts +40 -0
- package/dist/app/manifest.d.ts +56 -0
- package/dist/app/naming.d.ts +31 -0
- package/dist/app/record-key.d.ts +17 -0
- package/dist/app/ticket.d.ts +37 -0
- package/dist/app/vocabulary.generated.d.ts +20 -0
- package/dist/app-contract.js +124 -0
- package/dist/app.js +998 -0
- package/dist/auth/auth.d.ts +24 -6
- package/dist/auth/index.d.ts +2 -1
- package/dist/auth/ops.d.ts +8 -0
- package/dist/auth.js +230 -60
- package/dist/client.js +80 -15
- package/dist/codegen/preset.d.ts +3 -2
- package/dist/data/builtins.d.ts +17 -1
- package/dist/data/client.d.ts +10 -8
- package/dist/data/document.d.ts +13 -0
- package/dist/data/index.d.ts +15 -5
- package/dist/data/manifest.d.ts +31 -3
- package/dist/data/schema.d.ts +16 -9
- package/dist/data/selection.d.ts +15 -0
- package/dist/data/transport.d.ts +277 -23
- package/dist/data/ws-transport.d.ts +8 -2
- package/dist/data.js +313 -72
- package/dist/graphql/client.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/index.js +0 -9
- package/dist/live/drift.d.ts +9 -10
- package/dist/live.js +1 -1
- package/dist/next.js +158 -37
- package/dist/placement/index.d.ts +129 -0
- package/dist/placement.js +144 -0
- package/dist/push.js +212 -50
- package/dist/react.js +158 -37
- package/dist/server/callback-client.d.ts +30 -9
- package/dist/server/continue-run.d.ts +56 -0
- package/dist/server/index.d.ts +6 -3
- package/dist/server/operation-types.d.ts +114 -65
- package/dist/server/read-dispatch.d.ts +53 -0
- package/dist/server/require-scoped-token.d.ts +71 -7
- package/dist/server/verify-scoped-token.d.ts +109 -3
- package/dist/server.js +566 -138
- package/dist/transport/foir-http.d.ts +103 -5
- package/package.json +24 -6
package/dist/server.js
CHANGED
|
@@ -117,15 +117,17 @@ async function loadJwks(jwksUrl) {
|
|
|
117
117
|
return keys;
|
|
118
118
|
}
|
|
119
119
|
function parseSubject(sub) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
const first = sub.indexOf("|");
|
|
121
|
+
const second = first === -1 ? -1 : sub.indexOf("|", first + 1);
|
|
122
|
+
if (first === -1 || second === -1) {
|
|
123
|
+
throw new Error("malformed sub: expected <tenant>|<project>|<app>");
|
|
123
124
|
}
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
const tenantId = sub.slice(0, first);
|
|
126
|
+
const projectId = sub.slice(first + 1, second);
|
|
127
|
+
if (!tenantId || !projectId) {
|
|
128
|
+
throw new Error("malformed sub: empty tenant or project");
|
|
127
129
|
}
|
|
128
|
-
return { tenantId, projectId, appName };
|
|
130
|
+
return { tenantId, projectId, appName: sub.slice(second + 1) };
|
|
129
131
|
}
|
|
130
132
|
function recordsReadModels(claims) {
|
|
131
133
|
return claims.rrm ?? [];
|
|
@@ -134,16 +136,25 @@ function recordsWriteModels(claims) {
|
|
|
134
136
|
return claims.rwm ?? [];
|
|
135
137
|
}
|
|
136
138
|
async function verifyScopedToken(token, jwksUrl, options) {
|
|
139
|
+
return verifyWithKeys(token, () => loadJwks(jwksUrl), void 0, options);
|
|
140
|
+
}
|
|
141
|
+
async function verifyWithKeys(token, loadKeys, refetchForUnknownKid, options) {
|
|
137
142
|
const expectedIssuer = options?.expectedIssuer ?? "foir-platform";
|
|
138
143
|
const parts = token.split(".");
|
|
139
144
|
if (parts.length !== 3) throw new Error("malformed token");
|
|
140
145
|
const [rawHeader, rawPayload, rawSig] = parts;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
let header;
|
|
147
|
+
try {
|
|
148
|
+
header = JSON.parse(new TextDecoder().decode(base64UrlDecode(rawHeader)));
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error("unreadable token header");
|
|
151
|
+
}
|
|
152
|
+
if (header.alg !== "EdDSA") throw new Error(`unexpected alg ${String(header.alg)}`);
|
|
153
|
+
if (typeof header.kid !== "string" || !header.kid) throw new Error("missing kid");
|
|
154
|
+
let keys = await loadKeys();
|
|
155
|
+
if (!keys.has(header.kid) && refetchForUnknownKid) {
|
|
156
|
+
keys = await refetchForUnknownKid() ?? keys;
|
|
157
|
+
}
|
|
147
158
|
const key = keys.get(header.kid);
|
|
148
159
|
if (!key) throw new Error(`unknown kid ${header.kid}`);
|
|
149
160
|
const signingInput = new TextEncoder().encode(`${rawHeader}.${rawPayload}`);
|
|
@@ -158,10 +169,13 @@ async function verifyScopedToken(token, jwksUrl, options) {
|
|
|
158
169
|
)
|
|
159
170
|
);
|
|
160
171
|
if (!ok) throw new Error("signature invalid");
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
let claims;
|
|
173
|
+
try {
|
|
174
|
+
claims = JSON.parse(new TextDecoder().decode(base64UrlDecode(rawPayload)));
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error("unreadable token payload");
|
|
177
|
+
}
|
|
178
|
+
const now = Math.floor((options?.now?.() ?? Date.now()) / 1e3);
|
|
165
179
|
if (claims.iss !== expectedIssuer) throw new Error(`bad iss ${claims.iss}`);
|
|
166
180
|
if (typeof claims.exp !== "number" || claims.exp < now) {
|
|
167
181
|
throw new Error("token expired");
|
|
@@ -183,35 +197,174 @@ function parseContextHeader(raw) {
|
|
|
183
197
|
}
|
|
184
198
|
return out;
|
|
185
199
|
}
|
|
200
|
+
var ScopedTokenUnavailableError = class extends Error {
|
|
201
|
+
constructor(message, options) {
|
|
202
|
+
super(message);
|
|
203
|
+
this.name = "ScopedTokenUnavailableError";
|
|
204
|
+
if (options?.cause !== void 0) this.cause = options.cause;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var VERIFIER_TTL_MS = 60 * 60 * 1e3;
|
|
208
|
+
var VERIFIER_TIMEOUT_MS = 5e3;
|
|
209
|
+
var KID_MISS_COOLDOWN_MS = 6e4;
|
|
210
|
+
var FAILURE_COOLDOWN_MS = 6e4;
|
|
211
|
+
function createScopedTokenVerifier(options) {
|
|
212
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
213
|
+
const now = options.now ?? Date.now;
|
|
214
|
+
const timeoutMs = options.timeoutMs ?? VERIFIER_TIMEOUT_MS;
|
|
215
|
+
const ttlMs = options.ttlMs ?? VERIFIER_TTL_MS;
|
|
216
|
+
let cache;
|
|
217
|
+
let inFlight;
|
|
218
|
+
let lastKidMissRefetch = 0;
|
|
219
|
+
let nextAttemptAt = 0;
|
|
220
|
+
let staleServes = 0;
|
|
221
|
+
let refetchFailures = 0;
|
|
222
|
+
let notModified = 0;
|
|
223
|
+
async function fetchKeys(held) {
|
|
224
|
+
try {
|
|
225
|
+
const headers = { accept: "application/json" };
|
|
226
|
+
if (held?.etag) headers["if-none-match"] = held.etag;
|
|
227
|
+
const res = await doFetch(options.jwksUrl, {
|
|
228
|
+
...options.requestInit,
|
|
229
|
+
headers,
|
|
230
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
231
|
+
});
|
|
232
|
+
if (res.status === 304 && held) {
|
|
233
|
+
notModified += 1;
|
|
234
|
+
nextAttemptAt = 0;
|
|
235
|
+
cache = { ...held, fetchedAt: now() };
|
|
236
|
+
return cache;
|
|
237
|
+
}
|
|
238
|
+
if (!res.ok) throw new Error(`jwks fetch ${res.status}`);
|
|
239
|
+
const doc = await res.json();
|
|
240
|
+
const keys2 = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const jwk of doc.keys ?? []) {
|
|
242
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.kid || !jwk.x) continue;
|
|
243
|
+
const raw = base64UrlDecode(jwk.x);
|
|
244
|
+
keys2.set(
|
|
245
|
+
jwk.kid,
|
|
246
|
+
await crypto.subtle.importKey(
|
|
247
|
+
"raw",
|
|
248
|
+
raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength),
|
|
249
|
+
{ name: "Ed25519" },
|
|
250
|
+
false,
|
|
251
|
+
["verify"]
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (keys2.size === 0) throw new Error("jwks carried no Ed25519 keys");
|
|
256
|
+
nextAttemptAt = 0;
|
|
257
|
+
cache = { keys: keys2, fetchedAt: now(), etag: res.headers.get("etag") ?? void 0 };
|
|
258
|
+
return cache;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
refetchFailures += 1;
|
|
261
|
+
nextAttemptAt = now() + FAILURE_COOLDOWN_MS;
|
|
262
|
+
if (held) {
|
|
263
|
+
staleServes += 1;
|
|
264
|
+
try {
|
|
265
|
+
options.onStale?.({ error, ageMs: now() - held.fetchedAt, staleServes });
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
return held;
|
|
269
|
+
}
|
|
270
|
+
throw new ScopedTokenUnavailableError(
|
|
271
|
+
`the JWKS could not be read and no keys are held: ${error instanceof Error ? error.message : String(error)}`,
|
|
272
|
+
{ cause: error }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function keys(force) {
|
|
277
|
+
const held = cache;
|
|
278
|
+
const at = now();
|
|
279
|
+
if (!force && held && at - held.fetchedAt < ttlMs) return Promise.resolve(held);
|
|
280
|
+
if (at < nextAttemptAt && !inFlight) {
|
|
281
|
+
if (held) {
|
|
282
|
+
staleServes += 1;
|
|
283
|
+
return Promise.resolve(held);
|
|
284
|
+
}
|
|
285
|
+
return Promise.reject(
|
|
286
|
+
new ScopedTokenUnavailableError(
|
|
287
|
+
`the JWKS could not be read and no keys are held; the next attempt is in ${Math.ceil((nextAttemptAt - at) / 1e3)}s`
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
inFlight ??= fetchKeys(held).finally(() => {
|
|
292
|
+
inFlight = void 0;
|
|
293
|
+
});
|
|
294
|
+
return inFlight;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
verify(token) {
|
|
298
|
+
return verifyWithKeys(
|
|
299
|
+
token,
|
|
300
|
+
async () => (await keys(false)).keys,
|
|
301
|
+
async () => {
|
|
302
|
+
const at = now();
|
|
303
|
+
if (at - lastKidMissRefetch < KID_MISS_COOLDOWN_MS) return null;
|
|
304
|
+
lastKidMissRefetch = at;
|
|
305
|
+
return (await keys(true)).keys;
|
|
306
|
+
},
|
|
307
|
+
{ expectedIssuer: options.expectedIssuer, now }
|
|
308
|
+
);
|
|
309
|
+
},
|
|
310
|
+
async warm() {
|
|
311
|
+
try {
|
|
312
|
+
await keys(false);
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
stats() {
|
|
317
|
+
return {
|
|
318
|
+
keyCount: cache?.keys.size ?? 0,
|
|
319
|
+
ageMs: cache ? now() - cache.fetchedAt : null,
|
|
320
|
+
staleServes,
|
|
321
|
+
refetchFailures,
|
|
322
|
+
notModified
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
186
327
|
|
|
187
328
|
// src/server/require-scoped-token.ts
|
|
188
|
-
var
|
|
189
|
-
var
|
|
329
|
+
var FOIR_TOKEN_HEADER = "X-Foir-Token";
|
|
330
|
+
var FOIR_CONTEXT_HEADER = "X-Foir-Context";
|
|
331
|
+
var TOKEN_HEADER = FOIR_TOKEN_HEADER;
|
|
332
|
+
var CONTEXT_HEADER = FOIR_CONTEXT_HEADER;
|
|
190
333
|
var DEFAULT_JWKS_URL = "https://api.foir.dev/.well-known/jwks.json";
|
|
191
334
|
var ScopedTokenError = class extends Error {
|
|
192
335
|
status;
|
|
193
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Why, for a route that answers the cases differently: `missing` (the
|
|
338
|
+
* request carried no token at all, so it did not come from the platform),
|
|
339
|
+
* `invalid` (a token that does not verify), `forbidden` (a valid token for
|
|
340
|
+
* another tenant or project) and `unavailable` (the token could not be
|
|
341
|
+
* checked).
|
|
342
|
+
*/
|
|
343
|
+
reason;
|
|
344
|
+
constructor(message, status, reason) {
|
|
194
345
|
super(message);
|
|
195
346
|
this.name = "ScopedTokenError";
|
|
196
347
|
this.status = status;
|
|
348
|
+
this.reason = reason ?? (status === 403 ? "forbidden" : status === 503 ? "unavailable" : "invalid");
|
|
197
349
|
}
|
|
198
350
|
};
|
|
351
|
+
var LEVEL_CLAIM = "org_id";
|
|
199
352
|
async function requireScopedToken(request, options) {
|
|
200
353
|
const token = request.headers.get(TOKEN_HEADER);
|
|
201
354
|
if (!token) {
|
|
202
355
|
throw new ScopedTokenError(
|
|
203
356
|
`missing ${TOKEN_HEADER} header \u2014 this route only accepts platform-dispatched requests`,
|
|
204
|
-
401
|
|
357
|
+
401,
|
|
358
|
+
"missing"
|
|
205
359
|
);
|
|
206
360
|
}
|
|
207
361
|
let claims;
|
|
208
362
|
try {
|
|
209
|
-
claims = await verifyScopedToken(
|
|
210
|
-
token,
|
|
211
|
-
options?.jwksUrl ?? DEFAULT_JWKS_URL,
|
|
212
|
-
options
|
|
213
|
-
);
|
|
363
|
+
claims = options?.verifier ? await options.verifier.verify(token) : await verifyScopedToken(token, options?.jwksUrl ?? DEFAULT_JWKS_URL, options);
|
|
214
364
|
} catch (err) {
|
|
365
|
+
if (err instanceof ScopedTokenUnavailableError) {
|
|
366
|
+
throw new ScopedTokenError(`the scoped token could not be checked: ${err.message}`, 503);
|
|
367
|
+
}
|
|
215
368
|
throw new ScopedTokenError(
|
|
216
369
|
`invalid scoped token: ${err instanceof Error ? err.message : String(err)}`,
|
|
217
370
|
401
|
|
@@ -238,111 +391,126 @@ async function requireScopedToken(request, options) {
|
|
|
238
391
|
403
|
|
239
392
|
);
|
|
240
393
|
}
|
|
394
|
+
const rawContext = request.headers.get(CONTEXT_HEADER);
|
|
395
|
+
const level = claims.ctx?.[LEVEL_CLAIM];
|
|
241
396
|
return {
|
|
242
397
|
claims,
|
|
243
398
|
subject,
|
|
244
|
-
context: parseContextHeader(
|
|
399
|
+
context: parseContextHeader(rawContext),
|
|
400
|
+
// The token handed over is the one verified above, not a fresh read of the
|
|
401
|
+
// header, so the client presents exactly what this guard accepted.
|
|
402
|
+
dispatch: { token: () => token, context: rawContext },
|
|
403
|
+
orgId: typeof level === "string" ? level : null,
|
|
404
|
+
...typeof claims.cid === "string" && claims.cid !== "" ? { customerId: claims.cid } : {},
|
|
405
|
+
expiresAt: new Date(claims.exp * 1e3),
|
|
406
|
+
tokenId: claims.jti
|
|
245
407
|
};
|
|
246
408
|
}
|
|
247
409
|
|
|
248
|
-
// src/server/
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
410
|
+
// src/server/read-dispatch.ts
|
|
411
|
+
var OperationPayloadError = class extends Error {
|
|
412
|
+
status;
|
|
413
|
+
constructor(message) {
|
|
414
|
+
super(message);
|
|
415
|
+
this.name = "OperationPayloadError";
|
|
416
|
+
this.status = 400;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
420
|
+
function refuse(message) {
|
|
421
|
+
throw new OperationPayloadError(`[foir] not an operation dispatch body: ${message}`);
|
|
422
|
+
}
|
|
423
|
+
function requireString(from, key, where, allowEmpty = false) {
|
|
424
|
+
const value = from[key];
|
|
425
|
+
if (typeof value !== "string" || !allowEmpty && value === "") {
|
|
426
|
+
refuse(`${where}.${key} is ${describe(value)}, expected a${allowEmpty ? "" : " non-empty"} string`);
|
|
427
|
+
}
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
function describe(value) {
|
|
431
|
+
if (value === void 0) return "missing";
|
|
432
|
+
if (value === null) return "null";
|
|
433
|
+
if (Array.isArray(value)) return "an array";
|
|
434
|
+
return `a ${typeof value}`;
|
|
435
|
+
}
|
|
436
|
+
async function readDispatch(request) {
|
|
437
|
+
let text;
|
|
438
|
+
try {
|
|
439
|
+
text = await request.text();
|
|
440
|
+
} catch (err) {
|
|
441
|
+
throw new OperationPayloadError(
|
|
442
|
+
`[foir] could not read the dispatch body (has something already consumed it?): ${err instanceof Error ? err.message : String(err)}`
|
|
255
443
|
);
|
|
256
444
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
445
|
+
let body;
|
|
446
|
+
try {
|
|
447
|
+
body = JSON.parse(text);
|
|
448
|
+
} catch {
|
|
449
|
+
refuse("the body is not JSON");
|
|
261
450
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
451
|
+
if (!isObject(body)) refuse(`the body is ${describe(body)}, expected a JSON object`);
|
|
452
|
+
if (typeof body.executionId !== "string" || typeof body.operationKey !== "string") {
|
|
453
|
+
refuse(
|
|
454
|
+
"it has no executionId and operationKey. The platform sends the run, not the input: the caller's input is under `input`"
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
requireString(body, "executionId", "body");
|
|
458
|
+
requireString(body, "operationKey", "body");
|
|
459
|
+
const trigger = body.trigger;
|
|
460
|
+
if (!isObject(trigger)) refuse(`body.trigger is ${describe(trigger)}, expected an object`);
|
|
461
|
+
requireString(trigger, "type", "body.trigger");
|
|
462
|
+
const context = body.context;
|
|
463
|
+
if (!isObject(context)) refuse(`body.context is ${describe(context)}, expected an object`);
|
|
464
|
+
requireString(context, "tenantId", "body.context");
|
|
465
|
+
requireString(context, "projectId", "body.context");
|
|
466
|
+
requireString(context, "userId", "body.context", true);
|
|
467
|
+
requireString(context, "timestamp", "body.context");
|
|
468
|
+
if (context.customerId !== void 0) requireString(context, "customerId", "body.context");
|
|
469
|
+
const input = body.input;
|
|
470
|
+
if (input !== null && !isObject(input)) {
|
|
471
|
+
refuse(`body.input is ${describe(input)}, expected an object`);
|
|
472
|
+
}
|
|
473
|
+
const payload = { ...body, input: input ?? {} };
|
|
474
|
+
for (const block of ["content", "record", "triggerContext", "callback", "continuation"]) {
|
|
475
|
+
const value = payload[block];
|
|
476
|
+
if (value === void 0) continue;
|
|
477
|
+
if (value === null) {
|
|
478
|
+
delete payload[block];
|
|
479
|
+
continue;
|
|
284
480
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
481
|
+
if (!isObject(value)) refuse(`body.${block} is ${describe(value)}, expected an object`);
|
|
482
|
+
}
|
|
483
|
+
if (isObject(payload.continuation)) {
|
|
484
|
+
requireString(payload.continuation, "cursor", "body.continuation");
|
|
485
|
+
if (typeof payload.continuation.slice !== "number") {
|
|
486
|
+
refuse(`body.continuation.slice is ${describe(payload.continuation.slice)}, expected a number`);
|
|
288
487
|
}
|
|
289
|
-
return out;
|
|
290
|
-
};
|
|
291
|
-
return {
|
|
292
|
-
complete: (result) => post(callback.mutations.complete, "status cancelled", { result }),
|
|
293
|
-
fail: (code, message, options = {}) => post(callback.mutations.fail, "status cancelled", {
|
|
294
|
-
code,
|
|
295
|
-
message,
|
|
296
|
-
retryable: options.retryable,
|
|
297
|
-
details: options.details
|
|
298
|
-
}),
|
|
299
|
-
progress: (update) => post(callback.mutations.progress, "status cancelled applied", {
|
|
300
|
-
pct: update.pct,
|
|
301
|
-
message: update.message,
|
|
302
|
-
metadata: update.metadata
|
|
303
|
-
}),
|
|
304
|
-
cancel: () => post(callback.mutations.cancel, "status cancelled", {})
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
function fieldArgsForMutation(mutationName, vars) {
|
|
308
|
-
const parts = [];
|
|
309
|
-
for (const key of Object.keys(vars)) {
|
|
310
|
-
if (vars[key] === void 0) continue;
|
|
311
|
-
parts.push(`, $${key}: ${graphQLTypeFor(mutationName, key)}`);
|
|
312
488
|
}
|
|
313
|
-
return
|
|
489
|
+
return payload;
|
|
314
490
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
491
|
+
|
|
492
|
+
// src/server/continue-run.ts
|
|
493
|
+
var CONTINUATION_MEDIA_TYPE = "application/vnd.foir.continuation+json";
|
|
494
|
+
var MAX_CONTINUATION_CURSOR_BYTES = 16 * 1024;
|
|
495
|
+
var MAX_CONTINUATION_DELAY_MS = 15 * 60 * 1e3;
|
|
496
|
+
function continueRun(continuation) {
|
|
497
|
+
const { cursor, delayMs, progressPct } = continuation;
|
|
498
|
+
if (typeof cursor !== "string" || cursor === "") {
|
|
499
|
+
throw new TypeError("continueRun: a continuation carries the cursor the next slice resumes from");
|
|
320
500
|
}
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
function graphQLTypeFor(mutationName, key) {
|
|
324
|
-
if (key === "code" || key === "message") return "String!";
|
|
325
|
-
if (key === "pct") return "Int";
|
|
326
|
-
if (key === "retryable") return "Boolean";
|
|
327
|
-
if (key === "details" || key === "metadata") return "JSON";
|
|
328
|
-
if (key === "result") {
|
|
329
|
-
const base = mutationName.replace(/^complete/, "").replace(/Execution$/, "");
|
|
330
|
-
return `${base}ExecutionResultInput!`;
|
|
501
|
+
if (new TextEncoder().encode(cursor).length > MAX_CONTINUATION_CURSOR_BYTES) {
|
|
502
|
+
throw new RangeError(`continueRun: a cursor is at most ${MAX_CONTINUATION_CURSOR_BYTES} bytes`);
|
|
331
503
|
}
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
["sign"]
|
|
343
|
-
);
|
|
344
|
-
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
|
|
345
|
-
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
504
|
+
if (delayMs !== void 0 && !(Number.isInteger(delayMs) && delayMs >= 0 && delayMs <= MAX_CONTINUATION_DELAY_MS)) {
|
|
505
|
+
throw new RangeError(`continueRun: delayMs is a whole number of milliseconds from 0 to ${MAX_CONTINUATION_DELAY_MS}`);
|
|
506
|
+
}
|
|
507
|
+
if (progressPct !== void 0 && !(Number.isInteger(progressPct) && progressPct >= 0 && progressPct <= 100)) {
|
|
508
|
+
throw new RangeError("continueRun: progressPct is a whole percentage from 0 to 100");
|
|
509
|
+
}
|
|
510
|
+
return new Response(JSON.stringify(continuation), {
|
|
511
|
+
status: 200,
|
|
512
|
+
headers: { "Content-Type": CONTINUATION_MEDIA_TYPE }
|
|
513
|
+
});
|
|
346
514
|
}
|
|
347
515
|
|
|
348
516
|
// src/transport/foir-http.ts
|
|
@@ -398,7 +566,12 @@ var FoirRequestError = class extends Error {
|
|
|
398
566
|
* failed on the first attempt for an hour.
|
|
399
567
|
*/
|
|
400
568
|
get retryable() {
|
|
401
|
-
if (this.status === void 0)
|
|
569
|
+
if (this.status === void 0) {
|
|
570
|
+
if (this.graphQLErrors?.length || this.prunedSelections?.length) {
|
|
571
|
+
return this.code === "RATE_LIMITED" || this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
572
|
+
}
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
402
575
|
if (this.status === 429 || this.code === "RATE_LIMITED") return true;
|
|
403
576
|
if (this.status >= 500) return true;
|
|
404
577
|
return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
@@ -428,19 +601,50 @@ function readFoirRetryAfterMs(headers) {
|
|
|
428
601
|
const seconds = Number(headers.get("Retry-After"));
|
|
429
602
|
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
430
603
|
}
|
|
604
|
+
var DISPATCH_RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
605
|
+
"x-api-key",
|
|
606
|
+
"authorization",
|
|
607
|
+
"x-foir-token",
|
|
608
|
+
"x-foir-context"
|
|
609
|
+
]);
|
|
610
|
+
function refuseCredentialHeaders(extra) {
|
|
611
|
+
if (!extra) return;
|
|
612
|
+
for (const name of Object.keys(extra)) {
|
|
613
|
+
if (DISPATCH_RESERVED_HEADERS.has(name.toLowerCase())) {
|
|
614
|
+
throw new FoirRequestError(
|
|
615
|
+
`[foir] a client built from a dispatch token cannot send "${name}" in \`headers\`. The dispatch token is the credential, and an api key or a bearer beside it would make the request two actors at once. Build a separate client for the other credential.`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
431
621
|
function buildFoirHeaders(input) {
|
|
432
622
|
const headers = {
|
|
433
|
-
"Content-Type": "application/json"
|
|
434
|
-
"x-api-key": input.apiKey
|
|
623
|
+
"Content-Type": "application/json"
|
|
435
624
|
};
|
|
436
|
-
if (input.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
headers["
|
|
625
|
+
if (input.dispatchToken !== void 0) {
|
|
626
|
+
refuseCredentialHeaders(input.extra);
|
|
627
|
+
headers["X-Foir-Token"] = input.dispatchToken;
|
|
628
|
+
if (input.dispatchContext) {
|
|
629
|
+
headers["X-Foir-Context"] = input.dispatchContext;
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
headers["x-api-key"] = input.apiKey;
|
|
633
|
+
if (input.draftChannel) {
|
|
634
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
635
|
+
}
|
|
636
|
+
if (input.bearerToken) {
|
|
637
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
638
|
+
}
|
|
639
|
+
if (input.orgId) {
|
|
640
|
+
headers["X-Org-Id"] = input.orgId;
|
|
641
|
+
}
|
|
642
|
+
if (input.orgUrl) {
|
|
643
|
+
headers["X-Org-Url"] = input.orgUrl;
|
|
644
|
+
}
|
|
645
|
+
if (input.visitorAddress) {
|
|
646
|
+
headers[FOIR_VISITOR_IP_HEADER] = input.visitorAddress;
|
|
647
|
+
}
|
|
444
648
|
}
|
|
445
649
|
if (input.extra) {
|
|
446
650
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -456,13 +660,226 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
456
660
|
}
|
|
457
661
|
return base + path;
|
|
458
662
|
}
|
|
663
|
+
var FoirAuthDowngradedError = class extends FoirRequestError {
|
|
664
|
+
downgrade;
|
|
665
|
+
constructor(downgrade) {
|
|
666
|
+
super(
|
|
667
|
+
`[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}), so the request was served as ${downgrade.servedAs}`,
|
|
668
|
+
{ graphQLErrors: [{ message: "customer token rejected", extensions: { code: "AUTH_DOWNGRADED" } }] }
|
|
669
|
+
);
|
|
670
|
+
this.name = "FoirAuthDowngradedError";
|
|
671
|
+
this.downgrade = downgrade;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
var FoirSessionExpiredError = class extends FoirRequestError {
|
|
675
|
+
constructor(options) {
|
|
676
|
+
super("[foir] the customer session has expired and could not be refreshed: sign in again", {
|
|
677
|
+
cause: options?.cause,
|
|
678
|
+
graphQLErrors: [{ message: "session expired", extensions: { code: "SESSION_EXPIRED" } }]
|
|
679
|
+
});
|
|
680
|
+
this.name = "FoirSessionExpiredError";
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// src/transport/retry.ts
|
|
685
|
+
var DEFAULT_ATTEMPTS = 3;
|
|
686
|
+
var DEFAULT_BASE_DELAY_MS = 200;
|
|
687
|
+
var DEFAULT_MAX_DELAY_MS = 2e4;
|
|
688
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
689
|
+
function foirRetryDelayMs(error, attempt, policy = {}) {
|
|
690
|
+
if (!(error instanceof FoirRequestError) || !error.retryable) return null;
|
|
691
|
+
const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
692
|
+
const stated = error.retryAfterMs;
|
|
693
|
+
if (stated !== void 0) return Math.min(stated, max);
|
|
694
|
+
const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
695
|
+
return Math.min(base * 2 ** (attempt - 1), max);
|
|
696
|
+
}
|
|
697
|
+
async function withFoirRetry(fn, policy = {}) {
|
|
698
|
+
const attempts = Math.max(1, policy.attempts ?? DEFAULT_ATTEMPTS);
|
|
699
|
+
const sleep = policy.sleep ?? defaultSleep;
|
|
700
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
701
|
+
try {
|
|
702
|
+
return await fn();
|
|
703
|
+
} catch (error) {
|
|
704
|
+
const delayMs = attempt >= attempts ? null : foirRetryDelayMs(error, attempt, policy);
|
|
705
|
+
if (delayMs === null) throw error;
|
|
706
|
+
policy.onRetry?.({ attempt, delayMs, error });
|
|
707
|
+
await sleep(delayMs);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/server/callback-client.ts
|
|
713
|
+
function createCallbackClient(callback, opts) {
|
|
714
|
+
const { executionId } = opts;
|
|
715
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
716
|
+
if (!fetchImpl) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
"createCallbackClient: no fetch available; pass opts.fetch on runtimes without a global fetch"
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
if (!executionId) throw new Error("createCallbackClient: executionId is required");
|
|
722
|
+
if (!callback?.token || !callback?.mutations) {
|
|
723
|
+
throw new Error("createCallbackClient: callback block is missing required fields");
|
|
724
|
+
}
|
|
725
|
+
if (!callback.gqlEndpoint && !opts.apiUrl) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
"createCallbackClient: nowhere to report to. The dispatch named no callback.gqlEndpoint and no apiUrl was passed."
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
const endpoint = callback.gqlEndpoint || foirApiEndpoint(opts.apiUrl ?? "", "/graphql");
|
|
731
|
+
const signingSecret = opts.signingSecret || callback.signingKey;
|
|
732
|
+
if (!signingSecret) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
"createCallbackClient: nothing to sign with. The dispatch carried no callback.signingKey and no signingSecret was passed."
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
const post = async (mutationName, fields, variables) => {
|
|
738
|
+
const query = `mutation ${mutationName}Mutation($id: String!${fieldArgsForMutation(mutationName, variables)}) { ${mutationName}(id: $id${fieldArgsCallSite(mutationName, variables)}) { ${fields} } }`;
|
|
739
|
+
const body = JSON.stringify({
|
|
740
|
+
query,
|
|
741
|
+
variables: { id: executionId, ...variables }
|
|
742
|
+
});
|
|
743
|
+
const signature = await hmacSha256Hex(signingSecret, body);
|
|
744
|
+
return withFoirRetry(async () => {
|
|
745
|
+
let resp;
|
|
746
|
+
try {
|
|
747
|
+
resp = await fetchImpl(endpoint, {
|
|
748
|
+
method: "POST",
|
|
749
|
+
headers: {
|
|
750
|
+
"Content-Type": "application/json",
|
|
751
|
+
Authorization: `Bearer ${callback.token}`,
|
|
752
|
+
"X-Foir-Signature": `sha256=${signature}`
|
|
753
|
+
},
|
|
754
|
+
body,
|
|
755
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
756
|
+
});
|
|
757
|
+
} catch (err) {
|
|
758
|
+
if (opts.signal?.aborted) throw err;
|
|
759
|
+
throw new FoirRequestError(`callback ${mutationName} got no answer`, { cause: err });
|
|
760
|
+
}
|
|
761
|
+
if (!resp.ok) {
|
|
762
|
+
throw new FoirRequestError(
|
|
763
|
+
`callback ${mutationName} failed with HTTP ${resp.status}: ${await resp.text()}`,
|
|
764
|
+
{ status: resp.status }
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const json = await resp.json();
|
|
768
|
+
if (json.errors?.length) {
|
|
769
|
+
throw new FoirRequestError(`callback ${mutationName} GraphQL error: ${json.errors[0].message}`, {
|
|
770
|
+
status: resp.status,
|
|
771
|
+
graphQLErrors: json.errors
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
const out = json.data?.[mutationName];
|
|
775
|
+
if (!out) {
|
|
776
|
+
throw new FoirRequestError(`callback ${mutationName} returned no data`, { status: resp.status });
|
|
777
|
+
}
|
|
778
|
+
return out;
|
|
779
|
+
}, opts.retry);
|
|
780
|
+
};
|
|
781
|
+
return {
|
|
782
|
+
complete: (result) => post(callback.mutations.complete, "status cancelled", { result }),
|
|
783
|
+
fail: (code, message, options = {}) => post(callback.mutations.fail, "status cancelled", {
|
|
784
|
+
code,
|
|
785
|
+
message,
|
|
786
|
+
retryable: options.retryable,
|
|
787
|
+
details: options.details
|
|
788
|
+
}),
|
|
789
|
+
progress: (update) => post(callback.mutations.progress, "status cancelled applied", {
|
|
790
|
+
pct: update.pct,
|
|
791
|
+
message: update.message,
|
|
792
|
+
metadata: update.metadata
|
|
793
|
+
}),
|
|
794
|
+
cancel: () => post(callback.mutations.cancel, "status cancelled", {})
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function fieldArgsForMutation(mutationName, vars) {
|
|
798
|
+
const parts = [];
|
|
799
|
+
for (const key of Object.keys(vars)) {
|
|
800
|
+
if (vars[key] === void 0) continue;
|
|
801
|
+
parts.push(`, $${key}: ${graphQLTypeFor(mutationName, key)}`);
|
|
802
|
+
}
|
|
803
|
+
return parts.join("");
|
|
804
|
+
}
|
|
805
|
+
function fieldArgsCallSite(_mutationName, vars) {
|
|
806
|
+
const parts = [];
|
|
807
|
+
for (const key of Object.keys(vars)) {
|
|
808
|
+
if (vars[key] === void 0) continue;
|
|
809
|
+
parts.push(`, ${key}: $${key}`);
|
|
810
|
+
}
|
|
811
|
+
return parts.join("");
|
|
812
|
+
}
|
|
813
|
+
function graphQLTypeFor(mutationName, key) {
|
|
814
|
+
if (key === "code" || key === "message") return "String!";
|
|
815
|
+
if (key === "pct") return "Int";
|
|
816
|
+
if (key === "retryable") return "Boolean";
|
|
817
|
+
if (key === "details" || key === "metadata") return "JSON";
|
|
818
|
+
if (key === "result") {
|
|
819
|
+
const base = mutationName.replace(/^complete/, "").replace(/Execution$/, "");
|
|
820
|
+
return `${base}ExecutionResultInput!`;
|
|
821
|
+
}
|
|
822
|
+
if (key === "message") return "String";
|
|
823
|
+
return "String";
|
|
824
|
+
}
|
|
825
|
+
async function hmacSha256Hex(secret, body) {
|
|
826
|
+
const enc = new TextEncoder();
|
|
827
|
+
const key = await crypto.subtle.importKey(
|
|
828
|
+
"raw",
|
|
829
|
+
enc.encode(secret),
|
|
830
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
831
|
+
false,
|
|
832
|
+
["sign"]
|
|
833
|
+
);
|
|
834
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
|
|
835
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
836
|
+
}
|
|
459
837
|
|
|
460
838
|
// src/data/transport.ts
|
|
839
|
+
async function resolveVisitorAddress(config) {
|
|
840
|
+
const source = config.visitor;
|
|
841
|
+
const value = typeof source === "function" ? await source() : source;
|
|
842
|
+
const address = value?.trim();
|
|
843
|
+
return address ? address : void 0;
|
|
844
|
+
}
|
|
461
845
|
async function resolveBearerToken(config) {
|
|
462
846
|
const source = config.bearerToken;
|
|
463
847
|
if (typeof source === "function") return await source() ?? void 0;
|
|
464
848
|
return source ?? void 0;
|
|
465
849
|
}
|
|
850
|
+
async function resolveDispatchToken(dispatch) {
|
|
851
|
+
const token = (await dispatch.token())?.trim();
|
|
852
|
+
if (!token) {
|
|
853
|
+
throw new FoirRequestError(
|
|
854
|
+
"[foir] the dispatch token provider returned no token. A client built from a dispatch calls Foir as that operation and holds no other credential. Build it from the token the endpoint was handed: `(await requireScopedToken(request)).dispatch`."
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
return token;
|
|
858
|
+
}
|
|
859
|
+
async function resolveFoirAuth(config) {
|
|
860
|
+
if (config.dispatch !== void 0) {
|
|
861
|
+
return {
|
|
862
|
+
headers: buildFoirHeaders({
|
|
863
|
+
dispatchToken: await resolveDispatchToken(config.dispatch),
|
|
864
|
+
dispatchContext: config.dispatch.context ?? void 0,
|
|
865
|
+
extra: config.headers
|
|
866
|
+
})
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
const bearerToken = await resolveBearerToken(config);
|
|
870
|
+
return {
|
|
871
|
+
headers: buildFoirHeaders({
|
|
872
|
+
apiKey: config.apiKey,
|
|
873
|
+
draftChannel: config.channel === "draft",
|
|
874
|
+
bearerToken,
|
|
875
|
+
orgId: config.orgId,
|
|
876
|
+
orgUrl: config.orgUrl,
|
|
877
|
+
visitorAddress: await resolveVisitorAddress(config),
|
|
878
|
+
extra: config.headers
|
|
879
|
+
}),
|
|
880
|
+
key: { apiKey: config.apiKey, bearerToken }
|
|
881
|
+
};
|
|
882
|
+
}
|
|
466
883
|
|
|
467
884
|
// src/data/ws-transport.ts
|
|
468
885
|
var GQL = {
|
|
@@ -500,6 +917,11 @@ var FoirSocketTransport = class {
|
|
|
500
917
|
keepAlive = null;
|
|
501
918
|
retryTimer = null;
|
|
502
919
|
constructor(config) {
|
|
920
|
+
if (config.dispatch !== void 0) {
|
|
921
|
+
throw new FoirRequestError(
|
|
922
|
+
"[foir] a client built from a dispatch token cannot subscribe. The subscription socket authenticates an api key, an admin token or a customer bearer, and has no path for a dispatch token. Queries and mutations made as the operation go to /graphql and work as they are."
|
|
923
|
+
);
|
|
924
|
+
}
|
|
503
925
|
this.config = config;
|
|
504
926
|
this.policy = { ...DEFAULT_RECONNECT, ...config.reconnect ?? {} };
|
|
505
927
|
}
|
|
@@ -571,13 +993,7 @@ var FoirSocketTransport = class {
|
|
|
571
993
|
async sendConnectionInit(socket) {
|
|
572
994
|
this.connecting = false;
|
|
573
995
|
try {
|
|
574
|
-
const headers =
|
|
575
|
-
apiKey: this.config.apiKey,
|
|
576
|
-
draftChannel: this.config.channel === "draft",
|
|
577
|
-
bearerToken: await resolveBearerToken(this.config),
|
|
578
|
-
orgId: this.config.orgId,
|
|
579
|
-
extra: this.config.headers
|
|
580
|
-
});
|
|
996
|
+
const { headers } = await resolveFoirAuth(this.config);
|
|
581
997
|
const { "Content-Type": _contentType, ...params } = headers;
|
|
582
998
|
const extra = await this.config.connectionParams?.() ?? {};
|
|
583
999
|
this.send(socket, {
|
|
@@ -684,7 +1100,7 @@ var FoirSocketTransport = class {
|
|
|
684
1100
|
return;
|
|
685
1101
|
}
|
|
686
1102
|
case GQL.error: {
|
|
687
|
-
const error = new FoirRequestError(`[foir] subscription refused: ${
|
|
1103
|
+
const error = new FoirRequestError(`[foir] subscription refused: ${describe2(message.payload)}`, {
|
|
688
1104
|
graphQLErrors: graphQLErrorItems(message.payload)
|
|
689
1105
|
});
|
|
690
1106
|
if (!message.id) {
|
|
@@ -805,17 +1221,17 @@ function graphQLErrorItems(payload) {
|
|
|
805
1221
|
const { message, path, extensions } = entry;
|
|
806
1222
|
if (extensions === void 0 && path === void 0) continue;
|
|
807
1223
|
items.push({
|
|
808
|
-
message: typeof message === "string" ? message :
|
|
1224
|
+
message: typeof message === "string" ? message : describe2(entry),
|
|
809
1225
|
...Array.isArray(path) ? { path } : {},
|
|
810
1226
|
...typeof extensions === "object" && extensions !== null ? { extensions } : {}
|
|
811
1227
|
});
|
|
812
1228
|
}
|
|
813
1229
|
return items.length > 0 ? items : void 0;
|
|
814
1230
|
}
|
|
815
|
-
function
|
|
1231
|
+
function describe2(payload) {
|
|
816
1232
|
if (payload == null) return "no reason given";
|
|
817
1233
|
if (typeof payload === "string") return payload;
|
|
818
|
-
if (Array.isArray(payload)) return payload.map(
|
|
1234
|
+
if (Array.isArray(payload)) return payload.map(describe2).join("; ");
|
|
819
1235
|
if (typeof payload === "object") {
|
|
820
1236
|
const message = payload.message;
|
|
821
1237
|
if (typeof message === "string") return message;
|
|
@@ -1084,15 +1500,26 @@ function asEventsSocketError(error, requestedCursor) {
|
|
|
1084
1500
|
return error;
|
|
1085
1501
|
}
|
|
1086
1502
|
export {
|
|
1503
|
+
CONTINUATION_MEDIA_TYPE,
|
|
1504
|
+
FOIR_CONTEXT_HEADER,
|
|
1087
1505
|
FOIR_EVENTS_PATH,
|
|
1088
1506
|
FOIR_EVENTS_RETENTION_DAYS,
|
|
1089
1507
|
FOIR_EVENTS_SCOPE,
|
|
1508
|
+
FOIR_TOKEN_HEADER,
|
|
1509
|
+
FoirAuthDowngradedError,
|
|
1090
1510
|
FoirEventCursorExpiredError,
|
|
1091
1511
|
FoirRequestError,
|
|
1512
|
+
FoirSessionExpiredError,
|
|
1513
|
+
MAX_CONTINUATION_CURSOR_BYTES,
|
|
1514
|
+
MAX_CONTINUATION_DELAY_MS,
|
|
1515
|
+
OperationPayloadError,
|
|
1092
1516
|
ScopedTokenError,
|
|
1517
|
+
ScopedTokenUnavailableError,
|
|
1093
1518
|
ServerClientError,
|
|
1519
|
+
continueRun,
|
|
1094
1520
|
createCallbackClient,
|
|
1095
1521
|
createEventsClient,
|
|
1522
|
+
createScopedTokenVerifier,
|
|
1096
1523
|
createServerClient,
|
|
1097
1524
|
isFoirEventCursorExpired,
|
|
1098
1525
|
isFoirForbidden,
|
|
@@ -1101,6 +1528,7 @@ export {
|
|
|
1101
1528
|
isFoirUnauthenticated,
|
|
1102
1529
|
parseContextHeader,
|
|
1103
1530
|
parseSubject,
|
|
1531
|
+
readDispatch,
|
|
1104
1532
|
recordsReadModels,
|
|
1105
1533
|
recordsWriteModels,
|
|
1106
1534
|
requireScopedToken,
|