@foir/sdk 0.4.0 → 0.7.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 +325 -0
- package/README.md +63 -0
- package/dist/auth/auth.d.ts +140 -0
- package/dist/auth/hosted.d.ts +40 -0
- package/dist/auth/index.d.ts +15 -0
- package/dist/auth/operations.d.ts +123 -0
- package/dist/auth/session-client.d.ts +76 -0
- package/dist/auth-session.js +137 -0
- package/dist/auth.js +238 -26
- package/dist/client/platform-client.d.ts +29 -0
- package/dist/client.js +290 -32
- package/dist/codegen/block-registry.d.ts +4 -8
- package/dist/codegen/block-scaffold.d.ts +4 -8
- package/dist/codegen/index.d.ts +8 -62
- package/dist/codegen/preset.d.ts +60 -0
- package/dist/codegen/shared.d.ts +25 -0
- package/dist/components/EditorProvider.d.ts +36 -0
- package/dist/content/content-renderer.d.ts +47 -0
- package/dist/content/index.d.ts +12 -0
- package/dist/content/richtext.d.ts +70 -0
- package/dist/data/builtins.d.ts +296 -0
- package/dist/data/client.d.ts +94 -0
- package/dist/data/index.d.ts +54 -0
- package/dist/data/manifest.d.ts +161 -0
- package/dist/data/schema.d.ts +243 -0
- package/dist/data/selection.d.ts +330 -0
- package/dist/data/transport.d.ts +107 -0
- package/dist/data.js +309 -0
- package/dist/graphql/cdn-route.d.ts +42 -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 +14 -0
- package/dist/media/media.d.ts +139 -0
- package/dist/media/useProgressiveLoad.d.ts +23 -0
- package/dist/media.js +5 -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 +230 -26
- 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 +230 -26
- 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/index.d.ts +19 -0
- package/dist/react/localStorageAdapter.d.ts +10 -0
- package/dist/react.js +230 -26
- package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
- package/dist/server/callback-client.d.ts +92 -0
- package/dist/server/index.d.ts +8 -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/transport/foir-http.d.ts +217 -0
- package/dist/transport/retry.d.ts +61 -0
- package/dist/types/index.d.ts +124 -0
- package/package.json +23 -13
- package/dist/auth.d.ts +0 -382
- package/dist/client.d.ts +0 -83
- 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
package/dist/data.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// src/transport/foir-http.ts
|
|
2
|
+
function isFoirPreview(env) {
|
|
3
|
+
return env.FOIR_PREVIEW === "true";
|
|
4
|
+
}
|
|
5
|
+
function resolveFoirApiKey(env) {
|
|
6
|
+
return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
7
|
+
}
|
|
8
|
+
var FoirRequestError = class extends Error {
|
|
9
|
+
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
10
|
+
status;
|
|
11
|
+
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
12
|
+
graphQLErrors;
|
|
13
|
+
cause;
|
|
14
|
+
constructor(message, options) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "FoirRequestError";
|
|
17
|
+
this.cause = options?.cause;
|
|
18
|
+
this.status = options?.status;
|
|
19
|
+
this.graphQLErrors = options?.graphQLErrors;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The code of the first error, if the response carried one.
|
|
23
|
+
*
|
|
24
|
+
* A getter rather than a stored field so it stays correct for an error
|
|
25
|
+
* constructed anywhere, including by a caller in a test.
|
|
26
|
+
*/
|
|
27
|
+
get code() {
|
|
28
|
+
return this.graphQLErrors?.[0]?.extensions?.code;
|
|
29
|
+
}
|
|
30
|
+
/** The correlation id, if the response carried one. */
|
|
31
|
+
get requestId() {
|
|
32
|
+
return this.graphQLErrors?.[0]?.extensions?.requestId;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether retrying this request could succeed.
|
|
36
|
+
*
|
|
37
|
+
* The 429 is the one that has to be in here and is easy to leave out: it sits
|
|
38
|
+
* among the 4xx statuses that are otherwise permanent, so a policy written as
|
|
39
|
+
* "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
|
|
40
|
+
* own. A storefront wrote that policy, its key saturated, and every read
|
|
41
|
+
* failed on the first attempt for an hour.
|
|
42
|
+
*/
|
|
43
|
+
get retryable() {
|
|
44
|
+
if (this.status === void 0) return true;
|
|
45
|
+
if (this.status === 429 || this.code === "RATE_LIMITED") return true;
|
|
46
|
+
if (this.status >= 500) return true;
|
|
47
|
+
return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* How long to wait before retrying, in milliseconds, when the server said.
|
|
51
|
+
* Undefined when it did not, which means the caller picks its own backoff.
|
|
52
|
+
*/
|
|
53
|
+
get retryAfterMs() {
|
|
54
|
+
const seconds = this.graphQLErrors?.[0]?.extensions?.retryAfter;
|
|
55
|
+
return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function buildFoirHeaders(input) {
|
|
59
|
+
const headers = {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"x-api-key": input.apiKey
|
|
62
|
+
};
|
|
63
|
+
if (input.draftChannel) {
|
|
64
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
65
|
+
}
|
|
66
|
+
if (input.bearerToken) {
|
|
67
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
68
|
+
}
|
|
69
|
+
if (input.extra) {
|
|
70
|
+
for (const [key, value] of Object.entries(input.extra)) {
|
|
71
|
+
headers[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return headers;
|
|
75
|
+
}
|
|
76
|
+
function readFoirEnvelope(json) {
|
|
77
|
+
if (json.errors?.length) {
|
|
78
|
+
const msg = json.errors.map((e) => e.message).join("; ");
|
|
79
|
+
if (json.data) {
|
|
80
|
+
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
81
|
+
} else {
|
|
82
|
+
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
83
|
+
graphQLErrors: json.errors
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!json.data) {
|
|
88
|
+
throw new FoirRequestError("[foir] empty GraphQL response");
|
|
89
|
+
}
|
|
90
|
+
return json.data;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/data/transport.ts
|
|
94
|
+
function foirDataEndpoint(apiUrl, path) {
|
|
95
|
+
const base = apiUrl.trim().replace(/\/+$/, "").replace(/\/graphql$/i, "");
|
|
96
|
+
if (!base) {
|
|
97
|
+
throw new FoirRequestError("[foir] apiUrl is empty \u2014 pass the Foir API origin");
|
|
98
|
+
}
|
|
99
|
+
return base + path;
|
|
100
|
+
}
|
|
101
|
+
async function resolveBearerToken(config) {
|
|
102
|
+
const source = config.bearerToken;
|
|
103
|
+
if (typeof source === "function") return await source() ?? void 0;
|
|
104
|
+
return source ?? void 0;
|
|
105
|
+
}
|
|
106
|
+
async function readErrorBody(res) {
|
|
107
|
+
try {
|
|
108
|
+
const json = await res.json();
|
|
109
|
+
return json?.errors?.length ? json.errors : void 0;
|
|
110
|
+
} catch {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function postFoirOp(config, body) {
|
|
115
|
+
const url = foirDataEndpoint(config.apiUrl, "/v1/query");
|
|
116
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
117
|
+
const headers = buildFoirHeaders({
|
|
118
|
+
apiKey: config.apiKey,
|
|
119
|
+
draftChannel: config.channel === "draft",
|
|
120
|
+
bearerToken: await resolveBearerToken(config),
|
|
121
|
+
extra: config.headers
|
|
122
|
+
});
|
|
123
|
+
let res;
|
|
124
|
+
try {
|
|
125
|
+
res = await doFetch(url, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers,
|
|
128
|
+
body: JSON.stringify(body),
|
|
129
|
+
...config.requestInit
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw new FoirRequestError("[foir] network error", { cause: err });
|
|
133
|
+
}
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
const errors = await readErrorBody(res);
|
|
136
|
+
const detail = errors ? `: ${errors.map((e) => e.message).join("; ")}` : "";
|
|
137
|
+
throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}${detail}`, {
|
|
138
|
+
status: res.status,
|
|
139
|
+
graphQLErrors: errors
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const json = await res.json();
|
|
143
|
+
const data = readFoirEnvelope(json);
|
|
144
|
+
const roots = Object.keys(data);
|
|
145
|
+
if (roots.length !== 1) {
|
|
146
|
+
throw new FoirRequestError(
|
|
147
|
+
`[foir] expected exactly one root field in the /v1/query response, got ${roots.length}` + (roots.length ? ` (${roots.join(", ")})` : "")
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return data[roots[0]];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/data/manifest.ts
|
|
154
|
+
async function fetchFoirManifest(config) {
|
|
155
|
+
const url = foirDataEndpoint(config.apiUrl, "/v1/manifest");
|
|
156
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
157
|
+
let res;
|
|
158
|
+
try {
|
|
159
|
+
res = await doFetch(url, {
|
|
160
|
+
method: "GET",
|
|
161
|
+
headers: buildFoirHeaders({
|
|
162
|
+
apiKey: config.apiKey,
|
|
163
|
+
draftChannel: config.channel === "draft",
|
|
164
|
+
bearerToken: await resolveBearerToken(config),
|
|
165
|
+
extra: config.headers
|
|
166
|
+
}),
|
|
167
|
+
...config.requestInit
|
|
168
|
+
});
|
|
169
|
+
} catch (err) {
|
|
170
|
+
throw new FoirRequestError("[foir] network error", { cause: err });
|
|
171
|
+
}
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
throw new FoirRequestError(
|
|
174
|
+
`[foir] GET /v1/manifest failed: HTTP ${res.status} ${res.statusText}`,
|
|
175
|
+
{ status: res.status }
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return await res.json();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/data/client.ts
|
|
182
|
+
var NON_MODEL_PROPS = /* @__PURE__ */ new Set([
|
|
183
|
+
"then",
|
|
184
|
+
"catch",
|
|
185
|
+
"finally",
|
|
186
|
+
"toJSON",
|
|
187
|
+
"toString",
|
|
188
|
+
"valueOf",
|
|
189
|
+
"constructor",
|
|
190
|
+
"inspect",
|
|
191
|
+
"asymmetricMatch",
|
|
192
|
+
"$$typeof",
|
|
193
|
+
"_isMockFunction",
|
|
194
|
+
"nodeType"
|
|
195
|
+
]);
|
|
196
|
+
function selectionPaths(value, verb, prefix = "") {
|
|
197
|
+
if (Array.isArray(value)) return value;
|
|
198
|
+
if (!value || typeof value !== "object") return [];
|
|
199
|
+
const out = [];
|
|
200
|
+
for (const [key, child] of Object.entries(value)) {
|
|
201
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
202
|
+
if (child === true) {
|
|
203
|
+
out.push(path);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (child && typeof child === "object" && verb in child) {
|
|
207
|
+
out.push(...selectionPaths(child[verb], verb, path));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function splitQuery(query) {
|
|
213
|
+
const args = {};
|
|
214
|
+
let include;
|
|
215
|
+
let omit;
|
|
216
|
+
let pick;
|
|
217
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
218
|
+
if (value === void 0) continue;
|
|
219
|
+
if (key === "include") {
|
|
220
|
+
include = value;
|
|
221
|
+
} else if (key === "omit") {
|
|
222
|
+
omit = selectionPaths(value, "omit");
|
|
223
|
+
} else if (key === "pick") {
|
|
224
|
+
pick = selectionPaths(value, "pick");
|
|
225
|
+
} else {
|
|
226
|
+
args[key] = value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { args, include, omit, pick };
|
|
230
|
+
}
|
|
231
|
+
function createFoirClient(config) {
|
|
232
|
+
if (!config?.apiUrl || !config?.apiKey) {
|
|
233
|
+
throw new Error("[foir] createFoirClient needs both apiUrl and apiKey");
|
|
234
|
+
}
|
|
235
|
+
let driftChecked = false;
|
|
236
|
+
const maybeCheckDrift = () => {
|
|
237
|
+
const generated = config.schemaVersion;
|
|
238
|
+
if (driftChecked) return;
|
|
239
|
+
if (generated === void 0) return;
|
|
240
|
+
if (config.checkSchemaDrift === false) return;
|
|
241
|
+
driftChecked = true;
|
|
242
|
+
void fetchFoirManifest(config).then((manifest) => {
|
|
243
|
+
if (manifest.schemaVersion === generated) return;
|
|
244
|
+
const drift = {
|
|
245
|
+
generated,
|
|
246
|
+
server: manifest.schemaVersion,
|
|
247
|
+
channel: manifest.channel
|
|
248
|
+
};
|
|
249
|
+
if (config.onSchemaDrift) {
|
|
250
|
+
config.onSchemaDrift(drift);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
console.warn(
|
|
254
|
+
`[foir] schema drift: types were generated from schemaVersion ${drift.generated}, the server is serving ${drift.server} (${drift.channel}). Re-run \`foir types\` \u2014 new models and fields will be missing, and removed ones will still type-check.`
|
|
255
|
+
);
|
|
256
|
+
}).catch(() => {
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
const run = async (body) => {
|
|
260
|
+
const result = await postFoirOp(config, body);
|
|
261
|
+
maybeCheckDrift();
|
|
262
|
+
return result;
|
|
263
|
+
};
|
|
264
|
+
const opProxy = (address) => new Proxy({}, {
|
|
265
|
+
get(_target, op) {
|
|
266
|
+
if (typeof op !== "string" || NON_MODEL_PROPS.has(op)) return void 0;
|
|
267
|
+
return (query) => {
|
|
268
|
+
const { args, include, omit, pick } = splitQuery(query);
|
|
269
|
+
return run({ ...address, op, args, include, omit, pick });
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
const nsProxy = new Proxy({}, {
|
|
274
|
+
get(_target, namespace) {
|
|
275
|
+
if (typeof namespace !== "string" || NON_MODEL_PROPS.has(namespace)) return void 0;
|
|
276
|
+
return opProxy({ namespace });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const raw = (op) => run(op);
|
|
280
|
+
return new Proxy({}, {
|
|
281
|
+
get(_target, prop) {
|
|
282
|
+
if (prop === "ns") return nsProxy;
|
|
283
|
+
if (prop === "raw") return raw;
|
|
284
|
+
if (typeof prop !== "string" || NON_MODEL_PROPS.has(prop)) return void 0;
|
|
285
|
+
return opProxy({ model: prop });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function createFoirClientFromEnv(env, overrides) {
|
|
290
|
+
const apiKey = resolveFoirApiKey(env);
|
|
291
|
+
if (!env.PLATFORM_API_URL || !apiKey) {
|
|
292
|
+
throw new FoirRequestError(
|
|
293
|
+
"[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return createFoirClient({
|
|
297
|
+
...overrides,
|
|
298
|
+
apiUrl: env.PLATFORM_API_URL,
|
|
299
|
+
apiKey,
|
|
300
|
+
channel: isFoirPreview(env) ? "draft" : "published"
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
export {
|
|
304
|
+
FoirRequestError,
|
|
305
|
+
createFoirClient,
|
|
306
|
+
createFoirClientFromEnv,
|
|
307
|
+
fetchFoirManifest,
|
|
308
|
+
foirDataEndpoint
|
|
309
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
2
|
+
import { type FoirEnv } from '../transport/foir-http';
|
|
3
|
+
/**
|
|
4
|
+
* Whether this read can be served by the shared endpoint.
|
|
5
|
+
*
|
|
6
|
+
* These mirror the conditions the server refuses on, so a request that would be
|
|
7
|
+
* turned away is not sent there in the first place. The server is still the
|
|
8
|
+
* authority — it refuses rather than falling back, and `foirRequest` retries on
|
|
9
|
+
* `/graphql` when it does — but every condition checked here is one round trip
|
|
10
|
+
* saved.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isCdnEligible(env: FoirEnv, documentText: string, bearerToken?: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Build the shared-endpoint URL for a read.
|
|
15
|
+
*
|
|
16
|
+
* The credential goes in the URL deliberately: a shared cache keys on the URL
|
|
17
|
+
* alone, so a key that rode only in a header would let two callers with
|
|
18
|
+
* different reach collide on one entry and be served each other's body.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildCdnUrl(endpoint: string, apiKey: string, hash: string, variables?: unknown): string;
|
|
21
|
+
/** The APQ handshake object, in Apollo's shape because every client uses it. */
|
|
22
|
+
export declare function persistedQueryExtension(hash: string): {
|
|
23
|
+
persistedQuery: {
|
|
24
|
+
version: number;
|
|
25
|
+
sha256Hash: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* SHA-256 of the document text, hex encoded — the identity a persisted query is
|
|
30
|
+
* stored and retrieved under.
|
|
31
|
+
*
|
|
32
|
+
* Web Crypto rather than a bundled implementation: it is present in every
|
|
33
|
+
* runtime this SDK targets (workers, edge, Node 18+) and adds nothing to the
|
|
34
|
+
* caller's bundle.
|
|
35
|
+
*/
|
|
36
|
+
export declare function documentHash(documentText: string): Promise<string>;
|
|
37
|
+
/** The code the server answers with when it has never seen this hash. */
|
|
38
|
+
export declare const PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
|
|
39
|
+
/** The code the shared endpoint refuses with, so a caller knows to retry. */
|
|
40
|
+
export declare const NOT_SHAREABLE = "NOT_SHAREABLE";
|
|
41
|
+
/** Print a document once, so eligibility and the URL agree on the same text. */
|
|
42
|
+
export declare function printDocument<TData, TVariables>(document: TypedDocumentNode<TData, TVariables>): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@foir/sdk/client` — client-agnostic GraphQL transport for SSR/edge loaders.
|
|
3
|
+
*
|
|
4
|
+
* A thin auth transport, not a GraphQL client: it carries the Foir-specific
|
|
5
|
+
* `x-api-key` + preview schema channel and runs typed documents via `fetch`.
|
|
6
|
+
* Consumers that bring their own GraphQL client (Apollo/urql/react-query) use
|
|
7
|
+
* the generated `TypedDocumentNode`s directly and replicate those two headers.
|
|
8
|
+
*/
|
|
9
|
+
export { foirRequest, isFoirPreview, absolutiseFoirMediaUrl, FOIR_CDN_BASE, FoirRequestError, } from './foir-request';
|
|
10
|
+
export type { FoirEnv, FoirRequestOptions } from './foir-request';
|
|
11
|
+
/**
|
|
12
|
+
* Failure classification. Branch on these rather than on `err.message`: the
|
|
13
|
+
* codes are a stable contract, the wording is not.
|
|
14
|
+
*
|
|
15
|
+
* The one worth knowing about is `isFoirRateLimited`. It is the only 4xx that
|
|
16
|
+
* clears on its own, and a client that lumps it in with the rest fails every
|
|
17
|
+
* read for as long as the key stays saturated — which looks, from the outside,
|
|
18
|
+
* exactly like a site with no content.
|
|
19
|
+
*/
|
|
20
|
+
export { isFoirRateLimited, isFoirUnauthenticated, isFoirForbidden, isFoirNotFound, readFoirRateLimit, } from './foir-request';
|
|
21
|
+
export type { FoirErrorCode, FoirErrorExtensions, FoirGraphQLErrorItem, FoirRateLimitSnapshot, } from './foir-request';
|
|
22
|
+
/**
|
|
23
|
+
* Retry. `createFoirContentClient` applies this for you; these are exported for
|
|
24
|
+
* a caller running its own loop, so both use one definition of "worth retrying"
|
|
25
|
+
* rather than two that disagree about the 429.
|
|
26
|
+
*/
|
|
27
|
+
export { withFoirRetry, foirRetryDelayMs } from './foir-request';
|
|
28
|
+
export type { FoirRetryPolicy } from './foir-request';
|
|
29
|
+
export { createFoirContentClient } from './content-client';
|
|
30
|
+
export type { FoirContentClient, FoirContentClientConfig, FoirTokenProvider, } from './content-client';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-bound content client — forwards the customer bearer on every read
|
|
3
|
+
* when one is available, so auth/segment-targeted variants and customer-scoped
|
|
4
|
+
* records resolve out of the box, with no per-query wiring to forget.
|
|
5
|
+
*
|
|
6
|
+
* Resolution keys off the request's principal (see the platform's two-gate
|
|
7
|
+
* model): the `x-api-key` alone resolves the *anonymous baseline*
|
|
8
|
+
* (`context.authStatus = ""`); adding the customer bearer resolves the request
|
|
9
|
+
* as that customer (`context.authStatus = "authenticated"`, `customer_id` set).
|
|
10
|
+
*
|
|
11
|
+
* Caching: an api-key-only read is identical for every anonymous visitor and is
|
|
12
|
+
* shared-cacheable; a bearer-bearing read resolves per-customer and is not.
|
|
13
|
+
* Because the bearer only exists once a customer signs in, anonymous traffic
|
|
14
|
+
* keeps hitting the shared-cacheable baseline — only signed-in reads become
|
|
15
|
+
* per-customer. Bind this client wherever content is personalized (for most
|
|
16
|
+
* apps, everywhere the customer may be signed in) and use plain `foirRequest`
|
|
17
|
+
* for reads that must stay anonymous regardless of session.
|
|
18
|
+
*/
|
|
19
|
+
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
20
|
+
import type { FoirEnv, FoirRequestOptions } from './foir-request';
|
|
21
|
+
import { type FoirRetryPolicy } from '../transport/retry';
|
|
22
|
+
import type { FoirRateLimitSnapshot } from '../transport/foir-http';
|
|
23
|
+
export type { FoirTokenProvider } from '../transport/foir-http';
|
|
24
|
+
import type { FoirTokenProvider } from '../transport/foir-http';
|
|
25
|
+
export interface FoirContentClientConfig {
|
|
26
|
+
env: FoirEnv;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the current customer bearer before every request. When it yields a
|
|
29
|
+
* token the token is forwarded, so `authStatus`/segment targeting and
|
|
30
|
+
* customer-scoped records resolve as that customer; when it yields
|
|
31
|
+
* null/undefined the read hits the anonymous baseline. Omit for a client that
|
|
32
|
+
* is always anonymous (equivalent to calling `foirRequest` directly).
|
|
33
|
+
*/
|
|
34
|
+
getToken?: FoirTokenProvider;
|
|
35
|
+
/**
|
|
36
|
+
* Retry policy for transient failures — no response, 5xx, and 429.
|
|
37
|
+
*
|
|
38
|
+
* ON BY DEFAULT, which is the point of it being here. Retry that has to be
|
|
39
|
+
* opted into is retry each integrator writes for themselves, and the one
|
|
40
|
+
* detail they get wrong is the 429: it looks like the permanent 4xx family it
|
|
41
|
+
* sits in, and it is the only failure in that family that clears on its own.
|
|
42
|
+
* Pass `{ attempts: 1 }` to turn it off.
|
|
43
|
+
*/
|
|
44
|
+
retry?: FoirRetryPolicy;
|
|
45
|
+
/**
|
|
46
|
+
* Called with the `X-RateLimit-*` headers of every response. Wire it to your
|
|
47
|
+
* logger and you will see a key approaching its ceiling instead of finding
|
|
48
|
+
* out when it hits one.
|
|
49
|
+
*/
|
|
50
|
+
onRateLimit?: (snapshot: FoirRateLimitSnapshot | undefined) => void;
|
|
51
|
+
}
|
|
52
|
+
export interface FoirContentClient {
|
|
53
|
+
/**
|
|
54
|
+
* Run a typed document, forwarding the bound customer bearer when available.
|
|
55
|
+
* An explicit `options.bearerToken` still wins, so a single call can override
|
|
56
|
+
* the bound token (or force anonymous by passing `undefined` — the bound
|
|
57
|
+
* token only fills in when `options.bearerToken` is not supplied).
|
|
58
|
+
*/
|
|
59
|
+
request<TData, TVariables>(document: TypedDocumentNode<TData, TVariables>, variables?: TVariables, options?: FoirRequestOptions): Promise<TData>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create a {@link FoirContentClient} bound to an env + token source. Every
|
|
63
|
+
* `request` forwards the customer bearer when `getToken` yields one, upgrading
|
|
64
|
+
* that read from the anonymous baseline to the signed-in customer.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createFoirContentClient(config: FoirContentClientConfig): FoirContentClient;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foir GraphQL client — a single typed fetch wrapper for SSR/edge loaders.
|
|
3
|
+
*
|
|
4
|
+
* No GraphQL client (Apollo/urql) required: the Foir public API is a plain
|
|
5
|
+
* `POST` with an `x-api-key` header, and the dominant consumers (Hydrogen on
|
|
6
|
+
* Oxygen, React Router loaders, TanStack Start on CF Workers) fetch in
|
|
7
|
+
* server-only loaders and render — so Apollo's `InMemoryCache`/`useQuery`
|
|
8
|
+
* reactivity is dead weight, and a shared cache on a stateless multi-tenant
|
|
9
|
+
* edge worker is a cross-tenant hazard. This helper carries only the
|
|
10
|
+
* Foir-specific transport concerns (auth header, preview schema channel,
|
|
11
|
+
* error shape). Consumers on a GraphQL client (Apollo / urql / react-query /
|
|
12
|
+
* graphql-request) use the generated `TypedDocumentNode`s directly with their
|
|
13
|
+
* own client and replicate the two Foir-specific headers below — the
|
|
14
|
+
* `x-api-key` and, in preview, `X-Foir-Schema-Channel: draft`.
|
|
15
|
+
*
|
|
16
|
+
* Deduplicates the hand-copied `lib/foir/client.ts` that shipped identically
|
|
17
|
+
* across the Foir storefronts.
|
|
18
|
+
*
|
|
19
|
+
* The transport concerns themselves now live in `../transport/foir-http`, which
|
|
20
|
+
* has no `graphql` import, so `@foir/sdk/data` can reuse them without dragging
|
|
21
|
+
* `graphql` into a storefront bundle. Only `print` stayed here. Everything that
|
|
22
|
+
* module owns is re-exported below, unchanged, so this file's public surface is
|
|
23
|
+
* exactly what it always was.
|
|
24
|
+
*/
|
|
25
|
+
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
26
|
+
import { type FoirEnv, type FoirRequestOptions } from '../transport/foir-http';
|
|
27
|
+
export { FoirRequestError, isFoirPreview, isFoirRateLimited, isFoirUnauthenticated, isFoirForbidden, isFoirNotFound, readFoirRateLimit, type FoirEnv, type FoirErrorCode, type FoirErrorExtensions, type FoirGraphQLErrorItem, type FoirRateLimitSnapshot, type FoirRequestOptions, } from '../transport/foir-http';
|
|
28
|
+
export { withFoirRetry, foirRetryDelayMs, type FoirRetryPolicy, } from '../transport/retry';
|
|
29
|
+
/** Default Foir CDN origin used to absolutise relative media URLs. */
|
|
30
|
+
export declare const FOIR_CDN_BASE = "https://cdn.foir.dev/";
|
|
31
|
+
/**
|
|
32
|
+
* Foir returns `hlsManifestUrl` as a relative path while every other media
|
|
33
|
+
* URL is absolute. Prefix relative paths with the CDN host so the browser
|
|
34
|
+
* doesn't request them from the consumer's own origin. Already-absolute URLs
|
|
35
|
+
* pass through untouched. `cdnBase` is overridable for non-default CDN hosts.
|
|
36
|
+
*/
|
|
37
|
+
export declare function absolutiseFoirMediaUrl(url: string | null | undefined, cdnBase?: string): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Execute a typed GraphQL document against the Foir platform.
|
|
40
|
+
*
|
|
41
|
+
* Throws `FoirRequestError` on HTTP failure or GraphQL errors so the caller
|
|
42
|
+
* can decide whether to rethrow or fall back to local defaults. GraphQL can
|
|
43
|
+
* return partial data alongside errors; when data is present the errors are
|
|
44
|
+
* logged and the data returned (callers handle nulls), and we only throw when
|
|
45
|
+
* there's no data at all.
|
|
46
|
+
*/
|
|
47
|
+
export declare function foirRequest<TData, TVariables>(env: FoirEnv, document: TypedDocumentNode<TData, TVariables>, variables?: TVariables, options?: FoirRequestOptions): Promise<TData>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface UseAutoResizeOptions {
|
|
2
|
+
/** Minimum height to report (prevents too-small iframes). @default 400 */
|
|
3
|
+
minHeight?: number;
|
|
4
|
+
/** Debounce delay in milliseconds. @default 100 */
|
|
5
|
+
debounce?: number;
|
|
6
|
+
/** Whether auto-resize is enabled. @default true */
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Automatically resize the host iframe based on content height.
|
|
11
|
+
*
|
|
12
|
+
* @returns A callback ref to attach to your root container element.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* function MyEditor() {
|
|
17
|
+
* const containerRef = useAutoResize({ minHeight: 600 });
|
|
18
|
+
* return <div ref={containerRef}>...</div>;
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function useAutoResize(options?: UseAutoResizeOptions): (node: HTMLDivElement | null) => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EditorContextValue } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Access the Foir editor context.
|
|
4
|
+
*
|
|
5
|
+
* Must be used inside an `<EditorProvider>`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* function MyEditor() {
|
|
10
|
+
* const { isReady, init, client, updateField, setDirty } = useEditor();
|
|
11
|
+
* if (!isReady) return <p>Loading...</p>;
|
|
12
|
+
* return <form>...</form>;
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function useEditor(): EditorContextValue;
|