@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +325 -0
  2. package/README.md +63 -0
  3. package/dist/auth/auth.d.ts +140 -0
  4. package/dist/auth/hosted.d.ts +40 -0
  5. package/dist/auth/index.d.ts +15 -0
  6. package/dist/auth/operations.d.ts +123 -0
  7. package/dist/auth/session-client.d.ts +76 -0
  8. package/dist/auth-session.js +137 -0
  9. package/dist/auth.js +238 -26
  10. package/dist/client/platform-client.d.ts +29 -0
  11. package/dist/client.js +290 -32
  12. package/dist/codegen/block-registry.d.ts +4 -8
  13. package/dist/codegen/block-scaffold.d.ts +4 -8
  14. package/dist/codegen/index.d.ts +8 -62
  15. package/dist/codegen/preset.d.ts +60 -0
  16. package/dist/codegen/shared.d.ts +25 -0
  17. package/dist/components/EditorProvider.d.ts +36 -0
  18. package/dist/content/content-renderer.d.ts +47 -0
  19. package/dist/content/index.d.ts +12 -0
  20. package/dist/content/richtext.d.ts +70 -0
  21. package/dist/data/builtins.d.ts +296 -0
  22. package/dist/data/client.d.ts +94 -0
  23. package/dist/data/index.d.ts +54 -0
  24. package/dist/data/manifest.d.ts +161 -0
  25. package/dist/data/schema.d.ts +243 -0
  26. package/dist/data/selection.d.ts +330 -0
  27. package/dist/data/transport.d.ts +107 -0
  28. package/dist/data.js +309 -0
  29. package/dist/graphql/cdn-route.d.ts +42 -0
  30. package/dist/graphql/client.d.ts +30 -0
  31. package/dist/graphql/content-client.d.ts +66 -0
  32. package/dist/graphql/foir-request.d.ts +47 -0
  33. package/dist/hooks/useAutoResize.d.ts +22 -0
  34. package/dist/hooks/useEditor.d.ts +16 -0
  35. package/dist/index.d.ts +10 -394
  36. package/dist/media/BlurhashCanvas.d.ts +10 -0
  37. package/dist/media/ProgressiveMedia.d.ts +56 -0
  38. package/dist/media/index.d.ts +14 -0
  39. package/dist/media/media.d.ts +139 -0
  40. package/dist/media/useProgressiveLoad.d.ts +23 -0
  41. package/dist/media.js +5 -0
  42. package/dist/next/cookieAdapter.d.ts +26 -0
  43. package/dist/next/index.d.ts +15 -0
  44. package/dist/next/middleware.d.ts +49 -0
  45. package/dist/next/session.d.ts +53 -0
  46. package/dist/next.js +230 -26
  47. package/dist/protocol/constants.d.ts +6 -0
  48. package/dist/protocol/messages.d.ts +155 -0
  49. package/dist/protocol/validation.d.ts +8 -0
  50. package/dist/push/index.d.ts +14 -0
  51. package/dist/push/operations.d.ts +22 -0
  52. package/dist/push/web-push.d.ts +39 -0
  53. package/dist/push.js +230 -26
  54. package/dist/react/FoirProvider.d.ts +116 -0
  55. package/dist/react/OAuthButtons.d.ts +20 -0
  56. package/dist/react/SignIn.d.ts +22 -0
  57. package/dist/react/UserButton.d.ts +13 -0
  58. package/dist/react/index.d.ts +19 -0
  59. package/dist/react/localStorageAdapter.d.ts +10 -0
  60. package/dist/react.js +230 -26
  61. package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
  62. package/dist/server/callback-client.d.ts +92 -0
  63. package/dist/server/index.d.ts +8 -0
  64. package/dist/server/operation-types.d.ts +134 -0
  65. package/dist/server/require-scoped-token.d.ts +76 -0
  66. package/dist/server/server-client.d.ts +34 -0
  67. package/dist/server/types.d.ts +54 -0
  68. package/dist/server/verify-scoped-token.d.ts +100 -0
  69. package/dist/server/webhook-helpers.d.ts +36 -0
  70. package/dist/transport/foir-http.d.ts +217 -0
  71. package/dist/transport/retry.d.ts +61 -0
  72. package/dist/types/index.d.ts +124 -0
  73. package/package.json +23 -13
  74. package/dist/auth.d.ts +0 -382
  75. package/dist/client.d.ts +0 -83
  76. package/dist/content.d.ts +0 -96
  77. package/dist/media.d.ts +0 -234
  78. package/dist/next.d.ts +0 -178
  79. package/dist/push.d.ts +0 -109
  80. package/dist/react.d.ts +0 -319
  81. package/dist/server.d.ts +0 -534
  82. package/dist/shared-DkWlEd2Z.d.ts +0 -18
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `@foir/sdk/next` — server-side Foir customer auth for the Next.js App Router
3
+ * (the BFF pattern): an HttpOnly-cookie session adapter, server-side session
4
+ * helpers, a provider-callback exchange, and a route-protection middleware.
5
+ *
6
+ * This is the server counterpart to `@foir/sdk/react`. `next` is a peer
7
+ * dependency (for the middleware helper).
8
+ */
9
+ export { createFoirCookieAdapter, foirCookieName, } from './cookieAdapter';
10
+ export type { FoirCookieStore, CookieAdapterOptions } from './cookieAdapter';
11
+ export { getFoirSession, getFoirToken, foirSignOut, foirSignInWithPassword, foirRegisterWithPassword, foirSignInWithOtp, completeFoirProviderLogin, } from './session';
12
+ export { createFoirMiddleware } from './middleware';
13
+ export type { FoirMiddlewareConfig, FoirMiddlewareRequest } from './middleware';
14
+ export type { FoirCustomer, SessionAdapter } from '../auth';
15
+ export type { FoirEnv } from '../graphql/foir-request';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The slice of `NextRequest` the middleware reads. Typed structurally so the SDK
3
+ * doesn't need `next` installed to build — a real `NextRequest` satisfies it, so
4
+ * the returned function drops straight into `export const middleware`.
5
+ */
6
+ export interface FoirMiddlewareRequest {
7
+ cookies: {
8
+ get(name: string): {
9
+ value: string;
10
+ } | undefined;
11
+ };
12
+ nextUrl: {
13
+ pathname: string;
14
+ search: string;
15
+ };
16
+ url: string;
17
+ }
18
+ export interface FoirMiddlewareConfig {
19
+ /** Where to send unauthenticated requests (path or absolute URL). */
20
+ signInUrl: string;
21
+ /**
22
+ * The session cookie whose presence means "signed in". Defaults to the
23
+ * access-token cookie the cookie adapter writes (`foir_token`).
24
+ */
25
+ cookieName?: string;
26
+ /**
27
+ * Return true for requests that require auth. Omit to protect every request
28
+ * the middleware `matcher` passes through.
29
+ */
30
+ isProtected?(request: FoirMiddlewareRequest): boolean;
31
+ /** Query param carrying the original path on the sign-in URL. Default `returnTo`. */
32
+ returnParam?: string;
33
+ }
34
+ /**
35
+ * Build a Next.js middleware that redirects unauthenticated requests to your
36
+ * sign-in URL. Edge-safe: it checks for the presence of the session cookie only
37
+ * — the bearer itself is validated server-side when you call the Foir API — so
38
+ * pair it with a real `getFoirSession` check in protected route handlers /
39
+ * server components. Returns a standard `Response` (redirect) or `undefined`
40
+ * (continue), both of which Next.js middleware accepts.
41
+ *
42
+ * ```ts
43
+ * // middleware.ts
44
+ * import { createFoirMiddleware } from '@foir/sdk/next';
45
+ * export const middleware = createFoirMiddleware({ signInUrl: '/sign-in' });
46
+ * export const config = { matcher: ['/dashboard/:path*'] };
47
+ * ```
48
+ */
49
+ export declare function createFoirMiddleware(config: FoirMiddlewareConfig): (request: FoirMiddlewareRequest) => Response | undefined;
@@ -0,0 +1,53 @@
1
+ import type { FoirCustomer } from '../auth';
2
+ import type { FoirEnv } from '../graphql/foir-request';
3
+ import type { CookieAdapterOptions, FoirCookieStore } from './cookieAdapter';
4
+ /**
5
+ * Server-side session helpers for the Next.js BFF pattern. Each takes the Next
6
+ * cookie store (`await cookies()` in a route handler / server action, or the
7
+ * read-only `cookies()` in a Server Component for reads).
8
+ *
9
+ * Note on "verification": the customer access token is opaque to your app and
10
+ * validated by the Foir API on use, so `getFoirSession` returns the identity
11
+ * stored in the cookies rather than locally verifying a JWT. A rejected/expired
12
+ * bearer surfaces when you call the API (and `@foir/sdk/auth`'s `authedRequest`
13
+ * transparently refreshes it).
14
+ */
15
+ /** The signed-in customer from the session cookies, or null. */
16
+ export declare function getFoirSession(store: FoirCookieStore): Promise<FoirCustomer | null>;
17
+ /** The current access token from the session cookies, or null. */
18
+ export declare function getFoirToken(store: FoirCookieStore): Promise<string | null>;
19
+ /** Clear the session cookies (sign out). */
20
+ export declare function foirSignOut(store: FoirCookieStore, opts?: CookieAdapterOptions): Promise<void>;
21
+ interface PasswordArgs {
22
+ env: FoirEnv;
23
+ store: FoirCookieStore;
24
+ email: string;
25
+ password: string;
26
+ cookieOptions?: CookieAdapterOptions['cookieOptions'];
27
+ }
28
+ /** Password sign-in in a route handler; persists the session to cookies. */
29
+ export declare function foirSignInWithPassword({ env, store, email, password, cookieOptions, }: PasswordArgs): Promise<FoirCustomer>;
30
+ /** Register a customer in a route handler; persists the session to cookies. */
31
+ export declare function foirRegisterWithPassword({ env, store, email, password, cookieOptions, }: PasswordArgs): Promise<FoirCustomer>;
32
+ /** Complete an OTP sign-in in a route handler; persists the session to cookies. */
33
+ export declare function foirSignInWithOtp({ env, store, email, otp, cookieOptions, }: {
34
+ env: FoirEnv;
35
+ store: FoirCookieStore;
36
+ email: string;
37
+ otp: string;
38
+ cookieOptions?: CookieAdapterOptions['cookieOptions'];
39
+ }): Promise<FoirCustomer>;
40
+ /**
41
+ * Complete a provider (OAuth/OIDC) login in your callback route handler:
42
+ * exchange the `code` + `state` for tokens and persist them to the session
43
+ * cookies. Returns the customer.
44
+ */
45
+ export declare function completeFoirProviderLogin(opts: {
46
+ env: FoirEnv;
47
+ store: FoirCookieStore;
48
+ providerKey: string;
49
+ code?: string;
50
+ state?: string;
51
+ cookieOptions?: CookieAdapterOptions['cookieOptions'];
52
+ }): Promise<FoirCustomer>;
53
+ export {};
package/dist/next.js CHANGED
@@ -18,10 +18,18 @@ function createFoirCookieAdapter(store, opts = {}) {
18
18
  }
19
19
 
20
20
  // src/graphql/foir-request.ts
21
+ import { print as print2 } from "graphql";
22
+
23
+ // src/graphql/cdn-route.ts
21
24
  import { print } from "graphql";
25
+
26
+ // src/transport/foir-http.ts
22
27
  function isFoirPreview(env) {
23
28
  return env.FOIR_PREVIEW === "true";
24
29
  }
30
+ function resolveFoirApiKey(env) {
31
+ return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
32
+ }
25
33
  var FoirRequestError = class extends Error {
26
34
  /** HTTP status code, when the request failed at the HTTP layer. */
27
35
  status;
@@ -35,41 +43,83 @@ var FoirRequestError = class extends Error {
35
43
  this.status = options?.status;
36
44
  this.graphQLErrors = options?.graphQLErrors;
37
45
  }
46
+ /**
47
+ * The code of the first error, if the response carried one.
48
+ *
49
+ * A getter rather than a stored field so it stays correct for an error
50
+ * constructed anywhere, including by a caller in a test.
51
+ */
52
+ get code() {
53
+ return this.graphQLErrors?.[0]?.extensions?.code;
54
+ }
55
+ /** The correlation id, if the response carried one. */
56
+ get requestId() {
57
+ return this.graphQLErrors?.[0]?.extensions?.requestId;
58
+ }
59
+ /**
60
+ * Whether retrying this request could succeed.
61
+ *
62
+ * The 429 is the one that has to be in here and is easy to leave out: it sits
63
+ * among the 4xx statuses that are otherwise permanent, so a policy written as
64
+ * "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
65
+ * own. A storefront wrote that policy, its key saturated, and every read
66
+ * failed on the first attempt for an hour.
67
+ */
68
+ get retryable() {
69
+ if (this.status === void 0) return true;
70
+ if (this.status === 429 || this.code === "RATE_LIMITED") return true;
71
+ if (this.status >= 500) return true;
72
+ return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
73
+ }
74
+ /**
75
+ * How long to wait before retrying, in milliseconds, when the server said.
76
+ * Undefined when it did not, which means the caller picks its own backoff.
77
+ */
78
+ get retryAfterMs() {
79
+ const seconds = this.graphQLErrors?.[0]?.extensions?.retryAfter;
80
+ return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
81
+ }
38
82
  };
39
- async function foirRequest(env, document, variables, options) {
40
- const endpoint = env.PLATFORM_API_URL;
41
- const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
42
- if (!endpoint || !apiKey) {
43
- throw new FoirRequestError(
44
- "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
45
- );
83
+ function readFoirRateLimit(headers) {
84
+ const raw = {
85
+ limit: headers.get("X-RateLimit-Limit"),
86
+ remaining: headers.get("X-RateLimit-Remaining"),
87
+ resetAt: headers.get("X-RateLimit-Reset")
88
+ };
89
+ if (raw.limit === null || raw.remaining === null || raw.resetAt === null) {
90
+ return void 0;
91
+ }
92
+ const limit = Number(raw.limit);
93
+ const remaining = Number(raw.remaining);
94
+ const resetAt = Number(raw.resetAt);
95
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining) || !Number.isFinite(resetAt)) {
96
+ return void 0;
46
97
  }
98
+ return { limit, remaining, resetAt };
99
+ }
100
+ function readFoirRetryAfterMs(headers) {
101
+ const seconds = Number(headers.get("Retry-After"));
102
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
103
+ }
104
+ function buildFoirHeaders(input) {
47
105
  const headers = {
48
106
  "Content-Type": "application/json",
49
- "x-api-key": apiKey
107
+ "x-api-key": input.apiKey
50
108
  };
51
- if (isFoirPreview(env)) {
109
+ if (input.draftChannel) {
52
110
  headers["X-Foir-Schema-Channel"] = "draft";
53
111
  }
54
- if (options?.bearerToken) {
55
- headers["Authorization"] = `Bearer ${options.bearerToken}`;
112
+ if (input.bearerToken) {
113
+ headers["Authorization"] = `Bearer ${input.bearerToken}`;
56
114
  }
57
- let res;
58
- try {
59
- res = await fetch(endpoint, {
60
- method: "POST",
61
- headers,
62
- body: JSON.stringify({ query: print(document), variables })
63
- });
64
- } catch (err) {
65
- throw new FoirRequestError("[foir] network error", { cause: err });
66
- }
67
- if (!res.ok) {
68
- throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
69
- status: res.status
70
- });
115
+ if (input.extra) {
116
+ for (const [key, value] of Object.entries(input.extra)) {
117
+ headers[key] = value;
118
+ }
71
119
  }
72
- const json = await res.json();
120
+ return headers;
121
+ }
122
+ function readFoirEnvelope(json) {
73
123
  if (json.errors?.length) {
74
124
  const msg = json.errors.map((e) => e.message).join("; ");
75
125
  if (json.data) {
@@ -86,6 +136,150 @@ async function foirRequest(env, document, variables, options) {
86
136
  return json.data;
87
137
  }
88
138
 
139
+ // src/graphql/cdn-route.ts
140
+ var CDN_PATH = "/cdn/graphql";
141
+ function isCdnEligible(env, documentText, bearerToken) {
142
+ if (bearerToken) return false;
143
+ if (isFoirPreview(env)) return false;
144
+ const apiKey = resolveFoirApiKey(env);
145
+ if (!apiKey?.startsWith("pk_")) return false;
146
+ return !/\bmutation\b/.test(documentText);
147
+ }
148
+ function buildCdnUrl(endpoint, apiKey, hash, variables) {
149
+ const url = new URL(endpoint);
150
+ url.pathname = url.pathname.replace(/\/graphql\/?$/, "") + CDN_PATH;
151
+ url.searchParams.set("key", apiKey);
152
+ url.searchParams.set("extensions", JSON.stringify(persistedQueryExtension(hash)));
153
+ if (variables && Object.keys(variables).length > 0) {
154
+ url.searchParams.set("variables", JSON.stringify(variables));
155
+ }
156
+ return url.toString();
157
+ }
158
+ function persistedQueryExtension(hash) {
159
+ return { persistedQuery: { version: 1, sha256Hash: hash } };
160
+ }
161
+ async function documentHash(documentText) {
162
+ const digest = await crypto.subtle.digest(
163
+ "SHA-256",
164
+ new TextEncoder().encode(documentText)
165
+ );
166
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
167
+ }
168
+ var PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
169
+ var NOT_SHAREABLE = "NOT_SHAREABLE";
170
+
171
+ // src/graphql/foir-request.ts
172
+ async function foirRequest(env, document, variables, options) {
173
+ const endpoint = env.PLATFORM_API_URL;
174
+ const apiKey = resolveFoirApiKey(env);
175
+ if (!endpoint || !apiKey) {
176
+ throw new FoirRequestError(
177
+ "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
178
+ );
179
+ }
180
+ const headers = buildFoirHeaders({
181
+ apiKey,
182
+ draftChannel: isFoirPreview(env),
183
+ bearerToken: options?.bearerToken
184
+ });
185
+ const documentText = print2(document);
186
+ let res;
187
+ try {
188
+ if (isCdnEligible(env, documentText, options?.bearerToken)) {
189
+ const hash = await documentHash(documentText);
190
+ let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
191
+ method: "GET",
192
+ headers
193
+ });
194
+ if (await isPersistedQueryMiss(shared)) {
195
+ shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
196
+ method: "POST",
197
+ headers,
198
+ body: JSON.stringify({
199
+ query: documentText,
200
+ variables,
201
+ extensions: persistedQueryExtension(hash)
202
+ })
203
+ });
204
+ }
205
+ if (await isSharedRefusal(shared)) {
206
+ res = await fetch(endpoint, {
207
+ method: "POST",
208
+ headers,
209
+ body: JSON.stringify({ query: documentText, variables })
210
+ });
211
+ } else {
212
+ res = shared;
213
+ }
214
+ } else {
215
+ res = await fetch(endpoint, {
216
+ method: "POST",
217
+ headers,
218
+ body: JSON.stringify({ query: documentText, variables })
219
+ });
220
+ }
221
+ } catch (err) {
222
+ throw new FoirRequestError("[foir] network error", { cause: err });
223
+ }
224
+ options?.onRateLimit?.(readFoirRateLimit(res.headers));
225
+ if (!res.ok) {
226
+ const errors = await readErrorEnvelope(res);
227
+ const first = errors?.[0]?.message;
228
+ throw new FoirRequestError(
229
+ first ? `[foir] HTTP ${res.status} ${res.statusText}: ${first}` : `[foir] HTTP ${res.status} ${res.statusText}`,
230
+ { status: res.status, graphQLErrors: errors }
231
+ );
232
+ }
233
+ const json = await res.json();
234
+ return readFoirEnvelope(json);
235
+ }
236
+ async function readErrorEnvelope(res) {
237
+ let errors;
238
+ try {
239
+ const body = await res.json();
240
+ errors = body.errors;
241
+ } catch {
242
+ errors = void 0;
243
+ }
244
+ const headerRetryMs = readFoirRetryAfterMs(res.headers);
245
+ if (headerRetryMs === void 0) return errors;
246
+ const seconds = Math.round(headerRetryMs / 1e3);
247
+ if (!errors?.length) {
248
+ return [
249
+ {
250
+ message: res.statusText || `HTTP ${res.status}`,
251
+ extensions: { code: "RATE_LIMITED", retryAfter: seconds }
252
+ }
253
+ ];
254
+ }
255
+ if (errors[0].extensions?.retryAfter === void 0) {
256
+ errors[0] = {
257
+ ...errors[0],
258
+ extensions: { ...errors[0].extensions, retryAfter: seconds }
259
+ };
260
+ }
261
+ return errors;
262
+ }
263
+ async function isSharedRefusal(res) {
264
+ if (res.ok) return false;
265
+ try {
266
+ const body = await res.clone().json();
267
+ return body.errors?.some((e) => e.extensions?.code === NOT_SHAREABLE) ?? false;
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+ async function isPersistedQueryMiss(res) {
273
+ try {
274
+ const body = await res.clone().json();
275
+ return body.errors?.some(
276
+ (e) => e.extensions?.code === PERSISTED_QUERY_NOT_FOUND
277
+ ) ?? false;
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+
89
283
  // src/auth/operations.ts
90
284
  import { parse } from "graphql";
91
285
  var AUTH_RESULT_FIELDS = "token refreshToken customerId email";
@@ -134,6 +328,16 @@ var AuthProvidersDocument = parse(
134
328
  authProviders { key name type isDefault }
135
329
  }`
136
330
  );
331
+ var CustomerJoinWaitlistDocument = parse(
332
+ `mutation CustomerJoinWaitlist($email: String!) {
333
+ customerJoinWaitlist(email: $email) { id email status }
334
+ }`
335
+ );
336
+ var CustomerSignupContextDocument = parse(
337
+ `query CustomerSignupContext {
338
+ customerSignupContext { signupMode }
339
+ }`
340
+ );
137
341
 
138
342
  // src/auth/auth.ts
139
343
  var TOKEN_KEY = "foir:token";
@@ -0,0 +1,6 @@
1
+ /** Protocol version for compatibility checks. */
2
+ export declare const PROTOCOL_VERSION = "1.0";
3
+ /** Maximum allowed iframe resize height in pixels. */
4
+ export declare const MAX_RESIZE_HEIGHT = 100000;
5
+ /** Path segments that must be rejected to prevent prototype pollution. */
6
+ export declare const DANGEROUS_PATH_SEGMENTS: string[];
@@ -0,0 +1,155 @@
1
+ /**
2
+ * PostMessage protocol types between EIDE platform host and extension iframes.
3
+ *
4
+ * Inlined from the platform protocol to keep the SDK zero-dep.
5
+ */
6
+ /**
7
+ * Messages sent FROM the platform host TO the iframe extension.
8
+ */
9
+ export type HostToEditorMessage = {
10
+ type: 'init';
11
+ payload: {
12
+ values: Record<string, unknown>;
13
+ metadata?: Record<string, unknown>;
14
+ modelKey: string;
15
+ recordId: string;
16
+ naturalKey?: string;
17
+ versionId: string | null;
18
+ variantKey: string | null;
19
+ locale: string | null;
20
+ schema: unknown;
21
+ mode: 'edit' | 'readonly';
22
+ theme: 'light' | 'dark';
23
+ apiKey: string;
24
+ apiUrl: string;
25
+ connectionDomain?: string;
26
+ configMode?: 'editor' | 'sidebar';
27
+ selectedFieldKeys?: string[];
28
+ selectableFields?: Array<{
29
+ key: string;
30
+ label: string;
31
+ type: string;
32
+ }>;
33
+ localeContext?: {
34
+ currentLocale: string;
35
+ sourceLocale: string;
36
+ isTranslating: boolean;
37
+ locales: Array<{
38
+ code: string;
39
+ displayName: string;
40
+ isDefault: boolean;
41
+ }>;
42
+ };
43
+ };
44
+ } | {
45
+ type: 'values-changed';
46
+ payload: {
47
+ values: Record<string, unknown>;
48
+ source: 'remote';
49
+ };
50
+ } | {
51
+ type: 'variant-changed';
52
+ payload: {
53
+ variantKey: string;
54
+ values: Record<string, unknown>;
55
+ };
56
+ } | {
57
+ type: 'save-result';
58
+ payload: {
59
+ success: boolean;
60
+ error?: string;
61
+ versionId?: string;
62
+ };
63
+ } | {
64
+ type: 'publish-result';
65
+ payload: {
66
+ success: boolean;
67
+ error?: string;
68
+ };
69
+ } | {
70
+ type: 'theme-changed';
71
+ payload: {
72
+ theme: 'light' | 'dark';
73
+ };
74
+ } | {
75
+ type: 'selection-changed';
76
+ payload: {
77
+ selectedFieldKeys: string[];
78
+ fields: Array<{
79
+ key: string;
80
+ label: string;
81
+ type: string;
82
+ }>;
83
+ };
84
+ } | {
85
+ type: 'locale-changed';
86
+ payload: {
87
+ currentLocale: string;
88
+ sourceLocale: string;
89
+ isTranslating: boolean;
90
+ locales: Array<{
91
+ code: string;
92
+ displayName: string;
93
+ isDefault: boolean;
94
+ }>;
95
+ };
96
+ } | {
97
+ type: 'operation-result';
98
+ payload: {
99
+ requestId: string;
100
+ success: boolean;
101
+ result?: unknown;
102
+ error?: {
103
+ code: string;
104
+ message: string;
105
+ };
106
+ };
107
+ };
108
+ /**
109
+ * Messages sent FROM the iframe extension TO the platform host.
110
+ */
111
+ export type EditorToHostMessage = {
112
+ type: 'ready';
113
+ payload: Record<string, never>;
114
+ } | {
115
+ type: 'update-values';
116
+ payload: {
117
+ values: Record<string, unknown>;
118
+ };
119
+ } | {
120
+ type: 'update-field';
121
+ payload: {
122
+ path: string;
123
+ value: unknown;
124
+ };
125
+ } | {
126
+ type: 'request-save';
127
+ payload: Record<string, never>;
128
+ } | {
129
+ type: 'request-publish';
130
+ payload: Record<string, never>;
131
+ } | {
132
+ type: 'resize';
133
+ payload: {
134
+ height: number;
135
+ };
136
+ } | {
137
+ type: 'dirty';
138
+ payload: {
139
+ isDirty: boolean;
140
+ };
141
+ } | {
142
+ type: 'request-navigate';
143
+ payload: {
144
+ path: string;
145
+ };
146
+ } | {
147
+ type: 'request-operation';
148
+ payload: {
149
+ operationKey: string;
150
+ input?: Record<string, unknown>;
151
+ requestId: string;
152
+ /** Execution mode: 'sync' waits for completion, 'async' queues and returns. */
153
+ mode?: 'sync' | 'async';
154
+ };
155
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Runtime validation for postMessage payloads.
3
+ */
4
+ import type { HostToEditorMessage, EditorToHostMessage } from './messages';
5
+ /** Validate an incoming message from a custom editor iframe (editor -> host). */
6
+ export declare function isValidEditorMessage(data: unknown): data is EditorToHostMessage;
7
+ /** Validate an incoming message from the platform host (host -> editor). */
8
+ export declare function isValidHostMessage(data: unknown): data is HostToEditorMessage;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@foir/sdk/push` — native Web Push (VAPID) enrolment for browser web apps.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { enableWebPush } from '@foir/sdk/push';
7
+ *
8
+ * await enableWebPush({ adapter, env }); // registers /foir-sw.js + a WEB token
9
+ * ```
10
+ */
11
+ export { enableWebPush, disableWebPush, isWebPushSupported, } from './web-push';
12
+ export type { EnableWebPushOptions, EnableWebPushResult, DisableWebPushOptions, } from './web-push';
13
+ export { WebPushConfigDocument, RegisterDeviceTokenDocument, UnregisterDeviceTokenDocument, } from './operations';
14
+ export type { RegisterDeviceTokenInput } from './operations';
@@ -0,0 +1,22 @@
1
+ import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
2
+ /** The project's non-secret VAPID public key a browser subscribes with. */
3
+ export declare const WebPushConfigDocument: TypedDocumentNode<{
4
+ webPushConfig: {
5
+ vapidPublicKey: string;
6
+ } | null;
7
+ }, Record<string, never>>;
8
+ export interface RegisterDeviceTokenInput {
9
+ token: string;
10
+ platform: 'IOS' | 'ANDROID' | 'WEB';
11
+ deviceName?: string;
12
+ }
13
+ export declare const RegisterDeviceTokenDocument: TypedDocumentNode<{
14
+ registerDeviceToken: boolean;
15
+ }, {
16
+ input: RegisterDeviceTokenInput;
17
+ }>;
18
+ export declare const UnregisterDeviceTokenDocument: TypedDocumentNode<{
19
+ unregisterDeviceToken: boolean;
20
+ }, {
21
+ token: string;
22
+ }>;
@@ -0,0 +1,39 @@
1
+ import type { SessionAdapter } from '../auth/auth';
2
+ import type { FoirEnv } from '../graphql/foir-request';
3
+ export interface EnableWebPushOptions {
4
+ adapter: SessionAdapter;
5
+ env: FoirEnv;
6
+ /** Path the service worker is served from (must be same-origin). Default `/foir-sw.js`. */
7
+ swPath?: string;
8
+ /** Service-worker registration scope. Default `/`. */
9
+ scope?: string;
10
+ /** Optional label stored alongside the device token. */
11
+ deviceName?: string;
12
+ }
13
+ export type EnableWebPushResult = {
14
+ ok: true;
15
+ subscription: PushSubscription;
16
+ } | {
17
+ ok: false;
18
+ reason: 'unsupported' | 'denied' | 'no-config';
19
+ };
20
+ export interface DisableWebPushOptions {
21
+ adapter: SessionAdapter;
22
+ env: FoirEnv;
23
+ /** Scope the service worker was registered with. Default `/`. */
24
+ scope?: string;
25
+ }
26
+ /** True when the current browser supports the Web Push APIs this helper needs. */
27
+ export declare function isWebPushSupported(): boolean;
28
+ /**
29
+ * Subscribe the current browser to Foir Web Push and register the subscription
30
+ * as a `WEB` device token. Idempotent: reuses an existing subscription when one
31
+ * is already present. Returns `{ ok: false }` (never throws) when the browser
32
+ * is unsupported or the user declines permission.
33
+ */
34
+ export declare function enableWebPush(opts: EnableWebPushOptions): Promise<EnableWebPushResult>;
35
+ /**
36
+ * Unsubscribe the current browser from Web Push and deregister the device
37
+ * token. Returns `false` when there was no active subscription to remove.
38
+ */
39
+ export declare function disableWebPush(opts: DisableWebPushOptions): Promise<boolean>;