@foir/sdk 0.5.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 +281 -37
- 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 -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,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
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 (
|
|
109
|
+
if (input.draftChannel) {
|
|
52
110
|
headers["X-Foir-Schema-Channel"] = "draft";
|
|
53
111
|
}
|
|
54
|
-
if (
|
|
55
|
-
headers["Authorization"] = `Bearer ${
|
|
112
|
+
if (input.bearerToken) {
|
|
113
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
56
114
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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>;
|