@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,123 @@
1
+ /**
2
+ * Foir customer-auth GraphQL operations + result types.
3
+ *
4
+ * These are PLATFORM-STABLE primitives — `customerLogin`/`customerRegister`/
5
+ * `customerRefreshToken`/OTP/provider login are fixed-shape operations served
6
+ * by api-public for every project (gated per-project by the password/OTP/
7
+ * provider flags, but identical in shape when present). Unlike per-project
8
+ * model types, they don't vary by configured models, so the SDK owns the
9
+ * documents directly rather than relying on the consumer's codegen.
10
+ *
11
+ * The project-specific customer PROFILE (e.g. `myProfile.isAdmin`) is NOT here
12
+ * — that shape varies per project, so it stays consumer-injected (see
13
+ * `requireAuth`'s `resolveUser`).
14
+ */
15
+ import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
16
+ /** The common auth result every login/register/refresh mutation returns. */
17
+ export interface CustomerAuthResult {
18
+ token: string;
19
+ refreshToken: string;
20
+ customerId: string;
21
+ email: string;
22
+ }
23
+ /** Result of initiating a provider login — a redirect URL or an inline token. */
24
+ export interface ProviderLoginResult {
25
+ /** OAuth/OIDC authorization URL to redirect the user to (redirect flows). */
26
+ redirectUrl: string | null;
27
+ /** Inline access token (non-redirect provider flows). */
28
+ token: string | null;
29
+ }
30
+ /** A configured auth provider, as exposed to unauthenticated callers. */
31
+ export interface AuthProviderPublic {
32
+ key: string;
33
+ name: string;
34
+ /** OAUTH2 | TOKEN_SSO | SAML | OIDC | EXTERNAL */
35
+ type: string;
36
+ isDefault: boolean | null;
37
+ }
38
+ export interface ProviderLoginInput {
39
+ providerKey: string;
40
+ email?: string;
41
+ returnTo?: string;
42
+ /** Override the provider's default redirect_uri (must be allow-listed). */
43
+ redirectUri?: string;
44
+ }
45
+ export interface ProviderCallbackInput {
46
+ providerKey: string;
47
+ code?: string;
48
+ state?: string;
49
+ email?: string;
50
+ idToken?: string;
51
+ }
52
+ export interface ProviderOTPVerifyInput {
53
+ providerKey: string;
54
+ email: string;
55
+ otp: string;
56
+ }
57
+ /** The project's customer signup mode. */
58
+ export type SignupMode = "PUBLIC" | "RESTRICTED" | "WAITLIST" | "CLOSED";
59
+ /** The signup surface a self-hosted frontend reads to render the right UI. */
60
+ export interface SignupContext {
61
+ signupMode: SignupMode;
62
+ }
63
+ /** The queued (or existing pending) entry a waitlist join returns. */
64
+ export interface WaitlistJoinResult {
65
+ id: string;
66
+ email: string;
67
+ /** Waitlist lifecycle status; PENDING on a fresh join. */
68
+ status: string;
69
+ }
70
+ export declare const CustomerLoginDocument: TypedDocumentNode<{
71
+ customerLogin: CustomerAuthResult;
72
+ }, {
73
+ email: string;
74
+ password: string;
75
+ }>;
76
+ export declare const CustomerRegisterDocument: TypedDocumentNode<{
77
+ customerRegister: CustomerAuthResult;
78
+ }, {
79
+ email: string;
80
+ password: string;
81
+ }>;
82
+ export declare const CustomerRefreshTokenDocument: TypedDocumentNode<{
83
+ customerRefreshToken: CustomerAuthResult;
84
+ }, {
85
+ refreshToken: string;
86
+ }>;
87
+ export declare const CustomerRequestOTPDocument: TypedDocumentNode<{
88
+ customerRequestOTP: boolean;
89
+ }, {
90
+ email: string;
91
+ }>;
92
+ export declare const CustomerLoginOTPDocument: TypedDocumentNode<{
93
+ customerLoginOTP: CustomerAuthResult;
94
+ }, {
95
+ email: string;
96
+ otp: string;
97
+ }>;
98
+ export declare const CustomerLoginWithProviderDocument: TypedDocumentNode<{
99
+ customerLoginWithProvider: ProviderLoginResult;
100
+ }, {
101
+ input: ProviderLoginInput;
102
+ }>;
103
+ export declare const CustomerProviderCallbackDocument: TypedDocumentNode<{
104
+ customerProviderCallback: CustomerAuthResult;
105
+ }, {
106
+ input: ProviderCallbackInput;
107
+ }>;
108
+ export declare const CustomerProviderVerifyOTPDocument: TypedDocumentNode<{
109
+ customerProviderVerifyOTP: CustomerAuthResult;
110
+ }, {
111
+ input: ProviderOTPVerifyInput;
112
+ }>;
113
+ export declare const AuthProvidersDocument: TypedDocumentNode<{
114
+ authProviders: AuthProviderPublic[];
115
+ }, Record<string, never>>;
116
+ export declare const CustomerJoinWaitlistDocument: TypedDocumentNode<{
117
+ customerJoinWaitlist: WaitlistJoinResult;
118
+ }, {
119
+ email: string;
120
+ }>;
121
+ export declare const CustomerSignupContextDocument: TypedDocumentNode<{
122
+ customerSignupContext: SignupContext;
123
+ }, Record<string, never>>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Foir-hosted first-party sessions, from the browser — the model for a
3
+ * backend-less SPA (Vite, CRA, a static site) that wants no tokens in JS.
4
+ *
5
+ * The sibling `fetchHostedSession` in ./hosted does the same exchange from a
6
+ * SERVER: it reads the cookie off an incoming request and forwards it by hand.
7
+ * A browser cannot set a `Cookie` header, so that function is unusable from an
8
+ * SPA by construction. This one lets the browser send the cookie the only way it
9
+ * can — `credentials: 'include'` — and keeps the resulting bearer in memory.
10
+ *
11
+ * How the pieces divide:
12
+ *
13
+ * cookie — long-lived (30d), HttpOnly, first-party on your registrable
14
+ * domain, set by Foir when hosted login finalizes. JS cannot read
15
+ * it, so XSS cannot exfiltrate it.
16
+ * bearer — 15 minutes, held in a closure here. Never written to
17
+ * localStorage/sessionStorage/IndexedDB, never in a cookie of ours.
18
+ *
19
+ * There is no refresh token in this flow. Re-exchanging the cookie IS the
20
+ * refresh, which is why nothing here needs rotation or reuse-detection: the
21
+ * durable credential is an opaque server-side session handle, revocable at any
22
+ * time from the admin panel or `signOut()`.
23
+ *
24
+ * REQUIRES a verified custom login domain that shares a registrable domain with
25
+ * your app (app.acme.com + login.acme.com). That is what makes the cookie
26
+ * first-party and lets `SameSite=Lax` send it. The same endpoints exist on
27
+ * api.foir.dev but a browser cannot reach them there — the cookie would be
28
+ * cross-site, and that CORS surface deliberately refuses credentials.
29
+ */
30
+ export interface FoirSessionConfig {
31
+ /**
32
+ * Your verified custom login domain, e.g. `https://login.acme.com`. Must share
33
+ * a registrable domain with the app calling it.
34
+ */
35
+ loginOrigin: string;
36
+ /** Session-exchange path. Default `/session/token`. */
37
+ sessionPath?: string;
38
+ /** Logout path. Default `/session/logout`. */
39
+ logoutPath?: string;
40
+ /** Seconds before expiry to re-exchange. Default 60. */
41
+ refreshSkewSeconds?: number;
42
+ /**
43
+ * Called when the session goes away — the cookie expired, was revoked, or
44
+ * `signOut()` ran. Use it to redirect to `signInUrl()`.
45
+ */
46
+ onSignOut?: () => void;
47
+ }
48
+ /** The identity behind the current session. */
49
+ export interface FoirSessionUser {
50
+ customerId: string;
51
+ email: string;
52
+ }
53
+ export interface FoirSessionClient {
54
+ /**
55
+ * The current access token, exchanging or re-exchanging the cookie as needed.
56
+ * Null when there is no session. Concurrent callers share one in-flight
57
+ * exchange. Call this before each API request — do not cache the result.
58
+ */
59
+ getToken(): Promise<string | null>;
60
+ /** The signed-in customer, or null. Exchanges if the session isn't loaded yet. */
61
+ getUser(): Promise<FoirSessionUser | null>;
62
+ /** Revoke the session server-side and clear the cookie. Idempotent. */
63
+ signOut(): Promise<void>;
64
+ /** Where to send the browser to sign in. */
65
+ signInUrl(returnTo?: string): string;
66
+ /** Observe session changes. Returns an unsubscribe function. */
67
+ subscribe(listener: (user: FoirSessionUser | null) => void): () => void;
68
+ /** Cancel the refresh timer. Call on unmount. */
69
+ destroy(): void;
70
+ }
71
+ /**
72
+ * Create a session client. Safe to call during SSR — it does nothing until a
73
+ * method is invoked, and `getToken()` on the server returns null rather than
74
+ * throwing (there is no cookie jar to draw on).
75
+ */
76
+ export declare function createFoirSessionClient(config: FoirSessionConfig): FoirSessionClient;
@@ -0,0 +1,137 @@
1
+ // src/auth/session-client.ts
2
+ var DEFAULT_SESSION_PATH = "/session/token";
3
+ var DEFAULT_LOGOUT_PATH = "/session/logout";
4
+ var DEFAULT_REFRESH_SKEW_SECONDS = 60;
5
+ var MIN_REFRESH_DELAY_MS = 5e3;
6
+ var CSRF_HEADER = { "X-Foir-Session": "1" };
7
+ function createFoirSessionClient(config) {
8
+ const sessionUrl = `${trimSlash(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH}`;
9
+ const logoutUrl = `${trimSlash(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
10
+ const skewMs = (config.refreshSkewSeconds ?? DEFAULT_REFRESH_SKEW_SECONDS) * 1e3;
11
+ let cached = null;
12
+ let inflight = null;
13
+ let timer = null;
14
+ let destroyed = false;
15
+ const listeners = /* @__PURE__ */ new Set();
16
+ function emit(user) {
17
+ for (const l of listeners) l(user);
18
+ }
19
+ function clearTimer() {
20
+ if (timer !== null) {
21
+ clearTimeout(timer);
22
+ timer = null;
23
+ }
24
+ }
25
+ function scheduleRefresh(expiresAt) {
26
+ clearTimer();
27
+ if (destroyed || typeof setTimeout === "undefined") return;
28
+ const delay = Math.max(expiresAt - Date.now() - skewMs, MIN_REFRESH_DELAY_MS);
29
+ timer = setTimeout(() => {
30
+ void exchange();
31
+ }, delay);
32
+ timer.unref?.();
33
+ }
34
+ function forget() {
35
+ const had = cached !== null;
36
+ cached = null;
37
+ clearTimer();
38
+ if (had) emit(null);
39
+ }
40
+ function exchange() {
41
+ if (inflight) return inflight;
42
+ const pending = (async () => {
43
+ let res;
44
+ try {
45
+ res = await fetch(sessionUrl, {
46
+ method: "GET",
47
+ // The whole point: send the first-party cookie cross-origin (but
48
+ // same-site) to the login domain. Without this the cookie is not sent.
49
+ credentials: "include",
50
+ headers: { ...CSRF_HEADER }
51
+ });
52
+ } catch {
53
+ return cached;
54
+ }
55
+ if (res.status === 401) {
56
+ forget();
57
+ config.onSignOut?.();
58
+ return null;
59
+ }
60
+ if (!res.ok) return cached;
61
+ let json;
62
+ try {
63
+ json = await res.json();
64
+ } catch {
65
+ return cached;
66
+ }
67
+ if (!json.access_token || !json.customer_id) return cached;
68
+ const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 0;
69
+ const next = {
70
+ token: json.access_token,
71
+ expiresAt: Date.now() + expiresIn * 1e3,
72
+ user: { customerId: json.customer_id, email: json.email ?? "" }
73
+ };
74
+ const changed = cached?.user.customerId !== next.user.customerId;
75
+ cached = next;
76
+ scheduleRefresh(next.expiresAt);
77
+ if (changed) emit(next.user);
78
+ return next;
79
+ })();
80
+ inflight = pending;
81
+ void pending.finally(() => {
82
+ if (inflight === pending) inflight = null;
83
+ });
84
+ return pending;
85
+ }
86
+ function fresh() {
87
+ if (!cached) return null;
88
+ return Date.now() < cached.expiresAt - skewMs ? cached : null;
89
+ }
90
+ return {
91
+ async getToken() {
92
+ const hit = fresh();
93
+ if (hit) return hit.token;
94
+ const got = await exchange();
95
+ return got?.token ?? null;
96
+ },
97
+ async getUser() {
98
+ const hit = fresh();
99
+ if (hit) return hit.user;
100
+ const got = await exchange();
101
+ return got?.user ?? null;
102
+ },
103
+ async signOut() {
104
+ try {
105
+ await fetch(logoutUrl, {
106
+ method: "POST",
107
+ credentials: "include",
108
+ headers: { ...CSRF_HEADER }
109
+ });
110
+ } catch {
111
+ }
112
+ forget();
113
+ config.onSignOut?.();
114
+ },
115
+ signInUrl(returnTo) {
116
+ const dest = new URL(trimSlash(config.loginOrigin));
117
+ const target = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
118
+ if (target && target !== "/") dest.searchParams.set("returnTo", target);
119
+ return dest.toString();
120
+ },
121
+ subscribe(listener) {
122
+ listeners.add(listener);
123
+ return () => listeners.delete(listener);
124
+ },
125
+ destroy() {
126
+ destroyed = true;
127
+ clearTimer();
128
+ listeners.clear();
129
+ }
130
+ };
131
+ }
132
+ function trimSlash(s) {
133
+ return s.replace(/\/+$/, "");
134
+ }
135
+ export {
136
+ createFoirSessionClient
137
+ };
package/dist/auth.js CHANGED
@@ -1,8 +1,16 @@
1
1
  // src/graphql/foir-request.ts
2
+ import { print as print2 } from "graphql";
3
+
4
+ // src/graphql/cdn-route.ts
2
5
  import { print } from "graphql";
6
+
7
+ // src/transport/foir-http.ts
3
8
  function isFoirPreview(env) {
4
9
  return env.FOIR_PREVIEW === "true";
5
10
  }
11
+ function resolveFoirApiKey(env) {
12
+ return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
13
+ }
6
14
  var FoirRequestError = class extends Error {
7
15
  /** HTTP status code, when the request failed at the HTTP layer. */
8
16
  status;
@@ -16,41 +24,83 @@ var FoirRequestError = class extends Error {
16
24
  this.status = options?.status;
17
25
  this.graphQLErrors = options?.graphQLErrors;
18
26
  }
27
+ /**
28
+ * The code of the first error, if the response carried one.
29
+ *
30
+ * A getter rather than a stored field so it stays correct for an error
31
+ * constructed anywhere, including by a caller in a test.
32
+ */
33
+ get code() {
34
+ return this.graphQLErrors?.[0]?.extensions?.code;
35
+ }
36
+ /** The correlation id, if the response carried one. */
37
+ get requestId() {
38
+ return this.graphQLErrors?.[0]?.extensions?.requestId;
39
+ }
40
+ /**
41
+ * Whether retrying this request could succeed.
42
+ *
43
+ * The 429 is the one that has to be in here and is easy to leave out: it sits
44
+ * among the 4xx statuses that are otherwise permanent, so a policy written as
45
+ * "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
46
+ * own. A storefront wrote that policy, its key saturated, and every read
47
+ * failed on the first attempt for an hour.
48
+ */
49
+ get retryable() {
50
+ if (this.status === void 0) return true;
51
+ if (this.status === 429 || this.code === "RATE_LIMITED") return true;
52
+ if (this.status >= 500) return true;
53
+ return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
54
+ }
55
+ /**
56
+ * How long to wait before retrying, in milliseconds, when the server said.
57
+ * Undefined when it did not, which means the caller picks its own backoff.
58
+ */
59
+ get retryAfterMs() {
60
+ const seconds = this.graphQLErrors?.[0]?.extensions?.retryAfter;
61
+ return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
62
+ }
19
63
  };
20
- async function foirRequest(env, document, variables, options) {
21
- const endpoint = env.PLATFORM_API_URL;
22
- const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
23
- if (!endpoint || !apiKey) {
24
- throw new FoirRequestError(
25
- "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
26
- );
64
+ function readFoirRateLimit(headers) {
65
+ const raw = {
66
+ limit: headers.get("X-RateLimit-Limit"),
67
+ remaining: headers.get("X-RateLimit-Remaining"),
68
+ resetAt: headers.get("X-RateLimit-Reset")
69
+ };
70
+ if (raw.limit === null || raw.remaining === null || raw.resetAt === null) {
71
+ return void 0;
27
72
  }
73
+ const limit = Number(raw.limit);
74
+ const remaining = Number(raw.remaining);
75
+ const resetAt = Number(raw.resetAt);
76
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining) || !Number.isFinite(resetAt)) {
77
+ return void 0;
78
+ }
79
+ return { limit, remaining, resetAt };
80
+ }
81
+ function readFoirRetryAfterMs(headers) {
82
+ const seconds = Number(headers.get("Retry-After"));
83
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
84
+ }
85
+ function buildFoirHeaders(input) {
28
86
  const headers = {
29
87
  "Content-Type": "application/json",
30
- "x-api-key": apiKey
88
+ "x-api-key": input.apiKey
31
89
  };
32
- if (isFoirPreview(env)) {
90
+ if (input.draftChannel) {
33
91
  headers["X-Foir-Schema-Channel"] = "draft";
34
92
  }
35
- if (options?.bearerToken) {
36
- headers["Authorization"] = `Bearer ${options.bearerToken}`;
37
- }
38
- let res;
39
- try {
40
- res = await fetch(endpoint, {
41
- method: "POST",
42
- headers,
43
- body: JSON.stringify({ query: print(document), variables })
44
- });
45
- } catch (err) {
46
- throw new FoirRequestError("[foir] network error", { cause: err });
93
+ if (input.bearerToken) {
94
+ headers["Authorization"] = `Bearer ${input.bearerToken}`;
47
95
  }
48
- if (!res.ok) {
49
- throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
50
- status: res.status
51
- });
96
+ if (input.extra) {
97
+ for (const [key, value] of Object.entries(input.extra)) {
98
+ headers[key] = value;
99
+ }
52
100
  }
53
- const json = await res.json();
101
+ return headers;
102
+ }
103
+ function readFoirEnvelope(json) {
54
104
  if (json.errors?.length) {
55
105
  const msg = json.errors.map((e) => e.message).join("; ");
56
106
  if (json.data) {
@@ -67,6 +117,150 @@ async function foirRequest(env, document, variables, options) {
67
117
  return json.data;
68
118
  }
69
119
 
120
+ // src/graphql/cdn-route.ts
121
+ var CDN_PATH = "/cdn/graphql";
122
+ function isCdnEligible(env, documentText, bearerToken) {
123
+ if (bearerToken) return false;
124
+ if (isFoirPreview(env)) return false;
125
+ const apiKey = resolveFoirApiKey(env);
126
+ if (!apiKey?.startsWith("pk_")) return false;
127
+ return !/\bmutation\b/.test(documentText);
128
+ }
129
+ function buildCdnUrl(endpoint, apiKey, hash, variables) {
130
+ const url = new URL(endpoint);
131
+ url.pathname = url.pathname.replace(/\/graphql\/?$/, "") + CDN_PATH;
132
+ url.searchParams.set("key", apiKey);
133
+ url.searchParams.set("extensions", JSON.stringify(persistedQueryExtension(hash)));
134
+ if (variables && Object.keys(variables).length > 0) {
135
+ url.searchParams.set("variables", JSON.stringify(variables));
136
+ }
137
+ return url.toString();
138
+ }
139
+ function persistedQueryExtension(hash) {
140
+ return { persistedQuery: { version: 1, sha256Hash: hash } };
141
+ }
142
+ async function documentHash(documentText) {
143
+ const digest = await crypto.subtle.digest(
144
+ "SHA-256",
145
+ new TextEncoder().encode(documentText)
146
+ );
147
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
148
+ }
149
+ var PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
150
+ var NOT_SHAREABLE = "NOT_SHAREABLE";
151
+
152
+ // src/graphql/foir-request.ts
153
+ async function foirRequest(env, document, variables, options) {
154
+ const endpoint = env.PLATFORM_API_URL;
155
+ const apiKey = resolveFoirApiKey(env);
156
+ if (!endpoint || !apiKey) {
157
+ throw new FoirRequestError(
158
+ "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
159
+ );
160
+ }
161
+ const headers = buildFoirHeaders({
162
+ apiKey,
163
+ draftChannel: isFoirPreview(env),
164
+ bearerToken: options?.bearerToken
165
+ });
166
+ const documentText = print2(document);
167
+ let res;
168
+ try {
169
+ if (isCdnEligible(env, documentText, options?.bearerToken)) {
170
+ const hash = await documentHash(documentText);
171
+ let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
172
+ method: "GET",
173
+ headers
174
+ });
175
+ if (await isPersistedQueryMiss(shared)) {
176
+ shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
177
+ method: "POST",
178
+ headers,
179
+ body: JSON.stringify({
180
+ query: documentText,
181
+ variables,
182
+ extensions: persistedQueryExtension(hash)
183
+ })
184
+ });
185
+ }
186
+ if (await isSharedRefusal(shared)) {
187
+ res = await fetch(endpoint, {
188
+ method: "POST",
189
+ headers,
190
+ body: JSON.stringify({ query: documentText, variables })
191
+ });
192
+ } else {
193
+ res = shared;
194
+ }
195
+ } else {
196
+ res = await fetch(endpoint, {
197
+ method: "POST",
198
+ headers,
199
+ body: JSON.stringify({ query: documentText, variables })
200
+ });
201
+ }
202
+ } catch (err) {
203
+ throw new FoirRequestError("[foir] network error", { cause: err });
204
+ }
205
+ options?.onRateLimit?.(readFoirRateLimit(res.headers));
206
+ if (!res.ok) {
207
+ const errors = await readErrorEnvelope(res);
208
+ const first = errors?.[0]?.message;
209
+ throw new FoirRequestError(
210
+ first ? `[foir] HTTP ${res.status} ${res.statusText}: ${first}` : `[foir] HTTP ${res.status} ${res.statusText}`,
211
+ { status: res.status, graphQLErrors: errors }
212
+ );
213
+ }
214
+ const json = await res.json();
215
+ return readFoirEnvelope(json);
216
+ }
217
+ async function readErrorEnvelope(res) {
218
+ let errors;
219
+ try {
220
+ const body = await res.json();
221
+ errors = body.errors;
222
+ } catch {
223
+ errors = void 0;
224
+ }
225
+ const headerRetryMs = readFoirRetryAfterMs(res.headers);
226
+ if (headerRetryMs === void 0) return errors;
227
+ const seconds = Math.round(headerRetryMs / 1e3);
228
+ if (!errors?.length) {
229
+ return [
230
+ {
231
+ message: res.statusText || `HTTP ${res.status}`,
232
+ extensions: { code: "RATE_LIMITED", retryAfter: seconds }
233
+ }
234
+ ];
235
+ }
236
+ if (errors[0].extensions?.retryAfter === void 0) {
237
+ errors[0] = {
238
+ ...errors[0],
239
+ extensions: { ...errors[0].extensions, retryAfter: seconds }
240
+ };
241
+ }
242
+ return errors;
243
+ }
244
+ async function isSharedRefusal(res) {
245
+ if (res.ok) return false;
246
+ try {
247
+ const body = await res.clone().json();
248
+ return body.errors?.some((e) => e.extensions?.code === NOT_SHAREABLE) ?? false;
249
+ } catch {
250
+ return false;
251
+ }
252
+ }
253
+ async function isPersistedQueryMiss(res) {
254
+ try {
255
+ const body = await res.clone().json();
256
+ return body.errors?.some(
257
+ (e) => e.extensions?.code === PERSISTED_QUERY_NOT_FOUND
258
+ ) ?? false;
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+
70
264
  // src/auth/operations.ts
71
265
  import { parse } from "graphql";
72
266
  var AUTH_RESULT_FIELDS = "token refreshToken customerId email";
@@ -115,6 +309,16 @@ var AuthProvidersDocument = parse(
115
309
  authProviders { key name type isDefault }
116
310
  }`
117
311
  );
312
+ var CustomerJoinWaitlistDocument = parse(
313
+ `mutation CustomerJoinWaitlist($email: String!) {
314
+ customerJoinWaitlist(email: $email) { id email status }
315
+ }`
316
+ );
317
+ var CustomerSignupContextDocument = parse(
318
+ `query CustomerSignupContext {
319
+ customerSignupContext { signupMode }
320
+ }`
321
+ );
118
322
 
119
323
  // src/auth/auth.ts
120
324
  var TOKEN_KEY = "foir:token";
@@ -148,6 +352,12 @@ async function customerProviderVerifyOTP(env, input) {
148
352
  async function getAuthProviders(env) {
149
353
  return (await foirRequest(env, AuthProvidersDocument, {})).authProviders;
150
354
  }
355
+ async function customerSignupContext(env) {
356
+ return (await foirRequest(env, CustomerSignupContextDocument, {})).customerSignupContext;
357
+ }
358
+ async function customerJoinWaitlist(env, email) {
359
+ return (await foirRequest(env, CustomerJoinWaitlistDocument, { email })).customerJoinWaitlist;
360
+ }
151
361
  async function saveSession(adapter, result) {
152
362
  await adapter.set(TOKEN_KEY, result.token);
153
363
  await adapter.set(REFRESH_KEY, result.refreshToken);
@@ -422,6 +632,7 @@ export {
422
632
  authedRequest,
423
633
  clearSession,
424
634
  createFoirSessionClient,
635
+ customerJoinWaitlist,
425
636
  customerLogin,
426
637
  customerLoginOTP,
427
638
  customerLoginWithProvider,
@@ -430,6 +641,7 @@ export {
430
641
  customerRefreshToken,
431
642
  customerRegister,
432
643
  customerRequestOTP,
644
+ customerSignupContext,
433
645
  fetchHostedSession,
434
646
  getAuthProviders,
435
647
  getSessionCustomer,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Fetch-based GraphQL client for the EIDE platform API.
3
+ *
4
+ * Zero dependencies — uses the native Fetch API.
5
+ */
6
+ import type { PlatformClient, PlatformClientOptions } from '../types';
7
+ /** Error thrown when a platform API request fails. */
8
+ export declare class PlatformClientError extends Error {
9
+ status?: number | undefined;
10
+ response?: unknown | undefined;
11
+ constructor(message: string, status?: number | undefined, response?: unknown | undefined);
12
+ }
13
+ /**
14
+ * Create a PlatformClient instance.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const client = createPlatformClient({
19
+ * apiUrl: 'https://api.example.com/graphql',
20
+ * apiKey: 'sk_test_...',
21
+ * });
22
+ *
23
+ * const { data } = await client.query(
24
+ * `query GetProduct($id: ID!) { product(id: $id) { title } }`,
25
+ * { id: '123' }
26
+ * );
27
+ * ```
28
+ */
29
+ export declare function createPlatformClient(options: PlatformClientOptions): PlatformClient;