@foir/sdk 0.8.0 → 0.10.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 (57) hide show
  1. package/CHANGELOG.md +550 -28
  2. package/README.md +111 -0
  3. package/dist/auth/auth.d.ts +72 -1
  4. package/dist/auth/hosted.d.ts +93 -4
  5. package/dist/auth/index.d.ts +4 -4
  6. package/dist/auth/operations.d.ts +84 -0
  7. package/dist/auth/ops.d.ts +10 -0
  8. package/dist/auth.js +138 -19
  9. package/dist/client.js +63 -6
  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/content/content-renderer.d.ts +4 -5
  16. package/dist/content/index.d.ts +1 -1
  17. package/dist/content/richtext.d.ts +1 -1
  18. package/dist/data/builtins.d.ts +131 -12
  19. package/dist/data/client.d.ts +82 -1
  20. package/dist/data/decimal.d.ts +212 -0
  21. package/dist/data/document.d.ts +1 -1
  22. package/dist/data/index.d.ts +11 -9
  23. package/dist/data/manifest.d.ts +33 -6
  24. package/dist/data/schema.d.ts +2 -2
  25. package/dist/data/subscriptions.d.ts +36 -10
  26. package/dist/data/transport.d.ts +120 -3
  27. package/dist/data.js +377 -16
  28. package/dist/graphql/cdn-route.d.ts +1 -1
  29. package/dist/graphql/foir-request.d.ts +1 -1
  30. package/dist/live/coalesce.d.ts +57 -0
  31. package/dist/live/drift.d.ts +59 -0
  32. package/dist/live/events.d.ts +13 -0
  33. package/dist/live/headers.d.ts +53 -0
  34. package/dist/live/index.d.ts +54 -0
  35. package/dist/live/keys.d.ts +72 -0
  36. package/dist/live/manager.d.ts +77 -0
  37. package/dist/live/react/index.d.ts +143 -0
  38. package/dist/live/reconcile.d.ts +82 -0
  39. package/dist/live/resync.d.ts +59 -0
  40. package/dist/live/store.d.ts +134 -0
  41. package/dist/live/tagIndex.d.ts +29 -0
  42. package/dist/live/tanstack.d.ts +127 -0
  43. package/dist/live/writetoken.d.ts +86 -0
  44. package/dist/live-react.js +395 -0
  45. package/dist/live.js +845 -0
  46. package/dist/next/cacheTags.d.ts +245 -0
  47. package/dist/next/index.d.ts +7 -0
  48. package/dist/next.js +179 -6
  49. package/dist/push.js +81 -8
  50. package/dist/react.js +76 -5
  51. package/dist/realtime/index.d.ts +4 -5
  52. package/dist/server/events-client.d.ts +54 -4
  53. package/dist/server/index.d.ts +3 -2
  54. package/dist/server/require-scoped-token.d.ts +1 -1
  55. package/dist/server.js +454 -2
  56. package/dist/transport/foir-http.d.ts +113 -10
  57. package/package.json +10 -2
@@ -20,10 +20,10 @@
20
20
  * `models[key]` would skip them (an inexhaustive union, silently wrong at
21
21
  * runtime) or emit `never`.
22
22
  *
23
- * # These are ONE set, not two (TTS-1391)
23
+ * # These are ONE set, not two
24
24
  *
25
25
  * They used to be two: a value object for a plain field, and a separate
26
- * `<Name>Block` wrapper for the same thing inside a flexible field. TTS-1391
26
+ * `<Name>Block` wrapper for the same thing inside a flexible field. The rename
27
27
  * removed the wrapper — an item IS its value — so a plain `image` field and an
28
28
  * `image` inside a flexible field are the SAME GraphQL type, `Image`. There is
29
29
  * no `ImageValue` in a real schema and never will be again; the only difference
@@ -78,7 +78,7 @@ export interface Image {
78
78
  * `String` on the wire, not `String!`.
79
79
  *
80
80
  * It was non-null here and nullable on the flexible-field member until
81
- * TTS-1391, which is a §5.3.2 response-shape conflict the moment both are one
81
+ * the rename, which is a §5.3.2 response-shape conflict the moment both are one
82
82
  * type. The platform relaxed rather than tightened, because nullable is the
83
83
  * honest form — its own public `File` asset type already declares
84
84
  * `url: String` — so this follows.
@@ -132,6 +132,73 @@ export interface Richtext {
132
132
  json: FoirJsonValue | null;
133
133
  markdown: string | null;
134
134
  }
135
+ /**
136
+ * One run of styled text inside a passage.
137
+ *
138
+ * `marks` are named — `strong`, `em`, `code`, or a key into the passage's
139
+ * `markDefs` for anything that carries data (a link, and nothing else today).
140
+ * The bitmask the old editor stored is gone.
141
+ */
142
+ export interface ProseSpan {
143
+ __typename?: 'ProseSpan';
144
+ text: string | null;
145
+ marks: Array<string | null> | null;
146
+ }
147
+ /**
148
+ * A text entry in a prose flow — a paragraph, a heading, a quote, a list item.
149
+ *
150
+ * `kind` is the semantic shape and `token` is the design-token override that
151
+ * beats it when set. `list` + `level` carry list nesting flat rather than as a
152
+ * tree. A soft break inside a passage is a `\n` in a span's text, not a
153
+ * separate entry.
154
+ *
155
+ * `markDefs` stays an open JSON leaf by declaration: def types are extensible,
156
+ * and a typed object over an open shape would drop what it does not know.
157
+ */
158
+ export interface Passage {
159
+ __typename?: 'Passage';
160
+ _key: string | null;
161
+ kind: string | null;
162
+ token: string | null;
163
+ align: string | null;
164
+ list: string | null;
165
+ level: number | null;
166
+ language: string | null;
167
+ spans: Array<ProseSpan | null> | null;
168
+ markDefs: FoirJsonValue | null;
169
+ }
170
+ /**
171
+ * A prose field's read projection.
172
+ *
173
+ * `entries` is the typed reading and the one that keeps prose structured —
174
+ * `Passage` for a text-only field, and a union of `Passage` with the field's
175
+ * admitted embeds for one that places models. `html` and `markdown` are the
176
+ * passages only: an embed is omitted from both BY DECLARATION rather than
177
+ * rendered as invented markup, so a consumer that wants the embeds reads
178
+ * `entries`. `json` is the declared flow verbatim — the lossless escape hatch
179
+ * where an entry nobody recognises always survives.
180
+ *
181
+ * `E` is the entries element, filled in per field by `foir types`.
182
+ */
183
+ export interface Prose<E = Passage> {
184
+ html: string | null;
185
+ markdown: string | null;
186
+ json: FoirJsonValue | null;
187
+ entries: Array<E | null> | null;
188
+ }
189
+ /**
190
+ * One ancestor of a hierarchy record, root-first.
191
+ *
192
+ * Identity only, by design: a child's public URL already exposes every
193
+ * ancestor's slug, so the walk never widens content visibility. Anything more
194
+ * than identity goes through the `parent` reference like any other read.
195
+ */
196
+ export interface RecordAncestor {
197
+ __typename?: 'RecordAncestor';
198
+ _id: string;
199
+ _naturalKey: string | null;
200
+ _modelKey: string | null;
201
+ }
135
202
  /**
136
203
  * A reference whose target model is not pinned by the field's declaration.
137
204
  *
@@ -148,7 +215,7 @@ export interface Richtext {
148
215
  * 193 stored reference values carry an id and 92 carry a natural key and no id
149
216
  * — so a consumer must handle either.
150
217
  *
151
- * `_id` and `_type` are NOT here, and their absence is the TTS-1391 correction
218
+ * `_id` and `_type` are NOT here, and their absence is the correction
152
219
  * rather than an omission. They were the wrapper's own metadata: `_id` was the
153
220
  * ITEM's id (never the target's, which is why `_targetId` exists separately) and
154
221
  * `_type` was the union discriminator, reading `"reference"` on every reference
@@ -219,10 +286,10 @@ export interface CollabInfo {
219
286
  * The three wrappers below have no replacement, because the things they
220
287
  * described no longer exist:
221
288
  *
222
- * - `MediaBlock` — `media` stopped being a primitive at TTS-1391. It was
289
+ * - `MediaBlock` — `media` stopped being a primitive. It was
223
290
  * never in the proto FieldType enum, and it is the one primitive name a
224
291
  * project had also used as a model key.
225
- * - `TextBlock` — TTS-1406 removed it. GraphQL requires a union member to be
292
+ * - `TextBlock` — removed as well. GraphQL requires a union member to be
226
293
  * an object type and a text item's value is a bare scalar, which is the
227
294
  * whole reason a wrapper existed. An inline model with a text field is how
228
295
  * a text slot is authored.
@@ -300,7 +367,7 @@ export interface FoirDegraded<TTypename extends string = string> {
300
367
  * Four because depth is a budget against the server's 10,000-field ceiling and
301
368
  * four is past what a generated module usefully describes: on the live dev
302
369
  * project `product.findUnique` measures 318 fields at the default, 468 at 2 and
303
- * 618 at 3. Those levels all fit since TTS-1448 — they read 1,203 / 4,308 /
370
+ * 618 at 3. Those levels all fit now — they read 1,203 / 4,308 /
304
371
  * 13,623 when a union position was charged for every member — so the bound here
305
372
  * is about how much generated type surface is worth emitting, not about what
306
373
  * the server will run. A call
@@ -362,7 +429,8 @@ export interface FoirUpdateManyPayload {
362
429
  ids: string[];
363
430
  }
364
431
  /**
365
- * What a note is ABOUT — the closed set the platform's write path enforces.
432
+ * What a customer's note can be ABOUT: the customer-plane subset of the closed
433
+ * set the platform's write path enforces.
366
434
  *
367
435
  * On the wire it is `String!`, not a GraphQL enum, so the manifest publishes no
368
436
  * `enumValues` for it and `foir types` cannot generate this union. It is here
@@ -373,10 +441,14 @@ export interface FoirUpdateManyPayload {
373
441
  * with a model keyed `file` would make one value mean two things; a note on a
374
442
  * record is `'record'` plus the model key on `subjectModelKey`.
375
443
  *
376
- * This mirrors a platform-side set, and the obligation that comes with a mirror
377
- * (see the file header) applies: a kind added there widens this union in a
378
- * minor release, and until then a value outside it is a row no writer could
379
- * have produced.
444
+ * The platform set also holds `project` and `organization`, and this union
445
+ * deliberately does not. Both are operator constructs: the platform answers a
446
+ * note about either through a row policy no customer claim passes, so no note
447
+ * this surface reads or writes can carry them. This mirrors the part of the
448
+ * platform set a customer can reach, and the obligation that comes with a mirror
449
+ * (see the file header) applies: a kind the platform opens to customers widens
450
+ * this union in a minor release, and until then a value outside it is a row no
451
+ * customer writer could have produced.
380
452
  */
381
453
  export type FoirNoteSubjectType = 'record' | 'file' | 'model' | 'operation' | 'schedule' | 'customer';
382
454
  /**
@@ -483,6 +555,53 @@ export interface FoirCreateMyNoteArgs {
483
555
  */
484
556
  attachmentFileIds?: string[];
485
557
  }
558
+ /**
559
+ * `submitAnonymousNote` arguments — feedback from a visitor with no account.
560
+ *
561
+ * Present on the project's schema only while the project has opted in
562
+ * (Notes settings → "Accept anonymous notes"). The feedback widget does NOT
563
+ * use this: a signed-out visitor there is still asked to sign in, so this
564
+ * mutation is the only path a visitor's note can take.
565
+ *
566
+ * WRITE-ONLY. Nothing reads an anonymous note back on the public plane, which
567
+ * is why the result is a receipt rather than the note.
568
+ */
569
+ export interface FoirSubmitAnonymousNoteArgs {
570
+ /**
571
+ * Must be `'record'` — the only thing a visitor may write a note about.
572
+ * Narrower than `FoirCreateMyNoteArgs`, where a customer may also address a
573
+ * file or a model.
574
+ */
575
+ subjectType: 'record';
576
+ subjectId: string;
577
+ /**
578
+ * The record's model key. REQUIRED here, unlike on `createMyNote` where it is
579
+ * conditional: the subject is always a record on this lane, so the condition
580
+ * is always met.
581
+ */
582
+ subjectModelKey: string;
583
+ body: string;
584
+ /**
585
+ * Optional. Where the operators may reply.
586
+ *
587
+ * Captured as typed and NEVER verified — it is not treated as proof of who
588
+ * wrote the note, and nothing is sent to it to confirm it. Do not build a
589
+ * flow that assumes the address is reachable.
590
+ */
591
+ contactEmail?: string;
592
+ }
593
+ /** What `submitAnonymousNote` returns. */
594
+ export interface FoirAnonymousNoteReceipt {
595
+ /**
596
+ * A reference for this submission, to quote when following it up.
597
+ *
598
+ * NOT a handle: there is no read that takes it. Showing it to the visitor is
599
+ * the whole of its purpose.
600
+ */
601
+ reference: string;
602
+ /** When the platform recorded the submission. */
603
+ submittedAt: string;
604
+ }
486
605
  export type FoirDeviceType = 'DESKTOP' | 'MOBILE' | 'TABLET';
487
606
  export type FoirPlatformType = 'IOS' | 'ANDROID' | 'WEB';
488
607
  /**
@@ -95,6 +95,74 @@ export type FoirNamespaceClient<N extends {
95
95
  }> = {
96
96
  [OK in keyof N['ops']]: N['ops'][OK] extends FoirNamespaceOpDefinition ? FoirNamespaceOperation<N['ops'][OK]> : never;
97
97
  };
98
+ declare const foirDocumentResult: unique symbol;
99
+ declare const foirDocumentVariables: unique symbol;
100
+ /**
101
+ * A GraphQL document string that carries its own result and variable types.
102
+ *
103
+ * `raw` returns `unknown` because a hand-written document is opaque to the
104
+ * client, so every call site hand-writes a result type that nothing checks. That
105
+ * is the cost the escape hatch was never meant to have: the sites that most need
106
+ * type safety, the ones the typed client could not express, are the ones with
107
+ * none.
108
+ *
109
+ * A branded document moves that annotation from the call site to the document,
110
+ * once, next to the selection it describes. It is still an assertion rather than
111
+ * a proof, because nothing validates it against the live schema, but it is an
112
+ * assertion made in ONE place that every caller then inherits, and a wrong one
113
+ * is wrong somewhere a reader is looking at the query.
114
+ *
115
+ * The brands are phantom: they exist only in the type system, so a
116
+ * `TypedFoirDocument` is a plain string at runtime and can be logged, hashed or
117
+ * sent anywhere a string goes.
118
+ */
119
+ export type TypedFoirDocument<TResult, TVariables = Record<string, unknown>> = string & {
120
+ readonly [foirDocumentResult]: TResult;
121
+ readonly [foirDocumentVariables]: TVariables;
122
+ };
123
+ /**
124
+ * Brand a GraphQL document with the shape it returns.
125
+ *
126
+ * ```ts
127
+ * const RECENT = graphql<{ pages: { totalCount: number } }, { n: number }>(`
128
+ * query Recent($n: Int) { pages(first: $n) { totalCount } }
129
+ * `);
130
+ *
131
+ * const { pages } = await foir.raw(RECENT, { n: 5 });
132
+ * // ^? { totalCount: number }
133
+ * ```
134
+ *
135
+ * It does nothing at runtime beyond returning the string it was given. The value
136
+ * is entirely in the type, and in the fact that the type now lives beside the
137
+ * document rather than being retyped at each call.
138
+ *
139
+ * This does NOT make a bespoke selection type-checked against the schema. That
140
+ * is the larger half and wants generated documents rather than a helper. What it
141
+ * removes is the retyping, which is where the two real bugs came from: a result
142
+ * type that drifted from the selection it claimed to describe.
143
+ */
144
+ export declare function graphql<TResult = unknown, TVariables = Record<string, unknown>>(document: string): TypedFoirDocument<TResult, TVariables>;
145
+ /**
146
+ * Per-call options for {@link FoirClient.raw}.
147
+ */
148
+ export interface FoirRawOptions {
149
+ /**
150
+ * Treat a partial response as a failure.
151
+ *
152
+ * A GraphQL response can carry `errors` AND `data` together, and the default
153
+ * is to warn and return the data: a field the server could not resolve should
154
+ * not blank a whole page, and forward compatibility rests on it.
155
+ *
156
+ * On a hand-written document that default is the expensive one. There is no
157
+ * generated selection to have caught a wrong field name first, so selecting
158
+ * something that does not exist returns everything else and logs — a typo
159
+ * reads as a missing feature rather than a failure, and the hand-written
160
+ * result type asserts it was fine. `strict: true` turns that into a thrown
161
+ * `FoirRequestError`, which still carries the data on `partialData` so a
162
+ * caller can log what it declined to render.
163
+ */
164
+ strict?: boolean;
165
+ }
98
166
  export interface FoirClientCore<S extends FoirSchemaDefinition> {
99
167
  /**
100
168
  * Non-model root namespaces (`search`, `customers`, `notifications`).
@@ -126,7 +194,19 @@ export interface FoirClientCore<S extends FoirSchemaDefinition> {
126
194
  * );
127
195
  * ```
128
196
  */
129
- raw<T = unknown>(document: string, variables?: Record<string, unknown>): Promise<T>;
197
+ raw<TResult, TVariables extends Record<string, unknown>>(document: TypedFoirDocument<TResult, TVariables>, variables?: TVariables, options?: FoirRawOptions): Promise<TResult>;
198
+ /**
199
+ * The untyped form, for a document with no brand.
200
+ *
201
+ * The document type excludes a branded one deliberately. Without that, a
202
+ * branded call whose VARIABLES do not match falls through to this overload,
203
+ * whose `Record<string, unknown>` accepts anything, and the mistake compiles.
204
+ * The result would still be typed, so the call site reads as checked while the
205
+ * variables are not, which is the failure mode a brand exists to prevent.
206
+ */
207
+ raw<T = unknown>(document: string & {
208
+ readonly [foirDocumentResult]?: undefined;
209
+ }, variables?: Record<string, unknown>, options?: FoirRawOptions): Promise<T>;
130
210
  /**
131
211
  * Close the subscription socket and drop every live subscription.
132
212
  *
@@ -164,3 +244,4 @@ export declare function createFoirClient<S extends FoirSchemaDefinition = FoirSc
164
244
  export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition = FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<FoirClientConfig, 'apiUrl' | 'apiKey' | 'channel' | 'generated'> & {
165
245
  generated?: FoirGeneratedModule<S>;
166
246
  }): FoirClient<S>;
247
+ export {};
@@ -0,0 +1,212 @@
1
+ /**
2
+ * `FoirDecimal` — the exact numeric type a `decimal` field arrives as.
3
+ *
4
+ * A `decimal` field travels as a STRING on the wire, because JSON has one
5
+ * number type and every JavaScript consumer parses it into an IEEE-754 double
6
+ * whatever the server meant. Supabase demonstrates the failure: Postgres stores
7
+ * `numeric` exactly, PostgREST serialises it as a JSON number, and precision
8
+ * dies in `JSON.parse` before any application code runs. Exact storage with a
9
+ * lossy wire format buys nothing.
10
+ *
11
+ * So the string is the transport. It is deliberately NOT what you are handed:
12
+ * `"19.99" + "2.00"` is `"19.992.00"`, silently. This class is the thing that
13
+ * makes that unrepresentable.
14
+ *
15
+ * ---------------------------------------------------------------------------
16
+ * Representation
17
+ * ---------------------------------------------------------------------------
18
+ *
19
+ * A scaled `bigint`: `value = units / 10 ** scale`. `19.99` is
20
+ * `{ units: 1999n, scale: 2 }`. All arithmetic is integer arithmetic on
21
+ * `units`, so it is exact by construction rather than exact by care.
22
+ *
23
+ * `bigint` is ES2020 and the package targets ES2022, so this has NO dependency.
24
+ * That is deliberate: Prisma ships `Decimal.js` instances and prisma/prisma#16397
25
+ * has run for years because callers cannot get a `Decimal.js` whose version
26
+ * matches Prisma's. A type that crosses a package boundary should not carry a
27
+ * third party's identity with it.
28
+ *
29
+ * ---------------------------------------------------------------------------
30
+ * Scale is preserved, equality is numeric
31
+ * ---------------------------------------------------------------------------
32
+ *
33
+ * `decimal('19.90')` renders as `"19.90"`, not `"19.9"`. Trailing zeros are
34
+ * significant to anyone displaying money, and dropping them would make the
35
+ * round trip lossy in the one way people actually notice.
36
+ *
37
+ * But `equals` compares VALUES: `decimal('1.0').equals(decimal('1.00'))` is
38
+ * `true`. Textual comparison would be a trap of a different shape.
39
+ *
40
+ * ---------------------------------------------------------------------------
41
+ * Why `+` throws instead of concatenating
42
+ * ---------------------------------------------------------------------------
43
+ *
44
+ * `Symbol.toPrimitive` answers the `'string'` hint and throws on every other
45
+ * one. So:
46
+ *
47
+ * ```ts
48
+ * `Total: ${price}` // "Total: 19.99" — string hint, fine
49
+ * price + tax // TypeError, naming .add()
50
+ * price > limit // TypeError, naming .cmp()
51
+ * Number(price) // TypeError, naming .toNumber()
52
+ * ```
53
+ *
54
+ * TypeScript already rejects `price + tax` at compile time. The throw is for
55
+ * JavaScript callers and for anything that reaches this through `any`, where a
56
+ * silent `"19.992.00"` would otherwise reach a customer. Loud beats quiet: the
57
+ * whole reason this type exists is that the float bug was silent.
58
+ *
59
+ * `JSON.stringify` is answered by `toJSON`, so a decimal serialises back to the
60
+ * canonical string it arrived as — `{"price":"19.99"}`, never `{"price":{}}`.
61
+ * Prisma's most common support thread is that this behaviour was left implicit;
62
+ * it is pinned by a test here.
63
+ *
64
+ * ---------------------------------------------------------------------------
65
+ * If you are caching, cloning or comparing these, read this
66
+ * ---------------------------------------------------------------------------
67
+ *
68
+ * **Treat a decimal as an opaque leaf.** The state lives in `#`-private fields,
69
+ * which are invisible to every structural walk:
70
+ *
71
+ * ```ts
72
+ * Object.keys(price) // []
73
+ * Object.keys(structuredClone(price)) // [] <- no throw. silently {}.
74
+ * ```
75
+ *
76
+ * So a generic deep-clone, structural-share or deep-equal sees an EMPTY OBJECT.
77
+ * `structuredClone` does not throw, it just returns `{}`, and any equality that
78
+ * walks own enumerable keys calls two different decimals equal. This is the one
79
+ * that fails quietly, which on money is the worst place for it.
80
+ *
81
+ * Identity is `instanceof`. Equality is `.equals()`. Cloning is unnecessary —
82
+ * every operation returns a new instance and nothing mutates, so share the
83
+ * reference.
84
+ *
85
+ * **A JSON round trip returns a string, not a decimal.** Lossless in value,
86
+ * lossy in type:
87
+ *
88
+ * ```ts
89
+ * JSON.parse(JSON.stringify(price)) // "19.99", a string
90
+ * ```
91
+ *
92
+ * Rehydrate at the boundary — `decimal(value)` takes the string, and the
93
+ * generated types say which fields are decimals, so the revive is keyed on the
94
+ * schema rather than sniffed.
95
+ *
96
+ * **Scale is display, not identity — so normalise KEYS, never PAYLOADS.**
97
+ * `decimal('1.0').equals(decimal('1.00'))` is true while their `toString()`
98
+ * differs. Two consequences that pull in opposite directions:
99
+ *
100
+ * - Keying a cache on `toString()` splits one value into two entries. If you
101
+ * need a canonical key, `rescale()` to a fixed scale first.
102
+ * - But `rescale()` on a value you will SERVE rewrites what the author entered:
103
+ * `19.90` becomes `19.9`, and a price changes how it displays. Never
104
+ * normalise a stored payload.
105
+ *
106
+ * The cleanest way to hold both is to store the raw wire string and hydrate at
107
+ * the read boundary. The store then never holds two instances to choose
108
+ * between, and persistence is correct by construction.
109
+ */
110
+ /**
111
+ * How to resolve a value sitting exactly halfway between two representable
112
+ * results. Only `div`, `toFixed` and `rescale` can produce one — `add`, `sub`
113
+ * and `mul` are always exact.
114
+ *
115
+ * - `half-even` — banker's rounding. Ties go to the even neighbour, so a long
116
+ * run of them does not drift upward. The default for `div`, where the result
117
+ * is usually an intermediate value that will be summed.
118
+ * - `half-up` — ties go away from zero. What a person means by "round to 2dp",
119
+ * and the default for `toFixed`, which is a display operation.
120
+ * - `down` — truncate toward zero. Never rounds up, for the cases where over-
121
+ * stating is the expensive direction.
122
+ */
123
+ export type FoirDecimalRounding = 'half-even' | 'half-up' | 'down';
124
+ /** Options for {@link FoirDecimal.div}. */
125
+ export interface FoirDecimalDivOptions {
126
+ /**
127
+ * Decimal places in the result. Division is the one operation that cannot be
128
+ * exact — `1/3` has no finite decimal expansion — so a scale has to be named.
129
+ * Defaults to 20, which is wider than any currency and narrow enough to stay
130
+ * cheap.
131
+ */
132
+ scale?: number;
133
+ /** Defaults to `half-even`. */
134
+ rounding?: FoirDecimalRounding;
135
+ }
136
+ /** Anything that can be widened to a decimal without losing precision. */
137
+ export type FoirDecimalInput = string | bigint | FoirDecimal;
138
+ /**
139
+ * An exact decimal number.
140
+ *
141
+ * Construct from a string (the wire form), a `bigint`, or another decimal.
142
+ * A `number` is refused — see {@link FoirDecimal.fromNumber}.
143
+ */
144
+ export declare class FoirDecimal {
145
+ #private;
146
+ constructor(value: FoirDecimalInput);
147
+ /**
148
+ * Build a decimal from a JavaScript number, accepting the precision that
149
+ * number may already have lost.
150
+ *
151
+ * Deliberately not the constructor. It is for interop — a value read from a
152
+ * third-party JSON API that emitted a float — and naming it at every call
153
+ * site is the point. `fromNumber(0.1 + 0.2)` gives `0.30000000000000004`,
154
+ * because that is genuinely the value it was handed.
155
+ */
156
+ static fromNumber(value: number): FoirDecimal;
157
+ /** `true` if `value` parses as a decimal. Does not throw. */
158
+ static isDecimalString(value: unknown): value is string;
159
+ /** Decimal places carried by this value. `19.90` has scale 2. */
160
+ get scale(): number;
161
+ /** Re-express at `scale`, rounding away any digits that no longer fit. */
162
+ rescale(scale: number, rounding?: FoirDecimalRounding): FoirDecimal;
163
+ add(other: FoirDecimalInput): FoirDecimal;
164
+ sub(other: FoirDecimalInput): FoirDecimal;
165
+ /** Exact: scales add, so `1.10 × 1.10` is `1.2100` rather than `1.21`. */
166
+ mul(other: FoirDecimalInput): FoirDecimal;
167
+ /** The only inexact operation — see {@link FoirDecimalDivOptions}. */
168
+ div(other: FoirDecimalInput, options?: FoirDecimalDivOptions): FoirDecimal;
169
+ abs(): FoirDecimal;
170
+ neg(): FoirDecimal;
171
+ isZero(): boolean;
172
+ isNegative(): boolean;
173
+ /** `-1`, `0` or `1`. Use this rather than `<`, which throws. */
174
+ cmp(other: FoirDecimalInput): -1 | 0 | 1;
175
+ /** Numeric equality: `1.0` equals `1.00`. Scale is display, not identity. */
176
+ equals(other: FoirDecimalInput): boolean;
177
+ /**
178
+ * Fixed-place string, rounding `half-up` by default because this is the
179
+ * display operation and that is what a person means by "to 2dp".
180
+ */
181
+ toFixed(places: number, rounding?: FoirDecimalRounding): string;
182
+ /**
183
+ * The canonical wire form, preserving scale. Never exponent notation, so the
184
+ * output always parses back to an equal value.
185
+ */
186
+ toString(): string;
187
+ /** `JSON.stringify` emits the canonical string, not an object. Pinned by test. */
188
+ toJSON(): string;
189
+ /**
190
+ * Explicitly lossy — this is the boundary back into float, and naming it is
191
+ * the point. Fine for a chart axis; never for money you will add up.
192
+ */
193
+ toNumber(): number;
194
+ /**
195
+ * Answers the `'string'` hint so template literals work, and throws on every
196
+ * other hint so `+`, `<` and `Number()` fail loudly instead of silently
197
+ * concatenating or truncating.
198
+ */
199
+ [Symbol.toPrimitive](hint: string): string;
200
+ get [Symbol.toStringTag](): string;
201
+ }
202
+ /**
203
+ * Shorthand for `new FoirDecimal(value)`.
204
+ *
205
+ * ```ts
206
+ * import { decimal } from '@foir/sdk/data';
207
+ *
208
+ * const total = decimal(product.price).mul('3').add(shipping);
209
+ * console.log(total.toFixed(2)); // "62.97"
210
+ * ```
211
+ */
212
+ export declare function decimal(value: FoirDecimalInput): FoirDecimal;
@@ -215,7 +215,7 @@ export interface FoirNamespaceCall {
215
215
  * stopped in front of. Each level above 1 spends one of those stopping points.
216
216
  *
217
217
  * It is a BUDGET rather than a setting, though a much larger one since
218
- * TTS-1448: on a real project the same op measures 318 fields at level 1, 468
218
+ * On a real project the same op measures 318 fields at level 1, 468
219
219
  * at level 2 and 618 at level 3, against a server ceiling of 10,000. Those
220
220
  * were 837 / 3,513 / 14,217 while a union position was charged every member it
221
221
  * could hold rather than the one a value can be.
@@ -37,21 +37,21 @@
37
37
  * const anything = await foir.raw('{ designTokens { name value } }');
38
38
  * ```
39
39
  *
40
- * `FoirSchema` comes from `foir types` (TTS-875). The contract it generates
40
+ * `FoirSchema` comes from `foir types`. The contract it generates
41
41
  * against is written down in `schema.ts` — read that file before changing
42
42
  * anything here, because a generator is written against it.
43
43
  */
44
- export { createFoirClient, createFoirClientFromEnv } from './client';
45
- export type { FoirClient, FoirClientCore, FoirModelClient, FoirNamespaceClient } from './client';
44
+ export { createFoirClient, createFoirClientFromEnv, graphql } from './client';
45
+ export type { FoirClient, FoirClientCore, FoirModelClient, FoirNamespaceClient, FoirRawOptions, TypedFoirDocument, } from './client';
46
46
  export { fetchFoirManifest } from './manifest';
47
- export type { FoirManifest, FoirManifestComposite, FoirManifestModel, FoirManifestNamespace, FoirManifestOp, FoirManifestOpPayload, FoirManifestRelation, FoirManifestScalar, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionMember, FoirManifestSelectionUnion, FoirManifestSubscription, FoirManifestSubscriptionArgScope, FoirManifestUnclassifiedField, } from './manifest';
47
+ export type { FoirManifest, FoirManifestComposite, FoirManifestModel, FoirManifestNamespace, FoirManifestOp, FoirManifestOpPayload, FoirManifestRelation, FoirManifestScalar, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionMember, FoirManifestSelectionUnion, FoirManifestStructural, FoirManifestSubscription, FoirManifestSubscriptionArgScope, FoirManifestUnclassifiedField, } from './manifest';
48
48
  export { planFoirModelCall, planFoirNamespaceCall } from './document';
49
49
  export type { FoirCall, FoirDocumentPlan, FoirNamespaceCall, FoirNamespaceSelections, FoirRecordPath, FoirSelectionModel, FoirSelectionNamespace, FoirSelections, FoirSelectionSource, FoirSelectionUnions, } from './document';
50
50
  export { createFoirSubjectRegistry, recordFoirSubjects } from './subjects';
51
51
  export type { FoirSubject, FoirSubjectEntry, FoirSubjectOptions, FoirSubjectOrigin, FoirSubjectRegistry, } from './subjects';
52
- export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirCustomerNote, FoirCustomerNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteSubjectType, FoirPlatformType, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Reference, Richtext, Video, } from './builtins';
52
+ export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirSubmitAnonymousNoteArgs, FoirAnonymousNoteReceipt, FoirCustomerNote, FoirCustomerNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteSubjectType, FoirPlatformType, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
53
53
  /**
54
- * Pre-TTS-1391 spellings. The `<Name>Block` wrappers and the `<Name>Value`
54
+ * Pre-rename spellings. The `<Name>Block` wrappers and the `<Name>Value`
55
55
  * split are the same thing as the types above — an item IS its value — so each
56
56
  * of these is an alias, not a separate shape.
57
57
  *
@@ -60,14 +60,16 @@ export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAcces
60
60
  * emits, so there is nothing for them to alias. See `builtins.ts`.
61
61
  */
62
62
  export type { FileBlock, GeoPointValue, ImageBlock, ImageValue, ReferenceBlock, ReferenceValue, RichtextBlock, RichtextValue, VideoBlock, VideoValue, } from './builtins';
63
- export { foirDataEndpoint, foirRoute } from './transport';
63
+ export { FoirSchemaDriftError, foirDataEndpoint, foirRoute } from './transport';
64
64
  export type { FoirClientConfig, FoirGeneratedModule, FoirLane, FoirLaneReason, FoirRouteDecision, FoirRouteInput, FoirSchemaDrift, } from './transport';
65
65
  export { foirSocketEndpoint, FoirSocketTransport } from './ws-transport';
66
66
  export type { FoirReconnectPolicy, FoirSubscribeRequest, FoirSubscription, FoirSubscriptionHandlers, FoirWebSocketFactory, FoirWebSocketLike, } from './ws-transport';
67
67
  export { printSubscriptionDocument } from './subscription-document';
68
68
  export type { FoirSubscriptionArgument, PrintSubscriptionDocumentInput, } from './subscription-document';
69
- export type { FoirExecutionSubscribeOptions, FoirJobProgressEvent, FoirModelSubscriptions, FoirNamespaceSubscriptions, FoirNotificationEvent, FoirOperationExecutionEvent, FoirRecordChangedEvent, } from './subscriptions';
69
+ export type { FoirChangeSubscribeOptions, FoirExecutionSubscribeOptions, FoirJobProgressEvent, FoirModelSubscriptions, FoirNamespaceSubscriptions, FoirNotificationEvent, FoirOperationExecutionEvent, FoirRecordChangedEvent, } from './subscriptions';
70
70
  export type { FoirConnection, FoirFieldsShape, FoirModelDefinition, FoirNamespaceDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirRelationDefinition, FoirReturnDefinition, FoirSchemaDefinition, } from './schema';
71
71
  export type { FoirInclude, FoirModelOperation, FoirNamespaceOperation, FoirOmittableKey, FoirOpResult, FoirQuery, FoirRecordResult, } from './selection';
72
+ export { FoirDecimal, decimal } from './decimal';
73
+ export type { FoirDecimalDivOptions, FoirDecimalInput, FoirDecimalRounding, } from './decimal';
72
74
  export { FoirRequestError } from '../transport/foir-http';
73
- export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, } from '../transport/foir-http';
75
+ export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, } from '../transport/foir-http';
@@ -2,7 +2,7 @@
2
2
  * `GET /v1/manifest` — the per-caller surface descriptor, as a runtime type.
3
3
  *
4
4
  * This is the *runtime* mirror of `schema.ts`'s compile-time contract: what the
5
- * server actually sends, verbatim, so `foir types` (TTS-875) has a typed thing
5
+ * server actually sends, verbatim, so `foir types` has a typed thing
6
6
  * to read and the drift check has a version to compare. The manifest is already
7
7
  * the projection of `CachedSchema.SchemaForCaller(scopes)`, so what it lists is
8
8
  * exactly what this credential may do — no client-side filtering anywhere.
@@ -139,6 +139,23 @@ export interface FoirManifestOpPayload {
139
139
  */
140
140
  error?: string;
141
141
  }
142
+ /**
143
+ * A platform-owned structural field: a prose wrapper, or the hierarchy walk's
144
+ * `ancestors`. Nested and always returned, like a composite — but there is no
145
+ * model behind it, so the entry names its TYPE (`Prose`, `ProseCta`,
146
+ * `RecordAncestor`) and the shape is `builtins.ts`'s.
147
+ *
148
+ * ABSENT on a server that predates the bucket, where these fields landed in
149
+ * `unclassifiedFields` and dropped out of the selection entirely. Widen on
150
+ * absent, as everywhere else in this file.
151
+ */
152
+ export interface FoirManifestStructural {
153
+ key: string;
154
+ type: string;
155
+ list: boolean;
156
+ nullable: boolean;
157
+ deprecated?: boolean;
158
+ }
142
159
  /** A built field the projection has no bucket for. Reported rather than dropped. */
143
160
  export interface FoirManifestUnclassifiedField {
144
161
  key: string;
@@ -172,12 +189,14 @@ export interface FoirManifestModel {
172
189
  scalars: FoirManifestScalar[];
173
190
  relations: FoirManifestRelation[];
174
191
  composites: FoirManifestComposite[];
192
+ /** Platform-owned structural fields — prose wrappers, `ancestors`. */
193
+ structurals?: FoirManifestStructural[];
175
194
  /**
176
- * Block-union field key → member model keys. Members that are built-in block
177
- * primitives appear under their lowercase token (`text`, `image`, `video`,
178
- * `file`, `richtext`, `media`, `reference`, `flexible`) and have NO entry in
179
- * `models` — they are platform-owned, identical in every project, and typed
180
- * statically by `builtins.ts`.
195
+ * Union field key → member model keys. Members that are platform primitives
196
+ * appear under their lowercase token (`richtext`, `image`, `video`, `file`,
197
+ * `reference`, `geopoint`) and have NO entry in `models` — they are
198
+ * platform-owned, identical in every project, and typed statically by
199
+ * `builtins.ts`.
181
200
  */
182
201
  unions?: Record<string, string[]>;
183
202
  /** Op names of this model's typed lookups. Each also appears in `ops`. */
@@ -256,6 +275,14 @@ export interface FoirManifestNamespace {
256
275
  export interface FoirManifest {
257
276
  schemaVersion: number;
258
277
  channel: 'published' | 'draft';
278
+ /**
279
+ * The organisation whose composition this surface is, absent for the
280
+ * project's own. A credential confined to an organisation is served that
281
+ * organisation's surface without naming it, so this is how a reader of the
282
+ * manifest, or of a module generated from it, tells whose definitions it
283
+ * describes.
284
+ */
285
+ composedFor?: string;
259
286
  models: Record<string, FoirManifestModel>;
260
287
  /**
261
288
  * Keyed by namespace name (`auth`, `customers`, `files`, `notifications`,