@foir/sdk 0.7.0 → 0.8.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 (60) hide show
  1. package/CHANGELOG.md +805 -4
  2. package/README.md +46 -15
  3. package/dist/auth/auth.d.ts +104 -1
  4. package/dist/auth/index.d.ts +12 -6
  5. package/dist/auth/operations.d.ts +85 -64
  6. package/dist/auth/ops.d.ts +99 -0
  7. package/dist/auth.js +775 -106
  8. package/dist/chunk-IGNLWT3H.js +35 -0
  9. package/dist/chunk-OHYVKTGB.js +45 -0
  10. package/dist/chunk-TAL5H6VA.js +39 -0
  11. package/dist/client.js +25 -31
  12. package/dist/codegen/block-registry.d.ts +13 -19
  13. package/dist/codegen/block-registry.js +3 -31
  14. package/dist/codegen/block-scaffold.d.ts +12 -21
  15. package/dist/codegen/block-scaffold.js +3 -41
  16. package/dist/codegen/index.d.ts +9 -4
  17. package/dist/codegen/index.js +7 -11
  18. package/dist/codegen/item-registry.d.ts +28 -0
  19. package/dist/codegen/item-registry.js +7 -0
  20. package/dist/codegen/item-scaffold.d.ts +29 -0
  21. package/dist/codegen/item-scaffold.js +7 -0
  22. package/dist/codegen/preset.d.ts +14 -7
  23. package/dist/codegen/shared.d.ts +20 -9
  24. package/dist/content/content-renderer.d.ts +90 -24
  25. package/dist/content/index.d.ts +13 -7
  26. package/dist/content/richtext.d.ts +66 -18
  27. package/dist/content.js +227 -80
  28. package/dist/data/builtins.d.ts +302 -92
  29. package/dist/data/client.d.ts +97 -25
  30. package/dist/data/document.d.ts +251 -0
  31. package/dist/data/index.d.ts +38 -19
  32. package/dist/data/manifest.d.ts +266 -3
  33. package/dist/data/schema.d.ts +78 -8
  34. package/dist/data/selection.d.ts +231 -37
  35. package/dist/data/subjects.d.ts +221 -0
  36. package/dist/data/subscription-document.d.ts +75 -0
  37. package/dist/data/subscriptions.d.ts +181 -0
  38. package/dist/data/transport.d.ts +255 -37
  39. package/dist/data/ws-transport.d.ts +197 -0
  40. package/dist/data.js +1419 -45
  41. package/dist/graphql/cdn-route.d.ts +12 -28
  42. package/dist/media/index.d.ts +6 -2
  43. package/dist/media/media.d.ts +2 -2
  44. package/dist/media/screen-capture.d.ts +81 -0
  45. package/dist/media.js +97 -0
  46. package/dist/next.js +663 -182
  47. package/dist/push.js +700 -90
  48. package/dist/react/FoirFeedback.d.ts +62 -0
  49. package/dist/react/feedback-notes.d.ts +101 -0
  50. package/dist/react/index.d.ts +7 -1
  51. package/dist/react.js +1272 -194
  52. package/dist/realtime.js +1 -1
  53. package/dist/server/events-client.d.ts +279 -0
  54. package/dist/server/index.d.ts +4 -0
  55. package/dist/server.js +298 -0
  56. package/dist/transport/foir-http.d.ts +23 -6
  57. package/dist/transport/persisted-query.d.ts +68 -0
  58. package/dist/transport/retry.d.ts +1 -1
  59. package/package.json +13 -1
  60. package/dist/chunk-ADO5FJNM.js +0 -30
@@ -7,24 +7,27 @@
7
7
  * give four projects four chances to disagree about `ImageValue`.
8
8
  *
9
9
  * 1. **Value objects.** `GET /v1/manifest` reports a scalar's `type` as a
10
- * GraphQL named type, and eight of the names it can use are platform-owned
11
- * objects rather than leaves: `ImageValue`, `VideoValue`, `FileValue`,
12
- * `MediaValue`, `RichtextValue`, `GeoPointValue`, `ReferenceValue`,
13
- * `CollabInfo`. They are structurally nested but behave as leaves — always
14
- * present, never lazily hydrated, never `include`-able.
15
- *
16
- * 2. **Block primitives.** A block union's members are *model keys*, except
17
- * for eight lowercase tokens — `text`, `image`, `video`, `file`,
18
- * `richtext`, `media`, `reference`, `flexible` — which are members of the
19
- * real GraphQL union but have **no entry in `models`**. On the live dev
20
- * Marketing project, 7 of `tilly_page.blocks`'s 29 members are these. A
21
- * generator that resolved every member to `models[key]` would skip them
22
- * (an inexhaustive union, silently wrong at runtime) or emit `never`.
23
- *
24
- * Both sets are the manifest's own prescription, not this file's invention: the
25
- * payload reference says of the value objects that a client "ships static types
26
- * for them rather than generating them per project", and names the eight block
27
- * tokens as having no `models` entry by design.
10
+ * GraphQL named type, and several of the names it can use are
11
+ * platform-owned objects rather than leaves: `Image`, `Video`, `FileValue`,
12
+ * `Richtext`, `GeoPoint`, `Reference`, `CollabInfo`. They are structurally
13
+ * nested but behave as leaves — always present, never lazily hydrated,
14
+ * never `include`-able.
15
+ *
16
+ * 2. **Primitive members.** A flexible field's union members are *model keys*,
17
+ * except for six lowercase tokens — `richtext`, `image`, `video`, `file`,
18
+ * `reference`, `geopoint` — which are members of the real GraphQL union but
19
+ * have **no entry in `models`**. A generator that resolved every member to
20
+ * `models[key]` would skip them (an inexhaustive union, silently wrong at
21
+ * runtime) or emit `never`.
22
+ *
23
+ * # These are ONE set, not two (TTS-1391)
24
+ *
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
27
+ * removed the wrapper — an item IS its value — so a plain `image` field and an
28
+ * `image` inside a flexible field are the SAME GraphQL type, `Image`. There is
29
+ * no `ImageValue` in a real schema and never will be again; the only difference
30
+ * between the two positions is whether `__typename` was selected.
28
31
  *
29
32
  * ---------------------------------------------------------------------------
30
33
  * The obligation that comes with a static table
@@ -42,6 +45,7 @@
42
45
  * Field shapes verified against deployed dev's introspection on 2026-07-28 and
43
46
  * against `api-public/internal/schema/field_mapping.go`.
44
47
  */
48
+ import type { FoirConnection } from './schema';
45
49
  /** A `JSON` scalar. Deliberately not `any`: reading through it needs a check. */
46
50
  export type FoirJsonValue = string | number | boolean | null | FoirJsonValue[] | {
47
51
  [key: string]: FoirJsonValue;
@@ -69,8 +73,18 @@ export interface ImageVariantSet {
69
73
  _3xl: ImageVariant | null;
70
74
  _4xl: ImageVariant | null;
71
75
  }
72
- export interface ImageValue {
73
- url: string;
76
+ export interface Image {
77
+ /**
78
+ * `String` on the wire, not `String!`.
79
+ *
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
82
+ * type. The platform relaxed rather than tightened, because nullable is the
83
+ * honest form — its own public `File` asset type already declares
84
+ * `url: String` — so this follows.
85
+ */
86
+ __typename?: 'Image';
87
+ url: string | null;
74
88
  width: number | null;
75
89
  height: number | null;
76
90
  alt: string | null;
@@ -86,8 +100,9 @@ export interface ImageValue {
86
100
  /** AVIF tier set (smaller, narrower support). The preferred `<picture>` source. */
87
101
  avif: ImageVariantSet | null;
88
102
  }
89
- export interface VideoValue {
90
- url: string;
103
+ export interface Video {
104
+ __typename?: 'Video';
105
+ url: string | null;
91
106
  thumbnailUrl: string | null;
92
107
  previewUrl: string | null;
93
108
  hlsManifestUrl: string | null;
@@ -100,28 +115,62 @@ export interface VideoValue {
100
115
  fileSize: number | null;
101
116
  }
102
117
  export interface FileValue {
118
+ __typename?: 'FileValue';
103
119
  url: string | null;
104
120
  filename: string | null;
105
121
  mimeType: string | null;
106
122
  size: number | null;
107
123
  }
108
- export interface GeoPointValue {
124
+ export interface GeoPoint {
125
+ __typename?: 'GeoPoint';
109
126
  lat: number | null;
110
127
  lng: number | null;
111
128
  }
112
- export interface RichtextValue {
129
+ export interface Richtext {
130
+ __typename?: 'Richtext';
113
131
  html: string | null;
114
132
  json: FoirJsonValue | null;
115
133
  markdown: string | null;
116
134
  }
117
135
  /**
118
- * A foreign key, and the reason a consumer never writes `include` to get one:
119
- * this arrives with the record, complete, under the complete-by-default
120
- * selection model. `post.author._id` is always there and always typed present.
136
+ * A reference whose target model is not pinned by the field's declaration.
137
+ *
138
+ * A reference field that declares ONE `referenceTypes` entry resolves to that
139
+ * model's own type, and one declaring several resolves to a `<Members>Ref`
140
+ * union — so this type is what an undeclared reference resolves to, and what a
141
+ * flexible field's `reference` member is.
142
+ *
143
+ * # What makes it followable
144
+ *
145
+ * `_targetModelKey` names the model and `_targetId` OR `_targetNaturalKey`
146
+ * identifies the row within it: `foir[_targetModelKey].findUnique({ where: { _id:
147
+ * _targetId } })`. Both identifier shapes are live — on the dev database 101 of
148
+ * 193 stored reference values carry an id and 92 carry a natural key and no id
149
+ * — so a consumer must handle either.
150
+ *
151
+ * `_id` and `_type` are NOT here, and their absence is the TTS-1391 correction
152
+ * rather than an omission. They were the wrapper's own metadata: `_id` was the
153
+ * ITEM's id (never the target's, which is why `_targetId` exists separately) and
154
+ * `_type` was the union discriminator, reading `"reference"` on every reference
155
+ * ever stored. The wrapper is gone, so neither field is in the schema, and a
156
+ * consumer selecting them got `undefined` while the types promised a string.
121
157
  */
122
- export interface ReferenceValue {
123
- _id: string | null;
124
- _type: string | null;
158
+ export interface Reference<TRecord = FoirUnexpandedRecord> {
159
+ __typename?: 'Reference';
160
+ /** The model to fetch the target from. Null when the target no longer exists. */
161
+ _targetModelKey: string | null;
162
+ /** The target's id, when the reference stored one. */
163
+ _targetId: string | null;
164
+ /** The target's natural key, when the reference names its target that way. */
165
+ _targetNaturalKey: string | null;
166
+ /**
167
+ * The resolved target, over the project's `Record` union — every model in the
168
+ * project. Left as a type parameter so the generated file can name that
169
+ * union; the default is the honest answer for a consumer who has not narrowed
170
+ * it.
171
+ */
172
+ record: TRecord | null;
173
+ /** A stored SPELLING of the model. Read as an input to `_targetModelKey`. */
125
174
  _schema: string | null;
126
175
  naturalKey: string | null;
127
176
  _preview: FoirJsonValue | null;
@@ -153,69 +202,54 @@ export interface CollabInfo {
153
202
  roomName: string;
154
203
  }
155
204
  /**
156
- * The metadata every block member carries, and the two discriminants an
157
- * exhaustive `switch` narrows on.
158
- *
159
- * `__typename` is the one to prefer. The op compiler emits it on the union's
160
- * own selection set for every union it expands, and it is the only field that
161
- * cannot be `omit`ed (no omit path can name it). `_type` is the foir block-type
162
- * token — the model key for a model-derived member, the lowercase primitive
163
- * token otherwise — and is deliberately NOT protected from `omit` by the
164
- * server, so a caller who omits it loses the second discriminant while keeping
165
- * the first.
166
- */
167
- export interface FoirBlockMeta<TType extends string, TTypename extends string> {
168
- __typename: TTypename;
169
- _type: TType;
170
- _id: string | null;
171
- _key: string | null;
172
- _label: string | null;
173
- }
174
- export interface RichtextBlock extends FoirBlockMeta<'richtext', 'RichtextBlock'>, RichtextValue {
175
- }
176
- export interface ImageBlock extends FoirBlockMeta<'image', 'ImageBlock'>, Omit<ImageValue, 'url'> {
177
- /**
178
- * Nullable here, `String!` on `ImageValue`. Wrapping a value type as a block
179
- * drops NonNull (`wrapValueFieldsAsBlock`), and reporting it non-null would
180
- * be a lie a consumer's generated types cannot recover from.
181
- */
182
- url: string | null;
183
- }
184
- export interface VideoBlock extends FoirBlockMeta<'video', 'VideoBlock'>, Omit<VideoValue, 'url'> {
185
- /** Nullable here, `String!` on `VideoValue` — see `ImageBlock.url`. */
186
- url: string | null;
187
- }
188
- export interface FileBlock extends FoirBlockMeta<'file', 'FileBlock'>, FileValue {
189
- }
190
- export interface MediaBlock extends FoirBlockMeta<'media', 'MediaBlock'>, MediaValue {
191
- }
192
- export interface TextBlock extends FoirBlockMeta<'text', 'TextBlock'> {
193
- value: string | null;
194
- }
195
- /**
196
- * A reference to another record.
205
+ * `Image`, `Video`, `FileValue`, `Richtext`, `GeoPoint` and `Reference` above
206
+ * ARE the members. There is no separate item type, because an item is its
207
+ * value: `... on Image { url }` means the same thing inside a flexible field
208
+ * and at a plain `image` field, and one fragment reads it in both positions.
197
209
  *
198
- * `record` is the resolved target, over the project's `Record` union — every
199
- * model in the project. It is left as a type parameter so the generated file
200
- * can name that union; the default is the honest answer for a consumer who has
201
- * not narrowed it.
202
- */
203
- export interface ReferenceBlock<TRecord = FoirUnexpandedRecord> extends FoirBlockMeta<'reference', 'ReferenceBlock'>, Omit<ReferenceValue, '_id' | '_type'> {
204
- record: TRecord | null;
205
- }
206
- /**
207
- * The fallback member, for a `flexible` field in a project with no block types
208
- * to offer. Its `_type` is whatever the stored value carried, so — uniquely
209
- * among the primitives — it is not a literal.
210
+ * `__typename` is optional on each, matching what graphql-codegen emits: it is
211
+ * present whenever selected, and the op compiler always selects it on a union's
212
+ * own selection set — it is the one field no `omit` path can name. Narrowing on
213
+ * it is exact regardless, because the literal types are disjoint.
214
+ *
215
+ * `_type` was the second discriminant and is gone with the envelope. It was
216
+ * never protected from `omit` anyway, so `__typename` was always the one to
217
+ * prefer.
218
+ *
219
+ * The three wrappers below have no replacement, because the things they
220
+ * described no longer exist:
221
+ *
222
+ * - `MediaBlock` — `media` stopped being a primitive at TTS-1391. It was
223
+ * never in the proto FieldType enum, and it is the one primitive name a
224
+ * project had also used as a model key.
225
+ * - `TextBlock` — TTS-1406 removed it. GraphQL requires a union member to be
226
+ * an object type and a text item's value is a bare scalar, which is the
227
+ * whole reason a wrapper existed. An inline model with a text field is how
228
+ * a text slot is authored.
229
+ * - `FlexibleBlock` — there is no fallback member. An item whose `_type`
230
+ * names nothing is nulled out of its list and reported, which is what a
231
+ * stored null already read as.
210
232
  */
211
- export interface FlexibleBlock {
212
- __typename: 'FlexibleBlock';
213
- _type: string | null;
214
- _id: string | null;
215
- _key: string | null;
216
- _label: string | null;
217
- value: FoirJsonValue | null;
218
- }
233
+ /** @deprecated Renamed to {@link Image}. The wrapper is gone; this IS the image. */
234
+ export type ImageBlock = Image;
235
+ /** @deprecated Renamed to {@link Video}. */
236
+ export type VideoBlock = Video;
237
+ /** @deprecated Renamed to {@link FileValue}. */
238
+ export type FileBlock = FileValue;
239
+ /** @deprecated Renamed to {@link Richtext}. */
240
+ export type RichtextBlock = Richtext;
241
+ /** @deprecated Renamed to {@link GeoPoint}. */
242
+ export type GeoPointValue = GeoPoint;
243
+ /** @deprecated Renamed to {@link Image}. */
244
+ export type ImageValue = Image;
245
+ /** @deprecated Renamed to {@link Video}. */
246
+ export type VideoValue = Video;
247
+ /** @deprecated Renamed to {@link Richtext}. */
248
+ export type RichtextValue = Richtext;
249
+ /** @deprecated Renamed to {@link Reference}. */
250
+ export type ReferenceValue<TRecord = FoirUnexpandedRecord> = Reference<TRecord>;
251
+ /** @deprecated Renamed to {@link Reference}. */
252
+ export type ReferenceBlock<TRecord = FoirUnexpandedRecord> = Reference<TRecord>;
219
253
  /**
220
254
  * What a nested union or a re-entered object resolves to.
221
255
  *
@@ -225,11 +259,65 @@ export interface FlexibleBlock {
225
259
  * `tilly_page.blocks → TillyPageBlock.blocks` cycle is reachable in two hops
226
260
  * with no `include` and no caller input at all. At a degraded position the
227
261
  * response carries `__typename` and nothing else, so that is what the generated
228
- * type says. Reach the rest with `foir.raw()`.
262
+ * type says.
263
+ *
264
+ * # Reading past one
265
+ *
266
+ * A stopping point in front of a UNION can be spent a level on:
267
+ * `foir.product.findMany({ depth: 2 })` expands one more, and the response then
268
+ * carries that union's members here rather than `__typename` alone. A level
269
+ * costs one more expansion at each stopping point inside the expansion — so it
270
+ * compounds where there are several, and costs nothing per union MEMBER — and
271
+ * the call is refused client-side, with the arithmetic, when it would cross the
272
+ * server's field ceiling. See `FoirNamespaceCall.depth`.
273
+ *
274
+ * The TYPE follows the level. A generated composite on the path to a stopping
275
+ * point takes a `<D>` level parameter (see `FoirDepthLevel`), so at `depth: 2`
276
+ * the position resolves to the expanded union — `<Field>Blocks`, the type
277
+ * exported beside the `<Field>BlocksDegraded` named here — rather than to this
278
+ * one. The caller narrows on `__typename` and reads the member's own field; no
279
+ * cast, and no hand-written widening.
280
+ *
281
+ * `D` defaults to `1`, so a call passing no `depth` reaches THIS type at the
282
+ * stopping point exactly as it always did.
283
+ *
284
+ * A stopping point that is a composite CYCLE rather than a union — `menu.items
285
+ * .children` — has no union to expand and stays `__typename` at every depth.
229
286
  */
230
287
  export interface FoirDegraded<TTypename extends string = string> {
231
288
  __typename: TTypename;
232
289
  }
290
+ /**
291
+ * The union levels the generated types describe — the type-level half of
292
+ * `depth`.
293
+ *
294
+ * A generated composite on the path to a stopping point takes one of these as
295
+ * a parameter, defaulted to `1`, and the model's shape at each level is named
296
+ * under `FoirModelDefinition.atDepth`. `1` is the complete-by-default document
297
+ * and is what every existing reference to a generated interface resolves to,
298
+ * which is why adding the parameter changed no result type.
299
+ *
300
+ * Four because depth is a budget against the server's 10,000-field ceiling and
301
+ * four is past what a generated module usefully describes: on the live dev
302
+ * 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 /
304
+ * 13,623 when a union position was charged for every member — so the bound here
305
+ * is about how much generated type surface is worth emitting, not about what
306
+ * the server will run. A call
307
+ * asking for more is not an error: `atDepth` simply has no entry, the result
308
+ * falls back to the default shape, and the caller narrows by hand exactly as
309
+ * they did before. Under-promising is the safe direction, and the wire is
310
+ * unaffected — the printer's budget is a runtime number with no cap here.
311
+ */
312
+ export type FoirDepthLevel = 1 | 2 | 3 | 4;
313
+ /**
314
+ * `FoirDepthBelow[3] === 2` — one union level spent.
315
+ *
316
+ * Mirrors the printer's `depth - 1` at an expanded stopping point
317
+ * (`document.ts`, `printFields`), and lives beside `FoirDepthLevel` so the two
318
+ * cannot disagree about how many levels exist.
319
+ */
320
+ export type FoirDepthBelow = [never, never, 1, 2, 3];
233
321
  /** A `Record`-union value the compiler did not expand. See `FoirDegraded`. */
234
322
  export type FoirUnexpandedRecord = FoirDegraded;
235
323
  /**
@@ -273,6 +361,128 @@ export interface FoirUpdateManyPayload {
273
361
  count: number;
274
362
  ids: string[];
275
363
  }
364
+ /**
365
+ * What a note is ABOUT — the closed set the platform's write path enforces.
366
+ *
367
+ * On the wire it is `String!`, not a GraphQL enum, so the manifest publishes no
368
+ * `enumValues` for it and `foir types` cannot generate this union. It is here
369
+ * for the same reason the value objects are: platform-owned, identical in every
370
+ * project, and otherwise hand-written once per consumer.
371
+ *
372
+ * It is never a MODEL KEY. Model keys are the project's to choose, so a project
373
+ * with a model keyed `file` would make one value mean two things; a note on a
374
+ * record is `'record'` plus the model key on `subjectModelKey`.
375
+ *
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.
380
+ */
381
+ export type FoirNoteSubjectType = 'record' | 'file' | 'model' | 'operation' | 'schedule' | 'customer';
382
+ /**
383
+ * One file the customer submitted with their note.
384
+ *
385
+ * The file's own facts, and nothing about the platform's handling of it: when
386
+ * the link row was written and which retention clock it starts are not the
387
+ * author's business. What is here is enough to render "you sent this".
388
+ */
389
+ export interface FoirCustomerNoteAttachment {
390
+ /** The file's id — the same one `createMyNote` was given. */
391
+ fileId: string;
392
+ filename: string;
393
+ mimeType: string;
394
+ /** Size in bytes, as stored. Authoritative; not the number the uploader declared. */
395
+ size: number;
396
+ url: string;
397
+ }
398
+ /**
399
+ * One note on the customer plane — `foir.ns.notes.myNotes` and
400
+ * `foir.ns.notes.createMyNote`.
401
+ *
402
+ * Deliberately narrower than the note your team sees in the dashboard. There is
403
+ * no visibility, no resolution note, no mentions and no metadata: those are the
404
+ * project's view of a thread, not the author's, and the customer-facing type
405
+ * does not merely omit them from the document — the surface does not carry them.
406
+ */
407
+ export interface FoirCustomerNote {
408
+ id: string;
409
+ /** The note's prose. */
410
+ body: string | null;
411
+ subjectType: FoirNoteSubjectType;
412
+ subjectId: string;
413
+ /** The record's model key. Present only on a `record` subject. */
414
+ subjectModelKey: string | null;
415
+ /** Set when this note is a reply within a thread. */
416
+ parentNoteId: string | null;
417
+ threadRootId: string | null;
418
+ isResolved: boolean;
419
+ /**
420
+ * The files submitted with this note. Empty when there are none.
421
+ *
422
+ * The one place the customer surface is WIDER than "the author's own prose",
423
+ * and deliberately: an author who attached a screenshot has to be able to see
424
+ * that it arrived, or the only readings left to them are that it failed
425
+ * silently or that it was taken away.
426
+ */
427
+ attachments: FoirCustomerNoteAttachment[];
428
+ /** ISO-8601. */
429
+ createdAt: string;
430
+ }
431
+ /**
432
+ * What a note surface has to know about the project before it renders anything
433
+ * — `foir.ns.notes.noteConfig`.
434
+ *
435
+ * Charged `notes:write` rather than `notes:read`, and refused to a caller with
436
+ * no customer session, because the only caller it is useful to is one that can
437
+ * submit. So a composer can read it as its single readiness probe: an answer
438
+ * means this visitor may write, and a refusal names which half is missing.
439
+ */
440
+ export interface FoirNoteConfig {
441
+ /**
442
+ * Whether this project's note surfaces may OFFER to capture a screenshot.
443
+ *
444
+ * It gates the offer, not the storing. Capture stays opt-in per submission —
445
+ * the browser's own picker is the consent gate and it cannot be bypassed.
446
+ */
447
+ screenshotsEnabled: boolean;
448
+ }
449
+ /** `myNotes` arguments. Forward-only cursor pagination, like every list read. */
450
+ export interface FoirMyNotesArgs {
451
+ first?: number;
452
+ after?: string;
453
+ /** Narrow to one kind of subject. */
454
+ subjectType?: FoirNoteSubjectType;
455
+ /** Narrow to one subject — a record id, a file id, a model key. */
456
+ subjectId?: string;
457
+ /** Read one thread. Returns only the notes in it this caller can see. */
458
+ threadRootId?: string;
459
+ }
460
+ /**
461
+ * `myNotes` — the notes this customer wrote, plus the replies your team
462
+ * deliberately sent them. Never another customer's, and never an operator's
463
+ * internal ones.
464
+ */
465
+ export type FoirMyNotesResult = FoirConnection<FoirCustomerNote>;
466
+ /** `createMyNote` arguments. */
467
+ export interface FoirCreateMyNoteArgs {
468
+ subjectType: FoirNoteSubjectType;
469
+ subjectId: string;
470
+ /** Required when `subjectType` is `'record'`, refused otherwise. */
471
+ subjectModelKey?: string;
472
+ body: string;
473
+ /** Reply within an existing thread. */
474
+ parentNoteId?: string;
475
+ /**
476
+ * Files to submit with this note, already uploaded through
477
+ * `createFileUpload` / `confirmFileUpload`.
478
+ *
479
+ * A file this caller cannot read is REFUSED and the note is not written. That
480
+ * is not a rough edge to code around: a create that accepted the note and
481
+ * dropped the attachment would leave the author believing they sent a
482
+ * screenshot nobody has.
483
+ */
484
+ attachmentFileIds?: string[];
485
+ }
276
486
  export type FoirDeviceType = 'DESKTOP' | 'MOBILE' | 'TABLET';
277
487
  export type FoirPlatformType = 'IOS' | 'ANDROID' | 'WEB';
278
488
  /**
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * ```ts
5
5
  * import { createFoirClient } from '@foir/sdk/data';
6
- * import { type FoirSchema, foirSchemaVersion } from './foir.generated';
6
+ * import * as generated from './foir.generated';
7
7
  *
8
- * const foir = createFoirClient<FoirSchema>({
8
+ * const foir = createFoirClient({
9
9
  * apiUrl: process.env.PLATFORM_API_URL!,
10
10
  * apiKey: process.env.PLATFORM_API_KEY!,
11
- * schemaVersion: foirSchemaVersion,
11
+ * generated,
12
12
  * });
13
13
  *
14
14
  * const block = await foir.tilly_block.findUnique({
@@ -18,6 +18,19 @@
18
18
  * });
19
19
  * ```
20
20
  *
21
+ * One import and one call. `FoirSchema` is inferred from the module rather than
22
+ * restated, and the four runtime exports the client reads are read off it rather
23
+ * than named one at a time at every call site — see `resolveGeneratedConfig`
24
+ * below for why that indirection is worth having.
25
+ *
26
+ * The individual fields (`schemaVersion`, `selections`, `namespaceSelections`,
27
+ * `selectionUnions`) still work, and still win where both are given.
28
+ *
29
+ * The call is the same call it always was. What changed underneath is what goes
30
+ * on the wire: the client loads that op's selection tree, applies
31
+ * `omit`/`pick`/`include`, prints a GraphQL document, hashes it and sends the
32
+ * hash. See `document.ts` for the printer and `transport.ts` for the routing.
33
+ *
21
34
  * ---------------------------------------------------------------------------
22
35
  * Why this is a Proxy
23
36
  * ---------------------------------------------------------------------------
@@ -29,28 +42,54 @@
29
42
  * reached. A two-level `Proxy` does that in a few lines and costs one property
30
43
  * lookup per call.
31
44
  *
32
- * The alternative — fetching `/v1/manifest` at construction — would put a
33
- * round trip in front of the first query on every cold edge invocation, to
34
- * learn something the types already know.
45
+ * ---------------------------------------------------------------------------
46
+ * Why `selections` is a config field and not a fetch
47
+ * ---------------------------------------------------------------------------
48
+ *
49
+ * The trees ARE runtime data — that is the one thing the generated module has to
50
+ * carry across the type/value boundary, alongside `foirSchemaVersion`. Passing
51
+ * them in costs nothing and is the intended path.
52
+ *
53
+ * When they are absent the client fetches `GET /v1/manifest` once, at first use,
54
+ * and reads the same trees off it. That is a FALLBACK and never the default,
55
+ * for the reason this file has always given: it puts a round trip in front of
56
+ * the first query on every cold edge invocation, to learn something the
57
+ * generated file already knows. What it buys is that `@foir/sdk/data` still
58
+ * works with zero codegen, which keeps the getting-started path honest.
59
+ *
60
+ * It has to stay reachable, and it must not be reachable by ACCIDENT. Those
61
+ * pull in opposite directions and the split is: a client built with no generated
62
+ * module and no `selections` has chosen the fallback, and says so once; a client
63
+ * built with a `generated` module that cannot serve the printer is refused where
64
+ * it was constructed. There is no input that quietly produces the round trip.
35
65
  *
36
66
  * ---------------------------------------------------------------------------
37
67
  * Why namespaces live under `.ns`
38
68
  * ---------------------------------------------------------------------------
39
69
  *
40
- * `POST /v1/query` takes `model` OR `namespace`, never both, and refuses the
41
- * request if you send both. Since there is no runtime schema (above), the
42
- * client cannot tell which one `foir.search` means. So the addressing carries
43
- * it: `foir.<model>.<op>()` sends `model`, `foir.ns.<namespace>.<op>()` sends
44
- * `namespace`. `ns` and `raw` are therefore reserved top-level names.
70
+ * Since there is no runtime schema (above), the client cannot tell whether
71
+ * `foir.search` means a model or a namespace. So the addressing carries it:
72
+ * `foir.<model>.<op>()` addresses a model, `foir.ns.<namespace>.<op>()` a
73
+ * namespace. `ns` and `raw` are therefore reserved top-level names.
45
74
  */
46
75
  import type { FoirModelDefinition, FoirNamespaceOpDefinition, FoirOpDefinition, FoirSchemaDefinition } from './schema';
47
76
  import type { FoirModelOperation, FoirNamespaceOperation } from './selection';
48
77
  import { type FoirEnv } from '../transport/foir-http';
49
- import { type FoirClientConfig, type FoirRawOp } from './transport';
50
- /** The ops available on one model for this credential. */
78
+ import { type FoirClientConfig, type FoirGeneratedModule } from './transport';
79
+ import { type FoirModelSubscriptions, type FoirNamespaceSubscriptions } from './subscriptions';
80
+ /**
81
+ * The ops available on one model for this credential, plus its subscriptions.
82
+ *
83
+ * `onChange` is on every model rather than gated on a generated flag, and that
84
+ * is correct rather than lazy: `recordChanged` charges
85
+ * `records:read:<modelKey>`, which is the SAME scope that put the model in
86
+ * `FoirSchema` at all. A model this credential cannot watch is a model it
87
+ * cannot read, so it is already absent from `S['models']` and the accessor
88
+ * cannot be reached.
89
+ */
51
90
  export type FoirModelClient<S extends FoirSchemaDefinition, M extends FoirModelDefinition> = {
52
91
  [OK in keyof M['ops']]: M['ops'][OK] extends FoirOpDefinition ? FoirModelOperation<S, M, M['ops'][OK]> : never;
53
- };
92
+ } & FoirModelSubscriptions;
54
93
  export type FoirNamespaceClient<N extends {
55
94
  ops: object;
56
95
  }> = {
@@ -59,21 +98,44 @@ export type FoirNamespaceClient<N extends {
59
98
  export interface FoirClientCore<S extends FoirSchemaDefinition> {
60
99
  /**
61
100
  * Non-model root namespaces (`search`, `customers`, `notifications`).
62
- * Separate accessor because the wire distinguishes `model` from `namespace`
63
- * and the client has no runtime schema to disambiguate with.
101
+ * Separate accessor because the addressing has to distinguish a model from a
102
+ * namespace and the client has no runtime schema to disambiguate with.
64
103
  */
65
104
  readonly ns: {
66
- [NK in keyof S['namespaces']]: FoirNamespaceClient<S['namespaces'][NK]>;
105
+ [NK in keyof S['namespaces']]: FoirNamespaceClient<S['namespaces'][NK]> & FoirNamespaceSubscriptions<NK>;
67
106
  };
68
107
  /**
69
- * Untyped passthrough to `POST /v1/query`.
108
+ * Untyped passthrough — a raw GraphQL document, executed on this credential.
109
+ *
110
+ * It changed meaning with the transport, and deliberately so. It used to take
111
+ * a JSON op because that was the wire; now the wire is GraphQL, and an escape
112
+ * hatch that could only say what the JSON op could say would be an escape from
113
+ * nothing. As a document it reaches everything the credential can reach —
114
+ * including the root fields no model or namespace claims (`designTokens`,
115
+ * `record`, `locales`, `resolvedLocale`, the whole customer-auth mutation
116
+ * set), which the JSON op could not address at all.
117
+ *
118
+ * Returns the whole `data` object rather than a single root field, because a
119
+ * hand-written document may legitimately have several. Routing, the persisted
120
+ * -query handshake and `FoirRequestError` are exactly as on the typed path.
121
+ *
122
+ * ```ts
123
+ * const { pages } = await foir.raw<{ pages: { totalCount: number } }>(
124
+ * 'query Recent($n: Int) { pages(first: $n, orderBy: { createdAt: DESC }) { totalCount } }',
125
+ * { n: 5 },
126
+ * );
127
+ * ```
128
+ */
129
+ raw<T = unknown>(document: string, variables?: Record<string, unknown>): Promise<T>;
130
+ /**
131
+ * Close the subscription socket and drop every live subscription.
70
132
  *
71
- * Every generated-type system needs an escape hatch; shipping one
72
- * deliberately is better than people forking the client when the generated
73
- * types cannot express something. Returns the single root field, same as a
74
- * typed call, and throws the same `FoirRequestError`.
133
+ * Only needed by a long-lived process that discards a client — a browser tab
134
+ * or an edge invocation just goes away. The socket is lazy and self-closing
135
+ * (it opens on the first subscription and closes when the last is cancelled),
136
+ * so a client that only queries never has one to dispose of.
75
137
  */
76
- raw<T = unknown>(op: FoirRawOp): Promise<T>;
138
+ dispose(): void;
77
139
  }
78
140
  /**
79
141
  * The client, shaped by the credential's manifest. An op that is not in
@@ -83,12 +145,22 @@ export interface FoirClientCore<S extends FoirSchemaDefinition> {
83
145
  export type FoirClient<S extends FoirSchemaDefinition> = FoirClientCore<S> & {
84
146
  [MK in keyof S['models']]: FoirModelClient<S, S['models'][MK]>;
85
147
  };
86
- export declare function createFoirClient<S extends FoirSchemaDefinition>(config: FoirClientConfig): FoirClient<S>;
148
+ export declare function createFoirClient<S extends FoirSchemaDefinition = FoirSchemaDefinition>(clientConfig: FoirClientConfig<S>): FoirClient<S>;
87
149
  /**
88
150
  * `createFoirClient` from the `PLATFORM_API_URL` / `PLATFORM_API_KEY` /
89
151
  * `FOIR_PREVIEW` convention `@foir/sdk/client` already uses, so a storefront
90
152
  * that has an env wired for GraphQL can add the data client without a second
91
153
  * set of variables. Preview picks the `sk_` `PLATFORM_PREVIEW_API_KEY` and the
92
154
  * draft schema channel, exactly as `foirRequest` does.
155
+ *
156
+ * ```ts
157
+ * import * as generated from './foir.generated';
158
+ * const foir = createFoirClientFromEnv(env, { generated });
159
+ * ```
160
+ *
161
+ * `generated` is spelled out in the overrides type rather than left inside the
162
+ * `Omit<…>` so `S` is inferred from it here exactly as it is on `createFoirClient`.
93
163
  */
94
- export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<FoirClientConfig, 'apiUrl' | 'apiKey' | 'channel'>): FoirClient<S>;
164
+ export declare function createFoirClientFromEnv<S extends FoirSchemaDefinition = FoirSchemaDefinition>(env: FoirEnv, overrides?: Omit<FoirClientConfig, 'apiUrl' | 'apiKey' | 'channel' | 'generated'> & {
165
+ generated?: FoirGeneratedModule<S>;
166
+ }): FoirClient<S>;