@foir/sdk 0.1.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/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @foir/sdk
2
+
3
+ Official Foir SDK — client-agnostic typed GraphQL transport, customer auth,
4
+ progressive media, content rendering, realtime collaboration, and editor/server
5
+ helpers for building apps on the [Foir](https://foir.dev) platform.
6
+
7
+ The SDK is split into independent subpath exports so consumers only pull what
8
+ they use. Everything heavy (`react`, `graphql`, `yjs`, `hls.js`, …) is an
9
+ optional peer dependency gated to the subpath that needs it.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add @foir/sdk
15
+ # plus the peers for the subpaths you use, e.g.:
16
+ pnpm add graphql # for @foir/sdk/client + @foir/sdk/auth
17
+ pnpm add react # for @foir/sdk (editor), /media, /content
18
+ pnpm add yjs y-websocket # for @foir/sdk/realtime
19
+ ```
20
+
21
+ ## Subpath exports
22
+
23
+ | Import | What it is | Peers |
24
+ | --- | --- | --- |
25
+ | `@foir/sdk` | Editor iframe client — `EditorProvider`, `useEditor`, `useAutoResize`, `createPlatformClient`, host↔editor message protocol | `react` |
26
+ | `@foir/sdk/client` | Client-agnostic GraphQL transport for SSR/edge loaders — `foirRequest`, `isFoirPreview`, `absolutiseFoirMediaUrl` | `graphql` |
27
+ | `@foir/sdk/auth` | Framework-agnostic customer auth — login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `authedRequest`, guards | `graphql` |
28
+ | `@foir/sdk/server` | Server-side helpers for extension/config API backends — `createServerClient`, `verifyWebhookSignature`, `verifyScopedToken`, callback client | — |
29
+ | `@foir/sdk/media` | Progressive media — `ProgressiveMedia`, `BlurhashCanvas`, `useProgressiveLoad`, variant-tier adapters | `react` (`hls.js`, `blurhash` optional) |
30
+ | `@foir/sdk/content` | Content-rendering primitives — `createContentRenderer` block dispatch + Lexical `RichTextRenderer` | `react` |
31
+ | `@foir/sdk/realtime` | Thin collaboration helper over `y-websocket` — canonical room addressing, presence, token refresh | `yjs`, `y-websocket` |
32
+ | `@foir/sdk/codegen` | Build-time graphql-codegen preset (`foirCodegen`) + exhaustive block-registry plugin | `@graphql-codegen/cli`, `@graphql-codegen/client-preset` (devDeps) |
33
+
34
+ ## Usage
35
+
36
+ ### Typed GraphQL from a server loader
37
+
38
+ `foirRequest` is a typed `fetch` wrapper, not a GraphQL client: it carries the
39
+ Foir-specific transport concerns (`x-api-key`, preview schema channel, error
40
+ shape) and runs generated `TypedDocumentNode`s. Bring-your-own-client consumers
41
+ (Apollo/urql/react-query) use the generated documents directly and replicate
42
+ those headers.
43
+
44
+ ```ts
45
+ import { foirRequest, type FoirEnv } from '@foir/sdk/client';
46
+ import { HomePageDocument } from './generated/graphql';
47
+
48
+ export async function loader({ context }: LoaderArgs) {
49
+ const env: FoirEnv = {
50
+ PLATFORM_API_URL: context.env.PLATFORM_API_URL,
51
+ PLATFORM_API_KEY: context.env.PLATFORM_API_KEY,
52
+ };
53
+ const data = await foirRequest(env, HomePageDocument, { slug: 'home' });
54
+ return data.homePage;
55
+ }
56
+ ```
57
+
58
+ ### Customer auth
59
+
60
+ ```ts
61
+ import {
62
+ loginWithPassword,
63
+ authedRequest,
64
+ type SessionAdapter,
65
+ } from '@foir/sdk/auth';
66
+ import { MyProfileDocument } from './generated/graphql';
67
+
68
+ // SessionAdapter = { get, set, delete } over your session store (cookie, KV…).
69
+ const customer = await loginWithPassword(adapter, env, { email, password });
70
+
71
+ // Bearer-authed request with automatic refresh-and-retry on expired sessions.
72
+ const { myProfile } = await authedRequest(adapter, env, MyProfileDocument);
73
+ ```
74
+
75
+ ### Codegen
76
+
77
+ ```ts
78
+ // codegen.ts
79
+ import { foirCodegen } from '@foir/sdk/codegen';
80
+
81
+ export default foirCodegen({
82
+ apiUrl: process.env.PLATFORM_API_URL!,
83
+ apiKey: process.env.PLATFORM_API_KEY!,
84
+ // documents: 'app/graphql/foir/**/*.graphql' (default)
85
+ // outputDir: './app/gql/foir/' (default)
86
+ });
87
+ ```
88
+
89
+ ### Editor extensions
90
+
91
+ ```tsx
92
+ import { EditorProvider, useEditor } from '@foir/sdk';
93
+
94
+ function App() {
95
+ return (
96
+ <EditorProvider>
97
+ <MyEditor />
98
+ </EditorProvider>
99
+ );
100
+ }
101
+
102
+ function MyEditor() {
103
+ const { init, client } = useEditor(); // record context + platform GraphQL client
104
+ // ...
105
+ }
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
package/dist/auth.d.ts ADDED
@@ -0,0 +1,305 @@
1
+ import { TypedDocumentNode } from '@graphql-typed-document-node/core';
2
+
3
+ /**
4
+ * Foir GraphQL client — a single typed fetch wrapper for SSR/edge loaders.
5
+ *
6
+ * No GraphQL client (Apollo/urql) required: the Foir public API is a plain
7
+ * `POST` with an `x-api-key` header, and the dominant consumers (Hydrogen on
8
+ * Oxygen, React Router loaders, TanStack Start on CF Workers) fetch in
9
+ * server-only loaders and render — so Apollo's `InMemoryCache`/`useQuery`
10
+ * reactivity is dead weight, and a shared cache on a stateless multi-tenant
11
+ * edge worker is a cross-tenant hazard. This helper carries only the
12
+ * Foir-specific transport concerns (auth header, preview schema channel,
13
+ * error shape). Consumers on a GraphQL client (Apollo / urql / react-query /
14
+ * graphql-request) use the generated `TypedDocumentNode`s directly with their
15
+ * own client and replicate the two Foir-specific headers below — the
16
+ * `x-api-key` and, in preview, `X-Foir-Schema-Channel: draft`.
17
+ *
18
+ * Deduplicates the hand-copied `lib/foir/client.ts` that shipped identically
19
+ * across the Foir storefronts.
20
+ */
21
+
22
+ interface FoirEnv {
23
+ PLATFORM_API_URL?: string;
24
+ PLATFORM_API_KEY?: string;
25
+ PLATFORM_PREVIEW_API_KEY?: string;
26
+ FOIR_PREVIEW?: string;
27
+ }
28
+
29
+ /**
30
+ * Foir customer-auth GraphQL operations + result types.
31
+ *
32
+ * These are PLATFORM-STABLE primitives — `customerLogin`/`customerRegister`/
33
+ * `customerRefreshToken`/OTP/provider login are fixed-shape operations served
34
+ * by api-public for every project (gated per-project by the password/OTP/
35
+ * provider flags, but identical in shape when present). Unlike per-project
36
+ * model types, they don't vary by configured models, so the SDK owns the
37
+ * documents directly rather than relying on the consumer's codegen.
38
+ *
39
+ * The project-specific customer PROFILE (e.g. `myProfile.isAdmin`) is NOT here
40
+ * — that shape varies per project, so it stays consumer-injected (see
41
+ * `requireAuth`'s `resolveUser`).
42
+ */
43
+
44
+ /** The common auth result every login/register/refresh mutation returns. */
45
+ interface CustomerAuthResult {
46
+ token: string;
47
+ refreshToken: string;
48
+ customerId: string;
49
+ email: string;
50
+ }
51
+ /** Result of initiating a provider login — a redirect URL or an inline token. */
52
+ interface ProviderLoginResult {
53
+ /** OAuth/OIDC authorization URL to redirect the user to (redirect flows). */
54
+ redirectUrl: string | null;
55
+ /** Inline access token (non-redirect provider flows). */
56
+ token: string | null;
57
+ }
58
+ /** A configured auth provider, as exposed to unauthenticated callers. */
59
+ interface AuthProviderPublic {
60
+ key: string;
61
+ name: string;
62
+ /** OAUTH2 | TOKEN_SSO | SAML | OIDC | EXTERNAL */
63
+ type: string;
64
+ isDefault: boolean | null;
65
+ }
66
+ interface ProviderLoginInput {
67
+ providerKey: string;
68
+ email?: string;
69
+ returnTo?: string;
70
+ /** Override the provider's default redirect_uri (must be allow-listed). */
71
+ redirectUri?: string;
72
+ }
73
+ interface ProviderCallbackInput {
74
+ providerKey: string;
75
+ code?: string;
76
+ state?: string;
77
+ email?: string;
78
+ idToken?: string;
79
+ }
80
+ interface ProviderOTPVerifyInput {
81
+ providerKey: string;
82
+ email: string;
83
+ otp: string;
84
+ }
85
+ declare const CustomerLoginDocument: TypedDocumentNode<{
86
+ customerLogin: CustomerAuthResult;
87
+ }, {
88
+ email: string;
89
+ password: string;
90
+ }>;
91
+ declare const CustomerRegisterDocument: TypedDocumentNode<{
92
+ customerRegister: CustomerAuthResult;
93
+ }, {
94
+ email: string;
95
+ password: string;
96
+ }>;
97
+ declare const CustomerRefreshTokenDocument: TypedDocumentNode<{
98
+ customerRefreshToken: CustomerAuthResult;
99
+ }, {
100
+ refreshToken: string;
101
+ }>;
102
+ declare const CustomerRequestOTPDocument: TypedDocumentNode<{
103
+ customerRequestOTP: boolean;
104
+ }, {
105
+ email: string;
106
+ }>;
107
+ declare const CustomerLoginOTPDocument: TypedDocumentNode<{
108
+ customerLoginOTP: CustomerAuthResult;
109
+ }, {
110
+ email: string;
111
+ otp: string;
112
+ }>;
113
+ declare const CustomerLoginWithProviderDocument: TypedDocumentNode<{
114
+ customerLoginWithProvider: ProviderLoginResult;
115
+ }, {
116
+ input: ProviderLoginInput;
117
+ }>;
118
+ declare const CustomerProviderCallbackDocument: TypedDocumentNode<{
119
+ customerProviderCallback: CustomerAuthResult;
120
+ }, {
121
+ input: ProviderCallbackInput;
122
+ }>;
123
+ declare const CustomerProviderVerifyOTPDocument: TypedDocumentNode<{
124
+ customerProviderVerifyOTP: CustomerAuthResult;
125
+ }, {
126
+ input: ProviderOTPVerifyInput;
127
+ }>;
128
+ declare const AuthProvidersDocument: TypedDocumentNode<{
129
+ authProviders: AuthProviderPublic[];
130
+ }, Record<string, never>>;
131
+
132
+ /**
133
+ * Foir customer auth — framework-agnostic flows, token lifecycle, and guards.
134
+ *
135
+ * Session I/O is injected via {@link SessionAdapter} (no Hydrogen/React-Router
136
+ * coupling). Pre-login flows authenticate with the project `x-api-key`;
137
+ * post-login reads add the customer bearer. The project-specific profile (e.g.
138
+ * `isAdmin`) is resolved by a consumer-supplied `resolveUser` — the SDK never
139
+ * hardcodes the per-project customer profile shape.
140
+ */
141
+
142
+ /** An access token + its rotating refresh token. */
143
+ interface FoirAuthTokens {
144
+ token: string;
145
+ refreshToken: string;
146
+ }
147
+ /** The customer identity carried by an auth result. */
148
+ interface FoirCustomer {
149
+ id: string;
150
+ email: string;
151
+ }
152
+ /** The resolved customer for a guarded surface. */
153
+ interface FoirUser {
154
+ id: string;
155
+ email: string;
156
+ isAdmin: boolean;
157
+ }
158
+ /**
159
+ * Injected session store. Methods may be sync or async so consumers can plug
160
+ * in a cookie session (Hydrogen/Remix), a KV store, etc.
161
+ */
162
+ interface SessionAdapter {
163
+ get(key: string): string | null | undefined | Promise<string | null | undefined>;
164
+ set(key: string, value: string): void | Promise<void>;
165
+ unset(key: string): void | Promise<void>;
166
+ }
167
+ /** Login with email + password. Requires the project to have passwords enabled. */
168
+ declare function customerLogin(env: FoirEnv, vars: {
169
+ email: string;
170
+ password: string;
171
+ }): Promise<CustomerAuthResult>;
172
+ /** Register a new customer with email + password. */
173
+ declare function customerRegister(env: FoirEnv, vars: {
174
+ email: string;
175
+ password: string;
176
+ }): Promise<CustomerAuthResult>;
177
+ /** Request a one-time password be emailed to the customer. */
178
+ declare function customerRequestOTP(env: FoirEnv, email: string): Promise<boolean>;
179
+ /** Complete an OTP login. */
180
+ declare function customerLoginOTP(env: FoirEnv, vars: {
181
+ email: string;
182
+ otp: string;
183
+ }): Promise<CustomerAuthResult>;
184
+ /** Rotate an access token using a refresh token. */
185
+ declare function customerRefreshToken(env: FoirEnv, refreshToken: string): Promise<CustomerAuthResult>;
186
+ /**
187
+ * Initiate provider (OAuth/OIDC) login — returns a `redirectUrl` to send the
188
+ * user to, or an inline `token` for non-redirect providers.
189
+ */
190
+ declare function customerLoginWithProvider(env: FoirEnv, input: ProviderLoginInput): Promise<ProviderLoginResult>;
191
+ /** Complete a provider login from the OAuth callback (`code`/`state`). */
192
+ declare function customerProviderCallback(env: FoirEnv, input: ProviderCallbackInput): Promise<CustomerAuthResult>;
193
+ /** Verify an OTP issued by an external provider. */
194
+ declare function customerProviderVerifyOTP(env: FoirEnv, input: ProviderOTPVerifyInput): Promise<CustomerAuthResult>;
195
+ /** List the project's configured auth providers (for a login UI). */
196
+ declare function getAuthProviders(env: FoirEnv): Promise<AuthProviderPublic[]>;
197
+ /** Persist an auth result to the session; returns the customer identity. */
198
+ declare function saveSession(adapter: SessionAdapter, result: CustomerAuthResult): Promise<FoirCustomer>;
199
+ /** Clear all Foir auth keys from the session. */
200
+ declare function clearSession(adapter: SessionAdapter): Promise<void>;
201
+ /** Read the stored access token (null when signed out). */
202
+ declare function getSessionToken(adapter: SessionAdapter): Promise<string | null>;
203
+ /** Read the stored customer identity (null when signed out). */
204
+ declare function getSessionCustomer(adapter: SessionAdapter): Promise<FoirCustomer | null>;
205
+ /** Login with email + password and persist the session. */
206
+ declare function loginWithPassword(adapter: SessionAdapter, env: FoirEnv, vars: {
207
+ email: string;
208
+ password: string;
209
+ }): Promise<FoirCustomer>;
210
+ /** Register with email + password and persist the session. */
211
+ declare function registerWithPassword(adapter: SessionAdapter, env: FoirEnv, vars: {
212
+ email: string;
213
+ password: string;
214
+ }): Promise<FoirCustomer>;
215
+ /** Complete an OTP login and persist the session. */
216
+ declare function loginWithOTP(adapter: SessionAdapter, env: FoirEnv, vars: {
217
+ email: string;
218
+ otp: string;
219
+ }): Promise<FoirCustomer>;
220
+ /**
221
+ * Refresh the access token from the stored refresh token, persisting the
222
+ * rotated pair. Returns the new tokens, or null when there's no refresh token
223
+ * or the refresh fails (the session is cleared in that case).
224
+ */
225
+ declare function refreshSession(adapter: SessionAdapter, env: FoirEnv): Promise<FoirAuthTokens | null>;
226
+ /**
227
+ * Run a bearer-authed GraphQL operation with refresh-with-retry: uses the
228
+ * stored access token, and on a 401-shaped failure (rejected/expired bearer)
229
+ * attempts a single token refresh and retries once. Any other error — network
230
+ * blips, server errors, validation — propagates without a retry so transient
231
+ * mutation failures can't be duplicated. Throws the original error if there's
232
+ * no refresh path.
233
+ */
234
+ declare function authedRequest<TData, TVariables>(adapter: SessionAdapter, env: FoirEnv, document: TypedDocumentNode<TData, TVariables>, variables?: TVariables): Promise<TData>;
235
+ interface RequireAuthOptions {
236
+ /**
237
+ * Resolve the full {@link FoirUser} (including the project-specific
238
+ * `isAdmin`) from the session identity. Typically runs the project's
239
+ * `myProfile` query via {@link authedRequest}. Defaults to `isAdmin: false`.
240
+ */
241
+ resolveUser?: (ctx: {
242
+ token: string;
243
+ customer: FoirCustomer;
244
+ }) => FoirUser | Promise<FoirUser>;
245
+ /**
246
+ * Called when there's no valid session — throw your framework's redirect
247
+ * (e.g. `throw redirect(loginUrl)`). Must not return.
248
+ */
249
+ onUnauthenticated: () => never | Promise<never>;
250
+ }
251
+ /**
252
+ * Require a signed-in customer. Resolves the {@link FoirUser} via
253
+ * `resolveUser` (or a minimal identity), or invokes `onUnauthenticated` when
254
+ * there's no session.
255
+ */
256
+ declare function requireAuth(adapter: SessionAdapter, options: RequireAuthOptions): Promise<FoirUser>;
257
+ interface RequireAdminOptions extends RequireAuthOptions {
258
+ /** Called when the user is authenticated but not an admin. Must not return. */
259
+ onForbidden: () => never | Promise<never>;
260
+ }
261
+ /** Require a signed-in customer who is also an admin. */
262
+ declare function requireAdmin(adapter: SessionAdapter, options: RequireAdminOptions): Promise<FoirUser>;
263
+
264
+ /**
265
+ * Hosted-login bridge — for storefronts that delegate sign-in to a Foir
266
+ * white-label front door (the `customerLoginWithFoir` flow) instead of
267
+ * rendering their own login UI.
268
+ *
269
+ * The front door sets a first-party session cookie on the registrable domain;
270
+ * because that domain is shared, the cookie reaches the storefront, which
271
+ * exchanges it server-side for a short-lived customer bearer + identity. The
272
+ * cookie is the durable credential (re-exchanged on bearer expiry) — there's
273
+ * no separate refresh token in this flow.
274
+ *
275
+ * Every URL/cookie/path is CONFIG, not a constant, so a domain restructure
276
+ * is a config change, not a code change.
277
+ */
278
+ interface HostedLoginConfig {
279
+ /** White-label front-door origin, e.g. `https://login.example.com`. */
280
+ loginOrigin: string;
281
+ /** Session cookie name set on the registrable domain. Default `foir_customer_session`. */
282
+ cookieName?: string;
283
+ /** Session-exchange path on the front door. Default `/session/token`. */
284
+ sessionPath?: string;
285
+ }
286
+ /**
287
+ * Build the redirect URL to the hosted front door, forwarding a best-effort
288
+ * absolute `returnTo` (honoured if the front door supports it).
289
+ */
290
+ declare function hostedLoginUrl(config: HostedLoginConfig, returnTo?: string): string;
291
+ /** The identity + bearer returned by a successful hosted-session exchange. */
292
+ interface HostedSession {
293
+ token: string;
294
+ customerId: string;
295
+ email: string;
296
+ }
297
+ /**
298
+ * Exchange the hosted-login cookie on an incoming request for a customer
299
+ * bearer + identity (server-to-server: forwards the cookie to the front
300
+ * door's session endpoint with the CSRF header). Returns null when there's no
301
+ * cookie or it's expired/revoked.
302
+ */
303
+ declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<HostedSession | null>;
304
+
305
+ export { type AuthProviderPublic, AuthProvidersDocument, type CustomerAuthResult, CustomerLoginDocument, CustomerLoginOTPDocument, CustomerLoginWithProviderDocument, CustomerProviderCallbackDocument, CustomerProviderVerifyOTPDocument, CustomerRefreshTokenDocument, CustomerRegisterDocument, CustomerRequestOTPDocument, type FoirAuthTokens, type FoirCustomer, type FoirUser, type HostedLoginConfig, type HostedSession, type ProviderCallbackInput, type ProviderLoginInput, type ProviderLoginResult, type ProviderOTPVerifyInput, type RequireAdminOptions, type RequireAuthOptions, type SessionAdapter, authedRequest, clearSession, customerLogin, customerLoginOTP, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, customerRefreshToken, customerRegister, customerRequestOTP, fetchHostedSession, getAuthProviders, getSessionCustomer, getSessionToken, hostedLoginUrl, loginWithOTP, loginWithPassword, refreshSession, registerWithPassword, requireAdmin, requireAuth, saveSession };