@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
@@ -13,6 +13,11 @@
13
13
  * anything else — `sk_`, a customer bearer, a mutation, the draft channel
14
14
  * → `POST /graphql` with the `x-api-key` header and the APQ body.
15
15
  *
16
+ * a call made AS an operation, with its dispatch token
17
+ * → `POST /graphql` with `X-Foir-Token` and the dispatch's `X-Foir-Context`,
18
+ * and no `x-api-key`. Always this lane: the shared one takes its
19
+ * credential from the URL, and a dispatch token has no key to put there.
20
+ *
16
21
  * All five conditions the server's shared-lane gate refuses on are evaluated
17
22
  * HERE, locally: this code is constructing the URL, it can read the key prefix,
18
23
  * it knows whether a bearer resolved, the manifest told it whether the op is a
@@ -55,10 +60,11 @@
55
60
  * with more force here — this client is per-credential, so anything cached
56
61
  * across instances is cached across tenants.
57
62
  */
58
- import { type FoirTokenProvider } from '../transport/foir-http';
63
+ import { FoirRequestError, type FoirTokenProvider } from '../transport/foir-http';
59
64
  import type { FoirDocumentPlan, FoirNamespaceSelections, FoirSelections, FoirSelectionUnions } from './document';
60
65
  import type { FoirSchemaDefinition } from './schema';
61
66
  import type { FoirSubjectRegistry } from './subjects';
67
+ import type { FoirManifestSubscription } from './manifest';
62
68
  import type { FoirReconnectPolicy, FoirWebSocketFactory } from './ws-transport';
63
69
  /**
64
70
  * `foir.generated.ts`, whole — the shape a namespace import of it has.
@@ -83,18 +89,103 @@ import type { FoirReconnectPolicy, FoirWebSocketFactory } from './ws-transport';
83
89
  * Its runtime value is `undefined` by design and nothing reads it.
84
90
  */
85
91
  export interface FoirGeneratedModule<S extends FoirSchemaDefinition = FoirSchemaDefinition> {
86
- /** `foirSchemaVersion` — the manifest version these types came from. Drives the drift check. */
87
- readonly foirSchemaVersion: number;
92
+ /**
93
+ * `foirSurfaceVersion`: the surface these types were generated from, a hash
94
+ * over exactly the parts of the manifest `foir types` reads. Drives the drift
95
+ * check, which compares it with the one the server is serving now.
96
+ */
97
+ readonly foirSurfaceVersion: string;
98
+ /**
99
+ * `foirSurfaceHashes`: the components that version is folded from, keyed as
100
+ * the manifest keys them (`models.<key>`, `namespaces.<name>`, …). What lets a
101
+ * drift name the models that moved rather than just say something did.
102
+ */
103
+ readonly foirSurfaceHashes: Readonly<Record<string, string>>;
88
104
  /** `foirSelections` — one entry per model, in the printer's own input shape. */
89
105
  readonly foirSelections: FoirSelections;
90
106
  /** `foirNamespaceSelections` — the same, per namespace op. */
91
107
  readonly foirNamespaceSelections: FoirNamespaceSelections;
92
108
  /** `foirSelectionUnions` — each union expanded once; a tree references one by name. */
93
109
  readonly foirSelectionUnions: FoirSelectionUnions;
110
+ /**
111
+ * `foirSubscriptions` — the Subscription root fields this credential reaches,
112
+ * with their resolved payload selections. Lets a subscribing client print its
113
+ * document from the module instead of fetching `GET /v1/manifest`.
114
+ *
115
+ * OPTIONAL, and deliberately not in `GENERATED_EXPORTS`. That list is checked
116
+ * strictly — a module missing any of it is refused as stale or hand-assembled
117
+ * — so requiring this one would break every client built against a module
118
+ * generated before it existed, at construction, for a round trip they were
119
+ * already happily paying. Absent means "fetch the manifest", which is exactly
120
+ * what those clients do today.
121
+ */
122
+ readonly foirSubscriptions?: readonly FoirManifestSubscription[];
94
123
  /** Type carrier for `FoirSchema`. Never read: its runtime value is `undefined`. */
95
124
  readonly foirSchemaType: S;
125
+ /**
126
+ * `foirCredential`: present when the module was generated for an operation
127
+ * (`foir types --operation <key>`), naming it. Its types are the surface that
128
+ * operation's dispatch token is served, so the module is for a client built
129
+ * from that token and for no other: a dispatch client requires it, and a key
130
+ * client refuses it. Absent from a module generated for a key.
131
+ */
132
+ readonly foirCredential?: FoirOperationCredential;
133
+ }
134
+ /**
135
+ * What a module `foir types --operation <key>` generated says about itself:
136
+ * the operation whose dispatch token its types describe.
137
+ */
138
+ export interface FoirOperationCredential {
139
+ readonly operation: string;
96
140
  }
97
- export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDefinition> {
141
+ /** A module generated for an api key: the kind a key client takes. */
142
+ export type FoirKeyGeneratedModule<S extends FoirSchemaDefinition = FoirSchemaDefinition> = FoirGeneratedModule<S> & {
143
+ readonly foirCredential?: undefined;
144
+ };
145
+ /** A module generated for an operation: the kind a dispatch client takes. */
146
+ export type FoirOperationGeneratedModule<S extends FoirSchemaDefinition = FoirSchemaDefinition> = FoirGeneratedModule<S> & {
147
+ readonly foirCredential: FoirOperationCredential;
148
+ };
149
+ /**
150
+ * The credential of a client built inside an operation endpoint: the dispatch
151
+ * token the platform handed it.
152
+ *
153
+ * ```ts
154
+ * const { dispatch } = await requireScopedToken(request, { expectedProjectId });
155
+ * const foir = createFoirClient({ apiUrl, dispatch, generated });
156
+ * ```
157
+ *
158
+ * The endpoint calls Foir AS the operation, with the capabilities the operation
159
+ * declared, and holds no key of its own. api-public accepts the token as a
160
+ * caller in its own right: it builds the schema overlay from the token's
161
+ * capabilities, and it reads the organization and the customer from the
162
+ * token's claims. So nothing a key client configures beside its key
163
+ * (`bearerToken`, `orgId`, `channel`) means anything here, and none of it is
164
+ * accepted.
165
+ *
166
+ * `requireScopedToken` returns one of these, built from the token it verified.
167
+ * Build one by hand only when the token reaches you some other way.
168
+ */
169
+ export interface FoirDispatchCredential {
170
+ /**
171
+ * The dispatch token, sent as `X-Foir-Token`. Resolved before every request,
172
+ * so a caller holding a refreshed token hands the client the current one
173
+ * rather than the one it was built with. A provider that yields nothing fails
174
+ * the request: there is no other credential to fall back to.
175
+ */
176
+ token: FoirTokenProvider;
177
+ /**
178
+ * The dispatch's `X-Foir-Context` header, forwarded as it arrived so the
179
+ * requests the operation makes carry the run's correlation metadata. Pass
180
+ * `request.headers.get('X-Foir-Context')` as it is; absent is fine.
181
+ */
182
+ context?: string | null;
183
+ }
184
+ /**
185
+ * A client that calls Foir with an api key: a storefront's `pk_`, a server's
186
+ * `sk_`, optionally for a signed-in customer.
187
+ */
188
+ export interface FoirKeyClientConfig<S extends FoirSchemaDefinition = FoirSchemaDefinition> {
98
189
  /**
99
190
  * Foir public API origin, e.g. `https://api.foir.dev`.
100
191
  *
@@ -105,10 +196,13 @@ export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDef
105
196
  apiUrl: string;
106
197
  /** `pk_…` or `sk_…`. The manifest, and therefore the generated types, are shaped by it. */
107
198
  apiKey: string;
199
+ /** A key client has no dispatch token; see {@link FoirDispatchClientConfig}. */
200
+ dispatch?: never;
108
201
  /**
109
- * Read the draft schema channel (`X-Foir-Schema-Channel: draft`). Needs a
110
- * `sk_` key with `drafts:read`; the header is ignored under public keys.
111
- * Defaults to `'published'`.
202
+ * Read the draft schema channel (`X-Foir-Schema-Channel: draft`). Needs a key
203
+ * holding `drafts:read`: a preview environment's own `pk_`, or an `sk_`. A
204
+ * public key without it is served the published copy; a secret key without
205
+ * it is refused. Defaults to `'published'`.
112
206
  */
113
207
  channel?: 'published' | 'draft';
114
208
  /**
@@ -117,6 +211,68 @@ export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDef
117
211
  * per-request; a bare string is used as-is.
118
212
  */
119
213
  bearerToken?: string | FoirTokenProvider;
214
+ /**
215
+ * The organization every request from this client acts in, sent as
216
+ * `X-Org-Id`.
217
+ *
218
+ * For the PROJECT credential of a product built on organizations: the
219
+ * back-office, or the multi-tenant app that has just resolved
220
+ * `<sub>.theapp.com` to an organization and now acts inside it. The server
221
+ * resolves the id against the credential's OWN project — an unknown,
222
+ * suspended or another project's organization all answer `NOT_FOUND`, so
223
+ * nothing enumerates — and binds the request to it: reads are confined to
224
+ * that organization plus the project-shared tier, writes are stamped into
225
+ * it, and an organization-owned record created without an explicit owner
226
+ * belongs to it.
227
+ *
228
+ * ⚠️ IT NAMES A SUBTREE, NOT A NODE. Organizations nest, and reach runs
229
+ * downward: name a reseller and the reads include every client beneath it as
230
+ * well as the reseller's own rows. Name the client and you get the client
231
+ * and nothing above or beside it. So the id to send is the NARROWEST one
232
+ * that covers what this request is about — sending the group when you meant
233
+ * one branch is not an error anywhere, it is a wider answer that looks
234
+ * right.
235
+ *
236
+ * A credential already confined to an organization may only name its own
237
+ * (`PERMISSION_DENIED` otherwise), so this cannot steer an organization key
238
+ * elsewhere. And a request that also carries a customer bearer is refused
239
+ * unless that customer stands in the named organization. The storefront
240
+ * pattern is still the confined credential, one per organization; this is
241
+ * for the side of the product that holds the project key and has to choose.
242
+ *
243
+ * Routing: a request naming an organization always goes to the origin
244
+ * endpoint, never the shared one. The answer is one organization's, and a
245
+ * cache in front of the caller keys by URL, where this header does not
246
+ * appear.
247
+ */
248
+ orgId?: string;
249
+ /**
250
+ * The organization every request from this client acts in, named by the
251
+ * address of the page it is for, sent as `X-Org-Url`.
252
+ *
253
+ * For an app that serves many organizations from its own addresses: a path
254
+ * per organization (`saasapp.com/soho`), a subdomain each, or a domain an
255
+ * organization brought. Pass the page URL and the server finds the
256
+ * organization whose registered address (its app base URL, or a verified
257
+ * custom domain bound to it) is the longest prefix of that URL at a
258
+ * path-segment boundary, so `saasapp.com/soho` answers for `/soho/menu` and
259
+ * never for `/sohonews`. It works with the publishable key the page already
260
+ * ships.
261
+ *
262
+ * ```ts
263
+ * const foir = createFoirClient({ apiUrl, apiKey, generated, orgUrl: window.location.href });
264
+ * ```
265
+ *
266
+ * It is `orgId` by another spelling and bounded the same way: a URL no
267
+ * organization's address covers answers `NOT_FOUND` exactly as an unknown id
268
+ * does, a credential confined to an organization is answered about that
269
+ * organization alone, and the organization is a selection rather than
270
+ * authority. Sent with `orgId`, the two must name the same organization.
271
+ * `resolveOrganizationAddress(url)` answers the same id up front.
272
+ *
273
+ * Routing: always the origin endpoint, for the reason `orgId` is.
274
+ */
275
+ orgUrl?: string;
120
276
  /**
121
277
  * The generated module, whole — **the documented way to wire a client**.
122
278
  *
@@ -125,8 +281,8 @@ export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDef
125
281
  * const foir = createFoirClient({ apiUrl, apiKey, generated });
126
282
  * ```
127
283
  *
128
- * It supplies `schemaVersion`, `selections`, `namespaceSelections` and
129
- * `selectionUnions` in one member, and carries `FoirSchema` with it so the
284
+ * It supplies `surfaceVersion`, `surfaceHashes`, `selections`,
285
+ * `namespaceSelections` and `selectionUnions` in one member, and carries `FoirSchema` with it so the
130
286
  * type parameter is inferred rather than restated. An artifact added to
131
287
  * `foir types` later then costs a call site nothing.
132
288
  *
@@ -139,8 +295,12 @@ export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDef
139
295
  * Omitting `generated` AND `selections` asks for the manifest fallback, which
140
296
  * is supported, announced (see `announceManifestFallback`) and never reached
141
297
  * by accident.
298
+ *
299
+ * A module generated for an operation (`foir types --operation`) is refused
300
+ * here: its types are what that operation's dispatch token is served, not
301
+ * what this key is.
142
302
  */
143
- generated?: FoirGeneratedModule<S>;
303
+ generated?: FoirKeyGeneratedModule<S>;
144
304
  /**
145
305
  * The runtime half of `foir.generated.ts` — `foirSelections`, the resolved
146
306
  * complete-by-default selection per model.
@@ -187,7 +347,11 @@ export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDef
187
347
  * `subjects.add(...)`.
188
348
  */
189
349
  subjects?: FoirSubjectRegistry;
190
- /** Extra headers, applied last so they can override the ones above. */
350
+ /**
351
+ * Extra headers, applied last so they can override the ones above. A client
352
+ * built from a dispatch token refuses a credential header here (`x-api-key`,
353
+ * `Authorization`, `X-Foir-Token`, `X-Foir-Context`).
354
+ */
191
355
  headers?: Readonly<Record<string, string>>;
192
356
  /**
193
357
  * `fetch` implementation. Defaults to `globalThis.fetch`. Injectable for
@@ -201,15 +365,69 @@ export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDef
201
365
  */
202
366
  requestInit?: Omit<RequestInit, 'method' | 'headers' | 'body'>;
203
367
  /**
204
- * `foirSchemaVersion` from the generated module. When set, the client
205
- * compares it against the server's manifest once and warns on mismatch.
206
- * Carried by `generated`; set it here only to override that.
368
+ * `foirSurfaceVersion` from the generated module. When set, the client
369
+ * compares it against the surface the server is serving, once, and reports
370
+ * drift when they differ. Carried by `generated`; set it here only to
371
+ * override that.
207
372
  */
208
- schemaVersion?: number;
209
- /** Set `false` to suppress the drift check even when `schemaVersion` is set. */
373
+ surfaceVersion?: string;
374
+ /**
375
+ * `foirSurfaceHashes` from the generated module, so a drift can name what
376
+ * moved. Carried by `generated`; set it here only to override that.
377
+ */
378
+ surfaceHashes?: Readonly<Record<string, string>>;
379
+ /**
380
+ * Subscription descriptors, from the generated module's `foirSubscriptions`.
381
+ *
382
+ * Carried by `generated`; set it here only to override that. When present a
383
+ * subscribing client prints from these and never fetches `GET /v1/manifest`;
384
+ * when absent it fetches once, as it always has.
385
+ */
386
+ subscriptions?: readonly FoirManifestSubscription[];
387
+ /** Set `false` to suppress the drift check even when `surfaceVersion` is set. */
210
388
  checkSchemaDrift?: boolean;
211
389
  /** Called instead of `console.warn` when drift is detected. */
212
390
  onSchemaDrift?: (drift: FoirSchemaDrift) => void;
391
+ /**
392
+ * Refuse typed calls once drift is detected, instead of serving the stale
393
+ * shape. **On by default**; pass `false` to opt out.
394
+ *
395
+ * The asymmetry this closes: an unknown ARGUMENT is refused client-side
396
+ * before the server sees it, while a field the server has and the generated
397
+ * selection does not is simply not requested. The second is the common case
398
+ * when a schema evolves, and it surfaces as absent data rather than as a
399
+ * tooling problem — so it is the one that needs to be loud.
400
+ *
401
+ * It is the default rather than an opt-in because the thing it prevents is
402
+ * SILENT. An opt-in guard against a silent failure only protects the people
403
+ * who already suspected the failure existed, and the warning it replaces is
404
+ * one more line to scroll past.
405
+ *
406
+ * Two things it deliberately does not do.
407
+ *
408
+ * It does not fail the FIRST call. Drift is only known once the manifest
409
+ * comes back, and that request is made off the response path after a call has
410
+ * already been served. A client that refused retroactively would have to
411
+ * block every first call on a second round trip, which is the cost the
412
+ * warning was designed to avoid.
413
+ *
414
+ * It does not touch `raw`. A hand-written document asks for exactly the
415
+ * fields it names, so it cannot silently omit one — raw is immune to this
416
+ * class of bug by construction, and refusing it would punish the escape
417
+ * hatch people reach for precisely because it is immune.
418
+ *
419
+ * Turning it off is never the remedy for a drift. The version it compares
420
+ * is the SURFACE the types were generated from, which moves only when
421
+ * regenerating would write a different file, so every drift it reports is a
422
+ * real one and the fix is `foir types`. The opt-out exists for an app that
423
+ * knowingly ships a stale module for a while, and it says so at the call
424
+ * site, where a reader can see it:
425
+ *
426
+ * ```ts
427
+ * createFoirClient({ strictSchemaDrift: false }) // serve stale, warn only
428
+ * ```
429
+ */
430
+ strictSchemaDrift?: boolean;
213
431
  /**
214
432
  * Set `false` to silence the one-line notice a client prints when it is built
215
433
  * with neither `generated` nor `selections` and will therefore read its
@@ -260,12 +478,86 @@ export interface FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDef
260
478
  */
261
479
  connectionParams?: () => Record<string, string> | Promise<Record<string, string>>;
262
480
  }
481
+ /**
482
+ * The options that belong to a key: the key itself, what it is presented with
483
+ * (a customer bearer, an organization, a channel), and the subscription socket,
484
+ * which only a key can open.
485
+ */
486
+ type FoirKeySideOption = 'apiKey' | 'dispatch' | 'channel' | 'bearerToken' | 'orgId' | 'orgUrl' | 'wsUrl' | 'webSocketImpl' | 'reconnect' | 'connectionParams';
487
+ /**
488
+ * A client that calls Foir AS an operation, from inside the endpoint the
489
+ * operation dispatched to. See {@link FoirDispatchCredential}.
490
+ *
491
+ * Everything a key client takes except the key-side options, which are typed
492
+ * `never` rather than left out, so passing one is a compile error that names
493
+ * it. No `apiKey` above all: the dispatch token is the credential, and an
494
+ * endpoint that holds a key as well has become the server in the middle the
495
+ * operation exists to replace.
496
+ */
497
+ export interface FoirDispatchClientConfig<S extends FoirSchemaDefinition = FoirSchemaDefinition> extends Omit<FoirKeyClientConfig<S>, FoirKeySideOption | 'generated'> {
498
+ /** The dispatch token this endpoint was handed, and the dispatch's context header. */
499
+ dispatch: FoirDispatchCredential;
500
+ /**
501
+ * The module `foir types --operation <key>` generated for this operation.
502
+ * Required: its types are the surface this operation's token is served, and
503
+ * a module generated for a key would type calls the token is refused. A
504
+ * dispatch whose context names a different operation is refused too.
505
+ *
506
+ * ```ts
507
+ * // foir types --operation checkout --out src/checkout.generated.ts
508
+ * import * as generated from './checkout.generated';
509
+ * const foir = createFoirClient({ apiUrl, dispatch, generated });
510
+ * ```
511
+ */
512
+ generated: FoirOperationGeneratedModule<S>;
513
+ apiKey?: never;
514
+ channel?: never;
515
+ bearerToken?: never;
516
+ orgId?: never;
517
+ orgUrl?: never;
518
+ wsUrl?: never;
519
+ webSocketImpl?: never;
520
+ reconnect?: never;
521
+ connectionParams?: never;
522
+ }
523
+ /**
524
+ * How a data client is configured: with an api key, or with an operation's
525
+ * dispatch token. Exactly one of the two. They are different actors, and there
526
+ * is no configuration that accepts either.
527
+ */
528
+ export type FoirClientConfig<S extends FoirSchemaDefinition = FoirSchemaDefinition> = FoirKeyClientConfig<S> | FoirDispatchClientConfig<S>;
529
+ /**
530
+ * The credential and transport half of a client config, which is everything a
531
+ * request reads. A manifest fetch takes this rather than a whole client config:
532
+ * what it asks for does not depend on a generated module, and a dispatch token
533
+ * asks for its own surface before any module exists.
534
+ */
535
+ export type FoirRequestConfig = Omit<FoirKeyClientConfig, 'generated'> | Omit<FoirDispatchClientConfig, 'generated'>;
263
536
  export interface FoirSchemaDrift {
264
- /** Manifest version the local types were generated from. */
265
- generated: number;
266
- /** Manifest version the server is serving now. */
267
- server: number;
537
+ /** The surface version the local types were generated from. */
538
+ generated: string;
539
+ /** The surface version the server is serving now. */
540
+ server: string;
268
541
  channel: string;
542
+ /**
543
+ * What moved, as the manifest keys its components: `models.<key>`,
544
+ * `namespaces.<name>`, `selectionUnions.<name>`, `subscriptions`,
545
+ * `composedFor`. A component added or removed since generation is listed
546
+ * too. Empty only when the module was wired without `surfaceHashes`.
547
+ */
548
+ changed: readonly string[];
549
+ }
550
+ /**
551
+ * Thrown by a typed call under `strictSchemaDrift` once drift is detected.
552
+ *
553
+ * A subclass rather than a plain `FoirRequestError` so an app can tell "your
554
+ * generated module is stale" from "this request failed", which want different
555
+ * handling: the first is fixed by a command on the developer's machine and
556
+ * will fail identically until they run it, the second is worth a retry.
557
+ */
558
+ export declare class FoirSchemaDriftError extends FoirRequestError {
559
+ readonly drift: FoirSchemaDrift;
560
+ constructor(message: string, drift: FoirSchemaDrift);
269
561
  }
270
562
  /**
271
563
  * Build a `/v1/...` URL from whatever the caller put in `apiUrl`.
@@ -276,15 +568,46 @@ export interface FoirSchemaDrift {
276
568
  * than a re-export so `@foir/sdk/data`'s public surface is unchanged.
277
569
  */
278
570
  export declare function foirDataEndpoint(apiUrl: string, path: `/${string}`): string;
279
- /** Resolve `config.bearerToken`, whether it is a string, a function, or absent. */
280
- export declare function resolveBearerToken(config: Pick<FoirClientConfig, 'bearerToken'>): Promise<string | undefined>;
571
+ /** One request's credential, resolved now. */
572
+ export interface FoirResolvedAuth {
573
+ /** The headers the request carries, `extra` included. */
574
+ headers: Record<string, string>;
575
+ /**
576
+ * The key and the resolved bearer, when the credential is a key. Absent for a
577
+ * dispatch token, which has no key to put in a URL and so never takes the
578
+ * shared lane.
579
+ */
580
+ key?: {
581
+ apiKey: string;
582
+ bearerToken?: string;
583
+ };
584
+ }
585
+ /**
586
+ * The credential a request carries, resolved at the moment it is sent.
587
+ *
588
+ * The one place every transport (`/graphql`, `GET /v1/manifest`, the socket's
589
+ * `connection_init`) asks, so the three cannot drift into authenticating a
590
+ * client differently. Providers are called here, once per request, and not at
591
+ * construction.
592
+ */
593
+ export declare function resolveFoirAuth(config: FoirRequestConfig): Promise<FoirResolvedAuth>;
594
+ /**
595
+ * Refuse a config that names no credential, or two.
596
+ *
597
+ * The types already say this. It is checked again at runtime for a caller in
598
+ * plain JavaScript, or one who spread two configs together, because the
599
+ * failure it prevents is quiet: a client handed both a key and a dispatch token
600
+ * would send one and discard the other, and which actor made each request
601
+ * would depend on a precedence rule nobody chose.
602
+ */
603
+ export declare function assertFoirCredential(config: FoirClientConfig): void;
281
604
  /** Which GraphQL endpoint a call is sent to. */
282
605
  export type FoirLane = 'shared' | 'origin';
283
606
  /**
284
607
  * Why a call did not take the shared lane. One name per condition the server's
285
608
  * gate refuses on, so a decision can be asserted rather than inferred.
286
609
  */
287
- export type FoirLaneReason = 'key' | 'bearer' | 'operation' | 'channel';
610
+ export type FoirLaneReason = 'key' | 'bearer' | 'org' | 'operation' | 'channel' | 'floor';
288
611
  export interface FoirRouteDecision {
289
612
  lane: FoirLane;
290
613
  /** Set only on the origin lane: the first condition that ruled the shared one out. */
@@ -294,8 +617,21 @@ export interface FoirRouteInput {
294
617
  apiKey: string;
295
618
  /** The RESOLVED bearer, not the provider — a provider that yields null is not a bearer. */
296
619
  bearerToken?: string;
620
+ /** The organization the call acts in (`X-Org-Id`), when it names one. */
621
+ orgId?: string;
622
+ /** The page URL the call names its organization by (`X-Org-Url`), when it does. */
623
+ orgUrl?: string;
297
624
  mutation: boolean;
298
625
  channel?: 'published' | 'draft';
626
+ /**
627
+ * A write token from an earlier mutation, making this read a FLOORED one:
628
+ * serve a cached entry only if it resolved at or above the write's position.
629
+ *
630
+ * The token itself is opaque here — resolving it to a seq is the server's job
631
+ * and needs the event log. All this needs to know is that one is present,
632
+ * because that alone decides the lane.
633
+ */
634
+ writeToken?: string;
299
635
  }
300
636
  /**
301
637
  * Where this call goes.
@@ -322,4 +658,7 @@ export declare function executeFoirDocument<T>(config: FoirClientConfig, plan: F
322
658
  * what it gives up is the single-root-field unwrapping, because a document
323
659
  * written by hand may legitimately have several.
324
660
  */
325
- export declare function executeFoirRawDocument<T>(config: FoirClientConfig, document: string, mutation: boolean, variables?: Record<string, unknown>): Promise<T>;
661
+ export declare function executeFoirRawDocument<T>(config: FoirClientConfig, document: string, mutation: boolean, variables?: Record<string, unknown>, options?: {
662
+ strict?: boolean;
663
+ }): Promise<T>;
664
+ export {};
@@ -41,9 +41,15 @@
41
41
  * `config.connectionParams`, which is a FUNCTION and is called on every
42
42
  * connect — so a short-lived credential is re-resolved on reconnect instead of
43
43
  * being replayed after it expired.
44
+ *
45
+ * An operation's dispatch token is not one of them. The server's
46
+ * `connection_init` authenticates a key, an admin token or a customer bearer
47
+ * and has no path for a dispatch token, so a client built from one is refused
48
+ * here, by name, rather than opening a socket the server will close as "Not
49
+ * authenticated".
44
50
  */
45
51
  import { FoirRequestError } from '../transport/foir-http';
46
- import { type FoirClientConfig } from './transport';
52
+ import { type FoirRequestConfig } from './transport';
47
53
  /**
48
54
  * The slice of `WebSocket` this transport uses.
49
55
  *
@@ -143,7 +149,7 @@ export declare class FoirSocketTransport {
143
149
  private disposed;
144
150
  private keepAlive;
145
151
  private retryTimer;
146
- constructor(config: FoirClientConfig);
152
+ constructor(config: FoirRequestConfig);
147
153
  /**
148
154
  * Start a subscription.
149
155
  *