@foir/sdk 0.5.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 (107) hide show
  1. package/CHANGELOG.md +1126 -0
  2. package/README.md +94 -0
  3. package/dist/auth/auth.d.ts +243 -0
  4. package/dist/auth/hosted.d.ts +40 -0
  5. package/dist/auth/index.d.ts +21 -0
  6. package/dist/auth/operations.d.ts +144 -0
  7. package/dist/auth/ops.d.ts +99 -0
  8. package/dist/auth/session-client.d.ts +76 -0
  9. package/dist/auth-session.js +137 -0
  10. package/dist/auth.js +973 -92
  11. package/dist/chunk-IGNLWT3H.js +35 -0
  12. package/dist/chunk-OHYVKTGB.js +45 -0
  13. package/dist/chunk-TAL5H6VA.js +39 -0
  14. package/dist/client/platform-client.d.ts +29 -0
  15. package/dist/client.js +275 -37
  16. package/dist/codegen/block-registry.d.ts +13 -23
  17. package/dist/codegen/block-registry.js +3 -31
  18. package/dist/codegen/block-scaffold.d.ts +12 -25
  19. package/dist/codegen/block-scaffold.js +3 -41
  20. package/dist/codegen/index.d.ts +12 -61
  21. package/dist/codegen/index.js +7 -11
  22. package/dist/codegen/item-registry.d.ts +28 -0
  23. package/dist/codegen/item-registry.js +7 -0
  24. package/dist/codegen/item-scaffold.d.ts +29 -0
  25. package/dist/codegen/item-scaffold.js +7 -0
  26. package/dist/codegen/preset.d.ts +67 -0
  27. package/dist/codegen/shared.d.ts +36 -0
  28. package/dist/components/EditorProvider.d.ts +36 -0
  29. package/dist/content/content-renderer.d.ts +113 -0
  30. package/dist/content/index.d.ts +18 -0
  31. package/dist/content/richtext.d.ts +118 -0
  32. package/dist/content.js +227 -80
  33. package/dist/data/builtins.d.ts +506 -0
  34. package/dist/data/client.d.ts +166 -0
  35. package/dist/data/document.d.ts +251 -0
  36. package/dist/data/index.d.ts +73 -0
  37. package/dist/data/manifest.d.ts +424 -0
  38. package/dist/data/schema.d.ts +313 -0
  39. package/dist/data/selection.d.ts +524 -0
  40. package/dist/data/subjects.d.ts +221 -0
  41. package/dist/data/subscription-document.d.ts +75 -0
  42. package/dist/data/subscriptions.d.ts +181 -0
  43. package/dist/data/transport.d.ts +325 -0
  44. package/dist/data/ws-transport.d.ts +197 -0
  45. package/dist/data.js +1683 -0
  46. package/dist/graphql/cdn-route.d.ts +26 -0
  47. package/dist/graphql/client.d.ts +30 -0
  48. package/dist/graphql/content-client.d.ts +66 -0
  49. package/dist/graphql/foir-request.d.ts +47 -0
  50. package/dist/hooks/useAutoResize.d.ts +22 -0
  51. package/dist/hooks/useEditor.d.ts +16 -0
  52. package/dist/index.d.ts +10 -394
  53. package/dist/media/BlurhashCanvas.d.ts +10 -0
  54. package/dist/media/ProgressiveMedia.d.ts +56 -0
  55. package/dist/media/index.d.ts +18 -0
  56. package/dist/media/media.d.ts +139 -0
  57. package/dist/media/screen-capture.d.ts +81 -0
  58. package/dist/media/useProgressiveLoad.d.ts +23 -0
  59. package/dist/media.js +102 -0
  60. package/dist/next/cookieAdapter.d.ts +26 -0
  61. package/dist/next/index.d.ts +15 -0
  62. package/dist/next/middleware.d.ts +49 -0
  63. package/dist/next/session.d.ts +53 -0
  64. package/dist/next.js +763 -78
  65. package/dist/protocol/constants.d.ts +6 -0
  66. package/dist/protocol/messages.d.ts +155 -0
  67. package/dist/protocol/validation.d.ts +8 -0
  68. package/dist/push/index.d.ts +14 -0
  69. package/dist/push/operations.d.ts +22 -0
  70. package/dist/push/web-push.d.ts +39 -0
  71. package/dist/push.js +892 -78
  72. package/dist/react/FoirFeedback.d.ts +62 -0
  73. package/dist/react/FoirProvider.d.ts +116 -0
  74. package/dist/react/OAuthButtons.d.ts +20 -0
  75. package/dist/react/SignIn.d.ts +22 -0
  76. package/dist/react/UserButton.d.ts +13 -0
  77. package/dist/react/feedback-notes.d.ts +101 -0
  78. package/dist/react/index.d.ts +25 -0
  79. package/dist/react/localStorageAdapter.d.ts +10 -0
  80. package/dist/react.js +1373 -91
  81. package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
  82. package/dist/realtime.js +1 -1
  83. package/dist/server/callback-client.d.ts +92 -0
  84. package/dist/server/events-client.d.ts +279 -0
  85. package/dist/server/index.d.ts +12 -0
  86. package/dist/server/operation-types.d.ts +134 -0
  87. package/dist/server/require-scoped-token.d.ts +76 -0
  88. package/dist/server/server-client.d.ts +34 -0
  89. package/dist/server/types.d.ts +54 -0
  90. package/dist/server/verify-scoped-token.d.ts +100 -0
  91. package/dist/server/webhook-helpers.d.ts +36 -0
  92. package/dist/server.js +298 -0
  93. package/dist/transport/foir-http.d.ts +234 -0
  94. package/dist/transport/persisted-query.d.ts +68 -0
  95. package/dist/transport/retry.d.ts +61 -0
  96. package/dist/types/index.d.ts +124 -0
  97. package/package.json +35 -13
  98. package/dist/auth.d.ts +0 -382
  99. package/dist/chunk-ADO5FJNM.js +0 -30
  100. package/dist/client.d.ts +0 -135
  101. package/dist/content.d.ts +0 -96
  102. package/dist/media.d.ts +0 -234
  103. package/dist/next.d.ts +0 -178
  104. package/dist/push.d.ts +0 -109
  105. package/dist/react.d.ts +0 -319
  106. package/dist/server.d.ts +0 -534
  107. package/dist/shared-DkWlEd2Z.d.ts +0 -18
@@ -0,0 +1,35 @@
1
+ import {
2
+ DEFAULT_GRAPHQL_IMPORT,
3
+ DEFAULT_SDK_IMPORT,
4
+ collectItemMembers
5
+ } from "./chunk-TAL5H6VA.js";
6
+
7
+ // src/codegen/item-registry.ts
8
+ var plugin = (schema, _documents, config = {}) => {
9
+ const members = collectItemMembers(schema, config.unions);
10
+ const graphqlImport = config.graphqlImport ?? DEFAULT_GRAPHQL_IMPORT;
11
+ const sdkImport = config.sdkImport ?? DEFAULT_SDK_IMPORT;
12
+ const registryName = config.registryName ?? "ItemRegistry";
13
+ return [
14
+ "/* eslint-disable */",
15
+ "// Generated by @foir/sdk/codegen/item-registry. Do not edit by hand.",
16
+ "// Regenerated on every codegen run \u2014 omitting an item type is a compile error.",
17
+ `import type { ItemComponent } from '${sdkImport}';`,
18
+ `import type {`,
19
+ ...members.map((m) => ` ${m},`),
20
+ `} from '${graphqlImport}';`,
21
+ "",
22
+ `/**`,
23
+ ` * Exhaustive map of every item \`__typename\` in the project schema to the`,
24
+ ` * component that renders it. Pass an instance to \`createContentRenderer\`.`,
25
+ ` */`,
26
+ `export type ${registryName}<Ctx = unknown> = {`,
27
+ ...members.map((m) => ` ${m}: ItemComponent<${m}, Ctx>;`),
28
+ `};`,
29
+ ""
30
+ ].join("\n");
31
+ };
32
+
33
+ export {
34
+ plugin
35
+ };
@@ -0,0 +1,45 @@
1
+ import {
2
+ DEFAULT_GRAPHQL_IMPORT,
3
+ DEFAULT_SDK_IMPORT,
4
+ collectItemMembers
5
+ } from "./chunk-TAL5H6VA.js";
6
+
7
+ // src/codegen/item-scaffold.ts
8
+ var plugin = (schema, _documents, config = {}) => {
9
+ const members = collectItemMembers(schema, config.unions);
10
+ const graphqlImport = config.graphqlImport ?? DEFAULT_GRAPHQL_IMPORT;
11
+ const sdkImport = config.sdkImport ?? DEFAULT_SDK_IMPORT;
12
+ const registryImport = config.registryImport ?? "./item-registry";
13
+ const ctx = config.ctxType ?? "unknown";
14
+ const stub = (m) => [
15
+ `function ${m}View(props: ItemComponentProps<${m}, ${ctx}>) {`,
16
+ ` // TODO: render ${m} from props.item (props.ctx, props.priority available)`,
17
+ ` void props;`,
18
+ ` return null;`,
19
+ `}`
20
+ ].join("\n");
21
+ return [
22
+ "/* eslint-disable */",
23
+ "// Scaffolded once by @foir/sdk/codegen/item-scaffold \u2014 you own this file.",
24
+ "// Implement each stub; the build stays green until you do.",
25
+ `import { createContentRenderer, type ItemComponentProps } from '${sdkImport}';`,
26
+ `import type { ItemRegistry } from '${registryImport}';`,
27
+ `import type {`,
28
+ ...members.map((m) => ` ${m},`),
29
+ `} from '${graphqlImport}';`,
30
+ ...config.ctxImport ? ["", config.ctxImport] : [],
31
+ "",
32
+ ...members.map(stub),
33
+ "",
34
+ `export const itemRegistry: ItemRegistry<${ctx}> = {`,
35
+ ...members.map((m) => ` ${m}: ${m}View,`),
36
+ `};`,
37
+ "",
38
+ `export const ContentRenderer = createContentRenderer<${ctx}>(itemRegistry);`,
39
+ ""
40
+ ].join("\n");
41
+ };
42
+
43
+ export {
44
+ plugin
45
+ };
@@ -0,0 +1,39 @@
1
+ // src/codegen/shared.ts
2
+ import { isUnionType } from "graphql";
3
+ var DEFAULT_GRAPHQL_IMPORT = "./graphql";
4
+ var DEFAULT_SDK_IMPORT = "@foir/sdk/content";
5
+ var PASSAGE = "Passage";
6
+ function collectItemMembers(schema, unionNames) {
7
+ const members = /* @__PURE__ */ new Set();
8
+ const missing = [];
9
+ if (unionNames && unionNames.length > 0) {
10
+ for (const name of unionNames) {
11
+ const type = schema.getType(name);
12
+ if (!type || !isUnionType(type)) {
13
+ missing.push(name);
14
+ continue;
15
+ }
16
+ for (const member of type.getTypes()) members.add(member.name);
17
+ }
18
+ } else {
19
+ for (const type of Object.values(schema.getTypeMap())) {
20
+ if (!isUnionType(type)) continue;
21
+ const names = type.getTypes().map((m) => m.name);
22
+ if (!names.includes(PASSAGE)) continue;
23
+ for (const name of names) members.add(name);
24
+ }
25
+ }
26
+ members.delete(PASSAGE);
27
+ if (members.size === 0) {
28
+ throw new Error(
29
+ `[foir codegen] no item members found. ` + (unionNames && unionNames.length > 0 ? `Looked for union(s): ${unionNames.join(", ")}` + (missing.length ? ` (not found or not a union: ${missing.join(", ")})` : "") : `No prose entries union (a union containing Passage) exists in the schema`) + `. Run \`foir push\`, then confirm the schema has a prose field that places models \u2014 a prose field's entries union is named by its materialised member set.`
30
+ );
31
+ }
32
+ return [...members].sort();
33
+ }
34
+
35
+ export {
36
+ DEFAULT_GRAPHQL_IMPORT,
37
+ DEFAULT_SDK_IMPORT,
38
+ collectItemMembers
39
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Fetch-based GraphQL client for the EIDE platform API.
3
+ *
4
+ * Zero dependencies — uses the native Fetch API.
5
+ */
6
+ import type { PlatformClient, PlatformClientOptions } from '../types';
7
+ /** Error thrown when a platform API request fails. */
8
+ export declare class PlatformClientError extends Error {
9
+ status?: number | undefined;
10
+ response?: unknown | undefined;
11
+ constructor(message: string, status?: number | undefined, response?: unknown | undefined);
12
+ }
13
+ /**
14
+ * Create a PlatformClient instance.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const client = createPlatformClient({
19
+ * apiUrl: 'https://api.example.com/graphql',
20
+ * apiKey: 'sk_test_...',
21
+ * });
22
+ *
23
+ * const { data } = await client.query(
24
+ * `query GetProduct($id: ID!) { product(id: $id) { title } }`,
25
+ * { id: '123' }
26
+ * );
27
+ * ```
28
+ */
29
+ export declare function createPlatformClient(options: PlatformClientOptions): PlatformClient;
package/dist/client.js CHANGED
@@ -1,13 +1,15 @@
1
1
  // src/graphql/foir-request.ts
2
+ import { print as print2 } from "graphql";
3
+
4
+ // src/graphql/cdn-route.ts
2
5
  import { print } from "graphql";
6
+
7
+ // src/transport/foir-http.ts
3
8
  function isFoirPreview(env) {
4
9
  return env.FOIR_PREVIEW === "true";
5
10
  }
6
- var FOIR_CDN_BASE = "https://cdn.foir.dev/";
7
- function absolutiseFoirMediaUrl(url, cdnBase = FOIR_CDN_BASE) {
8
- if (!url) return void 0;
9
- if (/^https?:\/\//i.test(url)) return url;
10
- return cdnBase + url.replace(/^\/+/, "");
11
+ function resolveFoirApiKey(env) {
12
+ return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
11
13
  }
12
14
  var FoirRequestError = class extends Error {
13
15
  /** HTTP status code, when the request failed at the HTTP layer. */
@@ -22,41 +24,95 @@ var FoirRequestError = class extends Error {
22
24
  this.status = options?.status;
23
25
  this.graphQLErrors = options?.graphQLErrors;
24
26
  }
27
+ /**
28
+ * The code of the first error, if the response carried one.
29
+ *
30
+ * A getter rather than a stored field so it stays correct for an error
31
+ * constructed anywhere, including by a caller in a test.
32
+ */
33
+ get code() {
34
+ return this.graphQLErrors?.[0]?.extensions?.code;
35
+ }
36
+ /** The correlation id, if the response carried one. */
37
+ get requestId() {
38
+ return this.graphQLErrors?.[0]?.extensions?.requestId;
39
+ }
40
+ /**
41
+ * Whether retrying this request could succeed.
42
+ *
43
+ * The 429 is the one that has to be in here and is easy to leave out: it sits
44
+ * among the 4xx statuses that are otherwise permanent, so a policy written as
45
+ * "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
46
+ * own. A storefront wrote that policy, its key saturated, and every read
47
+ * failed on the first attempt for an hour.
48
+ */
49
+ get retryable() {
50
+ if (this.status === void 0) return true;
51
+ if (this.status === 429 || this.code === "RATE_LIMITED") return true;
52
+ if (this.status >= 500) return true;
53
+ return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
54
+ }
55
+ /**
56
+ * How long to wait before retrying, in milliseconds, when the server said.
57
+ * Undefined when it did not, which means the caller picks its own backoff.
58
+ */
59
+ get retryAfterMs() {
60
+ const seconds = this.graphQLErrors?.[0]?.extensions?.retryAfter;
61
+ return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
62
+ }
25
63
  };
26
- async function foirRequest(env, document, variables, options) {
27
- const endpoint = env.PLATFORM_API_URL;
28
- const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
29
- if (!endpoint || !apiKey) {
30
- throw new FoirRequestError(
31
- "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
32
- );
64
+ function isFoirRateLimited(err) {
65
+ return err instanceof FoirRequestError && (err.status === 429 || err.code === "RATE_LIMITED");
66
+ }
67
+ function isFoirUnauthenticated(err) {
68
+ return err instanceof FoirRequestError && (err.status === 401 || err.code === "UNAUTHENTICATED");
69
+ }
70
+ function isFoirForbidden(err) {
71
+ return err instanceof FoirRequestError && (err.status === 403 || err.code === "PERMISSION_DENIED");
72
+ }
73
+ function isFoirNotFound(err) {
74
+ return err instanceof FoirRequestError && err.code === "NOT_FOUND";
75
+ }
76
+ function readFoirRateLimit(headers) {
77
+ const raw = {
78
+ limit: headers.get("X-RateLimit-Limit"),
79
+ remaining: headers.get("X-RateLimit-Remaining"),
80
+ resetAt: headers.get("X-RateLimit-Reset")
81
+ };
82
+ if (raw.limit === null || raw.remaining === null || raw.resetAt === null) {
83
+ return void 0;
33
84
  }
85
+ const limit = Number(raw.limit);
86
+ const remaining = Number(raw.remaining);
87
+ const resetAt = Number(raw.resetAt);
88
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining) || !Number.isFinite(resetAt)) {
89
+ return void 0;
90
+ }
91
+ return { limit, remaining, resetAt };
92
+ }
93
+ function readFoirRetryAfterMs(headers) {
94
+ const seconds = Number(headers.get("Retry-After"));
95
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
96
+ }
97
+ function buildFoirHeaders(input) {
34
98
  const headers = {
35
99
  "Content-Type": "application/json",
36
- "x-api-key": apiKey
100
+ "x-api-key": input.apiKey
37
101
  };
38
- if (isFoirPreview(env)) {
102
+ if (input.draftChannel) {
39
103
  headers["X-Foir-Schema-Channel"] = "draft";
40
104
  }
41
- if (options?.bearerToken) {
42
- headers["Authorization"] = `Bearer ${options.bearerToken}`;
105
+ if (input.bearerToken) {
106
+ headers["Authorization"] = `Bearer ${input.bearerToken}`;
43
107
  }
44
- let res;
45
- try {
46
- res = await fetch(endpoint, {
47
- method: "POST",
48
- headers,
49
- body: JSON.stringify({ query: print(document), variables })
50
- });
51
- } catch (err) {
52
- throw new FoirRequestError("[foir] network error", { cause: err });
53
- }
54
- if (!res.ok) {
55
- throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
56
- status: res.status
57
- });
108
+ if (input.extra) {
109
+ for (const [key, value] of Object.entries(input.extra)) {
110
+ headers[key] = value;
111
+ }
58
112
  }
59
- const json = await res.json();
113
+ return headers;
114
+ }
115
+ function readFoirEnvelope(json) {
60
116
  if (json.errors?.length) {
61
117
  const msg = json.errors.map((e) => e.message).join("; ");
62
118
  if (json.data) {
@@ -73,15 +129,190 @@ async function foirRequest(env, document, variables, options) {
73
129
  return json.data;
74
130
  }
75
131
 
132
+ // src/transport/persisted-query.ts
133
+ var FOIR_CDN_GRAPHQL_PATH = "/cdn/graphql";
134
+ function persistedQueryExtension(hash) {
135
+ return { persistedQuery: { version: 1, sha256Hash: hash } };
136
+ }
137
+ function buildCdnUrl(endpoint, apiKey, hash, variables) {
138
+ const url = new URL(endpoint);
139
+ url.pathname = url.pathname.replace(/\/graphql\/?$/, "") + FOIR_CDN_GRAPHQL_PATH;
140
+ url.searchParams.set("key", apiKey);
141
+ url.searchParams.set("extensions", JSON.stringify(persistedQueryExtension(hash)));
142
+ if (variables && Object.keys(variables).length > 0) {
143
+ url.searchParams.set("variables", JSON.stringify(variables));
144
+ }
145
+ return url.toString();
146
+ }
147
+ async function documentHash(documentText) {
148
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(documentText));
149
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
150
+ }
151
+ var PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
152
+ var NOT_SHAREABLE = "NOT_SHAREABLE";
153
+ async function hasFoirErrorCode(res, ...codes) {
154
+ try {
155
+ const body = await res.clone().json();
156
+ return body.errors?.some((e) => e.extensions?.code !== void 0 && codes.includes(e.extensions.code)) ?? false;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
162
+ // src/graphql/cdn-route.ts
163
+ function isCdnEligible(env, documentText, bearerToken) {
164
+ if (bearerToken) return false;
165
+ if (isFoirPreview(env)) return false;
166
+ const apiKey = resolveFoirApiKey(env);
167
+ if (!apiKey?.startsWith("pk_")) return false;
168
+ return !/\bmutation\b/.test(documentText);
169
+ }
170
+
171
+ // src/transport/retry.ts
172
+ var DEFAULT_ATTEMPTS = 3;
173
+ var DEFAULT_BASE_DELAY_MS = 200;
174
+ var DEFAULT_MAX_DELAY_MS = 2e4;
175
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
176
+ function foirRetryDelayMs(error, attempt, policy = {}) {
177
+ if (!(error instanceof FoirRequestError) || !error.retryable) return null;
178
+ const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
179
+ const stated = error.retryAfterMs;
180
+ if (stated !== void 0) return Math.min(stated, max);
181
+ const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
182
+ return Math.min(base * 2 ** (attempt - 1), max);
183
+ }
184
+ async function withFoirRetry(fn, policy = {}) {
185
+ const attempts = Math.max(1, policy.attempts ?? DEFAULT_ATTEMPTS);
186
+ const sleep = policy.sleep ?? defaultSleep;
187
+ for (let attempt = 1; ; attempt += 1) {
188
+ try {
189
+ return await fn();
190
+ } catch (error) {
191
+ const delayMs = attempt >= attempts ? null : foirRetryDelayMs(error, attempt, policy);
192
+ if (delayMs === null) throw error;
193
+ policy.onRetry?.({ attempt, delayMs, error });
194
+ await sleep(delayMs);
195
+ }
196
+ }
197
+ }
198
+
199
+ // src/graphql/foir-request.ts
200
+ var FOIR_CDN_BASE = "https://cdn.foir.dev/";
201
+ function absolutiseFoirMediaUrl(url, cdnBase = FOIR_CDN_BASE) {
202
+ if (!url) return void 0;
203
+ if (/^https?:\/\//i.test(url)) return url;
204
+ return cdnBase + url.replace(/^\/+/, "");
205
+ }
206
+ async function foirRequest(env, document, variables, options) {
207
+ const endpoint = env.PLATFORM_API_URL;
208
+ const apiKey = resolveFoirApiKey(env);
209
+ if (!endpoint || !apiKey) {
210
+ throw new FoirRequestError(
211
+ "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
212
+ );
213
+ }
214
+ const headers = buildFoirHeaders({
215
+ apiKey,
216
+ draftChannel: isFoirPreview(env),
217
+ bearerToken: options?.bearerToken
218
+ });
219
+ const documentText = print2(document);
220
+ let res;
221
+ try {
222
+ if (isCdnEligible(env, documentText, options?.bearerToken)) {
223
+ const hash = await documentHash(documentText);
224
+ let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
225
+ method: "GET",
226
+ headers
227
+ });
228
+ if (await isPersistedQueryMiss(shared)) {
229
+ shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
230
+ method: "POST",
231
+ headers,
232
+ body: JSON.stringify({
233
+ query: documentText,
234
+ variables,
235
+ extensions: persistedQueryExtension(hash)
236
+ })
237
+ });
238
+ }
239
+ if (await isSharedRefusal(shared)) {
240
+ res = await fetch(endpoint, {
241
+ method: "POST",
242
+ headers,
243
+ body: JSON.stringify({ query: documentText, variables })
244
+ });
245
+ } else {
246
+ res = shared;
247
+ }
248
+ } else {
249
+ res = await fetch(endpoint, {
250
+ method: "POST",
251
+ headers,
252
+ body: JSON.stringify({ query: documentText, variables })
253
+ });
254
+ }
255
+ } catch (err) {
256
+ throw new FoirRequestError("[foir] network error", { cause: err });
257
+ }
258
+ options?.onRateLimit?.(readFoirRateLimit(res.headers));
259
+ if (!res.ok) {
260
+ const errors = await readErrorEnvelope(res);
261
+ const first = errors?.[0]?.message;
262
+ throw new FoirRequestError(
263
+ first ? `[foir] HTTP ${res.status} ${res.statusText}: ${first}` : `[foir] HTTP ${res.status} ${res.statusText}`,
264
+ { status: res.status, graphQLErrors: errors }
265
+ );
266
+ }
267
+ const json = await res.json();
268
+ return readFoirEnvelope(json);
269
+ }
270
+ async function readErrorEnvelope(res) {
271
+ let errors;
272
+ try {
273
+ const body = await res.json();
274
+ errors = body.errors;
275
+ } catch {
276
+ errors = void 0;
277
+ }
278
+ const headerRetryMs = readFoirRetryAfterMs(res.headers);
279
+ if (headerRetryMs === void 0) return errors;
280
+ const seconds = Math.round(headerRetryMs / 1e3);
281
+ if (!errors?.length) {
282
+ return [
283
+ {
284
+ message: res.statusText || `HTTP ${res.status}`,
285
+ extensions: { code: "RATE_LIMITED", retryAfter: seconds }
286
+ }
287
+ ];
288
+ }
289
+ if (errors[0].extensions?.retryAfter === void 0) {
290
+ errors[0] = {
291
+ ...errors[0],
292
+ extensions: { ...errors[0].extensions, retryAfter: seconds }
293
+ };
294
+ }
295
+ return errors;
296
+ }
297
+ async function isSharedRefusal(res) {
298
+ return hasFoirErrorCode(res, NOT_SHAREABLE);
299
+ }
300
+ async function isPersistedQueryMiss(res) {
301
+ return hasFoirErrorCode(res, PERSISTED_QUERY_NOT_FOUND);
302
+ }
303
+
76
304
  // src/graphql/content-client.ts
77
305
  function createFoirContentClient(config) {
78
306
  return {
79
307
  async request(document, variables, options) {
80
- const bound = config.getToken ? await config.getToken() : null;
81
- return foirRequest(config.env, document, variables, {
82
- ...options,
83
- bearerToken: options?.bearerToken ?? bound ?? void 0
84
- });
308
+ return withFoirRetry(async () => {
309
+ const bound = config.getToken ? await config.getToken() : null;
310
+ return foirRequest(config.env, document, variables, {
311
+ ...options,
312
+ bearerToken: options?.bearerToken ?? bound ?? void 0,
313
+ onRateLimit: options?.onRateLimit ?? config.onRateLimit
314
+ });
315
+ }, config.retry);
85
316
  }
86
317
  };
87
318
  }
@@ -91,5 +322,12 @@ export {
91
322
  absolutiseFoirMediaUrl,
92
323
  createFoirContentClient,
93
324
  foirRequest,
94
- isFoirPreview
325
+ foirRetryDelayMs,
326
+ isFoirForbidden,
327
+ isFoirNotFound,
328
+ isFoirPreview,
329
+ isFoirRateLimited,
330
+ isFoirUnauthenticated,
331
+ readFoirRateLimit,
332
+ withFoirRetry
95
333
  };
@@ -1,27 +1,17 @@
1
- import { GraphQLSchema } from 'graphql';
2
- import { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
3
-
4
1
  /**
5
- * `@foir/sdk/codegen/block-registry` — a graphql-codegen plugin.
2
+ * `@foir/sdk/codegen/block-registry` — DEPRECATED alias.
6
3
  *
7
- * Introspects the project's public schema, collects every member of the block
8
- * union(s), and emits an EXHAUSTIVE `BlockRegistry` type — one key per block
9
- * `__typename`, each typed `BlockComponent<ThatBlock, Ctx>`. Because the type
10
- * requires every key, a project that adds an inline model (→ a new block
11
- * `__typename`) breaks the consumer's build until the new component is wired:
12
- * exhaustiveness by construction, replacing the hand-synced switch the
13
- * storefront's `BlockDigester` used to carry.
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`;
6
+ * this path re-exports it unchanged so an existing `codegen.ts` keeps working.
7
+ * Repoint the `plugins:` entry when convenient — this will be removed in a
8
+ * future major.
14
9
  *
15
- * Wired automatically by `foirCodegen()`. The generated file imports the block
16
- * types from the codegen output (`./graphql`) and `BlockComponent` from
17
- * `@foir/sdk/content`, so it pairs directly with `createContentRenderer`.
10
+ * Note that the emitted type is now named `ItemRegistry` by default and its
11
+ * components are typed `ItemComponent`. If you import the generated
12
+ * `BlockRegistry` by name, either pass `registryName: 'BlockRegistry'` or
13
+ * update the import; `@foir/sdk/content` still exports `BlockRegistry` as a
14
+ * deprecated alias of `ItemRegistry`, so the types remain compatible.
18
15
  */
19
-
20
- interface BlockRegistryConfig extends BlockPluginConfig {
21
- /** Name of the generated registry type. Default `BlockRegistry`. */
22
- registryName?: string;
23
- }
24
- /** graphql-codegen plugin entrypoint. */
25
- declare const plugin: (schema: GraphQLSchema, _documents: unknown, config?: BlockRegistryConfig) => string;
26
-
27
- export { type BlockRegistryConfig, plugin };
16
+ export { plugin } from './item-registry';
17
+ export type { ItemRegistryConfig, BlockRegistryConfig, } from './item-registry';
@@ -1,35 +1,7 @@
1
1
  import {
2
- DEFAULT_GRAPHQL_IMPORT,
3
- DEFAULT_SDK_IMPORT,
4
- DEFAULT_UNIONS,
5
- collectBlockMembers
6
- } from "../chunk-ADO5FJNM.js";
7
-
8
- // src/codegen/block-registry.ts
9
- var plugin = (schema, _documents, config = {}) => {
10
- const members = collectBlockMembers(schema, config.unions ?? DEFAULT_UNIONS);
11
- const graphqlImport = config.graphqlImport ?? DEFAULT_GRAPHQL_IMPORT;
12
- const sdkImport = config.sdkImport ?? DEFAULT_SDK_IMPORT;
13
- const registryName = config.registryName ?? "BlockRegistry";
14
- return [
15
- "/* eslint-disable */",
16
- "// Generated by @foir/sdk/codegen/block-registry. Do not edit by hand.",
17
- "// Regenerated on every codegen run \u2014 omitting a block type is a compile error.",
18
- `import type { BlockComponent } from '${sdkImport}';`,
19
- `import type {`,
20
- ...members.map((m) => ` ${m},`),
21
- `} from '${graphqlImport}';`,
22
- "",
23
- `/**`,
24
- ` * Exhaustive map of every block \`__typename\` in the project schema to the`,
25
- ` * component that renders it. Pass an instance to \`createContentRenderer\`.`,
26
- ` */`,
27
- `export type ${registryName}<Ctx = unknown> = {`,
28
- ...members.map((m) => ` ${m}: BlockComponent<${m}, Ctx>;`),
29
- `};`,
30
- ""
31
- ].join("\n");
32
- };
2
+ plugin
3
+ } from "../chunk-IGNLWT3H.js";
4
+ import "../chunk-TAL5H6VA.js";
33
5
  export {
34
6
  plugin
35
7
  };
@@ -1,28 +1,15 @@
1
- import { GraphQLSchema } from 'graphql';
2
- import { B as BlockPluginConfig } from '../shared-DkWlEd2Z.js';
3
-
4
1
  /**
5
- * `@foir/sdk/codegen/block-scaffold` — a one-shot graphql-codegen plugin.
2
+ * `@foir/sdk/codegen/block-scaffold` — DEPRECATED alias.
6
3
  *
7
- * Emits a ContentRenderer shell: a stub component per block `__typename`, a
8
- * `blockRegistry` of the (exhaustive) `BlockRegistry` type, and a
9
- * `createContentRenderer` wiring. Unlike the block-registry plugin (which
10
- * regenerates every run), this is a STARTING POINT the dev then owns — run it
11
- * once via `foirScaffold()`, then implement each stub. It compiles out of the
12
- * box (each stub is a typed no-op) so the build stays green while you fill them
13
- * in. Adding a new block type later surfaces as a compile error on
14
- * `blockRegistry` (via the regenerated `BlockRegistry` type), not here.
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`;
6
+ * this path re-exports it unchanged so an existing `codegen.ts` keeps working.
7
+ * Repoint the `plugins:` entry when convenient — this will be removed in a
8
+ * future major.
9
+ *
10
+ * The scaffold is a one-shot generator, so a file scaffolded under the old name
11
+ * is yours and is untouched by this. Newly scaffolded files export
12
+ * `itemRegistry` rather than `blockRegistry` and take `props.item`.
15
13
  */
16
-
17
- interface BlockScaffoldConfig extends BlockPluginConfig {
18
- /** Import path for the generated registry type. Default `./block-registry`. */
19
- registryImport?: string;
20
- /** Render-context type name threaded to every component. Default `unknown`. */
21
- ctxType?: string;
22
- /** Optional import line for the ctx type, e.g. `import type { Ctx } from '~/ctx'`. */
23
- ctxImport?: string;
24
- }
25
- /** graphql-codegen plugin entrypoint. */
26
- declare const plugin: (schema: GraphQLSchema, _documents: unknown, config?: BlockScaffoldConfig) => string;
27
-
28
- export { type BlockScaffoldConfig, plugin };
14
+ export { plugin } from './item-scaffold';
15
+ export type { ItemScaffoldConfig, BlockScaffoldConfig, } from './item-scaffold';
@@ -1,45 +1,7 @@
1
1
  import {
2
- DEFAULT_GRAPHQL_IMPORT,
3
- DEFAULT_SDK_IMPORT,
4
- DEFAULT_UNIONS,
5
- collectBlockMembers
6
- } from "../chunk-ADO5FJNM.js";
7
-
8
- // src/codegen/block-scaffold.ts
9
- var plugin = (schema, _documents, config = {}) => {
10
- const members = collectBlockMembers(schema, config.unions ?? DEFAULT_UNIONS);
11
- const graphqlImport = config.graphqlImport ?? DEFAULT_GRAPHQL_IMPORT;
12
- const sdkImport = config.sdkImport ?? DEFAULT_SDK_IMPORT;
13
- const registryImport = config.registryImport ?? "./block-registry";
14
- const ctx = config.ctxType ?? "unknown";
15
- const stub = (m) => [
16
- `function ${m}View(props: BlockComponentProps<${m}, ${ctx}>) {`,
17
- ` // TODO: render ${m} from props.block (props.ctx, props.priority available)`,
18
- ` void props;`,
19
- ` return null;`,
20
- `}`
21
- ].join("\n");
22
- return [
23
- "/* eslint-disable */",
24
- "// Scaffolded once by @foir/sdk/codegen/block-scaffold \u2014 you own this file.",
25
- "// Implement each stub; the build stays green until you do.",
26
- `import { createContentRenderer, type BlockComponentProps } from '${sdkImport}';`,
27
- `import type { BlockRegistry } from '${registryImport}';`,
28
- `import type {`,
29
- ...members.map((m) => ` ${m},`),
30
- `} from '${graphqlImport}';`,
31
- ...config.ctxImport ? ["", config.ctxImport] : [],
32
- "",
33
- ...members.map(stub),
34
- "",
35
- `export const blockRegistry: BlockRegistry<${ctx}> = {`,
36
- ...members.map((m) => ` ${m}: ${m}View,`),
37
- `};`,
38
- "",
39
- `export const ContentRenderer = createContentRenderer<${ctx}>(blockRegistry);`,
40
- ""
41
- ].join("\n");
42
- };
2
+ plugin
3
+ } from "../chunk-OHYVKTGB.js";
4
+ import "../chunk-TAL5H6VA.js";
43
5
  export {
44
6
  plugin
45
7
  };