@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.
- package/CHANGELOG.md +1126 -0
- package/README.md +94 -0
- package/dist/auth/auth.d.ts +243 -0
- package/dist/auth/hosted.d.ts +40 -0
- package/dist/auth/index.d.ts +21 -0
- package/dist/auth/operations.d.ts +144 -0
- package/dist/auth/ops.d.ts +99 -0
- package/dist/auth/session-client.d.ts +76 -0
- package/dist/auth-session.js +137 -0
- package/dist/auth.js +973 -92
- package/dist/chunk-IGNLWT3H.js +35 -0
- package/dist/chunk-OHYVKTGB.js +45 -0
- package/dist/chunk-TAL5H6VA.js +39 -0
- package/dist/client/platform-client.d.ts +29 -0
- package/dist/client.js +275 -37
- package/dist/codegen/block-registry.d.ts +13 -23
- package/dist/codegen/block-registry.js +3 -31
- package/dist/codegen/block-scaffold.d.ts +12 -25
- package/dist/codegen/block-scaffold.js +3 -41
- package/dist/codegen/index.d.ts +12 -61
- package/dist/codegen/index.js +7 -11
- package/dist/codegen/item-registry.d.ts +28 -0
- package/dist/codegen/item-registry.js +7 -0
- package/dist/codegen/item-scaffold.d.ts +29 -0
- package/dist/codegen/item-scaffold.js +7 -0
- package/dist/codegen/preset.d.ts +67 -0
- package/dist/codegen/shared.d.ts +36 -0
- package/dist/components/EditorProvider.d.ts +36 -0
- package/dist/content/content-renderer.d.ts +113 -0
- package/dist/content/index.d.ts +18 -0
- package/dist/content/richtext.d.ts +118 -0
- package/dist/content.js +227 -80
- package/dist/data/builtins.d.ts +506 -0
- package/dist/data/client.d.ts +166 -0
- package/dist/data/document.d.ts +251 -0
- package/dist/data/index.d.ts +73 -0
- package/dist/data/manifest.d.ts +424 -0
- package/dist/data/schema.d.ts +313 -0
- package/dist/data/selection.d.ts +524 -0
- package/dist/data/subjects.d.ts +221 -0
- package/dist/data/subscription-document.d.ts +75 -0
- package/dist/data/subscriptions.d.ts +181 -0
- package/dist/data/transport.d.ts +325 -0
- package/dist/data/ws-transport.d.ts +197 -0
- package/dist/data.js +1683 -0
- package/dist/graphql/cdn-route.d.ts +26 -0
- package/dist/graphql/client.d.ts +30 -0
- package/dist/graphql/content-client.d.ts +66 -0
- package/dist/graphql/foir-request.d.ts +47 -0
- package/dist/hooks/useAutoResize.d.ts +22 -0
- package/dist/hooks/useEditor.d.ts +16 -0
- package/dist/index.d.ts +10 -394
- package/dist/media/BlurhashCanvas.d.ts +10 -0
- package/dist/media/ProgressiveMedia.d.ts +56 -0
- package/dist/media/index.d.ts +18 -0
- package/dist/media/media.d.ts +139 -0
- package/dist/media/screen-capture.d.ts +81 -0
- package/dist/media/useProgressiveLoad.d.ts +23 -0
- package/dist/media.js +102 -0
- package/dist/next/cookieAdapter.d.ts +26 -0
- package/dist/next/index.d.ts +15 -0
- package/dist/next/middleware.d.ts +49 -0
- package/dist/next/session.d.ts +53 -0
- package/dist/next.js +763 -78
- package/dist/protocol/constants.d.ts +6 -0
- package/dist/protocol/messages.d.ts +155 -0
- package/dist/protocol/validation.d.ts +8 -0
- package/dist/push/index.d.ts +14 -0
- package/dist/push/operations.d.ts +22 -0
- package/dist/push/web-push.d.ts +39 -0
- package/dist/push.js +892 -78
- package/dist/react/FoirFeedback.d.ts +62 -0
- package/dist/react/FoirProvider.d.ts +116 -0
- package/dist/react/OAuthButtons.d.ts +20 -0
- package/dist/react/SignIn.d.ts +22 -0
- package/dist/react/UserButton.d.ts +13 -0
- package/dist/react/feedback-notes.d.ts +101 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/localStorageAdapter.d.ts +10 -0
- package/dist/react.js +1373 -91
- package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
- package/dist/realtime.js +1 -1
- package/dist/server/callback-client.d.ts +92 -0
- package/dist/server/events-client.d.ts +279 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/operation-types.d.ts +134 -0
- package/dist/server/require-scoped-token.d.ts +76 -0
- package/dist/server/server-client.d.ts +34 -0
- package/dist/server/types.d.ts +54 -0
- package/dist/server/verify-scoped-token.d.ts +100 -0
- package/dist/server/webhook-helpers.d.ts +36 -0
- package/dist/server.js +298 -0
- package/dist/transport/foir-http.d.ts +234 -0
- package/dist/transport/persisted-query.d.ts +68 -0
- package/dist/transport/retry.d.ts +61 -0
- package/dist/types/index.d.ts +124 -0
- package/package.json +35 -13
- package/dist/auth.d.ts +0 -382
- package/dist/chunk-ADO5FJNM.js +0 -30
- package/dist/client.d.ts +0 -135
- package/dist/content.d.ts +0 -96
- package/dist/media.d.ts +0 -234
- package/dist/next.d.ts +0 -178
- package/dist/push.d.ts +0 -109
- package/dist/react.d.ts +0 -319
- package/dist/server.d.ts +0 -534
- 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.
|
|
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
|
-
"
|
|
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",
|