@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/data.js
CHANGED
|
@@ -62,7 +62,12 @@ var FoirRequestError = class extends Error {
|
|
|
62
62
|
* failed on the first attempt for an hour.
|
|
63
63
|
*/
|
|
64
64
|
get retryable() {
|
|
65
|
-
if (this.status === void 0)
|
|
65
|
+
if (this.status === void 0) {
|
|
66
|
+
if (this.graphQLErrors?.length || this.prunedSelections?.length) {
|
|
67
|
+
return this.code === "RATE_LIMITED" || this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
66
71
|
if (this.status === 429 || this.code === "RATE_LIMITED") return true;
|
|
67
72
|
if (this.status >= 500) return true;
|
|
68
73
|
return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
@@ -76,19 +81,50 @@ var FoirRequestError = class extends Error {
|
|
|
76
81
|
return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
|
|
77
82
|
}
|
|
78
83
|
};
|
|
84
|
+
var DISPATCH_RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
85
|
+
"x-api-key",
|
|
86
|
+
"authorization",
|
|
87
|
+
"x-foir-token",
|
|
88
|
+
"x-foir-context"
|
|
89
|
+
]);
|
|
90
|
+
function refuseCredentialHeaders(extra) {
|
|
91
|
+
if (!extra) return;
|
|
92
|
+
for (const name of Object.keys(extra)) {
|
|
93
|
+
if (DISPATCH_RESERVED_HEADERS.has(name.toLowerCase())) {
|
|
94
|
+
throw new FoirRequestError(
|
|
95
|
+
`[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.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
79
101
|
function buildFoirHeaders(input) {
|
|
80
102
|
const headers = {
|
|
81
|
-
"Content-Type": "application/json"
|
|
82
|
-
"x-api-key": input.apiKey
|
|
103
|
+
"Content-Type": "application/json"
|
|
83
104
|
};
|
|
84
|
-
if (input.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
headers["
|
|
105
|
+
if (input.dispatchToken !== void 0) {
|
|
106
|
+
refuseCredentialHeaders(input.extra);
|
|
107
|
+
headers["X-Foir-Token"] = input.dispatchToken;
|
|
108
|
+
if (input.dispatchContext) {
|
|
109
|
+
headers["X-Foir-Context"] = input.dispatchContext;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
headers["x-api-key"] = input.apiKey;
|
|
113
|
+
if (input.draftChannel) {
|
|
114
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
115
|
+
}
|
|
116
|
+
if (input.bearerToken) {
|
|
117
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
118
|
+
}
|
|
119
|
+
if (input.orgId) {
|
|
120
|
+
headers["X-Org-Id"] = input.orgId;
|
|
121
|
+
}
|
|
122
|
+
if (input.orgUrl) {
|
|
123
|
+
headers["X-Org-Url"] = input.orgUrl;
|
|
124
|
+
}
|
|
125
|
+
if (input.visitorAddress) {
|
|
126
|
+
headers[FOIR_VISITOR_IP_HEADER] = input.visitorAddress;
|
|
127
|
+
}
|
|
92
128
|
}
|
|
93
129
|
if (input.extra) {
|
|
94
130
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -104,6 +140,26 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
104
140
|
}
|
|
105
141
|
return base + path;
|
|
106
142
|
}
|
|
143
|
+
var FoirAuthDowngradedError = class extends FoirRequestError {
|
|
144
|
+
downgrade;
|
|
145
|
+
constructor(downgrade) {
|
|
146
|
+
super(
|
|
147
|
+
`[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}), so the request was served as ${downgrade.servedAs}`,
|
|
148
|
+
{ graphQLErrors: [{ message: "customer token rejected", extensions: { code: "AUTH_DOWNGRADED" } }] }
|
|
149
|
+
);
|
|
150
|
+
this.name = "FoirAuthDowngradedError";
|
|
151
|
+
this.downgrade = downgrade;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var FoirSessionExpiredError = class extends FoirRequestError {
|
|
155
|
+
constructor(options) {
|
|
156
|
+
super("[foir] the customer session has expired and could not be refreshed: sign in again", {
|
|
157
|
+
cause: options?.cause,
|
|
158
|
+
graphQLErrors: [{ message: "session expired", extensions: { code: "SESSION_EXPIRED" } }]
|
|
159
|
+
});
|
|
160
|
+
this.name = "FoirSessionExpiredError";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
107
163
|
var warnedDowngrade = null;
|
|
108
164
|
function reportAuthDowngrade(envelope) {
|
|
109
165
|
const downgrade = envelope.extensions?.auth;
|
|
@@ -132,12 +188,18 @@ function refusePrunedDocument(json, options) {
|
|
|
132
188
|
`[foir] selections stripped from this query: ${selections.map(describe2).join(", ")}`
|
|
133
189
|
);
|
|
134
190
|
}
|
|
191
|
+
function answeredAnything(data) {
|
|
192
|
+
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
193
|
+
}
|
|
135
194
|
function readFoirEnvelope(json, options) {
|
|
195
|
+
if (options?.rejectDowngrade && json.extensions?.auth) {
|
|
196
|
+
throw new FoirAuthDowngradedError(json.extensions.auth);
|
|
197
|
+
}
|
|
136
198
|
reportAuthDowngrade(json);
|
|
137
199
|
refusePrunedDocument(json, options);
|
|
138
200
|
if (json.errors?.length) {
|
|
139
201
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
140
|
-
if (json.data && !options?.strict) {
|
|
202
|
+
if (answeredAnything(json.data) && !options?.strict) {
|
|
141
203
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
142
204
|
} else {
|
|
143
205
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
@@ -194,15 +256,126 @@ var FoirSchemaDriftError = class extends FoirRequestError {
|
|
|
194
256
|
function foirDataEndpoint(apiUrl, path) {
|
|
195
257
|
return foirApiEndpoint(apiUrl, path);
|
|
196
258
|
}
|
|
259
|
+
async function resolveVisitorAddress(config) {
|
|
260
|
+
const source = config.visitor;
|
|
261
|
+
const value = typeof source === "function" ? await source() : source;
|
|
262
|
+
const address = value?.trim();
|
|
263
|
+
return address ? address : void 0;
|
|
264
|
+
}
|
|
197
265
|
async function resolveBearerToken(config) {
|
|
198
266
|
const source = config.bearerToken;
|
|
199
267
|
if (typeof source === "function") return await source() ?? void 0;
|
|
200
268
|
return source ?? void 0;
|
|
201
269
|
}
|
|
270
|
+
async function resolveDispatchToken(dispatch) {
|
|
271
|
+
const token = (await dispatch.token())?.trim();
|
|
272
|
+
if (!token) {
|
|
273
|
+
throw new FoirRequestError(
|
|
274
|
+
"[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`."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return token;
|
|
278
|
+
}
|
|
279
|
+
async function resolveFoirAuth(config) {
|
|
280
|
+
if (config.dispatch !== void 0) {
|
|
281
|
+
return {
|
|
282
|
+
headers: buildFoirHeaders({
|
|
283
|
+
dispatchToken: await resolveDispatchToken(config.dispatch),
|
|
284
|
+
dispatchContext: config.dispatch.context ?? void 0,
|
|
285
|
+
extra: config.headers
|
|
286
|
+
})
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const bearerToken = await resolveBearerToken(config);
|
|
290
|
+
return {
|
|
291
|
+
headers: buildFoirHeaders({
|
|
292
|
+
apiKey: config.apiKey,
|
|
293
|
+
draftChannel: config.channel === "draft",
|
|
294
|
+
bearerToken,
|
|
295
|
+
orgId: config.orgId,
|
|
296
|
+
orgUrl: config.orgUrl,
|
|
297
|
+
visitorAddress: await resolveVisitorAddress(config),
|
|
298
|
+
extra: config.headers
|
|
299
|
+
}),
|
|
300
|
+
key: { apiKey: config.apiKey, bearerToken }
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function assertFoirCredential(config) {
|
|
304
|
+
const loose = config;
|
|
305
|
+
const operation = generatedOperation(config.generated);
|
|
306
|
+
if (loose.dispatch === void 0) {
|
|
307
|
+
if (typeof loose.apiKey !== "string" || loose.apiKey === "") {
|
|
308
|
+
throw new FoirRequestError(
|
|
309
|
+
"[foir] createFoirClient needs both apiUrl and apiKey (or, inside an operation endpoint, apiUrl and dispatch)"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (loose.visitor !== void 0 && !loose.apiKey.startsWith("sk_")) {
|
|
313
|
+
throw new FoirRequestError(
|
|
314
|
+
"[foir] createFoirClient: `visitor` names the visitor a server calls for, and only a secret key holding visitors:forward may send it. A public key already calls as its visitor, and cannot hold the permission. Drop `visitor`, or build this client from your server's secret key."
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (operation !== void 0) {
|
|
318
|
+
throw new FoirRequestError(
|
|
319
|
+
`[foir] createFoirClient: the module passed as \`generated\` was generated for operation "${operation}" (\`foir types --operation ${operation}\`), so its types are what that operation's dispatch token is served, and this client holds an api key, which is served its own surface. Generate the key's types with \`foir types\`, or build this client from the dispatch token inside the operation's endpoint.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const keySide = [
|
|
325
|
+
"apiKey",
|
|
326
|
+
"bearerToken",
|
|
327
|
+
"refreshBearer",
|
|
328
|
+
"orgId",
|
|
329
|
+
"orgUrl",
|
|
330
|
+
"visitor",
|
|
331
|
+
"channel",
|
|
332
|
+
"wsUrl",
|
|
333
|
+
"webSocketImpl",
|
|
334
|
+
"reconnect",
|
|
335
|
+
"connectionParams"
|
|
336
|
+
].filter((name) => loose[name] !== void 0);
|
|
337
|
+
if (keySide.length > 0) {
|
|
338
|
+
throw new FoirRequestError(
|
|
339
|
+
`[foir] createFoirClient was given \`dispatch\` and ${keySide.map((n) => `\`${n}\``).join(", ")}. A dispatch token and an api key are different actors, and the dispatch token carries its own customer, organization and channel. Pass one credential: \`dispatch\` inside an operation endpoint, \`apiKey\` everywhere else.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const dispatch = loose.dispatch;
|
|
343
|
+
if (!dispatch || typeof dispatch.token !== "function") {
|
|
344
|
+
throw new FoirRequestError(
|
|
345
|
+
"[foir] `dispatch.token` must be a function that returns the token, e.g. `() => token`, so a refreshed token is read on each request rather than one captured at construction. `requireScopedToken` returns one ready to pass."
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
refuseCredentialHeaders(config.headers);
|
|
349
|
+
if (operation === void 0) {
|
|
350
|
+
throw new FoirRequestError(
|
|
351
|
+
"[foir] createFoirClient: a client built from a dispatch token takes the module `foir types --operation <operation key>` generated for that operation, as `generated`. Its types are what the operation's token is served; a module generated for a key types calls the token is refused, and without one nothing says which operation this is."
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const dispatched = contextOperation(dispatch.context);
|
|
355
|
+
if (dispatched !== void 0 && dispatched !== operation) {
|
|
356
|
+
throw new FoirRequestError(
|
|
357
|
+
`[foir] createFoirClient: the module passed as \`generated\` was generated for operation "${operation}", and this endpoint was dispatched for "${dispatched}". Generate this endpoint's types with \`foir types --operation ${dispatched}\`.`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function generatedOperation(generated) {
|
|
362
|
+
if (typeof generated !== "object" || generated === null) return void 0;
|
|
363
|
+
const credential = generated.foirCredential;
|
|
364
|
+
if (typeof credential !== "object" || credential === null) return void 0;
|
|
365
|
+
const operation = credential.operation;
|
|
366
|
+
return typeof operation === "string" && operation !== "" ? operation : void 0;
|
|
367
|
+
}
|
|
368
|
+
function contextOperation(context) {
|
|
369
|
+
for (const part of (context ?? "").split(";")) {
|
|
370
|
+
const [key, value] = part.split("=", 2);
|
|
371
|
+
if (key?.trim() === "operation" && value) return value.trim();
|
|
372
|
+
}
|
|
373
|
+
return void 0;
|
|
374
|
+
}
|
|
202
375
|
function foirRoute(input) {
|
|
203
376
|
if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
|
|
204
377
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
205
|
-
if (input.orgId) return { lane: "origin", reason: "org" };
|
|
378
|
+
if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
|
|
206
379
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
207
380
|
if (input.writeToken) return { lane: "origin", reason: "floor" };
|
|
208
381
|
if (input.channel === "draft") return { lane: "origin", reason: "channel" };
|
|
@@ -220,25 +393,45 @@ async function executeFoirDocument(config, plan) {
|
|
|
220
393
|
async function executeFoirRawDocument(config, document, mutation, variables, options) {
|
|
221
394
|
return await runFoirDocument(config, document, mutation, variables, options);
|
|
222
395
|
}
|
|
396
|
+
var renewing = /* @__PURE__ */ new WeakMap();
|
|
397
|
+
function renewBearerOnce(refresh) {
|
|
398
|
+
const running = renewing.get(refresh);
|
|
399
|
+
if (running) return running;
|
|
400
|
+
const next = refresh().finally(() => renewing.delete(refresh));
|
|
401
|
+
renewing.set(refresh, next);
|
|
402
|
+
return next;
|
|
403
|
+
}
|
|
223
404
|
async function runFoirDocument(config, document, mutation, variables, options) {
|
|
405
|
+
try {
|
|
406
|
+
return await sendFoirDocument(config, document, mutation, variables, options);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
const refresh = config.refreshBearer;
|
|
409
|
+
if (!(err instanceof FoirAuthDowngradedError) || !refresh || err.downgrade.reason === "wrong_project") {
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
const token = err.downgrade.reason === "expired" ? await renewBearerOnce(refresh) : null;
|
|
413
|
+
if (!token) throw new FoirSessionExpiredError({ cause: err });
|
|
414
|
+
try {
|
|
415
|
+
return await sendFoirDocument({ ...config, bearerToken: token }, document, mutation, variables, options);
|
|
416
|
+
} catch (again) {
|
|
417
|
+
if (again instanceof FoirAuthDowngradedError) throw new FoirSessionExpiredError({ cause: again });
|
|
418
|
+
throw again;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function sendFoirDocument(config, document, mutation, variables, options) {
|
|
224
423
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
225
424
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
226
|
-
const
|
|
227
|
-
const headers = buildFoirHeaders({
|
|
228
|
-
apiKey: config.apiKey,
|
|
229
|
-
draftChannel: config.channel === "draft",
|
|
230
|
-
bearerToken,
|
|
231
|
-
orgId: config.orgId,
|
|
232
|
-
extra: config.headers
|
|
233
|
-
});
|
|
425
|
+
const { headers, key } = await resolveFoirAuth(config);
|
|
234
426
|
const hash = await documentHash(document);
|
|
235
|
-
const
|
|
236
|
-
apiKey:
|
|
237
|
-
bearerToken,
|
|
427
|
+
const sharedKey = key !== void 0 && foirRoute({
|
|
428
|
+
apiKey: key.apiKey,
|
|
429
|
+
bearerToken: key.bearerToken,
|
|
238
430
|
orgId: config.orgId,
|
|
431
|
+
orgUrl: config.orgUrl,
|
|
239
432
|
mutation,
|
|
240
433
|
channel: config.channel
|
|
241
|
-
});
|
|
434
|
+
}).lane === "shared" ? key.apiKey : void 0;
|
|
242
435
|
const post = (body) => doFetch(endpoint, {
|
|
243
436
|
method: "POST",
|
|
244
437
|
headers,
|
|
@@ -248,8 +441,8 @@ async function runFoirDocument(config, document, mutation, variables, options) {
|
|
|
248
441
|
const extensions = persistedQueryExtension(hash);
|
|
249
442
|
let res;
|
|
250
443
|
try {
|
|
251
|
-
if (
|
|
252
|
-
res = await doFetch(buildCdnUrl(endpoint,
|
|
444
|
+
if (sharedKey !== void 0) {
|
|
445
|
+
res = await doFetch(buildCdnUrl(endpoint, sharedKey, hash, variables), {
|
|
253
446
|
method: "GET",
|
|
254
447
|
headers,
|
|
255
448
|
...config.requestInit
|
|
@@ -275,7 +468,7 @@ async function runFoirDocument(config, document, mutation, variables, options) {
|
|
|
275
468
|
});
|
|
276
469
|
}
|
|
277
470
|
const json = await res.json();
|
|
278
|
-
return readFoirEnvelope(json, options);
|
|
471
|
+
return readFoirEnvelope(json, { ...options, rejectDowngrade: Boolean(key?.bearerToken) });
|
|
279
472
|
}
|
|
280
473
|
async function readErrorBody(res) {
|
|
281
474
|
try {
|
|
@@ -287,20 +480,16 @@ async function readErrorBody(res) {
|
|
|
287
480
|
}
|
|
288
481
|
|
|
289
482
|
// src/data/manifest.ts
|
|
290
|
-
async function fetchFoirManifest(config) {
|
|
291
|
-
const
|
|
483
|
+
async function fetchFoirManifest(config, options = {}) {
|
|
484
|
+
const base = foirDataEndpoint(config.apiUrl, "/v1/manifest");
|
|
485
|
+
const url = options.operation === void 0 ? base : `${base}?operation=${encodeURIComponent(options.operation)}`;
|
|
292
486
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
487
|
+
const { headers } = await resolveFoirAuth(config);
|
|
293
488
|
let res;
|
|
294
489
|
try {
|
|
295
490
|
res = await doFetch(url, {
|
|
296
491
|
method: "GET",
|
|
297
|
-
headers
|
|
298
|
-
apiKey: config.apiKey,
|
|
299
|
-
draftChannel: config.channel === "draft",
|
|
300
|
-
bearerToken: await resolveBearerToken(config),
|
|
301
|
-
orgId: config.orgId,
|
|
302
|
-
extra: config.headers
|
|
303
|
-
}),
|
|
492
|
+
headers,
|
|
304
493
|
...config.requestInit
|
|
305
494
|
});
|
|
306
495
|
} catch (err) {
|
|
@@ -705,6 +894,13 @@ function planFoirModelCall(source, call) {
|
|
|
705
894
|
}
|
|
706
895
|
return planCall(source, opDef, `${call.model}.${call.op}`, operationName(model.typeName, call.op), call);
|
|
707
896
|
}
|
|
897
|
+
var CREATING_OPS = /* @__PURE__ */ new Set(["create", "submit"]);
|
|
898
|
+
function withCreateIdempotencyKey(source, model, op, args) {
|
|
899
|
+
if (!CREATING_OPS.has(op) || args.idempotencyKey !== void 0) return args;
|
|
900
|
+
const argTypes = source.models[model]?.ops?.[op]?.argTypes;
|
|
901
|
+
if (!argTypes || !("idempotencyKey" in argTypes)) return args;
|
|
902
|
+
return { ...args, idempotencyKey: globalThis.crypto.randomUUID() };
|
|
903
|
+
}
|
|
708
904
|
function planFoirNamespaceCall(source, call) {
|
|
709
905
|
const namespace = source.namespaces?.[call.namespace];
|
|
710
906
|
if (!namespace) {
|
|
@@ -862,6 +1058,11 @@ var FoirSocketTransport = class {
|
|
|
862
1058
|
keepAlive = null;
|
|
863
1059
|
retryTimer = null;
|
|
864
1060
|
constructor(config) {
|
|
1061
|
+
if (config.dispatch !== void 0) {
|
|
1062
|
+
throw new FoirRequestError(
|
|
1063
|
+
"[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."
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
865
1066
|
this.config = config;
|
|
866
1067
|
this.policy = { ...DEFAULT_RECONNECT, ...config.reconnect ?? {} };
|
|
867
1068
|
}
|
|
@@ -933,13 +1134,7 @@ var FoirSocketTransport = class {
|
|
|
933
1134
|
async sendConnectionInit(socket) {
|
|
934
1135
|
this.connecting = false;
|
|
935
1136
|
try {
|
|
936
|
-
const headers =
|
|
937
|
-
apiKey: this.config.apiKey,
|
|
938
|
-
draftChannel: this.config.channel === "draft",
|
|
939
|
-
bearerToken: await resolveBearerToken(this.config),
|
|
940
|
-
orgId: this.config.orgId,
|
|
941
|
-
extra: this.config.headers
|
|
942
|
-
});
|
|
1137
|
+
const { headers } = await resolveFoirAuth(this.config);
|
|
943
1138
|
const { "Content-Type": _contentType, ...params } = headers;
|
|
944
1139
|
const extra = await this.config.connectionParams?.() ?? {};
|
|
945
1140
|
this.send(socket, {
|
|
@@ -1546,7 +1741,8 @@ function splitQuery(query) {
|
|
|
1546
1741
|
return { args, include, omit, pick, depth };
|
|
1547
1742
|
}
|
|
1548
1743
|
var GENERATED_EXPORTS = [
|
|
1549
|
-
"
|
|
1744
|
+
"foirSurfaceVersion",
|
|
1745
|
+
"foirSurfaceHashes",
|
|
1550
1746
|
"foirSelections",
|
|
1551
1747
|
"foirNamespaceSelections",
|
|
1552
1748
|
"foirSelectionUnions"
|
|
@@ -1566,10 +1762,16 @@ function resolveGeneratedConfig(config) {
|
|
|
1566
1762
|
`[foir] createFoirClient: the module passed as \`generated\` is missing ${missing.join(", ")}. \`foir types\` emits ${GENERATED_EXPORTS.join(", ")} together, so a module missing one is stale or hand-assembled \u2014 re-run \`foir types\`. Reading the rest and fetching \`GET /v1/manifest\` for the remainder would hide that; omit \`generated\` entirely to ask for the manifest deliberately.`
|
|
1567
1763
|
);
|
|
1568
1764
|
}
|
|
1569
|
-
const version = generated["
|
|
1570
|
-
if (typeof version !== "
|
|
1765
|
+
const version = generated["foirSurfaceVersion"];
|
|
1766
|
+
if (typeof version !== "string" || version === "") {
|
|
1767
|
+
throw new FoirRequestError(
|
|
1768
|
+
"[foir] createFoirClient: `generated.foirSurfaceVersion` is not a surface version. It is what the drift check compares against the server. Re-run `foir types`."
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
const hashes = generated["foirSurfaceHashes"];
|
|
1772
|
+
if (!isRecord(hashes) || Object.values(hashes).some((h) => typeof h !== "string")) {
|
|
1571
1773
|
throw new FoirRequestError(
|
|
1572
|
-
"[foir] createFoirClient: `generated.
|
|
1774
|
+
"[foir] createFoirClient: `generated.foirSurfaceHashes` is not an object of component -> hash. It is how a drift names what moved. Re-run `foir types`."
|
|
1573
1775
|
);
|
|
1574
1776
|
}
|
|
1575
1777
|
const selections = generated["foirSelections"];
|
|
@@ -1610,13 +1812,57 @@ function resolveGeneratedConfig(config) {
|
|
|
1610
1812
|
}
|
|
1611
1813
|
return {
|
|
1612
1814
|
...config,
|
|
1613
|
-
|
|
1815
|
+
surfaceVersion: config.surfaceVersion ?? version,
|
|
1816
|
+
surfaceHashes: config.surfaceHashes ?? hashes,
|
|
1614
1817
|
selections: config.selections ?? selections,
|
|
1615
1818
|
namespaceSelections: config.namespaceSelections ?? namespaceSelections,
|
|
1616
1819
|
selectionUnions: config.selectionUnions ?? selectionUnions,
|
|
1617
1820
|
subscriptions: config.subscriptions ?? subscriptions
|
|
1618
1821
|
};
|
|
1619
1822
|
}
|
|
1823
|
+
function changedSurfaceParts(generated, served) {
|
|
1824
|
+
if (!generated || !served) return [];
|
|
1825
|
+
const out = /* @__PURE__ */ new Set();
|
|
1826
|
+
for (const [key, hash] of Object.entries(served)) {
|
|
1827
|
+
if (generated[key] !== hash) out.add(key);
|
|
1828
|
+
}
|
|
1829
|
+
for (const key of Object.keys(generated)) {
|
|
1830
|
+
if (served[key] === void 0) out.add(key);
|
|
1831
|
+
}
|
|
1832
|
+
return [...out].sort();
|
|
1833
|
+
}
|
|
1834
|
+
var SURFACE_SECTIONS = {
|
|
1835
|
+
models: ["model", "models"],
|
|
1836
|
+
namespaces: ["namespace", "namespaces"],
|
|
1837
|
+
selectionUnions: ["union", "unions"]
|
|
1838
|
+
};
|
|
1839
|
+
var SURFACE_WHOLES = {
|
|
1840
|
+
subscriptions: "the subscriptions",
|
|
1841
|
+
composedFor: "the organization it is composed for"
|
|
1842
|
+
};
|
|
1843
|
+
function describeChanged(changed) {
|
|
1844
|
+
const named = /* @__PURE__ */ new Map();
|
|
1845
|
+
const wholes = [];
|
|
1846
|
+
for (const part of changed) {
|
|
1847
|
+
const dot = part.indexOf(".");
|
|
1848
|
+
const section = dot < 0 ? part : part.slice(0, dot);
|
|
1849
|
+
if (dot < 0 || !SURFACE_SECTIONS[section]) {
|
|
1850
|
+
wholes.push(SURFACE_WHOLES[part] ?? part);
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
named.set(section, [...named.get(section) ?? [], `\`${part.slice(dot + 1)}\``]);
|
|
1854
|
+
}
|
|
1855
|
+
const phrases = [];
|
|
1856
|
+
for (const [section, [one, many]] of Object.entries(SURFACE_SECTIONS)) {
|
|
1857
|
+
const names = named.get(section);
|
|
1858
|
+
if (names?.length) phrases.push(`${names.length === 1 ? one : many} ${names.join(", ")}`);
|
|
1859
|
+
}
|
|
1860
|
+
return [...phrases, ...wholes].join("; ");
|
|
1861
|
+
}
|
|
1862
|
+
function describeDrift(drift) {
|
|
1863
|
+
const moved = describeChanged(drift.changed);
|
|
1864
|
+
return `the types were generated from surface ${drift.generated}, and the server is serving ${drift.server} (${drift.channel}).` + (moved ? ` Changed since: ${moved}.` : "");
|
|
1865
|
+
}
|
|
1620
1866
|
function announceFallback(config) {
|
|
1621
1867
|
if (config.generated !== void 0 || config.selections !== void 0) return;
|
|
1622
1868
|
if (config.announceManifestFallback === false) return;
|
|
@@ -1625,9 +1871,12 @@ function announceFallback(config) {
|
|
|
1625
1871
|
);
|
|
1626
1872
|
}
|
|
1627
1873
|
function createFoirClient(clientConfig) {
|
|
1628
|
-
if (!clientConfig?.apiUrl
|
|
1629
|
-
throw new Error(
|
|
1874
|
+
if (!clientConfig?.apiUrl) {
|
|
1875
|
+
throw new Error(
|
|
1876
|
+
"[foir] createFoirClient needs both apiUrl and apiKey (or, inside an operation endpoint, apiUrl and dispatch)"
|
|
1877
|
+
);
|
|
1630
1878
|
}
|
|
1879
|
+
assertFoirCredential(clientConfig);
|
|
1631
1880
|
const config = resolveGeneratedConfig(clientConfig);
|
|
1632
1881
|
announceFallback(config);
|
|
1633
1882
|
let driftChecked = false;
|
|
@@ -1652,31 +1901,19 @@ function createFoirClient(clientConfig) {
|
|
|
1652
1901
|
}
|
|
1653
1902
|
return manifest();
|
|
1654
1903
|
};
|
|
1655
|
-
const sameSurface = (a, b) => {
|
|
1656
|
-
const stable = (v) => {
|
|
1657
|
-
if (Array.isArray(v)) return v.map(stable);
|
|
1658
|
-
if (v && typeof v === "object") {
|
|
1659
|
-
return Object.keys(v).sort().map((k) => [k, stable(v[k])]);
|
|
1660
|
-
}
|
|
1661
|
-
return v;
|
|
1662
|
-
};
|
|
1663
|
-
return JSON.stringify(stable(a)) === JSON.stringify(stable(b));
|
|
1664
|
-
};
|
|
1665
1904
|
const maybeCheckDrift = () => {
|
|
1666
|
-
const generated = config.
|
|
1905
|
+
const generated = config.surfaceVersion;
|
|
1667
1906
|
if (driftChecked) return;
|
|
1668
1907
|
if (generated === void 0) return;
|
|
1669
1908
|
if (config.checkSchemaDrift === false) return;
|
|
1670
1909
|
driftChecked = true;
|
|
1671
1910
|
void manifest().then((served) => {
|
|
1672
|
-
if (served.
|
|
1673
|
-
if (sameSurface(config.selections, served.models) && sameSurface(config.namespaceSelections ?? {}, served.namespaces ?? {}) && sameSurface(config.selectionUnions ?? {}, served.selectionUnions ?? {})) {
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1911
|
+
if (!served.surfaceVersion || served.surfaceVersion === generated) return;
|
|
1676
1912
|
const drift = {
|
|
1677
1913
|
generated,
|
|
1678
|
-
server: served.
|
|
1679
|
-
channel: served.channel
|
|
1914
|
+
server: served.surfaceVersion,
|
|
1915
|
+
channel: served.channel,
|
|
1916
|
+
changed: changedSurfaceParts(config.surfaceHashes, served.surfaceHashes)
|
|
1680
1917
|
};
|
|
1681
1918
|
detectedDrift = drift;
|
|
1682
1919
|
if (config.onSchemaDrift) {
|
|
@@ -1684,7 +1921,7 @@ function createFoirClient(clientConfig) {
|
|
|
1684
1921
|
return;
|
|
1685
1922
|
}
|
|
1686
1923
|
console.warn(
|
|
1687
|
-
`[foir] schema drift:
|
|
1924
|
+
`[foir] schema drift: ${describeDrift(drift)} Re-run \`foir types\`: the selection trees this client prints from are the ones that surface published, so a new field is not merely untyped, it is not requested.`
|
|
1688
1925
|
);
|
|
1689
1926
|
}).catch(() => {
|
|
1690
1927
|
});
|
|
@@ -1692,17 +1929,18 @@ function createFoirClient(clientConfig) {
|
|
|
1692
1929
|
const assertNoStaleSelections = (what) => {
|
|
1693
1930
|
if (config.strictSchemaDrift === false || !detectedDrift) return;
|
|
1694
1931
|
throw new FoirSchemaDriftError(
|
|
1695
|
-
`[foir] refusing ${what}:
|
|
1932
|
+
`[foir] refusing ${what}: ${describeDrift(detectedDrift)} The selection trees this client prints from are the ones that surface published, so a field added since is not requested at all. Re-run \`foir types\`.`,
|
|
1696
1933
|
detectedDrift
|
|
1697
1934
|
);
|
|
1698
1935
|
};
|
|
1699
1936
|
const runModel = async (model, op, query) => {
|
|
1700
1937
|
assertNoStaleSelections(`${model}.${op}`);
|
|
1701
1938
|
const { args, include, omit, pick, depth } = splitQuery(query);
|
|
1702
|
-
const
|
|
1939
|
+
const source = await selectionSource();
|
|
1940
|
+
const plan = planFoirModelCall(source, {
|
|
1703
1941
|
model,
|
|
1704
1942
|
op,
|
|
1705
|
-
args,
|
|
1943
|
+
args: withCreateIdempotencyKey(source, model, op, args),
|
|
1706
1944
|
include,
|
|
1707
1945
|
omit,
|
|
1708
1946
|
pick,
|
|
@@ -2024,9 +2262,12 @@ function decimal(value) {
|
|
|
2024
2262
|
return new FoirDecimal(value);
|
|
2025
2263
|
}
|
|
2026
2264
|
export {
|
|
2265
|
+
FOIR_VISITOR_IP_HEADER,
|
|
2266
|
+
FoirAuthDowngradedError,
|
|
2027
2267
|
FoirDecimal,
|
|
2028
2268
|
FoirRequestError,
|
|
2029
2269
|
FoirSchemaDriftError,
|
|
2270
|
+
FoirSessionExpiredError,
|
|
2030
2271
|
FoirSocketTransport,
|
|
2031
2272
|
createFoirClient,
|
|
2032
2273
|
createFoirClientFromEnv,
|
package/dist/graphql/client.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Consumers that bring their own GraphQL client (Apollo/urql/react-query) use
|
|
7
7
|
* the generated `TypedDocumentNode`s directly and replicate those two headers.
|
|
8
8
|
*/
|
|
9
|
-
export { foirRequest, isFoirPreview, absolutiseFoirMediaUrl, FOIR_CDN_BASE, FoirRequestError, } from './foir-request';
|
|
9
|
+
export { foirRequest, isFoirPreview, absolutiseFoirMediaUrl, FOIR_CDN_BASE, FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, } from './foir-request';
|
|
10
10
|
export type { FoirEnv, FoirRequestOptions } from './foir-request';
|
|
11
11
|
/**
|
|
12
12
|
* Failure classification. Branch on these rather than on `err.message`: the
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
26
26
|
import { type FoirEnv, type FoirRequestOptions } from '../transport/foir-http';
|
|
27
|
-
export { FoirRequestError, isFoirPreview, isFoirRateLimited, isFoirUnauthenticated, isFoirForbidden, isFoirNotFound, readFoirRateLimit, type FoirEnv, type FoirErrorCode, type FoirErrorExtensions, type FoirGraphQLErrorItem, type FoirRateLimitSnapshot, type FoirRequestOptions, type FoirAuthDowngrade, } from '../transport/foir-http';
|
|
27
|
+
export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, isFoirPreview, isFoirRateLimited, isFoirUnauthenticated, isFoirForbidden, isFoirNotFound, readFoirRateLimit, type FoirEnv, type FoirErrorCode, type FoirErrorExtensions, type FoirGraphQLErrorItem, type FoirRateLimitSnapshot, type FoirRequestOptions, type FoirAuthDowngrade, } from '../transport/foir-http';
|
|
28
28
|
export { withFoirRetry, foirRetryDelayMs, type FoirRetryPolicy, } from '../transport/retry';
|
|
29
29
|
/** Default Foir CDN origin used to absolutise relative media URLs. */
|
|
30
30
|
export declare const FOIR_CDN_BASE = "https://cdn.foir.dev/";
|
package/dist/index.js
CHANGED
|
@@ -256,15 +256,6 @@ function EditorProvider({
|
|
|
256
256
|
type: "request-operation",
|
|
257
257
|
payload: { operationKey, input, requestId, mode: options?.mode ?? "sync" }
|
|
258
258
|
});
|
|
259
|
-
setTimeout(() => {
|
|
260
|
-
if (pendingOperationsRef.current.has(requestId)) {
|
|
261
|
-
pendingOperationsRef.current.delete(requestId);
|
|
262
|
-
resolve({
|
|
263
|
-
success: false,
|
|
264
|
-
error: { code: "TIMEOUT", message: "Operation timed out" }
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}, 6e4);
|
|
268
259
|
});
|
|
269
260
|
},
|
|
270
261
|
[postMessage]
|
package/dist/live/drift.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { FoirSchemaDrift } from '../data/transport';
|
|
|
15
15
|
* longer exists — until the tab is reloaded, which on a long-lived admin
|
|
16
16
|
* session can be days.
|
|
17
17
|
*
|
|
18
|
-
* The SDK already detects this: the data client compares the
|
|
18
|
+
* The SDK already detects this: the data client compares the surface version
|
|
19
19
|
* its generated types came from against the one the server is serving, and
|
|
20
20
|
* reports `FoirSchemaDrift`. What was missing is anyone acting on it.
|
|
21
21
|
*
|
|
@@ -36,16 +36,15 @@ export interface FoirDriftHandlerOptions {
|
|
|
36
36
|
/**
|
|
37
37
|
* Whether a reported drift is worth clearing the cache for.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Any surface other than the one the types were generated from is. The versions
|
|
40
|
+
* are content hashes, so there is no ahead or behind: a deploy that added a
|
|
41
|
+
* field and a rollback that removed one are both a different surface, and in
|
|
42
|
+
* both the cache holds responses resolved against a shape the code no longer
|
|
43
|
+
* reads, which is the torn state this exists to remove.
|
|
42
44
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* torn state this exists to remove. What is NOT actionable is equality — a
|
|
47
|
-
* client reporting no drift at all, which the detector should not produce but
|
|
48
|
-
* which costs nothing to refuse here rather than to clear the world for.
|
|
45
|
+
* What is NOT actionable is equality, or a version missing on either side: a
|
|
46
|
+
* report the detector should not produce, and one that costs nothing to refuse
|
|
47
|
+
* here rather than to clear the world for.
|
|
49
48
|
*/
|
|
50
49
|
export declare function foirDriftIsActionable(drift: FoirSchemaDrift): boolean;
|
|
51
50
|
/**
|
package/dist/live.js
CHANGED
|
@@ -800,7 +800,7 @@ function createFoirCoalescer(options) {
|
|
|
800
800
|
|
|
801
801
|
// src/live/drift.ts
|
|
802
802
|
function foirDriftIsActionable(drift) {
|
|
803
|
-
return
|
|
803
|
+
return Boolean(drift.generated) && Boolean(drift.server) && drift.generated !== drift.server;
|
|
804
804
|
}
|
|
805
805
|
function createFoirDriftHandler(options) {
|
|
806
806
|
return (drift) => {
|