@foir/sdk 0.7.0 → 0.9.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 +857 -4
- package/README.md +46 -15
- package/dist/auth/auth.d.ts +104 -1
- package/dist/auth/hosted.d.ts +22 -0
- package/dist/auth/index.d.ts +13 -7
- package/dist/auth/operations.d.ts +85 -64
- package/dist/auth/ops.d.ts +99 -0
- package/dist/auth.js +786 -106
- package/dist/chunk-IGNLWT3H.js +35 -0
- package/dist/chunk-OHYVKTGB.js +45 -0
- package/dist/chunk-TAL5H6VA.js +39 -0
- package/dist/client.js +25 -31
- package/dist/codegen/block-registry.d.ts +13 -19
- package/dist/codegen/block-registry.js +3 -31
- package/dist/codegen/block-scaffold.d.ts +12 -21
- package/dist/codegen/block-scaffold.js +3 -41
- package/dist/codegen/index.d.ts +9 -4
- package/dist/codegen/index.js +7 -11
- package/dist/codegen/item-registry.d.ts +28 -0
- package/dist/codegen/item-registry.js +7 -0
- package/dist/codegen/item-scaffold.d.ts +29 -0
- package/dist/codegen/item-scaffold.js +7 -0
- package/dist/codegen/preset.d.ts +14 -7
- package/dist/codegen/shared.d.ts +20 -9
- package/dist/content/content-renderer.d.ts +90 -24
- package/dist/content/index.d.ts +13 -7
- package/dist/content/richtext.d.ts +66 -18
- package/dist/content.js +227 -80
- package/dist/data/builtins.d.ts +369 -92
- package/dist/data/client.d.ts +97 -25
- package/dist/data/document.d.ts +251 -0
- package/dist/data/index.d.ts +38 -19
- package/dist/data/manifest.d.ts +290 -8
- package/dist/data/schema.d.ts +78 -8
- package/dist/data/selection.d.ts +231 -37
- package/dist/data/subjects.d.ts +221 -0
- package/dist/data/subscription-document.d.ts +75 -0
- package/dist/data/subscriptions.d.ts +181 -0
- package/dist/data/transport.d.ts +255 -37
- package/dist/data/ws-transport.d.ts +197 -0
- package/dist/data.js +1419 -45
- package/dist/graphql/cdn-route.d.ts +12 -28
- package/dist/media/index.d.ts +6 -2
- package/dist/media/media.d.ts +2 -2
- package/dist/media/screen-capture.d.ts +81 -0
- package/dist/media.js +97 -0
- package/dist/next.js +663 -182
- package/dist/push.js +700 -90
- package/dist/react/FoirFeedback.d.ts +62 -0
- package/dist/react/feedback-notes.d.ts +101 -0
- package/dist/react/index.d.ts +7 -1
- package/dist/react.js +1272 -194
- package/dist/realtime.js +1 -1
- package/dist/server/events-client.d.ts +279 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server.js +298 -0
- package/dist/transport/foir-http.d.ts +23 -6
- package/dist/transport/persisted-query.d.ts +68 -0
- package/dist/transport/retry.d.ts +1 -1
- package/package.json +13 -1
- package/dist/chunk-ADO5FJNM.js +0 -30
package/dist/auth.js
CHANGED
|
@@ -100,6 +100,13 @@ function buildFoirHeaders(input) {
|
|
|
100
100
|
}
|
|
101
101
|
return headers;
|
|
102
102
|
}
|
|
103
|
+
function foirApiEndpoint(apiUrl, path) {
|
|
104
|
+
const base = apiUrl.trim().replace(/\/+$/, "").replace(/\/graphql$/i, "");
|
|
105
|
+
if (!base) {
|
|
106
|
+
throw new FoirRequestError("[foir] apiUrl is empty \u2014 pass the Foir API origin");
|
|
107
|
+
}
|
|
108
|
+
return base + path;
|
|
109
|
+
}
|
|
103
110
|
function readFoirEnvelope(json) {
|
|
104
111
|
if (json.errors?.length) {
|
|
105
112
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
@@ -117,18 +124,14 @@ function readFoirEnvelope(json) {
|
|
|
117
124
|
return json.data;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
// src/
|
|
121
|
-
var
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
if (isFoirPreview(env)) return false;
|
|
125
|
-
const apiKey = resolveFoirApiKey(env);
|
|
126
|
-
if (!apiKey?.startsWith("pk_")) return false;
|
|
127
|
-
return !/\bmutation\b/.test(documentText);
|
|
127
|
+
// src/transport/persisted-query.ts
|
|
128
|
+
var FOIR_CDN_GRAPHQL_PATH = "/cdn/graphql";
|
|
129
|
+
function persistedQueryExtension(hash) {
|
|
130
|
+
return { persistedQuery: { version: 1, sha256Hash: hash } };
|
|
128
131
|
}
|
|
129
132
|
function buildCdnUrl(endpoint, apiKey, hash, variables) {
|
|
130
133
|
const url = new URL(endpoint);
|
|
131
|
-
url.pathname = url.pathname.replace(/\/graphql\/?$/, "") +
|
|
134
|
+
url.pathname = url.pathname.replace(/\/graphql\/?$/, "") + FOIR_CDN_GRAPHQL_PATH;
|
|
132
135
|
url.searchParams.set("key", apiKey);
|
|
133
136
|
url.searchParams.set("extensions", JSON.stringify(persistedQueryExtension(hash)));
|
|
134
137
|
if (variables && Object.keys(variables).length > 0) {
|
|
@@ -136,18 +139,29 @@ function buildCdnUrl(endpoint, apiKey, hash, variables) {
|
|
|
136
139
|
}
|
|
137
140
|
return url.toString();
|
|
138
141
|
}
|
|
139
|
-
function persistedQueryExtension(hash) {
|
|
140
|
-
return { persistedQuery: { version: 1, sha256Hash: hash } };
|
|
141
|
-
}
|
|
142
142
|
async function documentHash(documentText) {
|
|
143
|
-
const digest = await crypto.subtle.digest(
|
|
144
|
-
"SHA-256",
|
|
145
|
-
new TextEncoder().encode(documentText)
|
|
146
|
-
);
|
|
143
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(documentText));
|
|
147
144
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
148
145
|
}
|
|
149
146
|
var PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
|
|
150
147
|
var NOT_SHAREABLE = "NOT_SHAREABLE";
|
|
148
|
+
async function hasFoirErrorCode(res, ...codes) {
|
|
149
|
+
try {
|
|
150
|
+
const body = await res.clone().json();
|
|
151
|
+
return body.errors?.some((e) => e.extensions?.code !== void 0 && codes.includes(e.extensions.code)) ?? false;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/graphql/cdn-route.ts
|
|
158
|
+
function isCdnEligible(env, documentText, bearerToken) {
|
|
159
|
+
if (bearerToken) return false;
|
|
160
|
+
if (isFoirPreview(env)) return false;
|
|
161
|
+
const apiKey = resolveFoirApiKey(env);
|
|
162
|
+
if (!apiKey?.startsWith("pk_")) return false;
|
|
163
|
+
return !/\bmutation\b/.test(documentText);
|
|
164
|
+
}
|
|
151
165
|
|
|
152
166
|
// src/graphql/foir-request.ts
|
|
153
167
|
async function foirRequest(env, document, variables, options) {
|
|
@@ -242,83 +256,679 @@ async function readErrorEnvelope(res) {
|
|
|
242
256
|
return errors;
|
|
243
257
|
}
|
|
244
258
|
async function isSharedRefusal(res) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
259
|
+
return hasFoirErrorCode(res, NOT_SHAREABLE);
|
|
260
|
+
}
|
|
261
|
+
async function isPersistedQueryMiss(res) {
|
|
262
|
+
return hasFoirErrorCode(res, PERSISTED_QUERY_NOT_FOUND);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/data/document.ts
|
|
266
|
+
function buildPathTrie(paths) {
|
|
267
|
+
const root = { self: false, children: /* @__PURE__ */ new Map() };
|
|
268
|
+
for (const path of paths) {
|
|
269
|
+
let node = root;
|
|
270
|
+
for (const segment of path.split(".")) {
|
|
271
|
+
if (!segment) continue;
|
|
272
|
+
let next = node.children.get(segment);
|
|
273
|
+
if (!next) {
|
|
274
|
+
next = { self: false, children: /* @__PURE__ */ new Map() };
|
|
275
|
+
node.children.set(segment, next);
|
|
276
|
+
}
|
|
277
|
+
node = next;
|
|
278
|
+
}
|
|
279
|
+
node.self = true;
|
|
280
|
+
}
|
|
281
|
+
return root;
|
|
282
|
+
}
|
|
283
|
+
function childFor(node, field) {
|
|
284
|
+
return node.children.get(field.key) ?? (field.alias ? node.children.get(field.alias) : void 0);
|
|
285
|
+
}
|
|
286
|
+
function segmentFor(node, field) {
|
|
287
|
+
return node.children.has(field.key) ? field.key : field.alias ?? field.key;
|
|
288
|
+
}
|
|
289
|
+
var PROTECTED_ROOT_FIELD = "_id";
|
|
290
|
+
var MAX_QUERY_COMPLEXITY = 1e4;
|
|
291
|
+
var MAX_EMITTED_FIELDS = 4 * MAX_QUERY_COMPLEXITY;
|
|
292
|
+
var newBudget = () => ({ fields: 0, emitted: 0, stopped: false });
|
|
293
|
+
function charge(ctx, n = 1) {
|
|
294
|
+
ctx.budget.fields += n;
|
|
295
|
+
ctx.budget.emitted += n;
|
|
296
|
+
}
|
|
297
|
+
function mayExpand(ctx, depth) {
|
|
298
|
+
if (depth <= 1) return false;
|
|
299
|
+
if (ctx.budget.emitted > MAX_EMITTED_FIELDS) {
|
|
300
|
+
ctx.budget.stopped = true;
|
|
250
301
|
return false;
|
|
251
302
|
}
|
|
303
|
+
return true;
|
|
252
304
|
}
|
|
253
|
-
|
|
305
|
+
function printFields(fields, ctx, omit, pick, isRoot, path, depth) {
|
|
306
|
+
const parts = [];
|
|
307
|
+
for (const field of fields) {
|
|
308
|
+
if (field.relation) {
|
|
309
|
+
const named = pick ? childFor(pick, field) : void 0;
|
|
310
|
+
if (named) {
|
|
311
|
+
const segment = segmentFor(pick, field);
|
|
312
|
+
throw new FoirRequestError(
|
|
313
|
+
`[foir] pick "${path}${segment}": "${segment}" is a relation, and relations are not part of the default document \u2014 no pick can select one. ` + (path ? (
|
|
314
|
+
// Nested: there is no `include` form that reaches here at all.
|
|
315
|
+
// `include` addresses the ROOT model's own relations, and (nested)
|
|
316
|
+
// a related model's — never one reached through a composite or a
|
|
317
|
+
// union member. Saying "use include" would send the caller to a
|
|
318
|
+
// call that refuses them a second time.
|
|
319
|
+
`A relation at this position cannot be reached by \`include\` either \u2014 that addresses the model's own relations, not one inside a composite or a union member. Read it through the field that owns it, or drop "${path}${segment}" from the pick`
|
|
320
|
+
) : `Use include: { ${segment}: true } instead`)
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const protectedField = isRoot && field.key === PROTECTED_ROOT_FIELD;
|
|
326
|
+
const omitChild = omit ? childFor(omit, field) : void 0;
|
|
327
|
+
if (omitChild?.self && !protectedField) continue;
|
|
328
|
+
let pickChild;
|
|
329
|
+
if (pick) {
|
|
330
|
+
pickChild = childFor(pick, field);
|
|
331
|
+
if (!pickChild && !protectedField) continue;
|
|
332
|
+
if (pickChild?.self) pickChild = void 0;
|
|
333
|
+
}
|
|
334
|
+
const head = field.alias ? `${field.alias}: ${field.key}` : field.key;
|
|
335
|
+
const childPath = `${path}${pick && childFor(pick, field) ? segmentFor(pick, field) : field.key}.`;
|
|
336
|
+
if (field.truncated) {
|
|
337
|
+
if (field.union && mayExpand(ctx, depth)) {
|
|
338
|
+
const body = printUnion(field.union, ctx, omitChild, pickChild, childPath, depth - 1);
|
|
339
|
+
if (body) {
|
|
340
|
+
charge(ctx);
|
|
341
|
+
parts.push(`${head} ${body}`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
charge(ctx, 2);
|
|
346
|
+
parts.push(`${head} { __typename }`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (field.model) {
|
|
350
|
+
const body = printModelTree(field.model, ctx, depth);
|
|
351
|
+
if (body) {
|
|
352
|
+
charge(ctx);
|
|
353
|
+
parts.push(`${head} { ${body} }`);
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (field.union) {
|
|
358
|
+
const body = printUnion(field.union, ctx, omitChild, pickChild, childPath, depth);
|
|
359
|
+
if (body) {
|
|
360
|
+
charge(ctx);
|
|
361
|
+
parts.push(`${head} ${body}`);
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (field.fields?.length) {
|
|
366
|
+
const body = printFields(field.fields, ctx, omitChild, pickChild, false, childPath, depth);
|
|
367
|
+
if (body) {
|
|
368
|
+
charge(ctx);
|
|
369
|
+
parts.push(`${head} { ${body} }`);
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
charge(ctx);
|
|
374
|
+
parts.push(head);
|
|
375
|
+
}
|
|
376
|
+
return parts.join(" ");
|
|
377
|
+
}
|
|
378
|
+
function printModelTree(modelKey, ctx, depth) {
|
|
379
|
+
const model = ctx.source.models[modelKey];
|
|
380
|
+
if (!model?.selection) {
|
|
381
|
+
throw new FoirRequestError(
|
|
382
|
+
`[foir] the result shape references the model "${modelKey}", which publishes no selection \u2014 re-run \`foir types\``
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const narrowing = ctx.record?.modelKey === modelKey ? ctx.record : void 0;
|
|
386
|
+
const body = printFields(
|
|
387
|
+
model.selection.fields,
|
|
388
|
+
ctx,
|
|
389
|
+
narrowing?.omit,
|
|
390
|
+
narrowing?.pick,
|
|
391
|
+
true,
|
|
392
|
+
"",
|
|
393
|
+
depth
|
|
394
|
+
);
|
|
395
|
+
const includes = narrowing?.include ? printIncludes(ctx.source, model, modelKey, narrowing.include, ctx, depth) : "";
|
|
396
|
+
if (!body) {
|
|
397
|
+
return includes;
|
|
398
|
+
}
|
|
399
|
+
return includes ? `${body} ${includes}` : body;
|
|
400
|
+
}
|
|
401
|
+
var NO_FIELDS = { self: false, children: /* @__PURE__ */ new Map() };
|
|
402
|
+
function addressesMembers(node, union) {
|
|
403
|
+
for (const member of union.members) {
|
|
404
|
+
if (node.children.has(member.typeName)) return true;
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
function printUnion(unionName, ctx, omit, pick, path, depth) {
|
|
409
|
+
const union = ctx.unions[unionName];
|
|
410
|
+
if (!union) {
|
|
411
|
+
throw new FoirRequestError(
|
|
412
|
+
`[foir] the selection tree names the union "${unionName}", which is not in selectionUnions \u2014 re-run \`foir types\`, or pass selectionUnions alongside selections`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const perVariantPick = pick !== void 0 && addressesMembers(pick, union);
|
|
416
|
+
const perVariantOmit = omit !== void 0 && addressesMembers(omit, union);
|
|
417
|
+
charge(ctx);
|
|
418
|
+
const parts = ["__typename"];
|
|
419
|
+
const chargeBefore = ctx.budget.fields;
|
|
420
|
+
let widestMember = 0;
|
|
421
|
+
for (const member of union.members) {
|
|
422
|
+
let memberPick = pick;
|
|
423
|
+
if (perVariantPick) {
|
|
424
|
+
const named = pick.children.get(member.typeName);
|
|
425
|
+
memberPick = named === void 0 ? NO_FIELDS : named.self ? void 0 : named;
|
|
426
|
+
}
|
|
427
|
+
let memberOmit = omit;
|
|
428
|
+
if (perVariantOmit) {
|
|
429
|
+
const named = omit.children.get(member.typeName);
|
|
430
|
+
if (named?.self) continue;
|
|
431
|
+
memberOmit = named;
|
|
432
|
+
}
|
|
433
|
+
const memberPath = perVariantPick ? `${path}${member.typeName}.` : path;
|
|
434
|
+
const memberStart = ctx.budget.fields;
|
|
435
|
+
const body = printFields(member.fields, ctx, memberOmit, memberPick, false, memberPath, depth);
|
|
436
|
+
const memberCost = ctx.budget.fields - memberStart;
|
|
437
|
+
ctx.budget.fields = memberStart;
|
|
438
|
+
if (memberCost > widestMember) widestMember = memberCost;
|
|
439
|
+
if (body) parts.push(`... on ${member.typeName} { ${body} }`);
|
|
440
|
+
}
|
|
441
|
+
ctx.budget.fields = chargeBefore + widestMember;
|
|
442
|
+
return `{ ${parts.join(" ")} }`;
|
|
443
|
+
}
|
|
444
|
+
function printIncludes(source, model, modelKey, include, ctx, depth) {
|
|
445
|
+
const parts = [];
|
|
446
|
+
for (const [key, value] of Object.entries(include)) {
|
|
447
|
+
if (value === void 0 || value === false) continue;
|
|
448
|
+
const relation = model.relations?.find((r) => r.key === key);
|
|
449
|
+
if (!relation) {
|
|
450
|
+
throw new FoirRequestError(
|
|
451
|
+
`[foir] include "${key}": ${modelKey} has no such relation \u2014 re-run \`foir types\` if the model has changed`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const nested = value && typeof value === "object" && "include" in value ? value["include"] : void 0;
|
|
455
|
+
const body = relation.targets?.length ? printPolymorphicRelation(source, relation, ctx, nested, depth) : printRelationTarget(source, relation.target ?? "", ctx, nested, relation.key, depth);
|
|
456
|
+
charge(ctx, relation.connection ? 1 + CONNECTION_WRAPPER_FIELDS : 1);
|
|
457
|
+
parts.push(`${key} ${relation.connection ? wrapRelationConnection(body) : `{ ${body} }`}`);
|
|
458
|
+
}
|
|
459
|
+
return parts.join(" ");
|
|
460
|
+
}
|
|
461
|
+
var CONNECTION_WRAPPER_FIELDS = 9;
|
|
462
|
+
function wrapRelationConnection(body) {
|
|
463
|
+
return `{ edges { cursor node { ${body} } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount }`;
|
|
464
|
+
}
|
|
465
|
+
function printRelationTarget(source, targetKey, ctx, nested, relationKey, depth) {
|
|
466
|
+
const target = source.models[targetKey];
|
|
467
|
+
if (!target?.selection) {
|
|
468
|
+
throw new FoirRequestError(
|
|
469
|
+
`[foir] include "${relationKey}": no selection published for its target model "${targetKey}" \u2014 re-run \`foir types\``
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
const bare = { source, unions: ctx.unions, budget: ctx.budget };
|
|
473
|
+
const body = printFields(target.selection.fields, bare, void 0, void 0, true, "", depth);
|
|
474
|
+
const includes = nested ? printIncludes(source, target, targetKey, nested, bare, depth) : "";
|
|
475
|
+
return includes ? `${body} ${includes}` : body;
|
|
476
|
+
}
|
|
477
|
+
function printPolymorphicRelation(source, relation, ctx, nested, depth) {
|
|
478
|
+
charge(ctx);
|
|
479
|
+
const parts = ["__typename"];
|
|
480
|
+
const chargeBefore = ctx.budget.fields;
|
|
481
|
+
let widestTarget = 0;
|
|
482
|
+
for (const targetKey of relation.targets ?? []) {
|
|
483
|
+
const target = source.models[targetKey];
|
|
484
|
+
if (!target?.selection) {
|
|
485
|
+
throw new FoirRequestError(
|
|
486
|
+
`[foir] include "${relation.key}": no selection published for its target model "${targetKey}" \u2014 re-run \`foir types\``
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const targetStart = ctx.budget.fields;
|
|
490
|
+
const body = printRelationTarget(source, targetKey, ctx, nested, relation.key, depth);
|
|
491
|
+
const targetCost = ctx.budget.fields - targetStart;
|
|
492
|
+
ctx.budget.fields = targetStart;
|
|
493
|
+
if (targetCost > widestTarget) widestTarget = targetCost;
|
|
494
|
+
if (body) parts.push(`... on ${target.typeName} { ${body} }`);
|
|
495
|
+
}
|
|
496
|
+
ctx.budget.fields = chargeBefore + widestTarget;
|
|
497
|
+
return parts.join(" ");
|
|
498
|
+
}
|
|
499
|
+
var GRAPHQL_TYPE_REF = /^\[?\[?[_A-Za-z][_0-9A-Za-z]*!?\]?!?\]?!?$/;
|
|
500
|
+
function buildArguments(args, op, opLabel) {
|
|
501
|
+
const declared = op.args;
|
|
502
|
+
const definitions = [];
|
|
503
|
+
const call = [];
|
|
504
|
+
const variables = {};
|
|
505
|
+
for (const name of Object.keys(args).sort()) {
|
|
506
|
+
const value = args[name];
|
|
507
|
+
if (value === void 0) continue;
|
|
508
|
+
if (!declared?.includes(name)) {
|
|
509
|
+
throw new FoirRequestError(
|
|
510
|
+
`[foir] ${opLabel} does not take an argument "${name}" \u2014 it takes ${declared?.length ? declared.join(", ") : "no arguments"}`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
const type = op.argTypes?.[name];
|
|
514
|
+
if (!type) {
|
|
515
|
+
throw new FoirRequestError(
|
|
516
|
+
`[foir] ${opLabel}: the manifest publishes no type for the argument "${name}", so it cannot be sent as a variable \u2014 and inferring one from the value would guess wrong on every enum. Re-run \`foir types\` against a server that publishes argTypes, or use foir.raw() with your own variable definitions`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (!GRAPHQL_TYPE_REF.test(type)) {
|
|
520
|
+
throw new FoirRequestError(
|
|
521
|
+
`[foir] ${opLabel}: the manifest gives the argument "${name}" the type "${type}", which is not a GraphQL type reference`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
definitions.push(`$${name}: ${type}`);
|
|
525
|
+
call.push(`${name}: $${name}`);
|
|
526
|
+
variables[name] = toVariableValue(value, `${opLabel} argument "${name}"`);
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
definitions: definitions.length ? `(${definitions.join(", ")})` : "",
|
|
530
|
+
call: call.length ? `(${call.join(", ")})` : "",
|
|
531
|
+
variables
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function toVariableValue(value, label) {
|
|
535
|
+
if (value === null) return null;
|
|
536
|
+
const kind = typeof value;
|
|
537
|
+
if (kind === "string" || kind === "boolean") return value;
|
|
538
|
+
if (kind === "number") {
|
|
539
|
+
if (!Number.isFinite(value)) {
|
|
540
|
+
throw new FoirRequestError(
|
|
541
|
+
`[foir] ${label} is ${String(value)}, which has no JSON form and cannot be sent`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return value;
|
|
545
|
+
}
|
|
546
|
+
if (kind === "bigint") {
|
|
547
|
+
const asNumber = Number(value);
|
|
548
|
+
if (!Number.isSafeInteger(asNumber)) {
|
|
549
|
+
throw new FoirRequestError(
|
|
550
|
+
`[foir] ${label} is the bigint ${String(value)}, which is outside the range JSON can carry exactly`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return asNumber;
|
|
554
|
+
}
|
|
555
|
+
if (value instanceof Date) {
|
|
556
|
+
if (Number.isNaN(value.getTime())) {
|
|
557
|
+
throw new FoirRequestError(`[foir] ${label} is an invalid Date`);
|
|
558
|
+
}
|
|
559
|
+
return value.toISOString();
|
|
560
|
+
}
|
|
561
|
+
if (Array.isArray(value)) {
|
|
562
|
+
return value.filter((item) => item !== void 0).map((item, index) => toVariableValue(item, `${label}[${index}]`));
|
|
563
|
+
}
|
|
564
|
+
if (kind === "object") {
|
|
565
|
+
const out = {};
|
|
566
|
+
for (const [key, child] of Object.entries(value)) {
|
|
567
|
+
if (child === void 0) continue;
|
|
568
|
+
out[key] = toVariableValue(child, `${label}.${key}`);
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
throw new FoirRequestError(
|
|
573
|
+
`[foir] ${label} is a ${kind}, which has no JSON form and cannot be sent`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
function payloadRecordPaths(payload) {
|
|
577
|
+
if (payload.kind === "record") {
|
|
578
|
+
return payload.model ? [{ path: [], modelKey: payload.model }] : [];
|
|
579
|
+
}
|
|
580
|
+
const found = [];
|
|
581
|
+
const visit = (fields, prefix) => {
|
|
582
|
+
for (const field of fields ?? []) {
|
|
583
|
+
const at = [...prefix, field.alias ?? field.key];
|
|
584
|
+
if (field.model) found.push({ path: at, modelKey: field.model });
|
|
585
|
+
visit(field.fields, at);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
visit(payload.fields, []);
|
|
589
|
+
return found;
|
|
590
|
+
}
|
|
591
|
+
function printPayload(payload, ctx, root, opLabel, depth) {
|
|
592
|
+
switch (payload.kind) {
|
|
593
|
+
case "leaf":
|
|
594
|
+
return void 0;
|
|
595
|
+
case "record": {
|
|
596
|
+
if (!payload.model) {
|
|
597
|
+
throw new FoirRequestError(
|
|
598
|
+
`[foir] ${opLabel}: the result shape says it is a record but names no model \u2014 re-run \`foir types\``
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
return printModelTree(payload.model, ctx, depth);
|
|
602
|
+
}
|
|
603
|
+
case "union": {
|
|
604
|
+
if (!payload.union) {
|
|
605
|
+
throw new FoirRequestError(
|
|
606
|
+
`[foir] ${opLabel}: the result shape says it is a union but names none \u2014 re-run \`foir types\``
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return printUnion(payload.union, ctx, root.omit, root.pick, "", depth).slice(2, -2);
|
|
610
|
+
}
|
|
611
|
+
case "object": {
|
|
612
|
+
if (!payload.fields?.length) {
|
|
613
|
+
throw new FoirRequestError(
|
|
614
|
+
`[foir] ${opLabel}: the result shape is an object with no fields \u2014 re-run \`foir types\``
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return printFields(payload.fields, ctx, root.omit, root.pick, false, "", depth);
|
|
618
|
+
}
|
|
619
|
+
default:
|
|
620
|
+
throw new FoirRequestError(
|
|
621
|
+
`[foir] ${opLabel}: the result shape has the kind "${String(payload.kind)}", which this client does not know how to print \u2014 upgrade @foir/sdk`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
var DEFAULT_DEPTH = 1;
|
|
626
|
+
function planFoirNamespaceCall(source, call) {
|
|
627
|
+
const namespace = source.namespaces?.[call.namespace];
|
|
628
|
+
if (!namespace) {
|
|
629
|
+
throw new FoirRequestError(
|
|
630
|
+
`[foir] no namespace "${call.namespace}" in this credential's surface \u2014 re-run \`foir types\``
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
const opDef = namespace.ops?.[call.op];
|
|
634
|
+
if (!opDef) {
|
|
635
|
+
throw new FoirRequestError(
|
|
636
|
+
`[foir] the namespace "${call.namespace}" has no op "${call.op}" for this credential \u2014 re-run \`foir types\``
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return planCall(
|
|
640
|
+
source,
|
|
641
|
+
opDef,
|
|
642
|
+
`${call.namespace}.${call.op}`,
|
|
643
|
+
operationName(call.namespace, call.op),
|
|
644
|
+
call
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
function planCall(source, opDef, opLabel, name, call) {
|
|
648
|
+
const payload = opDef.payload;
|
|
649
|
+
if (!payload) {
|
|
650
|
+
throw new FoirRequestError(
|
|
651
|
+
`[foir] ${opLabel}: the manifest publishes no result shape for this op, so there is nothing to print \u2014 and synthesising one client-side is exactly the guess this client refuses to make. Re-run \`foir types\` against a server that publishes op payloads`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
if (payload.error) {
|
|
655
|
+
throw new FoirRequestError(
|
|
656
|
+
`[foir] ${opLabel}: the server could not resolve a result shape for this op: ${payload.error}`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (call.omit && call.pick) {
|
|
660
|
+
throw new FoirRequestError(
|
|
661
|
+
`[foir] ${opLabel}: "omit" and "pick" are mutually exclusive \u2014 pass one or the other`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (call.omit?.includes(PROTECTED_ROOT_FIELD)) {
|
|
665
|
+
throw new FoirRequestError(
|
|
666
|
+
`[foir] omit "${PROTECTED_ROOT_FIELD}": "${PROTECTED_ROOT_FIELD}" is required for correctness and cannot be removed`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const depth = call.depth ?? DEFAULT_DEPTH;
|
|
670
|
+
if (!Number.isInteger(depth) || depth < DEFAULT_DEPTH) {
|
|
671
|
+
throw new FoirRequestError(
|
|
672
|
+
`[foir] ${opLabel}: depth must be a whole number of union levels, ${DEFAULT_DEPTH} or more \u2014 got ${String(call.depth)}. ${DEFAULT_DEPTH} is the complete-by-default document; each level above it expands one further union`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const omit = call.omit?.length ? buildPathTrie(call.omit) : void 0;
|
|
676
|
+
const pick = call.pick?.length ? buildPathTrie(call.pick) : void 0;
|
|
677
|
+
const include = call.include && Object.keys(call.include).length ? call.include : void 0;
|
|
678
|
+
const narrowed = Boolean(omit || pick || include);
|
|
679
|
+
const recordPaths = payloadRecordPaths(payload);
|
|
680
|
+
const records = new Set(recordPaths.map((entry) => entry.modelKey));
|
|
681
|
+
if (records.size > 1 && narrowed) {
|
|
682
|
+
throw new FoirRequestError(
|
|
683
|
+
`[foir] ${opLabel}: the result carries records of more than one model (${[...records].sort().join(", ")}), so "include"/"pick"/"omit" do not say which one they mean \u2014 call it without them and narrow with foir.raw()`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const recordModel = records.size === 1 ? [...records][0] : void 0;
|
|
687
|
+
if (include && !recordModel) {
|
|
688
|
+
throw new FoirRequestError(
|
|
689
|
+
`[foir] ${opLabel}: "include" names relations of a record, and this op returns none`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const ctx = {
|
|
693
|
+
source,
|
|
694
|
+
unions: source.selectionUnions ?? {},
|
|
695
|
+
record: recordModel ? { modelKey: recordModel, omit, pick, include } : void 0,
|
|
696
|
+
budget: newBudget()
|
|
697
|
+
};
|
|
698
|
+
const root = recordModel ? {} : { omit, pick };
|
|
699
|
+
const selection = printPayload(payload, ctx, root, opLabel, depth);
|
|
700
|
+
if (selection === "") {
|
|
701
|
+
throw new FoirRequestError(
|
|
702
|
+
`[foir] ${opLabel}: the selection is empty after applying pick/omit \u2014 a GraphQL selection set cannot be empty`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
charge(ctx);
|
|
706
|
+
if (depth > DEFAULT_DEPTH && (ctx.budget.stopped || ctx.budget.fields > MAX_QUERY_COMPLEXITY)) {
|
|
707
|
+
const reason = ctx.budget.stopped ? `expands to more than ${MAX_EMITTED_FIELDS} fields to print, which is past what this client will build in one call` : `makes this document select ${ctx.budget.fields} fields, over the server's limit of ${MAX_QUERY_COMPLEXITY}`;
|
|
708
|
+
throw new FoirRequestError(
|
|
709
|
+
`[foir] ${opLabel}: depth ${depth} ${reason}. Each level re-expands every stopping point inside the expansion, so spend the level on less: narrow with "pick"/"omit" \u2014 including per-member, pick: { <field>: { on: { <MemberType>: [...] } } } \u2014 or ask for depth ${depth - 1}`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const args = buildArguments(call.args, opDef, opLabel);
|
|
713
|
+
const keyword = opDef.mutation ? "mutation" : "query";
|
|
714
|
+
const body = selection === void 0 ? "" : ` { ${selection} }`;
|
|
715
|
+
return {
|
|
716
|
+
document: `${keyword} ${name}${args.definitions} { ${opDef.field}${args.call}${body} }`,
|
|
717
|
+
variables: args.variables,
|
|
718
|
+
mutation: opDef.mutation === true,
|
|
719
|
+
rootField: opDef.field,
|
|
720
|
+
fields: ctx.budget.fields,
|
|
721
|
+
records: recordPaths
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function operationName(scope, op) {
|
|
725
|
+
const parts = `${scope} ${op}`.split(/[^0-9A-Za-z]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1));
|
|
726
|
+
const name = parts.join("");
|
|
727
|
+
return /^[_A-Za-z]/.test(name) ? name : `Foir${name}`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/data/transport.ts
|
|
731
|
+
function foirDataEndpoint(apiUrl, path) {
|
|
732
|
+
return foirApiEndpoint(apiUrl, path);
|
|
733
|
+
}
|
|
734
|
+
async function resolveBearerToken(config) {
|
|
735
|
+
const source = config.bearerToken;
|
|
736
|
+
if (typeof source === "function") return await source() ?? void 0;
|
|
737
|
+
return source ?? void 0;
|
|
738
|
+
}
|
|
739
|
+
function foirRoute(input) {
|
|
740
|
+
if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
|
|
741
|
+
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
742
|
+
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
743
|
+
if (input.channel === "draft") return { lane: "origin", reason: "channel" };
|
|
744
|
+
return { lane: "shared" };
|
|
745
|
+
}
|
|
746
|
+
async function executeFoirDocument(config, plan) {
|
|
747
|
+
const data = await runFoirDocument(config, plan.document, plan.mutation, plan.variables);
|
|
748
|
+
if (!(plan.rootField in data)) {
|
|
749
|
+
throw new FoirRequestError(
|
|
750
|
+
`[foir] the response has no "${plan.rootField}" field` + (Object.keys(data).length ? ` (got ${Object.keys(data).join(", ")})` : "")
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
return data[plan.rootField];
|
|
754
|
+
}
|
|
755
|
+
async function runFoirDocument(config, document, mutation, variables) {
|
|
756
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
757
|
+
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
758
|
+
const bearerToken = await resolveBearerToken(config);
|
|
759
|
+
const headers = buildFoirHeaders({
|
|
760
|
+
apiKey: config.apiKey,
|
|
761
|
+
draftChannel: config.channel === "draft",
|
|
762
|
+
bearerToken,
|
|
763
|
+
extra: config.headers
|
|
764
|
+
});
|
|
765
|
+
const hash = await documentHash(document);
|
|
766
|
+
const route = foirRoute({
|
|
767
|
+
apiKey: config.apiKey,
|
|
768
|
+
bearerToken,
|
|
769
|
+
mutation,
|
|
770
|
+
channel: config.channel
|
|
771
|
+
});
|
|
772
|
+
const post = (body) => doFetch(endpoint, {
|
|
773
|
+
method: "POST",
|
|
774
|
+
headers,
|
|
775
|
+
body: JSON.stringify(body),
|
|
776
|
+
...config.requestInit
|
|
777
|
+
});
|
|
778
|
+
const extensions = persistedQueryExtension(hash);
|
|
779
|
+
let res;
|
|
254
780
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
781
|
+
if (route.lane === "shared") {
|
|
782
|
+
res = await doFetch(buildCdnUrl(endpoint, config.apiKey, hash, variables), {
|
|
783
|
+
method: "GET",
|
|
784
|
+
headers,
|
|
785
|
+
...config.requestInit
|
|
786
|
+
});
|
|
787
|
+
if (await hasFoirErrorCode(res, PERSISTED_QUERY_NOT_FOUND, NOT_SHAREABLE)) {
|
|
788
|
+
res = await post({ query: document, variables, extensions });
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
res = await post({ variables, extensions });
|
|
792
|
+
if (await hasFoirErrorCode(res, PERSISTED_QUERY_NOT_FOUND)) {
|
|
793
|
+
res = await post({ query: document, variables, extensions });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
} catch (err) {
|
|
797
|
+
throw new FoirRequestError("[foir] network error", { cause: err });
|
|
798
|
+
}
|
|
799
|
+
if (!res.ok) {
|
|
800
|
+
const errors = await readErrorBody(res);
|
|
801
|
+
const detail = errors ? `: ${errors.map((e) => e.message).join("; ")}` : "";
|
|
802
|
+
throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}${detail}`, {
|
|
803
|
+
status: res.status,
|
|
804
|
+
graphQLErrors: errors
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
const json = await res.json();
|
|
808
|
+
return readFoirEnvelope(json);
|
|
809
|
+
}
|
|
810
|
+
async function readErrorBody(res) {
|
|
811
|
+
try {
|
|
812
|
+
const json = await res.json();
|
|
813
|
+
return json?.errors?.length ? json.errors : void 0;
|
|
259
814
|
} catch {
|
|
260
|
-
return
|
|
815
|
+
return void 0;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/data/manifest.ts
|
|
820
|
+
async function fetchFoirManifest(config) {
|
|
821
|
+
const url = foirDataEndpoint(config.apiUrl, "/v1/manifest");
|
|
822
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
823
|
+
let res;
|
|
824
|
+
try {
|
|
825
|
+
res = await doFetch(url, {
|
|
826
|
+
method: "GET",
|
|
827
|
+
headers: buildFoirHeaders({
|
|
828
|
+
apiKey: config.apiKey,
|
|
829
|
+
draftChannel: config.channel === "draft",
|
|
830
|
+
bearerToken: await resolveBearerToken(config),
|
|
831
|
+
extra: config.headers
|
|
832
|
+
}),
|
|
833
|
+
...config.requestInit
|
|
834
|
+
});
|
|
835
|
+
} catch (err) {
|
|
836
|
+
throw new FoirRequestError("[foir] network error", { cause: err });
|
|
837
|
+
}
|
|
838
|
+
if (!res.ok) {
|
|
839
|
+
throw new FoirRequestError(
|
|
840
|
+
`[foir] GET /v1/manifest failed: HTTP ${res.status} ${res.statusText}`,
|
|
841
|
+
{ status: res.status }
|
|
842
|
+
);
|
|
261
843
|
}
|
|
844
|
+
return await res.json();
|
|
262
845
|
}
|
|
263
846
|
|
|
264
|
-
// src/auth/
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
);
|
|
847
|
+
// src/auth/ops.ts
|
|
848
|
+
var AUTH_OPS = {
|
|
849
|
+
// Pre-credential reads a login screen renders itself from.
|
|
850
|
+
authConfig: "authConfig",
|
|
851
|
+
authProviders: "authProviders",
|
|
852
|
+
defaultAuthProvider: "defaultAuthProvider",
|
|
853
|
+
customerSignupContext: "customerSignupContext",
|
|
854
|
+
// Post-credential self reads.
|
|
855
|
+
currentUser: "currentUser",
|
|
856
|
+
myProviderToken: "myProviderToken",
|
|
857
|
+
// Methods.
|
|
858
|
+
customerLogin: "customerLogin",
|
|
859
|
+
customerRegister: "customerRegister",
|
|
860
|
+
customerRequestOTP: "customerRequestOTP",
|
|
861
|
+
customerLoginOTP: "customerLoginOTP",
|
|
862
|
+
// Session lifecycle.
|
|
863
|
+
customerRefreshToken: "customerRefreshToken",
|
|
864
|
+
customerLogout: "customerLogout",
|
|
865
|
+
customerJoinWaitlist: "customerJoinWaitlist",
|
|
866
|
+
// Password management and account lifecycle — the block that had no path.
|
|
867
|
+
customerRequestPasswordReset: "customerRequestPasswordReset",
|
|
868
|
+
customerResetPassword: "customerResetPassword",
|
|
869
|
+
customerUpdatePassword: "customerUpdatePassword",
|
|
870
|
+
customerVerifyEmail: "customerVerifyEmail",
|
|
871
|
+
deleteMyAccount: "deleteMyAccount",
|
|
872
|
+
// External identity providers.
|
|
873
|
+
customerLoginWithProvider: "customerLoginWithProvider",
|
|
874
|
+
customerProviderCallback: "customerProviderCallback",
|
|
875
|
+
customerProviderVerifyOTP: "customerProviderVerifyOTP"
|
|
876
|
+
};
|
|
877
|
+
function authClientConfig(env, options) {
|
|
878
|
+
const apiKey = resolveFoirApiKey(env);
|
|
879
|
+
if (!apiKey) {
|
|
880
|
+
throw new FoirRequestError(
|
|
881
|
+
"[foir] no api key configured: set PLATFORM_API_KEY (or PLATFORM_PREVIEW_API_KEY under FOIR_PREVIEW=true)"
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
const apiUrl = env.PLATFORM_API_URL;
|
|
885
|
+
if (!apiUrl) {
|
|
886
|
+
throw new FoirRequestError("[foir] no api url configured: set PLATFORM_API_URL");
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
apiUrl,
|
|
890
|
+
apiKey,
|
|
891
|
+
channel: isFoirPreview(env) ? "draft" : "published",
|
|
892
|
+
bearerToken: options?.bearerToken,
|
|
893
|
+
headers: options?.headers,
|
|
894
|
+
fetch: options?.fetch
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
var MANIFEST_CACHE_LIMIT = 8;
|
|
898
|
+
var manifestCache = /* @__PURE__ */ new Map();
|
|
899
|
+
function authManifest(config) {
|
|
900
|
+
const key = `${config.apiUrl}\0${config.apiKey}\0${config.channel ?? "published"}`;
|
|
901
|
+
const held = manifestCache.get(key);
|
|
902
|
+
if (held) return held;
|
|
903
|
+
const pending = fetchFoirManifest(config).catch((err) => {
|
|
904
|
+
manifestCache.delete(key);
|
|
905
|
+
throw err;
|
|
906
|
+
});
|
|
907
|
+
manifestCache.set(key, pending);
|
|
908
|
+
while (manifestCache.size > MANIFEST_CACHE_LIMIT) {
|
|
909
|
+
const oldest = manifestCache.keys().next();
|
|
910
|
+
if (oldest.done) break;
|
|
911
|
+
manifestCache.delete(oldest.value);
|
|
912
|
+
}
|
|
913
|
+
return pending;
|
|
914
|
+
}
|
|
915
|
+
async function authOp(env, op, args, options) {
|
|
916
|
+
const config = authClientConfig(env, options);
|
|
917
|
+
const manifest = await authManifest(config);
|
|
918
|
+
const plan = planFoirNamespaceCall(manifest, {
|
|
919
|
+
namespace: "auth",
|
|
920
|
+
op,
|
|
921
|
+
args: args ? stripUndefined(args) : {}
|
|
922
|
+
});
|
|
923
|
+
return executeFoirDocument(config, plan);
|
|
924
|
+
}
|
|
925
|
+
function stripUndefined(args) {
|
|
926
|
+
const out = {};
|
|
927
|
+
for (const [key, value] of Object.entries(args)) {
|
|
928
|
+
if (value !== void 0) out[key] = value;
|
|
929
|
+
}
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
322
932
|
|
|
323
933
|
// src/auth/auth.ts
|
|
324
934
|
var TOKEN_KEY = "foir:token";
|
|
@@ -326,37 +936,67 @@ var REFRESH_KEY = "foir:refreshToken";
|
|
|
326
936
|
var CUSTOMER_ID_KEY = "foir:customerId";
|
|
327
937
|
var EMAIL_KEY = "foir:email";
|
|
328
938
|
async function customerLogin(env, vars) {
|
|
329
|
-
return (
|
|
939
|
+
return authOp(env, AUTH_OPS.customerLogin, vars);
|
|
330
940
|
}
|
|
331
941
|
async function customerRegister(env, vars) {
|
|
332
|
-
return (
|
|
942
|
+
return authOp(env, AUTH_OPS.customerRegister, vars);
|
|
333
943
|
}
|
|
334
944
|
async function customerRequestOTP(env, email) {
|
|
335
|
-
return (
|
|
945
|
+
return authOp(env, AUTH_OPS.customerRequestOTP, { email });
|
|
336
946
|
}
|
|
337
947
|
async function customerLoginOTP(env, vars) {
|
|
338
|
-
return (
|
|
948
|
+
return authOp(env, AUTH_OPS.customerLoginOTP, vars);
|
|
339
949
|
}
|
|
340
950
|
async function customerRefreshToken(env, refreshToken) {
|
|
341
|
-
return (
|
|
951
|
+
return authOp(env, AUTH_OPS.customerRefreshToken, { refreshToken });
|
|
342
952
|
}
|
|
343
953
|
async function customerLoginWithProvider(env, input) {
|
|
344
|
-
return (
|
|
954
|
+
return authOp(env, AUTH_OPS.customerLoginWithProvider, { input });
|
|
345
955
|
}
|
|
346
956
|
async function customerProviderCallback(env, input) {
|
|
347
|
-
return (
|
|
957
|
+
return authOp(env, AUTH_OPS.customerProviderCallback, { input });
|
|
348
958
|
}
|
|
349
959
|
async function customerProviderVerifyOTP(env, input) {
|
|
350
|
-
return (
|
|
960
|
+
return authOp(env, AUTH_OPS.customerProviderVerifyOTP, { input });
|
|
351
961
|
}
|
|
352
962
|
async function getAuthProviders(env) {
|
|
353
|
-
return (
|
|
963
|
+
return authOp(env, AUTH_OPS.authProviders);
|
|
964
|
+
}
|
|
965
|
+
async function getAuthConfig(env) {
|
|
966
|
+
return authOp(env, AUTH_OPS.authConfig);
|
|
967
|
+
}
|
|
968
|
+
async function getDefaultAuthProvider(env) {
|
|
969
|
+
return authOp(env, AUTH_OPS.defaultAuthProvider);
|
|
354
970
|
}
|
|
355
971
|
async function customerSignupContext(env) {
|
|
356
|
-
return (
|
|
972
|
+
return authOp(env, AUTH_OPS.customerSignupContext);
|
|
357
973
|
}
|
|
358
974
|
async function customerJoinWaitlist(env, email) {
|
|
359
|
-
return (
|
|
975
|
+
return authOp(env, AUTH_OPS.customerJoinWaitlist, { email });
|
|
976
|
+
}
|
|
977
|
+
async function customerRequestPasswordReset(env, email) {
|
|
978
|
+
return authOp(env, AUTH_OPS.customerRequestPasswordReset, { email });
|
|
979
|
+
}
|
|
980
|
+
async function customerResetPassword(env, vars) {
|
|
981
|
+
return authOp(env, AUTH_OPS.customerResetPassword, vars);
|
|
982
|
+
}
|
|
983
|
+
async function customerVerifyEmail(env, token) {
|
|
984
|
+
return authOp(env, AUTH_OPS.customerVerifyEmail, { token });
|
|
985
|
+
}
|
|
986
|
+
async function customerUpdatePassword(env, vars, options) {
|
|
987
|
+
return authOp(env, AUTH_OPS.customerUpdatePassword, vars, options);
|
|
988
|
+
}
|
|
989
|
+
async function currentUser(env, options) {
|
|
990
|
+
return authOp(env, AUTH_OPS.currentUser, void 0, options);
|
|
991
|
+
}
|
|
992
|
+
async function myProviderToken(env, providerKey, options) {
|
|
993
|
+
return authOp(env, AUTH_OPS.myProviderToken, { providerKey }, options);
|
|
994
|
+
}
|
|
995
|
+
async function customerLogout(env, options) {
|
|
996
|
+
return authOp(env, AUTH_OPS.customerLogout, void 0, options);
|
|
997
|
+
}
|
|
998
|
+
async function deleteMyAccount(env, vars, options) {
|
|
999
|
+
return authOp(env, AUTH_OPS.deleteMyAccount, vars, options);
|
|
360
1000
|
}
|
|
361
1001
|
async function saveSession(adapter, result) {
|
|
362
1002
|
await adapter.set(TOKEN_KEY, result.token);
|
|
@@ -389,6 +1029,19 @@ async function registerWithPassword(adapter, env, vars) {
|
|
|
389
1029
|
async function loginWithOTP(adapter, env, vars) {
|
|
390
1030
|
return saveSession(adapter, await customerLoginOTP(env, vars));
|
|
391
1031
|
}
|
|
1032
|
+
async function resetPasswordAndSignIn(adapter, env, vars) {
|
|
1033
|
+
return saveSession(adapter, await customerResetPassword(env, vars));
|
|
1034
|
+
}
|
|
1035
|
+
async function signOut(adapter, env) {
|
|
1036
|
+
const token = await getSessionToken(adapter);
|
|
1037
|
+
if (token) {
|
|
1038
|
+
try {
|
|
1039
|
+
await customerLogout(env, { bearerToken: token });
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
await clearSession(adapter);
|
|
1044
|
+
}
|
|
392
1045
|
async function refreshSession(adapter, env) {
|
|
393
1046
|
const refreshToken = await adapter.get(REFRESH_KEY) ?? null;
|
|
394
1047
|
if (!refreshToken) return null;
|
|
@@ -423,6 +1076,17 @@ async function authedRequest(adapter, env, document, variables) {
|
|
|
423
1076
|
});
|
|
424
1077
|
}
|
|
425
1078
|
}
|
|
1079
|
+
async function authedOp(adapter, env, op, args) {
|
|
1080
|
+
const token = await getSessionToken(adapter);
|
|
1081
|
+
try {
|
|
1082
|
+
return await authOp(env, op, args, { bearerToken: token ?? void 0 });
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
if (!isAuthExpiredError(err)) throw err;
|
|
1085
|
+
const refreshed = await refreshSession(adapter, env);
|
|
1086
|
+
if (!refreshed) throw err;
|
|
1087
|
+
return authOp(env, op, args, { bearerToken: refreshed.token });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
426
1090
|
async function requireAuth(adapter, options) {
|
|
427
1091
|
const token = await getSessionToken(adapter);
|
|
428
1092
|
const customer = await getSessionCustomer(adapter);
|
|
@@ -447,6 +1111,7 @@ async function requireAdmin(adapter, options) {
|
|
|
447
1111
|
// src/auth/hosted.ts
|
|
448
1112
|
var DEFAULT_COOKIE_NAME = "foir_customer_session";
|
|
449
1113
|
var DEFAULT_SESSION_PATH = "/session/token";
|
|
1114
|
+
var DEFAULT_END_SESSION_PATH = "/session/end-session";
|
|
450
1115
|
function hostedLoginUrl(config, returnTo) {
|
|
451
1116
|
const dest = new URL(config.loginOrigin);
|
|
452
1117
|
if (returnTo && returnTo !== "/") {
|
|
@@ -454,6 +1119,15 @@ function hostedLoginUrl(config, returnTo) {
|
|
|
454
1119
|
}
|
|
455
1120
|
return dest.toString();
|
|
456
1121
|
}
|
|
1122
|
+
function hostedLogoutUrl(config, clientId, returnTo) {
|
|
1123
|
+
const path = config.endSessionPath ?? DEFAULT_END_SESSION_PATH;
|
|
1124
|
+
const dest = new URL(path, config.loginOrigin);
|
|
1125
|
+
dest.searchParams.set("client_id", clientId);
|
|
1126
|
+
if (returnTo) {
|
|
1127
|
+
dest.searchParams.set("post_logout_redirect_uri", returnTo);
|
|
1128
|
+
}
|
|
1129
|
+
return dest.toString();
|
|
1130
|
+
}
|
|
457
1131
|
async function fetchHostedSession(request, config) {
|
|
458
1132
|
const cookieName = config.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
459
1133
|
const sessionPath = config.sessionPath ?? DEFAULT_SESSION_PATH;
|
|
@@ -620,38 +1294,44 @@ function trimSlash(s) {
|
|
|
620
1294
|
return s.replace(/\/+$/, "");
|
|
621
1295
|
}
|
|
622
1296
|
export {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
CustomerLoginOTPDocument,
|
|
626
|
-
CustomerLoginWithProviderDocument,
|
|
627
|
-
CustomerProviderCallbackDocument,
|
|
628
|
-
CustomerProviderVerifyOTPDocument,
|
|
629
|
-
CustomerRefreshTokenDocument,
|
|
630
|
-
CustomerRegisterDocument,
|
|
631
|
-
CustomerRequestOTPDocument,
|
|
1297
|
+
AUTH_OPS,
|
|
1298
|
+
authedOp,
|
|
632
1299
|
authedRequest,
|
|
633
1300
|
clearSession,
|
|
634
1301
|
createFoirSessionClient,
|
|
1302
|
+
currentUser,
|
|
635
1303
|
customerJoinWaitlist,
|
|
636
1304
|
customerLogin,
|
|
637
1305
|
customerLoginOTP,
|
|
638
1306
|
customerLoginWithProvider,
|
|
1307
|
+
customerLogout,
|
|
639
1308
|
customerProviderCallback,
|
|
640
1309
|
customerProviderVerifyOTP,
|
|
641
1310
|
customerRefreshToken,
|
|
642
1311
|
customerRegister,
|
|
643
1312
|
customerRequestOTP,
|
|
1313
|
+
customerRequestPasswordReset,
|
|
1314
|
+
customerResetPassword,
|
|
644
1315
|
customerSignupContext,
|
|
1316
|
+
customerUpdatePassword,
|
|
1317
|
+
customerVerifyEmail,
|
|
1318
|
+
deleteMyAccount,
|
|
645
1319
|
fetchHostedSession,
|
|
1320
|
+
getAuthConfig,
|
|
646
1321
|
getAuthProviders,
|
|
1322
|
+
getDefaultAuthProvider,
|
|
647
1323
|
getSessionCustomer,
|
|
648
1324
|
getSessionToken,
|
|
649
1325
|
hostedLoginUrl,
|
|
1326
|
+
hostedLogoutUrl,
|
|
650
1327
|
loginWithOTP,
|
|
651
1328
|
loginWithPassword,
|
|
1329
|
+
myProviderToken,
|
|
652
1330
|
refreshSession,
|
|
653
1331
|
registerWithPassword,
|
|
654
1332
|
requireAdmin,
|
|
655
1333
|
requireAuth,
|
|
656
|
-
|
|
1334
|
+
resetPasswordAndSignIn,
|
|
1335
|
+
saveSession,
|
|
1336
|
+
signOut
|
|
657
1337
|
};
|