@foir/sdk 0.9.0 → 0.11.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +719 -27
  2. package/README.md +121 -2
  3. package/dist/auth/auth.d.ts +72 -1
  4. package/dist/auth/hosted.d.ts +71 -4
  5. package/dist/auth/index.d.ts +3 -3
  6. package/dist/auth/operations.d.ts +84 -0
  7. package/dist/auth/ops.d.ts +18 -0
  8. package/dist/auth.js +209 -47
  9. package/dist/client.js +101 -13
  10. package/dist/codegen/block-registry.d.ts +1 -1
  11. package/dist/codegen/block-scaffold.d.ts +1 -1
  12. package/dist/codegen/index.d.ts +1 -1
  13. package/dist/codegen/item-registry.d.ts +1 -1
  14. package/dist/codegen/item-scaffold.d.ts +1 -1
  15. package/dist/codegen/preset.d.ts +3 -2
  16. package/dist/content/content-renderer.d.ts +4 -5
  17. package/dist/content/index.d.ts +1 -1
  18. package/dist/content/richtext.d.ts +1 -1
  19. package/dist/data/builtins.d.ts +81 -13
  20. package/dist/data/client.d.ts +92 -9
  21. package/dist/data/decimal.d.ts +212 -0
  22. package/dist/data/document.d.ts +14 -1
  23. package/dist/data/index.d.ts +22 -10
  24. package/dist/data/manifest.d.ts +40 -4
  25. package/dist/data/schema.d.ts +18 -11
  26. package/dist/data/selection.d.ts +15 -0
  27. package/dist/data/subscriptions.d.ts +36 -10
  28. package/dist/data/transport.d.ts +363 -24
  29. package/dist/data/ws-transport.d.ts +8 -2
  30. package/dist/data.js +591 -64
  31. package/dist/graphql/cdn-route.d.ts +1 -1
  32. package/dist/graphql/foir-request.d.ts +1 -1
  33. package/dist/index.js +0 -9
  34. package/dist/live/coalesce.d.ts +57 -0
  35. package/dist/live/drift.d.ts +58 -0
  36. package/dist/live/events.d.ts +13 -0
  37. package/dist/live/headers.d.ts +53 -0
  38. package/dist/live/index.d.ts +54 -0
  39. package/dist/live/keys.d.ts +72 -0
  40. package/dist/live/manager.d.ts +77 -0
  41. package/dist/live/react/index.d.ts +143 -0
  42. package/dist/live/reconcile.d.ts +82 -0
  43. package/dist/live/resync.d.ts +59 -0
  44. package/dist/live/store.d.ts +134 -0
  45. package/dist/live/tagIndex.d.ts +29 -0
  46. package/dist/live/tanstack.d.ts +127 -0
  47. package/dist/live/writetoken.d.ts +86 -0
  48. package/dist/live-react.js +395 -0
  49. package/dist/live.js +845 -0
  50. package/dist/next/cacheTags.d.ts +245 -0
  51. package/dist/next/index.d.ts +7 -0
  52. package/dist/next.js +260 -34
  53. package/dist/push.js +163 -36
  54. package/dist/react.js +157 -33
  55. package/dist/realtime/index.d.ts +4 -5
  56. package/dist/server/events-client.d.ts +54 -4
  57. package/dist/server/index.d.ts +4 -2
  58. package/dist/server/operation-types.d.ts +86 -58
  59. package/dist/server/read-dispatch.d.ts +53 -0
  60. package/dist/server/require-scoped-token.d.ts +20 -1
  61. package/dist/server.js +602 -10
  62. package/dist/transport/foir-http.d.ts +168 -15
  63. package/package.json +10 -2
package/dist/data.js CHANGED
@@ -5,11 +5,30 @@ function isFoirPreview(env) {
5
5
  function resolveFoirApiKey(env) {
6
6
  return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
7
7
  }
8
+ var MALFORMED_PRUNE_REASONS = /* @__PURE__ */ new Set([
9
+ "unknown_field",
10
+ "subselections_on_scalar",
11
+ "missing_subselections"
12
+ ]);
8
13
  var FoirRequestError = class extends Error {
9
14
  /** HTTP status code, when the request failed at the HTTP layer. */
10
15
  status;
11
16
  /** GraphQL errors, when the request failed with a no-data error envelope. */
12
17
  graphQLErrors;
18
+ /**
19
+ * The `data` the server did return, when a STRICT read refused a partial
20
+ * response. Present only on that path: without it, opting into strictness
21
+ * would trade a silent wrong answer for a thrown one that discards the answer,
22
+ * and a caller that wants to log what it nearly rendered could not.
23
+ */
24
+ partialData;
25
+ /**
26
+ * The selections the server stripped, when the refusal was a pruned document
27
+ * rather than a partial error. Same purpose as `partialData`: a caller that
28
+ * wants to log what it nearly rendered, or name the offending path in a
29
+ * developer-facing message, has it without re-parsing anything.
30
+ */
31
+ prunedSelections;
13
32
  cause;
14
33
  constructor(message, options) {
15
34
  super(message);
@@ -17,6 +36,8 @@ var FoirRequestError = class extends Error {
17
36
  this.cause = options?.cause;
18
37
  this.status = options?.status;
19
38
  this.graphQLErrors = options?.graphQLErrors;
39
+ this.partialData = options?.partialData;
40
+ this.prunedSelections = options?.prunedSelections;
20
41
  }
21
42
  /**
22
43
  * The code of the first error, if the response carried one.
@@ -55,16 +76,46 @@ var FoirRequestError = class extends Error {
55
76
  return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
56
77
  }
57
78
  };
79
+ var DISPATCH_RESERVED_HEADERS = /* @__PURE__ */ new Set([
80
+ "x-api-key",
81
+ "authorization",
82
+ "x-foir-token",
83
+ "x-foir-context"
84
+ ]);
85
+ function refuseCredentialHeaders(extra) {
86
+ if (!extra) return;
87
+ for (const name of Object.keys(extra)) {
88
+ if (DISPATCH_RESERVED_HEADERS.has(name.toLowerCase())) {
89
+ throw new FoirRequestError(
90
+ `[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.`
91
+ );
92
+ }
93
+ }
94
+ }
58
95
  function buildFoirHeaders(input) {
59
96
  const headers = {
60
- "Content-Type": "application/json",
61
- "x-api-key": input.apiKey
97
+ "Content-Type": "application/json"
62
98
  };
63
- if (input.draftChannel) {
64
- headers["X-Foir-Schema-Channel"] = "draft";
65
- }
66
- if (input.bearerToken) {
67
- headers["Authorization"] = `Bearer ${input.bearerToken}`;
99
+ if (input.dispatchToken !== void 0) {
100
+ refuseCredentialHeaders(input.extra);
101
+ headers["X-Foir-Token"] = input.dispatchToken;
102
+ if (input.dispatchContext) {
103
+ headers["X-Foir-Context"] = input.dispatchContext;
104
+ }
105
+ } else {
106
+ headers["x-api-key"] = input.apiKey;
107
+ if (input.draftChannel) {
108
+ headers["X-Foir-Schema-Channel"] = "draft";
109
+ }
110
+ if (input.bearerToken) {
111
+ headers["Authorization"] = `Bearer ${input.bearerToken}`;
112
+ }
113
+ if (input.orgId) {
114
+ headers["X-Org-Id"] = input.orgId;
115
+ }
116
+ if (input.orgUrl) {
117
+ headers["X-Org-Url"] = input.orgUrl;
118
+ }
68
119
  }
69
120
  if (input.extra) {
70
121
  for (const [key, value] of Object.entries(input.extra)) {
@@ -80,14 +131,48 @@ function foirApiEndpoint(apiUrl, path) {
80
131
  }
81
132
  return base + path;
82
133
  }
83
- function readFoirEnvelope(json) {
134
+ var warnedDowngrade = null;
135
+ function reportAuthDowngrade(envelope) {
136
+ const downgrade = envelope.extensions?.auth;
137
+ if (!downgrade) {
138
+ warnedDowngrade = null;
139
+ return;
140
+ }
141
+ if (warnedDowngrade === downgrade.reason) return;
142
+ warnedDowngrade = downgrade.reason;
143
+ console.warn(
144
+ `[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}) \u2014 this request was served as ${downgrade.servedAs}, so auth-targeted content resolved to its anonymous variant. ` + (downgrade.reason === "expired" ? "Re-exchange the token and retry." : "Check which credential you are sending.")
145
+ );
146
+ }
147
+ function refusePrunedDocument(json, options) {
148
+ const selections = json.extensions?.pruned?.selections;
149
+ if (!selections?.length) return;
150
+ const describe2 = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
151
+ const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
152
+ if (malformed.length && options?.strict !== false) {
153
+ throw new FoirRequestError(
154
+ `[foir] the server could not serve part of this query and returned the rest without it: ${malformed.map(describe2).join(", ")}. Those selections do not exist on the schema this project is serving. Pass strict: false to get the partial answer instead of this error.`,
155
+ { prunedSelections: selections }
156
+ );
157
+ }
158
+ console.warn(
159
+ `[foir] selections stripped from this query: ${selections.map(describe2).join(", ")}`
160
+ );
161
+ }
162
+ function answeredAnything(data) {
163
+ return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
164
+ }
165
+ function readFoirEnvelope(json, options) {
166
+ reportAuthDowngrade(json);
167
+ refusePrunedDocument(json, options);
84
168
  if (json.errors?.length) {
85
169
  const msg = json.errors.map((e) => e.message).join("; ");
86
- if (json.data) {
170
+ if (answeredAnything(json.data) && !options?.strict) {
87
171
  console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
88
172
  } else {
89
173
  throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
90
- graphQLErrors: json.errors
174
+ graphQLErrors: json.errors,
175
+ partialData: json.data ?? void 0
91
176
  });
92
177
  }
93
178
  }
@@ -128,6 +213,14 @@ async function hasFoirErrorCode(res, ...codes) {
128
213
  }
129
214
 
130
215
  // src/data/transport.ts
216
+ var FoirSchemaDriftError = class extends FoirRequestError {
217
+ drift;
218
+ constructor(message, drift) {
219
+ super(message);
220
+ this.name = "FoirSchemaDriftError";
221
+ this.drift = drift;
222
+ }
223
+ };
131
224
  function foirDataEndpoint(apiUrl, path) {
132
225
  return foirApiEndpoint(apiUrl, path);
133
226
  }
@@ -136,10 +229,109 @@ async function resolveBearerToken(config) {
136
229
  if (typeof source === "function") return await source() ?? void 0;
137
230
  return source ?? void 0;
138
231
  }
232
+ async function resolveDispatchToken(dispatch) {
233
+ const token = (await dispatch.token())?.trim();
234
+ if (!token) {
235
+ throw new FoirRequestError(
236
+ "[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`."
237
+ );
238
+ }
239
+ return token;
240
+ }
241
+ async function resolveFoirAuth(config) {
242
+ if (config.dispatch !== void 0) {
243
+ return {
244
+ headers: buildFoirHeaders({
245
+ dispatchToken: await resolveDispatchToken(config.dispatch),
246
+ dispatchContext: config.dispatch.context ?? void 0,
247
+ extra: config.headers
248
+ })
249
+ };
250
+ }
251
+ const bearerToken = await resolveBearerToken(config);
252
+ return {
253
+ headers: buildFoirHeaders({
254
+ apiKey: config.apiKey,
255
+ draftChannel: config.channel === "draft",
256
+ bearerToken,
257
+ orgId: config.orgId,
258
+ orgUrl: config.orgUrl,
259
+ extra: config.headers
260
+ }),
261
+ key: { apiKey: config.apiKey, bearerToken }
262
+ };
263
+ }
264
+ function assertFoirCredential(config) {
265
+ const loose = config;
266
+ const operation = generatedOperation(config.generated);
267
+ if (loose.dispatch === void 0) {
268
+ if (typeof loose.apiKey !== "string" || loose.apiKey === "") {
269
+ throw new FoirRequestError(
270
+ "[foir] createFoirClient needs both apiUrl and apiKey (or, inside an operation endpoint, apiUrl and dispatch)"
271
+ );
272
+ }
273
+ if (operation !== void 0) {
274
+ throw new FoirRequestError(
275
+ `[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.`
276
+ );
277
+ }
278
+ return;
279
+ }
280
+ const keySide = [
281
+ "apiKey",
282
+ "bearerToken",
283
+ "orgId",
284
+ "orgUrl",
285
+ "channel",
286
+ "wsUrl",
287
+ "webSocketImpl",
288
+ "reconnect",
289
+ "connectionParams"
290
+ ].filter((name) => loose[name] !== void 0);
291
+ if (keySide.length > 0) {
292
+ throw new FoirRequestError(
293
+ `[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.`
294
+ );
295
+ }
296
+ const dispatch = loose.dispatch;
297
+ if (!dispatch || typeof dispatch.token !== "function") {
298
+ throw new FoirRequestError(
299
+ "[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."
300
+ );
301
+ }
302
+ refuseCredentialHeaders(config.headers);
303
+ if (operation === void 0) {
304
+ throw new FoirRequestError(
305
+ "[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."
306
+ );
307
+ }
308
+ const dispatched = contextOperation(dispatch.context);
309
+ if (dispatched !== void 0 && dispatched !== operation) {
310
+ throw new FoirRequestError(
311
+ `[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}\`.`
312
+ );
313
+ }
314
+ }
315
+ function generatedOperation(generated) {
316
+ if (typeof generated !== "object" || generated === null) return void 0;
317
+ const credential = generated.foirCredential;
318
+ if (typeof credential !== "object" || credential === null) return void 0;
319
+ const operation = credential.operation;
320
+ return typeof operation === "string" && operation !== "" ? operation : void 0;
321
+ }
322
+ function contextOperation(context) {
323
+ for (const part of (context ?? "").split(";")) {
324
+ const [key, value] = part.split("=", 2);
325
+ if (key?.trim() === "operation" && value) return value.trim();
326
+ }
327
+ return void 0;
328
+ }
139
329
  function foirRoute(input) {
140
330
  if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
141
331
  if (input.bearerToken) return { lane: "origin", reason: "bearer" };
332
+ if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
142
333
  if (input.mutation) return { lane: "origin", reason: "operation" };
334
+ if (input.writeToken) return { lane: "origin", reason: "floor" };
143
335
  if (input.channel === "draft") return { lane: "origin", reason: "channel" };
144
336
  return { lane: "shared" };
145
337
  }
@@ -152,26 +344,22 @@ async function executeFoirDocument(config, plan) {
152
344
  }
153
345
  return data[plan.rootField];
154
346
  }
155
- async function executeFoirRawDocument(config, document, mutation, variables) {
156
- return await runFoirDocument(config, document, mutation, variables);
347
+ async function executeFoirRawDocument(config, document, mutation, variables, options) {
348
+ return await runFoirDocument(config, document, mutation, variables, options);
157
349
  }
158
- async function runFoirDocument(config, document, mutation, variables) {
350
+ async function runFoirDocument(config, document, mutation, variables, options) {
159
351
  const doFetch = config.fetch ?? globalThis.fetch;
160
352
  const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
161
- const bearerToken = await resolveBearerToken(config);
162
- const headers = buildFoirHeaders({
163
- apiKey: config.apiKey,
164
- draftChannel: config.channel === "draft",
165
- bearerToken,
166
- extra: config.headers
167
- });
353
+ const { headers, key } = await resolveFoirAuth(config);
168
354
  const hash = await documentHash(document);
169
- const route = foirRoute({
170
- apiKey: config.apiKey,
171
- bearerToken,
355
+ const sharedKey = key !== void 0 && foirRoute({
356
+ apiKey: key.apiKey,
357
+ bearerToken: key.bearerToken,
358
+ orgId: config.orgId,
359
+ orgUrl: config.orgUrl,
172
360
  mutation,
173
361
  channel: config.channel
174
- });
362
+ }).lane === "shared" ? key.apiKey : void 0;
175
363
  const post = (body) => doFetch(endpoint, {
176
364
  method: "POST",
177
365
  headers,
@@ -181,8 +369,8 @@ async function runFoirDocument(config, document, mutation, variables) {
181
369
  const extensions = persistedQueryExtension(hash);
182
370
  let res;
183
371
  try {
184
- if (route.lane === "shared") {
185
- res = await doFetch(buildCdnUrl(endpoint, config.apiKey, hash, variables), {
372
+ if (sharedKey !== void 0) {
373
+ res = await doFetch(buildCdnUrl(endpoint, sharedKey, hash, variables), {
186
374
  method: "GET",
187
375
  headers,
188
376
  ...config.requestInit
@@ -208,7 +396,7 @@ async function runFoirDocument(config, document, mutation, variables) {
208
396
  });
209
397
  }
210
398
  const json = await res.json();
211
- return readFoirEnvelope(json);
399
+ return readFoirEnvelope(json, options);
212
400
  }
213
401
  async function readErrorBody(res) {
214
402
  try {
@@ -220,19 +408,16 @@ async function readErrorBody(res) {
220
408
  }
221
409
 
222
410
  // src/data/manifest.ts
223
- async function fetchFoirManifest(config) {
224
- const url = foirDataEndpoint(config.apiUrl, "/v1/manifest");
411
+ async function fetchFoirManifest(config, options = {}) {
412
+ const base = foirDataEndpoint(config.apiUrl, "/v1/manifest");
413
+ const url = options.operation === void 0 ? base : `${base}?operation=${encodeURIComponent(options.operation)}`;
225
414
  const doFetch = config.fetch ?? globalThis.fetch;
415
+ const { headers } = await resolveFoirAuth(config);
226
416
  let res;
227
417
  try {
228
418
  res = await doFetch(url, {
229
419
  method: "GET",
230
- headers: buildFoirHeaders({
231
- apiKey: config.apiKey,
232
- draftChannel: config.channel === "draft",
233
- bearerToken: await resolveBearerToken(config),
234
- extra: config.headers
235
- }),
420
+ headers,
236
421
  ...config.requestInit
237
422
  });
238
423
  } catch (err) {
@@ -637,6 +822,13 @@ function planFoirModelCall(source, call) {
637
822
  }
638
823
  return planCall(source, opDef, `${call.model}.${call.op}`, operationName(model.typeName, call.op), call);
639
824
  }
825
+ var CREATING_OPS = /* @__PURE__ */ new Set(["create", "submit"]);
826
+ function withCreateIdempotencyKey(source, model, op, args) {
827
+ if (!CREATING_OPS.has(op) || args.idempotencyKey !== void 0) return args;
828
+ const argTypes = source.models[model]?.ops?.[op]?.argTypes;
829
+ if (!argTypes || !("idempotencyKey" in argTypes)) return args;
830
+ return { ...args, idempotencyKey: globalThis.crypto.randomUUID() };
831
+ }
640
832
  function planFoirNamespaceCall(source, call) {
641
833
  const namespace = source.namespaces?.[call.namespace];
642
834
  if (!namespace) {
@@ -794,6 +986,11 @@ var FoirSocketTransport = class {
794
986
  keepAlive = null;
795
987
  retryTimer = null;
796
988
  constructor(config) {
989
+ if (config.dispatch !== void 0) {
990
+ throw new FoirRequestError(
991
+ "[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."
992
+ );
993
+ }
797
994
  this.config = config;
798
995
  this.policy = { ...DEFAULT_RECONNECT, ...config.reconnect ?? {} };
799
996
  }
@@ -865,12 +1062,7 @@ var FoirSocketTransport = class {
865
1062
  async sendConnectionInit(socket) {
866
1063
  this.connecting = false;
867
1064
  try {
868
- const headers = buildFoirHeaders({
869
- apiKey: this.config.apiKey,
870
- draftChannel: this.config.channel === "draft",
871
- bearerToken: await resolveBearerToken(this.config),
872
- extra: this.config.headers
873
- });
1065
+ const { headers } = await resolveFoirAuth(this.config);
874
1066
  const { "Content-Type": _contentType, ...params } = headers;
875
1067
  const extra = await this.config.connectionParams?.() ?? {};
876
1068
  this.send(socket, {
@@ -977,7 +1169,9 @@ var FoirSocketTransport = class {
977
1169
  return;
978
1170
  }
979
1171
  case GQL.error: {
980
- const error = new FoirRequestError(`[foir] subscription refused: ${describe(message.payload)}`);
1172
+ const error = new FoirRequestError(`[foir] subscription refused: ${describe(message.payload)}`, {
1173
+ graphQLErrors: graphQLErrorItems(message.payload)
1174
+ });
981
1175
  if (!message.id) {
982
1176
  this.failAll(error);
983
1177
  this.teardownSocket(true);
@@ -1088,6 +1282,21 @@ function defaultWebSocketFactory() {
1088
1282
  }
1089
1283
  return (url, protocols) => new Impl(url, protocols);
1090
1284
  }
1285
+ function graphQLErrorItems(payload) {
1286
+ const list = Array.isArray(payload) ? payload : payload == null ? [] : [payload];
1287
+ const items = [];
1288
+ for (const entry of list) {
1289
+ if (typeof entry !== "object" || entry === null) continue;
1290
+ const { message, path, extensions } = entry;
1291
+ if (extensions === void 0 && path === void 0) continue;
1292
+ items.push({
1293
+ message: typeof message === "string" ? message : describe(entry),
1294
+ ...Array.isArray(path) ? { path } : {},
1295
+ ...typeof extensions === "object" && extensions !== null ? { extensions } : {}
1296
+ });
1297
+ }
1298
+ return items.length > 0 ? items : void 0;
1299
+ }
1091
1300
  function describe(payload) {
1092
1301
  if (payload == null) return "no reason given";
1093
1302
  if (typeof payload === "string") return payload;
@@ -1133,12 +1342,19 @@ var FoirSubscriptionRouter = class {
1133
1342
  if (method !== "onChange") {
1134
1343
  throw new FoirRequestError(`[foir] ${modelKey}.${method} is not a subscription`);
1135
1344
  }
1345
+ const after = input.after;
1346
+ const args = [{ name: "modelKey", type: "String!" }];
1347
+ const variables = { modelKey };
1348
+ if (typeof after === "number") {
1349
+ args.push({ name: "after", type: "EventSeq" });
1350
+ variables.after = after;
1351
+ }
1136
1352
  return this.open(
1137
1353
  {
1138
1354
  field: "recordChanged",
1139
1355
  operationName: "FoirRecordChanged",
1140
- args: [{ name: "modelKey", type: "String!" }],
1141
- variables: { modelKey }
1356
+ args,
1357
+ variables
1142
1358
  },
1143
1359
  input
1144
1360
  );
@@ -1177,7 +1393,7 @@ var FoirSubscriptionRouter = class {
1177
1393
  args: [{ name: "id", type: "String!" }],
1178
1394
  variables: { id }
1179
1395
  },
1180
- handlers
1396
+ { ...handlers, onData: (event) => handlers.onData(executionEventWithEnumStatus(event)) }
1181
1397
  );
1182
1398
  }
1183
1399
  throw new FoirRequestError(`[foir] ns.${namespace}.${method} is not a subscription`);
@@ -1222,6 +1438,12 @@ var FoirSubscriptionRouter = class {
1222
1438
  };
1223
1439
  }
1224
1440
  };
1441
+ function executionEventWithEnumStatus(event) {
1442
+ if (event === null || typeof event !== "object") return event;
1443
+ const status = event.status;
1444
+ if (typeof status !== "string") return event;
1445
+ return { ...event, status: status.toUpperCase() };
1446
+ }
1225
1447
  function findSubscription(manifest, field) {
1226
1448
  const found = manifest.subscriptions?.find((s) => s.field === field);
1227
1449
  if (found) return found;
@@ -1369,6 +1591,9 @@ function recordsAt(value, path) {
1369
1591
  }
1370
1592
 
1371
1593
  // src/data/client.ts
1594
+ function graphql(document) {
1595
+ return document;
1596
+ }
1372
1597
  var NON_MODEL_PROPS = /* @__PURE__ */ new Set([
1373
1598
  "then",
1374
1599
  "catch",
@@ -1444,7 +1669,8 @@ function splitQuery(query) {
1444
1669
  return { args, include, omit, pick, depth };
1445
1670
  }
1446
1671
  var GENERATED_EXPORTS = [
1447
- "foirSchemaVersion",
1672
+ "foirSurfaceVersion",
1673
+ "foirSurfaceHashes",
1448
1674
  "foirSelections",
1449
1675
  "foirNamespaceSelections",
1450
1676
  "foirSelectionUnions"
@@ -1464,10 +1690,16 @@ function resolveGeneratedConfig(config) {
1464
1690
  `[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.`
1465
1691
  );
1466
1692
  }
1467
- const version = generated["foirSchemaVersion"];
1468
- if (typeof version !== "number" || !Number.isFinite(version)) {
1693
+ const version = generated["foirSurfaceVersion"];
1694
+ if (typeof version !== "string" || version === "") {
1695
+ throw new FoirRequestError(
1696
+ "[foir] createFoirClient: `generated.foirSurfaceVersion` is not a surface version. It is what the drift check compares against the server. Re-run `foir types`."
1697
+ );
1698
+ }
1699
+ const hashes = generated["foirSurfaceHashes"];
1700
+ if (!isRecord(hashes) || Object.values(hashes).some((h) => typeof h !== "string")) {
1469
1701
  throw new FoirRequestError(
1470
- "[foir] createFoirClient: `generated.foirSchemaVersion` is not a number. It is the manifest version the drift check compares against \u2014 re-run `foir types`."
1702
+ "[foir] createFoirClient: `generated.foirSurfaceHashes` is not an object of component -> hash. It is how a drift names what moved. Re-run `foir types`."
1471
1703
  );
1472
1704
  }
1473
1705
  const selections = generated["foirSelections"];
@@ -1500,28 +1732,83 @@ function resolveGeneratedConfig(config) {
1500
1732
  `[foir] createFoirClient: \`generated.foirSelectionUnions["${name}"]\` has no \`members\`. A tree that names this union would print an empty selection set. Re-run \`foir types\`.`
1501
1733
  );
1502
1734
  }
1735
+ const subscriptions = generated["foirSubscriptions"];
1736
+ if (subscriptions !== void 0 && !Array.isArray(subscriptions)) {
1737
+ throw new FoirRequestError(
1738
+ "[foir] createFoirClient: `generated.foirSubscriptions` is not an array of subscription descriptors. Re-run `foir types`."
1739
+ );
1740
+ }
1503
1741
  return {
1504
1742
  ...config,
1505
- schemaVersion: config.schemaVersion ?? version,
1743
+ surfaceVersion: config.surfaceVersion ?? version,
1744
+ surfaceHashes: config.surfaceHashes ?? hashes,
1506
1745
  selections: config.selections ?? selections,
1507
1746
  namespaceSelections: config.namespaceSelections ?? namespaceSelections,
1508
- selectionUnions: config.selectionUnions ?? selectionUnions
1747
+ selectionUnions: config.selectionUnions ?? selectionUnions,
1748
+ subscriptions: config.subscriptions ?? subscriptions
1509
1749
  };
1510
1750
  }
1751
+ function changedSurfaceParts(generated, served) {
1752
+ if (!generated || !served) return [];
1753
+ const out = /* @__PURE__ */ new Set();
1754
+ for (const [key, hash] of Object.entries(served)) {
1755
+ if (generated[key] !== hash) out.add(key);
1756
+ }
1757
+ for (const key of Object.keys(generated)) {
1758
+ if (served[key] === void 0) out.add(key);
1759
+ }
1760
+ return [...out].sort();
1761
+ }
1762
+ var SURFACE_SECTIONS = {
1763
+ models: ["model", "models"],
1764
+ namespaces: ["namespace", "namespaces"],
1765
+ selectionUnions: ["union", "unions"]
1766
+ };
1767
+ var SURFACE_WHOLES = {
1768
+ subscriptions: "the subscriptions",
1769
+ composedFor: "the organization it is composed for"
1770
+ };
1771
+ function describeChanged(changed) {
1772
+ const named = /* @__PURE__ */ new Map();
1773
+ const wholes = [];
1774
+ for (const part of changed) {
1775
+ const dot = part.indexOf(".");
1776
+ const section = dot < 0 ? part : part.slice(0, dot);
1777
+ if (dot < 0 || !SURFACE_SECTIONS[section]) {
1778
+ wholes.push(SURFACE_WHOLES[part] ?? part);
1779
+ continue;
1780
+ }
1781
+ named.set(section, [...named.get(section) ?? [], `\`${part.slice(dot + 1)}\``]);
1782
+ }
1783
+ const phrases = [];
1784
+ for (const [section, [one, many]] of Object.entries(SURFACE_SECTIONS)) {
1785
+ const names = named.get(section);
1786
+ if (names?.length) phrases.push(`${names.length === 1 ? one : many} ${names.join(", ")}`);
1787
+ }
1788
+ return [...phrases, ...wholes].join("; ");
1789
+ }
1790
+ function describeDrift(drift) {
1791
+ const moved = describeChanged(drift.changed);
1792
+ return `the types were generated from surface ${drift.generated}, and the server is serving ${drift.server} (${drift.channel}).` + (moved ? ` Changed since: ${moved}.` : "");
1793
+ }
1511
1794
  function announceFallback(config) {
1512
1795
  if (config.generated !== void 0 || config.selections !== void 0) return;
1513
1796
  if (config.announceManifestFallback === false) return;
1514
1797
  console.info(
1515
- "[foir] no generated module passed, so this client fetches `GET /v1/manifest` once at first use and prints from the trees it publishes. That works \u2014 it is what makes a zero-codegen start possible \u2014 but it puts a round trip in front of the first query on every cold start. Run `foir types`, then `import * as generated from './foir.generated'` and pass `generated`. Silence this with `announceManifestFallback: false`."
1798
+ "[foir] no generated module passed, so this client fetches `GET /v1/manifest` once at first use and prints from the trees it publishes. That works \u2014 it is what makes a zero-codegen start possible \u2014 but it puts a round trip in front of the first query on every cold start. Run `foir types`, then `import * as generated from './foir.generated'` and pass `generated`. Silence this with `announceManifestFallback: false`.\n[foir] A subscribe-only client can now take that advice too: `foir types` emits `foirSubscriptions` alongside the model trees, so a generated client prints its subscribe document from the module and makes no request at all. Weigh the module against one fetch behind a socket that lives for the whole session \u2014 on a phone the fallback may still be the better trade."
1516
1799
  );
1517
1800
  }
1518
1801
  function createFoirClient(clientConfig) {
1519
- if (!clientConfig?.apiUrl || !clientConfig?.apiKey) {
1520
- throw new Error("[foir] createFoirClient needs both apiUrl and apiKey");
1802
+ if (!clientConfig?.apiUrl) {
1803
+ throw new Error(
1804
+ "[foir] createFoirClient needs both apiUrl and apiKey (or, inside an operation endpoint, apiUrl and dispatch)"
1805
+ );
1521
1806
  }
1807
+ assertFoirCredential(clientConfig);
1522
1808
  const config = resolveGeneratedConfig(clientConfig);
1523
1809
  announceFallback(config);
1524
1810
  let driftChecked = false;
1811
+ let detectedDrift;
1525
1812
  let manifestOnce;
1526
1813
  const manifest = () => {
1527
1814
  if (!manifestOnce) {
@@ -1543,34 +1830,45 @@ function createFoirClient(clientConfig) {
1543
1830
  return manifest();
1544
1831
  };
1545
1832
  const maybeCheckDrift = () => {
1546
- const generated = config.schemaVersion;
1833
+ const generated = config.surfaceVersion;
1547
1834
  if (driftChecked) return;
1548
1835
  if (generated === void 0) return;
1549
1836
  if (config.checkSchemaDrift === false) return;
1550
1837
  driftChecked = true;
1551
1838
  void manifest().then((served) => {
1552
- if (served.schemaVersion === generated) return;
1839
+ if (!served.surfaceVersion || served.surfaceVersion === generated) return;
1553
1840
  const drift = {
1554
1841
  generated,
1555
- server: served.schemaVersion,
1556
- channel: served.channel
1842
+ server: served.surfaceVersion,
1843
+ channel: served.channel,
1844
+ changed: changedSurfaceParts(config.surfaceHashes, served.surfaceHashes)
1557
1845
  };
1846
+ detectedDrift = drift;
1558
1847
  if (config.onSchemaDrift) {
1559
1848
  config.onSchemaDrift(drift);
1560
1849
  return;
1561
1850
  }
1562
1851
  console.warn(
1563
- `[foir] schema drift: types were generated from schemaVersion ${drift.generated}, the server is serving ${drift.server} (${drift.channel}). Re-run \`foir types\` \u2014 the selection trees this client prints from are the ones that version published, so a new field is not merely untyped, it is not requested.`
1852
+ `[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.`
1564
1853
  );
1565
1854
  }).catch(() => {
1566
1855
  });
1567
1856
  };
1857
+ const assertNoStaleSelections = (what) => {
1858
+ if (config.strictSchemaDrift === false || !detectedDrift) return;
1859
+ throw new FoirSchemaDriftError(
1860
+ `[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\`.`,
1861
+ detectedDrift
1862
+ );
1863
+ };
1568
1864
  const runModel = async (model, op, query) => {
1865
+ assertNoStaleSelections(`${model}.${op}`);
1569
1866
  const { args, include, omit, pick, depth } = splitQuery(query);
1570
- const plan = planFoirModelCall(await selectionSource(), {
1867
+ const source = await selectionSource();
1868
+ const plan = planFoirModelCall(source, {
1571
1869
  model,
1572
1870
  op,
1573
- args,
1871
+ args: withCreateIdempotencyKey(source, model, op, args),
1574
1872
  include,
1575
1873
  omit,
1576
1874
  pick,
@@ -1582,6 +1880,7 @@ function createFoirClient(clientConfig) {
1582
1880
  return result;
1583
1881
  };
1584
1882
  const runNamespace = async (namespace, op, query) => {
1883
+ assertNoStaleSelections(`ns.${namespace}.${op}`);
1585
1884
  const { args, include, omit, pick, depth } = splitQuery(query);
1586
1885
  const plan = planFoirNamespaceCall(await selectionSource(), {
1587
1886
  namespace,
@@ -1597,11 +1896,18 @@ function createFoirClient(clientConfig) {
1597
1896
  maybeCheckDrift();
1598
1897
  return result;
1599
1898
  };
1899
+ const subscriptionManifest = () => {
1900
+ if (!config.subscriptions) return manifest();
1901
+ return Promise.resolve({
1902
+ subscriptions: config.subscriptions,
1903
+ selectionUnions: config.selectionUnions
1904
+ });
1905
+ };
1600
1906
  const subscriptions = /* @__PURE__ */ (() => {
1601
1907
  const held = { router: null };
1602
1908
  return {
1603
1909
  get() {
1604
- held.router ??= new FoirSubscriptionRouter(config, manifest);
1910
+ held.router ??= new FoirSubscriptionRouter(config, subscriptionManifest);
1605
1911
  return held.router;
1606
1912
  },
1607
1913
  dispose() {
@@ -1629,7 +1935,7 @@ function createFoirClient(clientConfig) {
1629
1935
  return opProxy({ namespace });
1630
1936
  }
1631
1937
  });
1632
- const raw = async (document, variables) => {
1938
+ const raw = async (document, variables, options) => {
1633
1939
  if (typeof document !== "string" || !document.trim()) {
1634
1940
  throw new FoirRequestError("[foir] raw() takes a GraphQL document string");
1635
1941
  }
@@ -1637,7 +1943,8 @@ function createFoirClient(clientConfig) {
1637
1943
  config,
1638
1944
  document,
1639
1945
  isFoirMutationDocument(document),
1640
- variables
1946
+ variables,
1947
+ options
1641
1948
  );
1642
1949
  maybeCheckDrift();
1643
1950
  return result;
@@ -1666,16 +1973,236 @@ function createFoirClientFromEnv(env, overrides) {
1666
1973
  channel: isFoirPreview(env) ? "draft" : "published"
1667
1974
  });
1668
1975
  }
1976
+
1977
+ // src/data/decimal.ts
1978
+ var DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
1979
+ var MAX_SCALE = 1e3;
1980
+ function pow10(n) {
1981
+ return 10n ** BigInt(n);
1982
+ }
1983
+ function divRound(numerator, denominator, rounding) {
1984
+ if (denominator === 0n) throw new RangeError("FoirDecimal: division by zero");
1985
+ const negative = numerator < 0n !== denominator < 0n;
1986
+ const n = numerator < 0n ? -numerator : numerator;
1987
+ const d = denominator < 0n ? -denominator : denominator;
1988
+ let quotient = n / d;
1989
+ const remainder = n % d;
1990
+ if (remainder !== 0n && rounding !== "down") {
1991
+ const doubled = remainder * 2n;
1992
+ if (doubled > d) {
1993
+ quotient += 1n;
1994
+ } else if (doubled === d) {
1995
+ if (rounding === "half-up" || quotient % 2n === 1n) quotient += 1n;
1996
+ }
1997
+ }
1998
+ return negative ? -quotient : quotient;
1999
+ }
2000
+ function parse(input) {
2001
+ const text = input.trim();
2002
+ if (!DECIMAL_PATTERN.test(text)) {
2003
+ throw new TypeError(
2004
+ `FoirDecimal: ${JSON.stringify(input)} is not a decimal. Expected a numeric string such as "19.99".`
2005
+ );
2006
+ }
2007
+ const [mantissa, exponentText] = text.split(/[eE]/);
2008
+ const exponent = exponentText ? Number(exponentText) : 0;
2009
+ const negative = mantissa.startsWith("-");
2010
+ const unsigned = negative || mantissa.startsWith("+") ? mantissa.slice(1) : mantissa;
2011
+ const [integerPart = "", fractionPart = ""] = unsigned.split(".");
2012
+ let units = BigInt(`${integerPart || "0"}${fractionPart}`);
2013
+ let scale = fractionPart.length - exponent;
2014
+ if (scale < 0) {
2015
+ units *= pow10(-scale);
2016
+ scale = 0;
2017
+ }
2018
+ if (scale > MAX_SCALE) {
2019
+ throw new RangeError(`FoirDecimal: scale ${scale} exceeds the maximum of ${MAX_SCALE}`);
2020
+ }
2021
+ return { units: negative ? -units : units, scale };
2022
+ }
2023
+ var FoirDecimal = class _FoirDecimal {
2024
+ // Not `readonly` only so `#of` can populate a fresh instance without going
2025
+ // back through the string parser. Never mutated after construction, and
2026
+ // `#`-private so immutability is not part of the public contract to keep.
2027
+ #units;
2028
+ #scale;
2029
+ constructor(value) {
2030
+ if (value instanceof _FoirDecimal) {
2031
+ this.#units = value.#units;
2032
+ this.#scale = value.#scale;
2033
+ return;
2034
+ }
2035
+ if (typeof value === "bigint") {
2036
+ this.#units = value;
2037
+ this.#scale = 0;
2038
+ return;
2039
+ }
2040
+ if (typeof value !== "string") {
2041
+ throw new TypeError(
2042
+ `FoirDecimal: refusing to construct from ${typeof value}. A JavaScript number is a float and may already have lost precision \u2014 pass the string, or use FoirDecimal.fromNumber() to accept that loss explicitly.`
2043
+ );
2044
+ }
2045
+ const { units, scale } = parse(value);
2046
+ this.#units = units;
2047
+ this.#scale = scale;
2048
+ }
2049
+ /**
2050
+ * Build a decimal from a JavaScript number, accepting the precision that
2051
+ * number may already have lost.
2052
+ *
2053
+ * Deliberately not the constructor. It is for interop — a value read from a
2054
+ * third-party JSON API that emitted a float — and naming it at every call
2055
+ * site is the point. `fromNumber(0.1 + 0.2)` gives `0.30000000000000004`,
2056
+ * because that is genuinely the value it was handed.
2057
+ */
2058
+ static fromNumber(value) {
2059
+ if (!Number.isFinite(value)) {
2060
+ throw new RangeError(`FoirDecimal: ${value} is not a finite number`);
2061
+ }
2062
+ return new _FoirDecimal(value.toPrecision(17));
2063
+ }
2064
+ /** `true` if `value` parses as a decimal. Does not throw. */
2065
+ static isDecimalString(value) {
2066
+ return typeof value === "string" && DECIMAL_PATTERN.test(value.trim());
2067
+ }
2068
+ /** Decimal places carried by this value. `19.90` has scale 2. */
2069
+ get scale() {
2070
+ return this.#scale;
2071
+ }
2072
+ /** Re-express at `scale`, rounding away any digits that no longer fit. */
2073
+ rescale(scale, rounding = "half-up") {
2074
+ if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
2075
+ throw new RangeError(`FoirDecimal: scale must be an integer in 0..${MAX_SCALE}, got ${scale}`);
2076
+ }
2077
+ if (scale === this.#scale) return this;
2078
+ const units = scale > this.#scale ? this.#units * pow10(scale - this.#scale) : divRound(this.#units, pow10(this.#scale - scale), rounding);
2079
+ return _FoirDecimal.#of(units, scale);
2080
+ }
2081
+ add(other) {
2082
+ const [a, b, scale] = _FoirDecimal.#align(this, other);
2083
+ return _FoirDecimal.#of(a + b, scale);
2084
+ }
2085
+ sub(other) {
2086
+ const [a, b, scale] = _FoirDecimal.#align(this, other);
2087
+ return _FoirDecimal.#of(a - b, scale);
2088
+ }
2089
+ /** Exact: scales add, so `1.10 × 1.10` is `1.2100` rather than `1.21`. */
2090
+ mul(other) {
2091
+ const rhs = new _FoirDecimal(other);
2092
+ return _FoirDecimal.#of(this.#units * rhs.#units, this.#scale + rhs.#scale);
2093
+ }
2094
+ /** The only inexact operation — see {@link FoirDecimalDivOptions}. */
2095
+ div(other, options = {}) {
2096
+ const { scale = 20, rounding = "half-even" } = options;
2097
+ if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
2098
+ throw new RangeError(`FoirDecimal: scale must be an integer in 0..${MAX_SCALE}, got ${scale}`);
2099
+ }
2100
+ const rhs = new _FoirDecimal(other);
2101
+ const numerator = this.#units * pow10(rhs.#scale + scale);
2102
+ const denominator = rhs.#units * pow10(this.#scale);
2103
+ return _FoirDecimal.#of(divRound(numerator, denominator, rounding), scale);
2104
+ }
2105
+ abs() {
2106
+ return this.#units < 0n ? _FoirDecimal.#of(-this.#units, this.#scale) : this;
2107
+ }
2108
+ neg() {
2109
+ return _FoirDecimal.#of(-this.#units, this.#scale);
2110
+ }
2111
+ isZero() {
2112
+ return this.#units === 0n;
2113
+ }
2114
+ isNegative() {
2115
+ return this.#units < 0n;
2116
+ }
2117
+ /** `-1`, `0` or `1`. Use this rather than `<`, which throws. */
2118
+ cmp(other) {
2119
+ const [a, b] = _FoirDecimal.#align(this, other);
2120
+ return a < b ? -1 : a > b ? 1 : 0;
2121
+ }
2122
+ /** Numeric equality: `1.0` equals `1.00`. Scale is display, not identity. */
2123
+ equals(other) {
2124
+ return this.cmp(other) === 0;
2125
+ }
2126
+ /**
2127
+ * Fixed-place string, rounding `half-up` by default because this is the
2128
+ * display operation and that is what a person means by "to 2dp".
2129
+ */
2130
+ toFixed(places, rounding = "half-up") {
2131
+ return this.rescale(places, rounding).toString();
2132
+ }
2133
+ /**
2134
+ * The canonical wire form, preserving scale. Never exponent notation, so the
2135
+ * output always parses back to an equal value.
2136
+ */
2137
+ toString() {
2138
+ const negative = this.#units < 0n;
2139
+ const digits = (negative ? -this.#units : this.#units).toString();
2140
+ if (this.#scale === 0) return negative ? `-${digits}` : digits;
2141
+ const padded = digits.padStart(this.#scale + 1, "0");
2142
+ const integerPart = padded.slice(0, padded.length - this.#scale);
2143
+ const fractionPart = padded.slice(padded.length - this.#scale);
2144
+ return `${negative ? "-" : ""}${integerPart}.${fractionPart}`;
2145
+ }
2146
+ /** `JSON.stringify` emits the canonical string, not an object. Pinned by test. */
2147
+ toJSON() {
2148
+ return this.toString();
2149
+ }
2150
+ /**
2151
+ * Explicitly lossy — this is the boundary back into float, and naming it is
2152
+ * the point. Fine for a chart axis; never for money you will add up.
2153
+ */
2154
+ toNumber() {
2155
+ return Number(this.toString());
2156
+ }
2157
+ /**
2158
+ * Answers the `'string'` hint so template literals work, and throws on every
2159
+ * other hint so `+`, `<` and `Number()` fail loudly instead of silently
2160
+ * concatenating or truncating.
2161
+ */
2162
+ [Symbol.toPrimitive](hint) {
2163
+ if (hint === "string") return this.toString();
2164
+ throw new TypeError(
2165
+ hint === "number" ? "FoirDecimal: cannot be coerced to a number implicitly. Use .toNumber() to accept the precision loss, or .cmp() to compare." : "FoirDecimal: cannot be coerced implicitly. Use .add() / .sub() / .mul() / .div() for arithmetic, .cmp() to compare, or .toString() for text."
2166
+ );
2167
+ }
2168
+ get [Symbol.toStringTag]() {
2169
+ return "FoirDecimal";
2170
+ }
2171
+ /** Skip parsing when the units are already known to be well-formed. */
2172
+ static #of(units, scale) {
2173
+ const instance = new _FoirDecimal(0n);
2174
+ instance.#units = units;
2175
+ instance.#scale = scale;
2176
+ return instance;
2177
+ }
2178
+ /** Bring two values to a common scale so their units can be compared. */
2179
+ static #align(left, right) {
2180
+ const rhs = new _FoirDecimal(right);
2181
+ const scale = Math.max(left.#scale, rhs.#scale);
2182
+ return [
2183
+ left.#units * pow10(scale - left.#scale),
2184
+ rhs.#units * pow10(scale - rhs.#scale),
2185
+ scale
2186
+ ];
2187
+ }
2188
+ };
2189
+ function decimal(value) {
2190
+ return new FoirDecimal(value);
2191
+ }
1669
2192
  export {
2193
+ FoirDecimal,
1670
2194
  FoirRequestError,
2195
+ FoirSchemaDriftError,
1671
2196
  FoirSocketTransport,
1672
2197
  createFoirClient,
1673
2198
  createFoirClientFromEnv,
1674
2199
  createFoirSubjectRegistry,
2200
+ decimal,
1675
2201
  fetchFoirManifest,
1676
2202
  foirDataEndpoint,
1677
2203
  foirRoute,
1678
2204
  foirSocketEndpoint,
2205
+ graphql,
1679
2206
  planFoirModelCall,
1680
2207
  planFoirNamespaceCall,
1681
2208
  printSubscriptionDocument,