@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,234 @@
1
+ /**
2
+ * Foir HTTP transport core — the `graphql`-free half of `graphql/foir-request.ts`.
3
+ *
4
+ * Everything here is shared by BOTH public data surfaces:
5
+ *
6
+ * - `@foir/sdk/client` — `foirRequest`, which prints a `TypedDocumentNode` and
7
+ * POSTs it to `/graphql`. Needs `graphql` for `print`.
8
+ * - `@foir/sdk/data` — `createFoirClient`, which prints a document from a
9
+ * selection tree and must not pull `graphql` into a storefront bundle at
10
+ * all. Printing a pre-resolved tree is string building.
11
+ *
12
+ * The split exists so the second one is possible. The Foir-specific transport
13
+ * concerns — which api key is in play, the preview schema channel header, the
14
+ * optional customer bearer, and the `FoirRequestError` shape — are identical on
15
+ * both, and duplicating them is exactly how the two surfaces would drift. So
16
+ * they live here once and `foir-request.ts` re-exports them unchanged; the only
17
+ * thing that stayed behind is the part that needs `print`.
18
+ *
19
+ * `fetch` only, no Node built-ins, and no module-level cache — the reasoning at
20
+ * the top of `graphql/foir-request.ts` applies verbatim: a shared cache on a
21
+ * stateless multi-tenant edge worker is a cross-tenant hazard.
22
+ */
23
+ export interface FoirEnv {
24
+ PLATFORM_API_URL?: string;
25
+ PLATFORM_API_KEY?: string;
26
+ PLATFORM_PREVIEW_API_KEY?: string;
27
+ FOIR_PREVIEW?: string;
28
+ }
29
+ /**
30
+ * Returns true when `FOIR_PREVIEW=true` is set in the environment.
31
+ *
32
+ * Preview is selected at the environment level, not per-request: load a
33
+ * preview env file that sets `FOIR_PREVIEW=true` + a `sk_…`
34
+ * `PLATFORM_PREVIEW_API_KEY` with `drafts:read` instead of the default env.
35
+ * The site-wide mode keeps draft semantics consistent — every loader sees
36
+ * the same preview/published view of the world. Intended for non-production
37
+ * surfaces only.
38
+ */
39
+ export declare function isFoirPreview(env: FoirEnv): boolean;
40
+ /**
41
+ * The api key a request should carry for this env: the `sk_` preview key when
42
+ * `FOIR_PREVIEW=true` (it is the one with `drafts:read`), the `pk_` key
43
+ * otherwise. Returns `undefined` when neither is configured, so the caller can
44
+ * raise the "add them to .env" error with its own wording.
45
+ */
46
+ export declare function resolveFoirApiKey(env: FoirEnv): string | undefined;
47
+ /**
48
+ * The machine-readable codes Foir puts in `extensions.code`. Stable contract —
49
+ * the platform's own note on them says never rename — so branching on these is
50
+ * the supported way to tell one failure from another.
51
+ *
52
+ * Note what is NOT here: there is no code for a missing record. A read that
53
+ * finds nothing resolves the field to `null` with no `errors` entry at all,
54
+ * because an absent record is an answer rather than a failure. `NOT_FOUND` only
55
+ * appears when a mutation names something that does not exist.
56
+ */
57
+ export type FoirErrorCode = 'INVALID_ARGUMENT' | 'NOT_FOUND' | 'ALREADY_EXISTS' | 'FAILED_PRECONDITION' | 'PERMISSION_DENIED' | 'UNAUTHENTICATED' | 'RATE_LIMITED' | 'TIMEOUT' | 'UNAVAILABLE' | 'CANCELED' | 'INTERNAL';
58
+ export interface FoirErrorExtensions {
59
+ /** Stable machine-readable code. Branch on this, never on `message`. */
60
+ code?: FoirErrorCode | string;
61
+ /** Correlation id. Quote it in a support request. */
62
+ requestId?: string;
63
+ /** On `RATE_LIMITED`: whole seconds to wait. Never less than 1. */
64
+ retryAfter?: number;
65
+ [key: string]: unknown;
66
+ }
67
+ export interface FoirGraphQLErrorItem {
68
+ message: string;
69
+ path?: Array<string | number>;
70
+ extensions?: FoirErrorExtensions;
71
+ }
72
+ /** What the `X-RateLimit-*` headers said on the response that carried them. */
73
+ export interface FoirRateLimitSnapshot {
74
+ /** Requests allowed per window for this credential. */
75
+ limit: number;
76
+ /** Requests left in the current window. */
77
+ remaining: number;
78
+ /** Unix seconds at which the window resets. */
79
+ resetAt: number;
80
+ }
81
+ export declare class FoirRequestError extends Error {
82
+ /** HTTP status code, when the request failed at the HTTP layer. */
83
+ readonly status?: number;
84
+ /** GraphQL errors, when the request failed with a no-data error envelope. */
85
+ readonly graphQLErrors?: ReadonlyArray<FoirGraphQLErrorItem>;
86
+ readonly cause?: unknown;
87
+ constructor(message: string, options?: {
88
+ cause?: unknown;
89
+ status?: number;
90
+ graphQLErrors?: ReadonlyArray<FoirGraphQLErrorItem>;
91
+ });
92
+ /**
93
+ * The code of the first error, if the response carried one.
94
+ *
95
+ * A getter rather than a stored field so it stays correct for an error
96
+ * constructed anywhere, including by a caller in a test.
97
+ */
98
+ get code(): FoirErrorCode | string | undefined;
99
+ /** The correlation id, if the response carried one. */
100
+ get requestId(): string | undefined;
101
+ /**
102
+ * Whether retrying this request could succeed.
103
+ *
104
+ * The 429 is the one that has to be in here and is easy to leave out: it sits
105
+ * among the 4xx statuses that are otherwise permanent, so a policy written as
106
+ * "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
107
+ * own. A storefront wrote that policy, its key saturated, and every read
108
+ * failed on the first attempt for an hour.
109
+ */
110
+ get retryable(): boolean;
111
+ /**
112
+ * How long to wait before retrying, in milliseconds, when the server said.
113
+ * Undefined when it did not, which means the caller picks its own backoff.
114
+ */
115
+ get retryAfterMs(): number | undefined;
116
+ }
117
+ /** True when the failure is Foir refusing on rate. */
118
+ export declare function isFoirRateLimited(err: unknown): err is FoirRequestError;
119
+ /**
120
+ * True when the credential was rejected. Distinguished from
121
+ * {@link isFoirRateLimited} because the two want opposite responses: a bad key
122
+ * is never going to start working, and retrying it just spends allowance.
123
+ */
124
+ export declare function isFoirUnauthenticated(err: unknown): err is FoirRequestError;
125
+ /** True when the credential is valid but not permitted to do this. */
126
+ export declare function isFoirForbidden(err: unknown): err is FoirRequestError;
127
+ /**
128
+ * True when a MUTATION named something that does not exist.
129
+ *
130
+ * Reads do not produce this. A read for a record that is not there resolves the
131
+ * field to `null` and returns no error at all, so `data.page === null` is the
132
+ * check for a missing page — not a catch.
133
+ */
134
+ export declare function isFoirNotFound(err: unknown): err is FoirRequestError;
135
+ /**
136
+ * Read the `X-RateLimit-*` headers off a response, when it carried them.
137
+ *
138
+ * The presence check is not defensive padding. `Number(null)` is 0, not NaN, so
139
+ * reading a missing header through `Number` alone yields a perfectly plausible
140
+ * snapshot saying zero requests remain — on every response from anything that
141
+ * does not set these headers, including a cache or a proxy in front of Foir. An
142
+ * app wired to warn on a low remaining count would then warn constantly, which
143
+ * is how a rate-limit alarm becomes noise people mute.
144
+ */
145
+ export declare function readFoirRateLimit(headers: Headers): FoirRateLimitSnapshot | undefined;
146
+ /**
147
+ * Seconds the server asked the caller to wait, from a 429's `Retry-After`
148
+ * header. Undefined when absent or unparseable.
149
+ *
150
+ * Read alongside `extensions.retryAfter`, which carries the same number in the
151
+ * body; the header is the one available when the body is unreadable.
152
+ */
153
+ export declare function readFoirRetryAfterMs(headers: Headers): number | undefined;
154
+ export interface FoirRequestOptions {
155
+ /**
156
+ * Customer bearer token. api-public still needs the `x-api-key` to resolve
157
+ * the project; the bearer adds the customer identity for self-scoped reads
158
+ * (e.g. `myProfile`). Used by `@foir/sdk/auth`'s post-login profile fetch.
159
+ */
160
+ bearerToken?: string;
161
+ /**
162
+ * Called with the `X-RateLimit-*` headers of every response, successful or
163
+ * not, and with `undefined` when a response carried none.
164
+ *
165
+ * Exists because those headers have always been on the wire and nothing has
166
+ * ever read them. A key can be at 200 of 10,000 remaining for twenty minutes
167
+ * before it starts refusing, and the only place that number appears is a
168
+ * header the SDK was throwing away. Log it, and you find out before your site
169
+ * does.
170
+ */
171
+ onRateLimit?: (snapshot: FoirRateLimitSnapshot | undefined) => void;
172
+ }
173
+ /**
174
+ * Supplies the current customer bearer. Sync or async so consumers can read a
175
+ * cookie session (Hydrogen/Remix), a KV store, an in-memory value, etc. Returns
176
+ * the token when signed in, or `null`/`undefined` when signed out.
177
+ *
178
+ * Lives here rather than beside `createFoirContentClient` because both data
179
+ * surfaces bind a session the same way, and a second copy of this alias is how
180
+ * they would end up accepting subtly different things.
181
+ */
182
+ export type FoirTokenProvider = () => string | null | undefined | Promise<string | null | undefined>;
183
+ export interface FoirHeaderInput {
184
+ apiKey: string;
185
+ /**
186
+ * Opt into the draft *schema* channel (`X-Foir-Schema-Channel: draft`) so
187
+ * freshly pushed model changes (new fields, new models) are visible at
188
+ * runtime — otherwise selections on draft-only fields are silently stripped
189
+ * and surface as confusing "fragment never used" errors. The header has no
190
+ * effect under public keys (the server ignores it), so it is safe to send
191
+ * whenever the caller is in preview.
192
+ */
193
+ draftChannel?: boolean;
194
+ bearerToken?: string;
195
+ /** Caller-supplied extras, applied last so a caller can override. */
196
+ extra?: Readonly<Record<string, string>>;
197
+ }
198
+ /**
199
+ * The exact header set every Foir data-plane request carries. Single
200
+ * definition on purpose: `/graphql` and `/cdn/graphql` authenticate identically,
201
+ * and a header that exists on one surface but not the other is a per-surface
202
+ * authorization difference nobody would notice until it mattered.
203
+ */
204
+ export declare function buildFoirHeaders(input: FoirHeaderInput): Record<string, string>;
205
+ /**
206
+ * Build a `/v1/...` URL from whatever the caller put in `apiUrl`.
207
+ *
208
+ * Tolerates the three things people actually pass: an origin, an origin with a
209
+ * trailing slash, and the conventional `PLATFORM_API_URL` value
210
+ * (`https://api.foir.dev/graphql`).
211
+ *
212
+ * Lives here rather than beside one of its callers because three surfaces now
213
+ * turn an `apiUrl` into a path on the API origin — `@foir/sdk/data`'s `/graphql`,
214
+ * `@foir/sdk/server`'s `/v1/events`, and anything that follows — and the
215
+ * tolerated inputs are a property of the config field, not of the endpoint. A
216
+ * second copy would accept a slightly different set, and the difference would
217
+ * only ever show up as one surface rejecting a `PLATFORM_API_URL` the other
218
+ * accepted. `data/transport.ts` re-exports it under its published name.
219
+ */
220
+ export declare function foirApiEndpoint(apiUrl: string, path: `/${string}`): string;
221
+ export interface FoirResponseEnvelope<T> {
222
+ data?: T;
223
+ errors?: Array<FoirGraphQLErrorItem>;
224
+ }
225
+ /**
226
+ * Unwrap a Foir `{ data, errors }` envelope.
227
+ *
228
+ * GraphQL can return partial data alongside errors; when data is present the
229
+ * errors are logged and the data returned (callers handle nulls), and we only
230
+ * throw when there is no data at all. Both Foir data-plane routes — `/graphql`
231
+ * and `/cdn/graphql` — hand their document to the same executor, so the two
232
+ * return this envelope and get the same treatment.
233
+ */
234
+ export declare function readFoirEnvelope<T>(json: FoirResponseEnvelope<T>): T;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Automatic persisted queries and the shared read lane — the `graphql`-free half
3
+ * of `graphql/cdn-route.ts`.
4
+ *
5
+ * Split out for the same reason `foir-http.ts` was split out of
6
+ * `graphql/foir-request.ts`: two surfaces need it and only one of them may
7
+ * import `graphql`. `@foir/sdk/client` prints a `TypedDocumentNode` and then
8
+ * hashes the text; `@foir/sdk/data` builds the text itself from a selection tree
9
+ * and must not drag the GraphQL parser into a storefront bundle. Everything
10
+ * below operates on a document STRING, so it belongs to neither.
11
+ *
12
+ * `graphql/cdn-route.ts` re-exports all of it unchanged, so that module's public
13
+ * surface is exactly what it always was; the only thing that stayed behind is
14
+ * the part that needs `print`.
15
+ */
16
+ /**
17
+ * The shared, cacheable read path. Same schema, same executor, same auth as
18
+ * `/graphql` — what differs is that a CDN in front of the caller can store the
19
+ * response, because the request is a GET and the credential is in the URL.
20
+ */
21
+ export declare const FOIR_CDN_GRAPHQL_PATH = "/cdn/graphql";
22
+ /** The APQ handshake object, in Apollo's shape because every client uses it. */
23
+ export declare function persistedQueryExtension(hash: string): {
24
+ persistedQuery: {
25
+ version: number;
26
+ sha256Hash: string;
27
+ };
28
+ };
29
+ /**
30
+ * Build the shared-endpoint URL for a read.
31
+ *
32
+ * The credential goes in the URL deliberately: a shared cache keys on the URL
33
+ * alone, so a key that rode only in a header would let two callers with
34
+ * different reach collide on one entry and be served each other's body. The
35
+ * server enforces this and names the refusal `NOT_SHAREABLE` with
36
+ * `condition: "credential"` when the key arrives only as a header.
37
+ */
38
+ export declare function buildCdnUrl(endpoint: string, apiKey: string, hash: string, variables?: unknown): string;
39
+ /**
40
+ * SHA-256 of the document text, hex encoded — the identity a persisted query is
41
+ * stored and retrieved under.
42
+ *
43
+ * Web Crypto rather than a bundled implementation: it is present in every
44
+ * runtime this SDK targets (workers, edge, Node 18+) and adds nothing to the
45
+ * caller's bundle. Reached through `globalThis`, never `import 'crypto'`, so
46
+ * the edge-safety assertion on `@foir/sdk/data`'s import graph still holds.
47
+ */
48
+ export declare function documentHash(documentText: string): Promise<string>;
49
+ /** The code the server answers with when it has never seen this hash. */
50
+ export declare const PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
51
+ /** The code the shared endpoint refuses with, so a caller knows to retry. */
52
+ export declare const NOT_SHAREABLE = "NOT_SHAREABLE";
53
+ /**
54
+ * Whether a response carries any of the handshake codes above.
55
+ *
56
+ * The response is CLONED before the body is read. A Response body can only be
57
+ * consumed once, and reading it here to inspect the code would leave the caller
58
+ * holding an empty stream on every successful read — turning a working fast
59
+ * path into silent data loss.
60
+ *
61
+ * Status is deliberately not part of the test. A persisted-query miss comes back
62
+ * **HTTP 200** with an errors array (verified against deployed dev), while a
63
+ * shareability refusal comes back 400; keying on the code alone reads both.
64
+ *
65
+ * Several codes at once rather than one call per code, because both callers ask
66
+ * about two and each call costs another clone and another parse of the same body.
67
+ */
68
+ export declare function hasFoirErrorCode(res: Response, ...codes: string[]): Promise<boolean>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Retry for the Foir data plane.
3
+ *
4
+ * It lives in the SDK rather than in a docs snippet because the snippet was
5
+ * getting written wrong. The rule everyone reaches for — retry 5xx, never retry
6
+ * 4xx — is right about every 4xx except the one that matters: a 429 means the
7
+ * key has spent its hourly allowance, the request is fine, and it will succeed
8
+ * once the window moves. A storefront implemented that rule faithfully, covering
9
+ * 502/503/504 only, and when its key saturated every read failed on the first
10
+ * attempt for a full hour. The reads it failed were rendering pages, and the
11
+ * pages came out empty.
12
+ *
13
+ * So the policy is: retry on no-response, on 5xx, and on 429 — and when the
14
+ * server said how long to wait, wait that long instead of guessing.
15
+ */
16
+ import { FoirRequestError } from './foir-http';
17
+ export interface FoirRetryPolicy {
18
+ /**
19
+ * Total attempts including the first. 1 disables retrying without needing a
20
+ * separate flag, which is what makes "off" expressible in the same shape.
21
+ */
22
+ attempts?: number;
23
+ /** Base delay for exponential backoff, in ms. Default 200. */
24
+ baseDelayMs?: number;
25
+ /**
26
+ * Ceiling on any single wait, in ms. Default 20_000.
27
+ *
28
+ * A rate-limit window is an hour, so a `Retry-After` on a saturated key can
29
+ * be most of one — far longer than a page render can wait. Capping it means a
30
+ * retry is attempted rather than the caller blocking for 40 minutes, and if
31
+ * that attempt fails too the error surfaces, which is the honest outcome. A
32
+ * caller that genuinely can wait sets a larger cap.
33
+ */
34
+ maxDelayMs?: number;
35
+ /** Called before each wait. For logging; must not throw. */
36
+ onRetry?: (info: {
37
+ attempt: number;
38
+ delayMs: number;
39
+ error: FoirRequestError;
40
+ }) => void;
41
+ /** Injectable sleep, so tests do not spend real seconds. */
42
+ sleep?: (ms: number) => Promise<void>;
43
+ }
44
+ /**
45
+ * How long to wait before attempt N+1, or null when this error should not be
46
+ * retried at all.
47
+ *
48
+ * Exported so a caller running its own loop gets the same answer as the built-in
49
+ * one; two implementations of "is this worth retrying" is how the 429 gets left
50
+ * out of one of them.
51
+ */
52
+ export declare function foirRetryDelayMs(error: unknown, attempt: number, policy?: FoirRetryPolicy): number | null;
53
+ /**
54
+ * Run `fn`, retrying transient Foir failures.
55
+ *
56
+ * Deliberately takes a thunk rather than wrapping a specific request shape, so
57
+ * the same policy covers `/graphql`, `/cdn/graphql` and anything either grows
58
+ * later. Non-Foir errors propagate untouched on the first throw — this is not a
59
+ * general-purpose retry helper and should not become one.
60
+ */
61
+ export declare function withFoirRetry<T>(fn: () => Promise<T>, policy?: FoirRetryPolicy): Promise<T>;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Public type definitions for the Editor SDK.
3
+ */
4
+ export type { EditorPlacement } from '../server/types.js';
5
+ /** Data provided by the host when the editor initializes. */
6
+ export interface EditorInit {
7
+ values: Record<string, unknown>;
8
+ metadata?: Record<string, unknown>;
9
+ modelKey: string;
10
+ recordId: string;
11
+ naturalKey?: string;
12
+ versionId: string | null;
13
+ variantKey: string | null;
14
+ locale: string | null;
15
+ schema: unknown;
16
+ mode: 'edit' | 'readonly';
17
+ theme: 'light' | 'dark';
18
+ /** Secret API key for public API access. */
19
+ apiKey: string;
20
+ /** Public API GraphQL endpoint URL (provided by host). */
21
+ apiUrl: string;
22
+ /** Config connection domain (e.g. my-store.myshopify.com) */
23
+ connectionDomain?: string;
24
+ /** How this editor is rendered in the admin UI. */
25
+ configMode?: 'editor' | 'sidebar';
26
+ /** Currently selected field keys (sidebar mode). */
27
+ selectedFieldKeys?: string[];
28
+ /** Available fields for selection (sidebar mode). */
29
+ selectableFields?: Array<{
30
+ key: string;
31
+ label: string;
32
+ type: string;
33
+ }>;
34
+ /** Locale context (sidebar mode). */
35
+ localeContext?: LocaleContext | null;
36
+ }
37
+ /** Locale context passed by the host for sidebar editors. */
38
+ export interface LocaleContext {
39
+ currentLocale: string;
40
+ sourceLocale: string;
41
+ isTranslating: boolean;
42
+ locales: Array<{
43
+ code: string;
44
+ displayName: string;
45
+ isDefault: boolean;
46
+ }>;
47
+ }
48
+ /** Value exposed by the EditorProvider context. */
49
+ export interface EditorContextValue {
50
+ /** Whether the editor has received init data from the host. */
51
+ isReady: boolean;
52
+ /** Initial data from the host (available after init). */
53
+ init: EditorInit | null;
54
+ /** Current host theme. */
55
+ theme: 'light' | 'dark';
56
+ /** Platform GraphQL client (available after init). */
57
+ client: PlatformClient;
58
+ /** Send complete form values to the host (full replacement). */
59
+ updateValues: (values: Record<string, unknown>) => void;
60
+ /** Send a single field update to the host. */
61
+ updateField: (path: string, value: unknown) => void;
62
+ /** Request the host to save the current values. */
63
+ requestSave: () => void;
64
+ /** Request the host to publish the current version. */
65
+ requestPublish: () => void;
66
+ /** Report iframe height for auto-resize. */
67
+ resize: (height: number) => void;
68
+ /** Report dirty state (unsaved changes). */
69
+ setDirty: (isDirty: boolean) => void;
70
+ /** Request the host to navigate to an admin path. */
71
+ requestNavigate: (path: string) => void;
72
+ /** Request the host to execute a platform operation. Returns the result.
73
+ * `mode` controls whether the platform waits for the operation to complete
74
+ * ('sync') or returns immediately after queueing ('async'). Defaults to 'sync'. */
75
+ requestOperation: (operationKey: string, input?: Record<string, unknown>, options?: {
76
+ mode?: 'sync' | 'async';
77
+ }) => Promise<{
78
+ success: boolean;
79
+ result?: unknown;
80
+ error?: {
81
+ code: string;
82
+ message: string;
83
+ };
84
+ }>;
85
+ /** Editor mode ('editor' or 'sidebar'). */
86
+ configMode: 'editor' | 'sidebar';
87
+ /** Currently selected field keys (sidebar mode). */
88
+ selectedFieldKeys: string[];
89
+ /** Available fields for selection (sidebar mode). */
90
+ selectableFields: Array<{
91
+ key: string;
92
+ label: string;
93
+ type: string;
94
+ }>;
95
+ /** Locale context (sidebar mode). */
96
+ localeContext: LocaleContext | null;
97
+ }
98
+ /** Options for creating a PlatformClient. */
99
+ export interface PlatformClientOptions {
100
+ /** GraphQL endpoint URL. */
101
+ apiUrl: string;
102
+ /** API key for authentication. */
103
+ apiKey: string;
104
+ }
105
+ /** A minimal GraphQL error shape. */
106
+ export interface GraphQLError {
107
+ message: string;
108
+ locations?: Array<{
109
+ line: number;
110
+ column: number;
111
+ }>;
112
+ path?: Array<string | number>;
113
+ extensions?: Record<string, unknown>;
114
+ }
115
+ /** Response shape from a GraphQL query/mutation. */
116
+ export interface GraphQLResponse<T = Record<string, unknown>> {
117
+ data: T | null;
118
+ errors?: GraphQLError[];
119
+ }
120
+ /** Fetch-based GraphQL client for platform API calls. */
121
+ export interface PlatformClient {
122
+ /** Execute a GraphQL query or mutation. */
123
+ query<T = Record<string, unknown>>(query: string, variables?: Record<string, unknown>): Promise<GraphQLResponse<T>>;
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.8.0",
4
4
  "description": "Official Foir SDK — editor iframe client + server-side platform helpers",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -14,45 +14,61 @@
14
14
  "import": "./dist/index.js"
15
15
  },
16
16
  "./server": {
17
- "types": "./dist/server.d.ts",
17
+ "types": "./dist/server/index.d.ts",
18
18
  "import": "./dist/server.js"
19
19
  },
20
20
  "./client": {
21
- "types": "./dist/client.d.ts",
21
+ "types": "./dist/graphql/client.d.ts",
22
22
  "import": "./dist/client.js"
23
23
  },
24
+ "./data": {
25
+ "types": "./dist/data/index.d.ts",
26
+ "import": "./dist/data.js"
27
+ },
24
28
  "./media": {
25
- "types": "./dist/media.d.ts",
29
+ "types": "./dist/media/index.d.ts",
26
30
  "import": "./dist/media.js"
27
31
  },
28
32
  "./content": {
29
- "types": "./dist/content.d.ts",
33
+ "types": "./dist/content/index.d.ts",
30
34
  "import": "./dist/content.js"
31
35
  },
32
36
  "./auth": {
33
- "types": "./dist/auth.d.ts",
37
+ "types": "./dist/auth/index.d.ts",
34
38
  "import": "./dist/auth.js"
35
39
  },
40
+ "./auth/session": {
41
+ "types": "./dist/auth/session-client.d.ts",
42
+ "import": "./dist/auth-session.js"
43
+ },
36
44
  "./react": {
37
- "types": "./dist/react.d.ts",
45
+ "types": "./dist/react/index.d.ts",
38
46
  "import": "./dist/react.js"
39
47
  },
40
48
  "./next": {
41
- "types": "./dist/next.d.ts",
49
+ "types": "./dist/next/index.d.ts",
42
50
  "import": "./dist/next.js"
43
51
  },
44
52
  "./push": {
45
- "types": "./dist/push.d.ts",
53
+ "types": "./dist/push/index.d.ts",
46
54
  "import": "./dist/push.js"
47
55
  },
48
56
  "./realtime": {
49
- "types": "./dist/realtime.d.ts",
57
+ "types": "./dist/realtime/index.d.ts",
50
58
  "import": "./dist/realtime.js"
51
59
  },
52
60
  "./codegen": {
53
61
  "types": "./dist/codegen/index.d.ts",
54
62
  "import": "./dist/codegen/index.js"
55
63
  },
64
+ "./codegen/item-registry": {
65
+ "types": "./dist/codegen/item-registry.d.ts",
66
+ "import": "./dist/codegen/item-registry.js"
67
+ },
68
+ "./codegen/item-scaffold": {
69
+ "types": "./dist/codegen/item-scaffold.d.ts",
70
+ "import": "./dist/codegen/item-scaffold.js"
71
+ },
56
72
  "./codegen/block-registry": {
57
73
  "types": "./dist/codegen/block-registry.d.ts",
58
74
  "import": "./dist/codegen/block-registry.js"
@@ -63,7 +79,8 @@
63
79
  }
64
80
  },
65
81
  "files": [
66
- "dist"
82
+ "dist",
83
+ "CHANGELOG.md"
67
84
  ],
68
85
  "keywords": [
69
86
  "foir",
@@ -119,16 +136,20 @@
119
136
  },
120
137
  "devDependencies": {
121
138
  "@graphql-codegen/cli": "^5.0.7",
139
+ "@testing-library/react": "^16.2.0",
122
140
  "@types/react": "^19.2.0",
123
141
  "@types/react-dom": "^19.2.0",
142
+ "@types/ws": "^8.5.13",
124
143
  "blurhash": "^2.0.5",
125
144
  "graphql": "^16.10.0",
126
145
  "hls.js": "^1.6.16",
146
+ "jsdom": "^27.0.0",
127
147
  "react": "^19.2.0",
128
148
  "react-dom": "^19.2.0",
129
149
  "tsup": "^8.5.1",
130
150
  "typescript": "5.9.2",
131
151
  "vitest": "^3.2.4",
152
+ "ws": "^8.18.0",
132
153
  "y-websocket": "^3.0.0",
133
154
  "yjs": "^13.6.30"
134
155
  },
@@ -136,8 +157,9 @@
136
157
  "node": ">=18.0.0"
137
158
  },
138
159
  "scripts": {
139
- "build": "tsup",
140
- "dev": "tsup --watch",
160
+ "build": "tsup && tsc -p tsconfig.build.json && node scripts/assert-export-types.mjs && node scripts/assert-public-exports.mjs",
161
+ "snapshot:exports": "node scripts/assert-public-exports.mjs --update",
162
+ "dev": "node scripts/dev-watch.mjs",
141
163
  "check-types": "tsc --noEmit",
142
164
  "test": "vitest run",
143
165
  "test:watch": "vitest watch",