@foir/sdk 0.9.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 +497 -27
  2. package/README.md +111 -0
  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 +10 -0
  8. package/dist/auth.js +127 -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 +64 -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 +10 -8
  23. package/dist/data/manifest.d.ts +9 -1
  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
package/dist/auth.js CHANGED
@@ -11,11 +11,30 @@ function isFoirPreview(env) {
11
11
  function resolveFoirApiKey(env) {
12
12
  return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
13
13
  }
14
+ var MALFORMED_PRUNE_REASONS = /* @__PURE__ */ new Set([
15
+ "unknown_field",
16
+ "subselections_on_scalar",
17
+ "missing_subselections"
18
+ ]);
14
19
  var FoirRequestError = class extends Error {
15
20
  /** HTTP status code, when the request failed at the HTTP layer. */
16
21
  status;
17
22
  /** GraphQL errors, when the request failed with a no-data error envelope. */
18
23
  graphQLErrors;
24
+ /**
25
+ * The `data` the server did return, when a STRICT read refused a partial
26
+ * response. Present only on that path: without it, opting into strictness
27
+ * would trade a silent wrong answer for a thrown one that discards the answer,
28
+ * and a caller that wants to log what it nearly rendered could not.
29
+ */
30
+ partialData;
31
+ /**
32
+ * The selections the server stripped, when the refusal was a pruned document
33
+ * rather than a partial error. Same purpose as `partialData`: a caller that
34
+ * wants to log what it nearly rendered, or name the offending path in a
35
+ * developer-facing message, has it without re-parsing anything.
36
+ */
37
+ prunedSelections;
19
38
  cause;
20
39
  constructor(message, options) {
21
40
  super(message);
@@ -23,6 +42,8 @@ var FoirRequestError = class extends Error {
23
42
  this.cause = options?.cause;
24
43
  this.status = options?.status;
25
44
  this.graphQLErrors = options?.graphQLErrors;
45
+ this.partialData = options?.partialData;
46
+ this.prunedSelections = options?.prunedSelections;
26
47
  }
27
48
  /**
28
49
  * The code of the first error, if the response carried one.
@@ -93,6 +114,9 @@ function buildFoirHeaders(input) {
93
114
  if (input.bearerToken) {
94
115
  headers["Authorization"] = `Bearer ${input.bearerToken}`;
95
116
  }
117
+ if (input.orgId) {
118
+ headers["X-Org-Id"] = input.orgId;
119
+ }
96
120
  if (input.extra) {
97
121
  for (const [key, value] of Object.entries(input.extra)) {
98
122
  headers[key] = value;
@@ -107,14 +131,45 @@ function foirApiEndpoint(apiUrl, path) {
107
131
  }
108
132
  return base + path;
109
133
  }
110
- function readFoirEnvelope(json) {
134
+ var warnedDowngrade = null;
135
+ function reportAuthDowngrade(envelope) {
136
+ const downgrade = envelope.extensions?.auth;
137
+ if (!downgrade) {
138
+ warnedDowngrade = null;
139
+ return;
140
+ }
141
+ if (warnedDowngrade === downgrade.reason) return;
142
+ warnedDowngrade = downgrade.reason;
143
+ console.warn(
144
+ `[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}) \u2014 this request was served as ${downgrade.servedAs}, so auth-targeted content resolved to its anonymous variant. ` + (downgrade.reason === "expired" ? "Re-exchange the token and retry." : "Check which credential you are sending.")
145
+ );
146
+ }
147
+ function refusePrunedDocument(json, options) {
148
+ const selections = json.extensions?.pruned?.selections;
149
+ if (!selections?.length) return;
150
+ const describe = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
151
+ const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
152
+ if (malformed.length && options?.strict !== false) {
153
+ throw new FoirRequestError(
154
+ `[foir] the server could not serve part of this query and returned the rest without it: ${malformed.map(describe).join(", ")}. Those selections do not exist on the schema this project is serving. Pass strict: false to get the partial answer instead of this error.`,
155
+ { prunedSelections: selections }
156
+ );
157
+ }
158
+ console.warn(
159
+ `[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
160
+ );
161
+ }
162
+ function readFoirEnvelope(json, options) {
163
+ reportAuthDowngrade(json);
164
+ refusePrunedDocument(json, options);
111
165
  if (json.errors?.length) {
112
166
  const msg = json.errors.map((e) => e.message).join("; ");
113
- if (json.data) {
167
+ if (json.data && !options?.strict) {
114
168
  console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
115
169
  } else {
116
170
  throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
117
- graphQLErrors: json.errors
171
+ graphQLErrors: json.errors,
172
+ partialData: json.data ?? void 0
118
173
  });
119
174
  }
120
175
  }
@@ -155,8 +210,9 @@ async function hasFoirErrorCode(res, ...codes) {
155
210
  }
156
211
 
157
212
  // src/graphql/cdn-route.ts
158
- function isCdnEligible(env, documentText, bearerToken) {
213
+ function isCdnEligible(env, documentText, bearerToken, orgId) {
159
214
  if (bearerToken) return false;
215
+ if (orgId) return false;
160
216
  if (isFoirPreview(env)) return false;
161
217
  const apiKey = resolveFoirApiKey(env);
162
218
  if (!apiKey?.startsWith("pk_")) return false;
@@ -175,12 +231,13 @@ async function foirRequest(env, document, variables, options) {
175
231
  const headers = buildFoirHeaders({
176
232
  apiKey,
177
233
  draftChannel: isFoirPreview(env),
178
- bearerToken: options?.bearerToken
234
+ bearerToken: options?.bearerToken,
235
+ orgId: options?.orgId
179
236
  });
180
237
  const documentText = print2(document);
181
238
  let res;
182
239
  try {
183
- if (isCdnEligible(env, documentText, options?.bearerToken)) {
240
+ if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId)) {
184
241
  const hash = await documentHash(documentText);
185
242
  let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
186
243
  method: "GET",
@@ -739,7 +796,9 @@ async function resolveBearerToken(config) {
739
796
  function foirRoute(input) {
740
797
  if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
741
798
  if (input.bearerToken) return { lane: "origin", reason: "bearer" };
799
+ if (input.orgId) return { lane: "origin", reason: "org" };
742
800
  if (input.mutation) return { lane: "origin", reason: "operation" };
801
+ if (input.writeToken) return { lane: "origin", reason: "floor" };
743
802
  if (input.channel === "draft") return { lane: "origin", reason: "channel" };
744
803
  return { lane: "shared" };
745
804
  }
@@ -752,7 +811,7 @@ async function executeFoirDocument(config, plan) {
752
811
  }
753
812
  return data[plan.rootField];
754
813
  }
755
- async function runFoirDocument(config, document, mutation, variables) {
814
+ async function runFoirDocument(config, document, mutation, variables, options) {
756
815
  const doFetch = config.fetch ?? globalThis.fetch;
757
816
  const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
758
817
  const bearerToken = await resolveBearerToken(config);
@@ -760,12 +819,14 @@ async function runFoirDocument(config, document, mutation, variables) {
760
819
  apiKey: config.apiKey,
761
820
  draftChannel: config.channel === "draft",
762
821
  bearerToken,
822
+ orgId: config.orgId,
763
823
  extra: config.headers
764
824
  });
765
825
  const hash = await documentHash(document);
766
826
  const route = foirRoute({
767
827
  apiKey: config.apiKey,
768
828
  bearerToken,
829
+ orgId: config.orgId,
769
830
  mutation,
770
831
  channel: config.channel
771
832
  });
@@ -805,7 +866,7 @@ async function runFoirDocument(config, document, mutation, variables) {
805
866
  });
806
867
  }
807
868
  const json = await res.json();
808
- return readFoirEnvelope(json);
869
+ return readFoirEnvelope(json, options);
809
870
  }
810
871
  async function readErrorBody(res) {
811
872
  try {
@@ -828,6 +889,7 @@ async function fetchFoirManifest(config) {
828
889
  apiKey: config.apiKey,
829
890
  draftChannel: config.channel === "draft",
830
891
  bearerToken: await resolveBearerToken(config),
892
+ orgId: config.orgId,
831
893
  extra: config.headers
832
894
  }),
833
895
  ...config.requestInit
@@ -869,6 +931,16 @@ var AUTH_OPS = {
869
931
  customerUpdatePassword: "customerUpdatePassword",
870
932
  customerVerifyEmail: "customerVerifyEmail",
871
933
  deleteMyAccount: "deleteMyAccount",
934
+ // The operator's organization context. TWO ops and no third: one to read
935
+ // which organizations the caller may stand in, one to say which. There is
936
+ // deliberately no per-call org argument anywhere in this SDK — the claim the
937
+ // row layer reads is resolved from the caller's own membership row on every
938
+ // request, so an application that never names an org cannot name the wrong
939
+ // one.
940
+ myOrganizations: "myOrganizations",
941
+ selectMyOrganization: "selectMyOrganization",
942
+ // Founding one. Present in a manifest only when the project opted in.
943
+ registerOperatorOrganization: "registerOperatorOrganization",
872
944
  // External identity providers.
873
945
  customerLoginWithProvider: "customerLoginWithProvider",
874
946
  customerProviderCallback: "customerProviderCallback",
@@ -890,6 +962,7 @@ function authClientConfig(env, options) {
890
962
  apiKey,
891
963
  channel: isFoirPreview(env) ? "draft" : "published",
892
964
  bearerToken: options?.bearerToken,
965
+ orgId: options?.orgId,
893
966
  headers: options?.headers,
894
967
  fetch: options?.fetch
895
968
  };
@@ -998,6 +1071,15 @@ async function customerLogout(env, options) {
998
1071
  async function deleteMyAccount(env, vars, options) {
999
1072
  return authOp(env, AUTH_OPS.deleteMyAccount, vars, options);
1000
1073
  }
1074
+ async function myOrganizations(env, vars, options) {
1075
+ return authOp(env, AUTH_OPS.myOrganizations, vars ?? {}, options);
1076
+ }
1077
+ async function selectOrganization(env, vars, options) {
1078
+ return authOp(env, AUTH_OPS.selectMyOrganization, vars, options);
1079
+ }
1080
+ async function registerOrganization(env, vars, options) {
1081
+ return authOp(env, AUTH_OPS.registerOperatorOrganization, vars, options);
1082
+ }
1001
1083
  async function saveSession(adapter, result) {
1002
1084
  await adapter.set(TOKEN_KEY, result.token);
1003
1085
  await adapter.set(REFRESH_KEY, result.refreshToken);
@@ -1135,25 +1217,48 @@ async function fetchHostedSession(request, config) {
1135
1217
  const match = cookie.match(
1136
1218
  new RegExp(`(?:^|;\\s*)${escapeRegExp(cookieName)}=([^;]+)`)
1137
1219
  );
1138
- if (!match) return null;
1220
+ if (!match) return { outcome: "signed-out", reason: "no-cookie" };
1221
+ let res;
1139
1222
  try {
1140
- const res = await fetch(`${config.loginOrigin}${sessionPath}`, {
1223
+ res = await fetch(`${config.loginOrigin}${sessionPath}`, {
1141
1224
  headers: {
1142
1225
  Cookie: `${cookieName}=${match[1]}`,
1143
1226
  "X-Foir-Session": "1"
1144
1227
  }
1145
1228
  });
1146
- if (!res.ok) return null;
1147
- const json = await res.json();
1148
- if (!json.access_token || !json.customer_id) return null;
1149
- return {
1150
- token: json.access_token,
1151
- customerId: json.customer_id,
1152
- email: json.email ?? ""
1153
- };
1229
+ } catch (cause) {
1230
+ return { outcome: "unavailable", reason: "network", cause };
1231
+ }
1232
+ if (!res.ok) {
1233
+ if (res.status === 401) {
1234
+ return { outcome: "signed-out", reason: "rejected" };
1235
+ }
1236
+ if (res.status === 403) {
1237
+ return { outcome: "unavailable", reason: "forbidden", httpStatus: res.status };
1238
+ }
1239
+ if (res.status === 429) {
1240
+ return { outcome: "unavailable", reason: "rate-limited", httpStatus: res.status };
1241
+ }
1242
+ return { outcome: "unavailable", reason: "server-error", httpStatus: res.status };
1243
+ }
1244
+ let json;
1245
+ try {
1246
+ json = await res.json();
1154
1247
  } catch {
1155
- return null;
1248
+ return { outcome: "unavailable", reason: "malformed", httpStatus: res.status };
1156
1249
  }
1250
+ if (!json.access_token || !json.customer_id) {
1251
+ return { outcome: "unavailable", reason: "malformed", httpStatus: res.status };
1252
+ }
1253
+ return {
1254
+ outcome: "active",
1255
+ session: {
1256
+ token: json.access_token,
1257
+ customerId: json.customer_id,
1258
+ email: json.email ?? "",
1259
+ expiresIn: typeof json.expires_in === "number" ? json.expires_in : 0
1260
+ }
1261
+ };
1157
1262
  }
1158
1263
  function escapeRegExp(s) {
1159
1264
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1326,12 +1431,15 @@ export {
1326
1431
  hostedLogoutUrl,
1327
1432
  loginWithOTP,
1328
1433
  loginWithPassword,
1434
+ myOrganizations,
1329
1435
  myProviderToken,
1330
1436
  refreshSession,
1437
+ registerOrganization,
1331
1438
  registerWithPassword,
1332
1439
  requireAdmin,
1333
1440
  requireAuth,
1334
1441
  resetPasswordAndSignIn,
1335
1442
  saveSession,
1443
+ selectOrganization,
1336
1444
  signOut
1337
1445
  };
package/dist/client.js CHANGED
@@ -11,11 +11,30 @@ function isFoirPreview(env) {
11
11
  function resolveFoirApiKey(env) {
12
12
  return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
13
13
  }
14
+ var MALFORMED_PRUNE_REASONS = /* @__PURE__ */ new Set([
15
+ "unknown_field",
16
+ "subselections_on_scalar",
17
+ "missing_subselections"
18
+ ]);
14
19
  var FoirRequestError = class extends Error {
15
20
  /** HTTP status code, when the request failed at the HTTP layer. */
16
21
  status;
17
22
  /** GraphQL errors, when the request failed with a no-data error envelope. */
18
23
  graphQLErrors;
24
+ /**
25
+ * The `data` the server did return, when a STRICT read refused a partial
26
+ * response. Present only on that path: without it, opting into strictness
27
+ * would trade a silent wrong answer for a thrown one that discards the answer,
28
+ * and a caller that wants to log what it nearly rendered could not.
29
+ */
30
+ partialData;
31
+ /**
32
+ * The selections the server stripped, when the refusal was a pruned document
33
+ * rather than a partial error. Same purpose as `partialData`: a caller that
34
+ * wants to log what it nearly rendered, or name the offending path in a
35
+ * developer-facing message, has it without re-parsing anything.
36
+ */
37
+ prunedSelections;
19
38
  cause;
20
39
  constructor(message, options) {
21
40
  super(message);
@@ -23,6 +42,8 @@ var FoirRequestError = class extends Error {
23
42
  this.cause = options?.cause;
24
43
  this.status = options?.status;
25
44
  this.graphQLErrors = options?.graphQLErrors;
45
+ this.partialData = options?.partialData;
46
+ this.prunedSelections = options?.prunedSelections;
26
47
  }
27
48
  /**
28
49
  * The code of the first error, if the response carried one.
@@ -105,6 +126,9 @@ function buildFoirHeaders(input) {
105
126
  if (input.bearerToken) {
106
127
  headers["Authorization"] = `Bearer ${input.bearerToken}`;
107
128
  }
129
+ if (input.orgId) {
130
+ headers["X-Org-Id"] = input.orgId;
131
+ }
108
132
  if (input.extra) {
109
133
  for (const [key, value] of Object.entries(input.extra)) {
110
134
  headers[key] = value;
@@ -112,14 +136,45 @@ function buildFoirHeaders(input) {
112
136
  }
113
137
  return headers;
114
138
  }
115
- function readFoirEnvelope(json) {
139
+ var warnedDowngrade = null;
140
+ function reportAuthDowngrade(envelope) {
141
+ const downgrade = envelope.extensions?.auth;
142
+ if (!downgrade) {
143
+ warnedDowngrade = null;
144
+ return;
145
+ }
146
+ if (warnedDowngrade === downgrade.reason) return;
147
+ warnedDowngrade = downgrade.reason;
148
+ console.warn(
149
+ `[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}) \u2014 this request was served as ${downgrade.servedAs}, so auth-targeted content resolved to its anonymous variant. ` + (downgrade.reason === "expired" ? "Re-exchange the token and retry." : "Check which credential you are sending.")
150
+ );
151
+ }
152
+ function refusePrunedDocument(json, options) {
153
+ const selections = json.extensions?.pruned?.selections;
154
+ if (!selections?.length) return;
155
+ const describe = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
156
+ const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
157
+ if (malformed.length && options?.strict !== false) {
158
+ throw new FoirRequestError(
159
+ `[foir] the server could not serve part of this query and returned the rest without it: ${malformed.map(describe).join(", ")}. Those selections do not exist on the schema this project is serving. Pass strict: false to get the partial answer instead of this error.`,
160
+ { prunedSelections: selections }
161
+ );
162
+ }
163
+ console.warn(
164
+ `[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
165
+ );
166
+ }
167
+ function readFoirEnvelope(json, options) {
168
+ reportAuthDowngrade(json);
169
+ refusePrunedDocument(json, options);
116
170
  if (json.errors?.length) {
117
171
  const msg = json.errors.map((e) => e.message).join("; ");
118
- if (json.data) {
172
+ if (json.data && !options?.strict) {
119
173
  console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
120
174
  } else {
121
175
  throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
122
- graphQLErrors: json.errors
176
+ graphQLErrors: json.errors,
177
+ partialData: json.data ?? void 0
123
178
  });
124
179
  }
125
180
  }
@@ -160,8 +215,9 @@ async function hasFoirErrorCode(res, ...codes) {
160
215
  }
161
216
 
162
217
  // src/graphql/cdn-route.ts
163
- function isCdnEligible(env, documentText, bearerToken) {
218
+ function isCdnEligible(env, documentText, bearerToken, orgId) {
164
219
  if (bearerToken) return false;
220
+ if (orgId) return false;
165
221
  if (isFoirPreview(env)) return false;
166
222
  const apiKey = resolveFoirApiKey(env);
167
223
  if (!apiKey?.startsWith("pk_")) return false;
@@ -214,12 +270,13 @@ async function foirRequest(env, document, variables, options) {
214
270
  const headers = buildFoirHeaders({
215
271
  apiKey,
216
272
  draftChannel: isFoirPreview(env),
217
- bearerToken: options?.bearerToken
273
+ bearerToken: options?.bearerToken,
274
+ orgId: options?.orgId
218
275
  });
219
276
  const documentText = print2(document);
220
277
  let res;
221
278
  try {
222
- if (isCdnEligible(env, documentText, options?.bearerToken)) {
279
+ if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId)) {
223
280
  const hash = await documentHash(documentText);
224
281
  let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
225
282
  method: "GET",
@@ -2,7 +2,7 @@
2
2
  * `@foir/sdk/codegen/block-registry` — DEPRECATED alias.
3
3
  *
4
4
  * `block` was one project's name for a flexible field, never a platform
5
- * concept (TTS-1391). The plugin moved to `@foir/sdk/codegen/item-registry`;
5
+ * concept. The plugin moved to `@foir/sdk/codegen/item-registry`;
6
6
  * this path re-exports it unchanged so an existing `codegen.ts` keeps working.
7
7
  * Repoint the `plugins:` entry when convenient — this will be removed in a
8
8
  * future major.
@@ -2,7 +2,7 @@
2
2
  * `@foir/sdk/codegen/block-scaffold` — DEPRECATED alias.
3
3
  *
4
4
  * `block` was one project's name for a flexible field, never a platform
5
- * concept (TTS-1391). The plugin moved to `@foir/sdk/codegen/item-scaffold`;
5
+ * concept. The plugin moved to `@foir/sdk/codegen/item-scaffold`;
6
6
  * this path re-exports it unchanged so an existing `codegen.ts` keeps working.
7
7
  * Repoint the `plugins:` entry when convenient — this will be removed in a
8
8
  * future major.
@@ -6,7 +6,7 @@
6
6
  * and item-scaffold plugins are wired by the preset and also available at
7
7
  * `@foir/sdk/codegen/item-registry` / `@foir/sdk/codegen/item-scaffold`.
8
8
  *
9
- * The pre-TTS-1391 paths `@foir/sdk/codegen/block-registry` and
9
+ * The pre-rename paths `@foir/sdk/codegen/block-registry` and
10
10
  * `/block-scaffold` still resolve, re-exporting those modules unchanged.
11
11
  */
12
12
  export { foirCodegen, foirScaffold } from './preset';
@@ -13,7 +13,7 @@
13
13
  * types from the codegen output (`./graphql`) and `ItemComponent` from
14
14
  * `@foir/sdk/content`, so it pairs directly with `createContentRenderer`.
15
15
  *
16
- * Reachable at the pre-TTS-1391 path `@foir/sdk/codegen/block-registry` too,
16
+ * Reachable at the pre-rename path `@foir/sdk/codegen/block-registry` too,
17
17
  * which re-exports this module unchanged.
18
18
  */
19
19
  import type { GraphQLSchema } from 'graphql';
@@ -10,7 +10,7 @@
10
10
  * in. Adding a new item type later surfaces as a compile error on
11
11
  * `itemRegistry` (via the regenerated `ItemRegistry` type), not here.
12
12
  *
13
- * Reachable at the pre-TTS-1391 path `@foir/sdk/codegen/block-scaffold` too,
13
+ * Reachable at the pre-rename path `@foir/sdk/codegen/block-scaffold` too,
14
14
  * which re-exports this module unchanged.
15
15
  */
16
16
  import type { GraphQLSchema } from 'graphql';
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * The "switch" is a typed object lookup. Exhaustiveness is enforced where the
17
17
  * registry is BUILT — against the exhaustive `ItemRegistry` type the codegen
18
- * plugin (EID-92) generates per project, so omitting an item type is a compile
18
+ * plugin generates per project, so omitting an item type is a compile
19
19
  * error there, not a runtime surprise here. The consumer can supply different
20
20
  * registries per surface (e.g. web vs email).
21
21
  *
@@ -24,8 +24,7 @@
24
24
  * A prose field holds ENTRIES. A modeller may name the field `blocks`,
25
25
  * `sections`, `modules` or anything else, and this renderer must not care:
26
26
  * dispatch is on `__typename` and never on a field name. The `Block*` spellings
27
- * below are deprecated aliases kept so existing consumers compile — see
28
- * TTS-1391.
27
+ * below are deprecated aliases kept so existing consumers compile.
29
28
  */
30
29
  import { type ReactNode } from 'react';
31
30
  import { type PassageLike } from './richtext';
@@ -41,7 +40,7 @@ export interface ItemComponentProps<TItem, TCtx = unknown> {
41
40
  /**
42
41
  * The same value as `item`.
43
42
  *
44
- * @deprecated Use `item`. Kept so components scaffolded before TTS-1391
43
+ * @deprecated Use `item`. Kept so components scaffolded before the rename
45
44
  * keep compiling; it is passed on every render and will be removed in a
46
45
  * future major.
47
46
  */
@@ -67,7 +66,7 @@ export interface EntriesLike {
67
66
  * The prose field's entries — the projection wrapper (`{ entries }`), or a
68
67
  * plain array — plus the context threaded to every component.
69
68
  *
70
- * `items` is canonical. `blocks` is the pre-TTS-1391 spelling and still
69
+ * `items` is canonical. `blocks` is the pre-rename spelling and still
71
70
  * accepted; passing both is a type error rather than a silent precedence rule,
72
71
  * because a caller who set both cannot be told which one this honoured.
73
72
  */
@@ -9,7 +9,7 @@
9
9
  export { createContentRenderer } from './content-renderer';
10
10
  export type { ItemLike, EntriesLike, ItemComponent, ItemComponentProps, ItemRegistry, ContentRendererProps, ContentRendererOptions, PassageRunProps, } from './content-renderer';
11
11
  /**
12
- * Pre-TTS-1391 spellings. `Block` was one project's name for a flexible field,
12
+ * Pre-rename spellings. `Block` was one project's name for a flexible field,
13
13
  * never a platform concept; these are kept so existing consumers compile and
14
14
  * will be removed in a future major.
15
15
  */
@@ -25,7 +25,7 @@
25
25
  import { type ReactNode, type JSX } from 'react';
26
26
  /**
27
27
  * safeHref allow-lists link schemes so stored prose can't smuggle a
28
- * `javascript:` (or `data:`/`vbscript:`) URL that executes on click (EID-207).
28
+ * `javascript:` (or `data:`/`vbscript:`) URL that executes on click.
29
29
  * Relative URLs, anchors, and the http(s)/mailto/tel schemes pass; anything
30
30
  * else collapses to '#'. Editors are not fully trusted, so this runs on the
31
31
  * structured render path, not only the html fallback.
@@ -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.
@@ -215,7 +215,7 @@ export interface RecordAncestor {
215
215
  * 193 stored reference values carry an id and 92 carry a natural key and no id
216
216
  * — so a consumer must handle either.
217
217
  *
218
- * `_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
219
219
  * rather than an omission. They were the wrapper's own metadata: `_id` was the
220
220
  * ITEM's id (never the target's, which is why `_targetId` exists separately) and
221
221
  * `_type` was the union discriminator, reading `"reference"` on every reference
@@ -286,10 +286,10 @@ export interface CollabInfo {
286
286
  * The three wrappers below have no replacement, because the things they
287
287
  * described no longer exist:
288
288
  *
289
- * - `MediaBlock` — `media` stopped being a primitive at TTS-1391. It was
289
+ * - `MediaBlock` — `media` stopped being a primitive. It was
290
290
  * never in the proto FieldType enum, and it is the one primitive name a
291
291
  * project had also used as a model key.
292
- * - `TextBlock` — TTS-1406 removed it. GraphQL requires a union member to be
292
+ * - `TextBlock` — removed as well. GraphQL requires a union member to be
293
293
  * an object type and a text item's value is a bare scalar, which is the
294
294
  * whole reason a wrapper existed. An inline model with a text field is how
295
295
  * a text slot is authored.
@@ -367,7 +367,7 @@ export interface FoirDegraded<TTypename extends string = string> {
367
367
  * Four because depth is a budget against the server's 10,000-field ceiling and
368
368
  * four is past what a generated module usefully describes: on the live dev
369
369
  * project `product.findUnique` measures 318 fields at the default, 468 at 2 and
370
- * 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 /
371
371
  * 13,623 when a union position was charged for every member — so the bound here
372
372
  * is about how much generated type surface is worth emitting, not about what
373
373
  * the server will run. A call
@@ -429,7 +429,8 @@ export interface FoirUpdateManyPayload {
429
429
  ids: string[];
430
430
  }
431
431
  /**
432
- * 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.
433
434
  *
434
435
  * On the wire it is `String!`, not a GraphQL enum, so the manifest publishes no
435
436
  * `enumValues` for it and `foir types` cannot generate this union. It is here
@@ -440,10 +441,14 @@ export interface FoirUpdateManyPayload {
440
441
  * with a model keyed `file` would make one value mean two things; a note on a
441
442
  * record is `'record'` plus the model key on `subjectModelKey`.
442
443
  *
443
- * This mirrors a platform-side set, and the obligation that comes with a mirror
444
- * (see the file header) applies: a kind added there widens this union in a
445
- * minor release, and until then a value outside it is a row no writer could
446
- * 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.
447
452
  */
448
453
  export type FoirNoteSubjectType = 'record' | 'file' | 'model' | 'operation' | 'schedule' | 'customer';
449
454
  /**
@@ -550,6 +555,53 @@ export interface FoirCreateMyNoteArgs {
550
555
  */
551
556
  attachmentFileIds?: string[];
552
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
+ }
553
605
  export type FoirDeviceType = 'DESKTOP' | 'MOBILE' | 'TABLET';
554
606
  export type FoirPlatformType = 'IOS' | 'ANDROID' | 'WEB';
555
607
  /**