@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.
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 +281 -37
  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 -135
  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
package/CHANGELOG.md ADDED
@@ -0,0 +1,325 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@foir/sdk`.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ Versioning follows the policy below.
7
+
8
+ ---
9
+
10
+ ## Semver policy
11
+
12
+ `@foir/sdk` is published to npm and installed by people outside this repo. Its
13
+ **public API is the set of named exports reachable through the subpaths in
14
+ `package.json#exports`** — nothing else. Anything reachable only by deep-importing
15
+ `@foir/sdk/dist/...` is private and may change in any release without notice.
16
+
17
+ That surface is pinned in [`public-exports.snapshot.json`](./public-exports.snapshot.json)
18
+ and enforced on every build by `scripts/assert-public-exports.mjs`. If a change
19
+ reds that guard, it belongs in this file.
20
+
21
+ ### Pre-1.0 note — breaking changes bump the MINOR
22
+
23
+ While the version is `0.x`, a breaking change bumps the **minor** (`0.5.0` →
24
+ `0.6.0`), not the major. This is not laziness about semver, it is what actually
25
+ protects consumers: npm resolves `^0.5.0` to `>=0.5.0 <0.6.0`, so for a `0.x`
26
+ package the minor — not the major — is the boundary an existing `pnpm install`
27
+ will not cross on its own. Once `1.0.0` ships, breaking changes move to the
28
+ major and this note goes away.
29
+
30
+ Read "MAJOR" below as "minor, while we are pre-1.0".
31
+
32
+ ### MAJOR — breaking
33
+
34
+ A change is breaking if code that compiled and ran against the previous version
35
+ can stop compiling, stop resolving, or start behaving differently:
36
+
37
+ - **Removing a named export** from any published subpath.
38
+ - **Renaming a named export.** A rename is a removal plus an addition; shipping
39
+ the old name as a deprecated alias for at least one minor is preferred.
40
+ - **Removing a subpath** from `package.json#exports`, or repointing one at a
41
+ module with a different surface.
42
+ - **Demoting a value export to type-only** (`export { X }` → `export type { X }`).
43
+ The type-checker stays happy at the re-export site and the runtime import
44
+ starts throwing, which is the worst possible failure shape.
45
+ - **Narrowing a type**: making an optional parameter or property required,
46
+ removing a member from a union or an enum, tightening a parameter's accepted
47
+ type, widening a return type, or adding a required field to an options object.
48
+ Rule of thumb — narrowing what we *accept* or widening what we *return* is
49
+ breaking; the reverse is not.
50
+ - **Changing the runtime behaviour of an existing call**: different network
51
+ endpoint, different request or response shape on the wire, different default
52
+ value, throwing where it used to return, returning where it used to throw, or
53
+ a change in what gets persisted (cookie name, storage key, header name).
54
+ - **Raising the floor**: a higher minimum Node version, a higher required
55
+ version of a peer dependency, or making an optional peer dependency required.
56
+
57
+ Every breaking change needs a `### Removed` or `### Changed` entry below that
58
+ **names each affected symbol** and states the migration.
59
+
60
+ ### MINOR — additive
61
+
62
+ - A new subpath, a new named export, or a new optional parameter/property.
63
+ - Widening what an existing call accepts, or narrowing what it returns.
64
+ - Promoting a type-only export to a value export.
65
+ - A new optional peer dependency.
66
+ - Deprecating something (the deprecation itself is additive; the eventual
67
+ removal is the breaking part).
68
+
69
+ Additive changes still red the export-surface guard — deliberately, so the
70
+ snapshot cannot rot into a rubber stamp. Refresh it with
71
+ `pnpm --filter @foir/sdk run snapshot:exports` and commit the result.
72
+
73
+ ### PATCH — fixes
74
+
75
+ - A bug fix that moves behaviour **toward** what the types and docs already
76
+ promised. If a caller could reasonably have been depending on the buggy
77
+ behaviour, it is a MAJOR, not a PATCH.
78
+ - Internal refactors, build/bundling changes, dependency bumps and performance
79
+ work with no observable change to the public surface.
80
+ - Documentation, comments and JSDoc.
81
+
82
+ ### Releasing
83
+
84
+ Bump `packages/sdk/package.json#version`, move `Unreleased` into a dated
85
+ section here, then push a `sdk-v<version>` tag — the tag, not the commit,
86
+ triggers `.github/workflows/sdk-publish.yml`.
87
+
88
+ `pnpm --filter @foir/sdk release:minor` does the bump and the tag together, but
89
+ only because `packages/sdk/.npmrc` sets `tag-version-prefix=sdk-v`. Without it
90
+ `pnpm version` tags `v<version>`, which does **not** match the workflow's
91
+ `sdk-v*` trigger — the bump would land and no publish would ever run. If you
92
+ move or drop that `.npmrc`, tag by hand:
93
+
94
+ ```sh
95
+ git tag sdk-v<version> && git push origin sdk-v<version>
96
+ ```
97
+
98
+ The tag must point at a commit on `main`: `main` is what is released, and a tag
99
+ on `dev` would publish unreviewed work to npm.
100
+
101
+ ---
102
+
103
+ ## [Unreleased]
104
+
105
+ Changes on `dev` that have not been published to npm.
106
+
107
+ ---
108
+
109
+ ## [0.7.0] — 2026-08-08
110
+
111
+ Everything additive, so a MINOR per the pre-1.0 note.
112
+
113
+ Ships 0.6.0 with it. That version was cut in the changelog and in
114
+ `package.json` on 2026-07-27 but no `sdk-v0.6.0` tag was ever pushed, and the
115
+ tag is what publishes, so npm went straight from 0.5.0 to here. The 0.6.0
116
+ section below is left as written rather than folded in, so the history says what
117
+ actually happened.
118
+
119
+ ### Added
120
+
121
+ - **Failure classification on `@foir/sdk/client`** — `isFoirRateLimited`,
122
+ `isFoirUnauthenticated`, `isFoirForbidden`, `isFoirNotFound`, plus `code`,
123
+ `requestId`, `retryable` and `retryAfterMs` on `FoirRequestError`, and the
124
+ `FoirErrorCode` / `FoirErrorExtensions` types behind them.
125
+
126
+ `FoirGraphQLErrorItem` now carries `extensions`, which is what makes any of
127
+ this possible: the platform has been sending `extensions.code` on every
128
+ rejection and the SDK's error type had nowhere to put it, so the only way to
129
+ tell one failure from another was to match the message.
130
+
131
+ - **Retry, on by default, in `createFoirContentClient`** — `withFoirRetry` and
132
+ `foirRetryDelayMs` are exported for callers running their own loop.
133
+
134
+ It covers no-response, `5xx`, and **`429`**. The last one is the reason this
135
+ is in the SDK rather than in a docs snippet. A 429 sits among the 4xx statuses
136
+ that are permanent, so the rule everyone writes — retry 5xx, never retry 4xx —
137
+ excludes the one failure in that family that clears on its own. A storefront
138
+ wrote exactly that rule; when its key saturated, every read failed on the
139
+ first attempt for an hour and the site served empty pages while the API was
140
+ healthy.
141
+
142
+ A server-stated `Retry-After` wins over backoff, capped by `maxDelayMs`
143
+ (default 20s) so an hour-long window does not block a render. Opt out with
144
+ `retry: { attempts: 1 }`.
145
+
146
+ - **`onRateLimit`** on `createFoirContentClient` and on `FoirRequestOptions`,
147
+ with `readFoirRateLimit` and the `FoirRateLimitSnapshot` type. Called with the
148
+ `X-RateLimit-*` headers of every response, so an app can log that a key is
149
+ approaching its ceiling instead of finding out when it hits one.
150
+
151
+ - **New subpath `@foir/sdk/auth/session`** — `createFoirSessionClient` and its
152
+ types (`FoirSessionClient`, `FoirSessionConfig`, `FoirSessionUser`) on their
153
+ own, without the rest of `./auth`.
154
+
155
+ Purely additive: `./auth` keeps exporting all four, unchanged, and nothing is
156
+ moved or renamed. What changes is the cost of reaching them. `./auth` also
157
+ exports the customer-auth GraphQL documents, which build their
158
+ TypedDocumentNodes with a top-level `parse()` that no bundler can treat as
159
+ pure — so a browser app importing only `createFoirSessionClient` was shipping
160
+ the whole `graphql` package to get it. `src/auth/session-client.ts` imports
161
+ nothing at all, and this subpath bundles it alone.
162
+
163
+ Like `./data`, it adds **no dependency and no peer dependency**:
164
+ `dist/auth-session.js` has no imports. An app on this subpath needs `graphql`
165
+ neither installed nor bundled. Use `./auth` when you want the login /
166
+ register / OTP / provider flows too; use this one for a backend-less SPA whose
167
+ only credential is the Fóir-hosted session cookie.
168
+
169
+ - **New subpath `@foir/sdk/data`** — the typed data client over
170
+ `POST /v1/query` (TTS-874). `createFoirClient<FoirSchema>({ apiUrl, apiKey })`
171
+ gives `foir.<model>.<op>(…)`, `foir.ns.<namespace>.<op>(…)` and an untyped
172
+ `foir.raw(op)`. The client's surface is shaped by the credential: an op that
173
+ is not in that key's manifest is not on the type, so `foir.page.create(…)`
174
+ under a `pk_` key is a compile error rather than a 400.
175
+
176
+ Selection is complete-by-default — every scalar always arrives, so the model
177
+ type is unconditionally true — with `include` to opt into relations and
178
+ `omit` to drop a heavy field. `_id` is not omittable, matching the server.
179
+
180
+ **This subpath has no `graphql` dependency**: it imports neither `graphql`,
181
+ `@graphql-typed-document-node/core` nor `@graphql-codegen/*`, and adds no new
182
+ peer dependency, required or optional. `dist/data.js` has no imports at all.
183
+
184
+ New exports on `./data`: `createFoirClient`, `createFoirClientFromEnv`,
185
+ `fetchFoirManifest`, `foirDataEndpoint`, `FoirRequestError`, and the type
186
+ surface (`FoirSchemaDefinition`, `FoirModelDefinition`,
187
+ `FoirRelationDefinition`, `FoirOpDefinition`, `FoirReturnDefinition`,
188
+ `FoirNamespaceDefinition`, `FoirNamespaceOpDefinition`, `FoirFieldsShape`,
189
+ `FoirConnection`, `FoirClient`, `FoirClientCore`, `FoirClientConfig`,
190
+ `FoirModelClient`, `FoirNamespaceClient`, `FoirModelOperation`,
191
+ `FoirNamespaceOperation`, `FoirQuery`, `FoirInclude`, `FoirOmittableKey`,
192
+ `FoirRecordResult`, `FoirOpResult`, `FoirRawOp`, `FoirSchemaDrift`,
193
+ `FoirManifest` + its members, and `FoirEnv` / `FoirGraphQLErrorItem` /
194
+ `FoirTokenProvider` re-exported for `./data`-only consumers).
195
+
196
+ Additive, so a MINOR bump at release time. No existing subpath's surface
197
+ changed — the export snapshot diff for this change is one new subpath and
198
+ nothing else.
199
+
200
+ ### Changed
201
+
202
+ - Internal only, no public surface change: the `graphql`-free half of
203
+ `src/graphql/foir-request.ts` moved to `src/transport/foir-http.ts` so
204
+ `./data` and `./client` share one header builder and one `FoirRequestError`
205
+ rather than forking the transport. `@foir/sdk/client` re-exports every symbol
206
+ it exported before, unchanged, and `FoirTokenProvider` is now declared in the
207
+ shared module and re-exported from its old home.
208
+
209
+ ---
210
+
211
+ ## [0.6.0] — 2026-07-27
212
+
213
+ First release since the public surface was pinned. Everything in it is
214
+ additive, so per the pre-1.0 note this is a MINOR: `^0.5.0` will not pick it up
215
+ on its own, which is the intended behaviour for a release that adds exports.
216
+
217
+ ### Added
218
+
219
+ - `@foir/sdk/auth`: `customerSignupContext` and `customerJoinWaitlist`, plus the
220
+ `SignupContext`, `SignupMode` and `WaitlistJoinResult` types — waitlist and
221
+ restricted-signup flows for projects that are not open-registration.
222
+ - A pinned public-export surface: `public-exports.snapshot.json` plus
223
+ `scripts/assert-public-exports.mjs`, wired into `pnpm build`. Deleting a named
224
+ export previously passed the full SDK CI lane — 125 tests and the export-map
225
+ guard all green — because nothing checked the contents of the declared files,
226
+ only that they existed.
227
+ - This changelog, and the semver policy above.
228
+
229
+ ### Changed
230
+
231
+ - Type declarations are emitted by a dedicated `tsc --emitDeclarationOnly` pass
232
+ instead of `tsup`'s built-in `dts`, whose parallel rollup workers
233
+ intermittently failed to flush a subpath's `.d.ts`; consumers saw
234
+ "Could not find a declaration file for module '@foir/sdk/realtime'".
235
+ `scripts/assert-export-types.mjs` now fails the build if any file the export
236
+ map promises is missing. No change to the public surface.
237
+ - The test suite pins `NODE_ENV=test` so an inherited `NODE_ENV=production`
238
+ cannot load React's production build and break every render test. Internal.
239
+
240
+ ---
241
+
242
+ ## [0.5.0] — 2026-07-15
243
+
244
+ ### Added
245
+
246
+ - `@foir/sdk/client`: `createFoirContentClient` — forwards the customer bearer
247
+ token on content reads, so authenticated and segment-targeted content works
248
+ without hand-rolling the header.
249
+
250
+ ## [0.4.0] — 2026-07-12
251
+
252
+ ### Added
253
+
254
+ - `@foir/sdk/auth`: `createFoirSessionClient`, plus the `FoirSessionClient`,
255
+ `FoirSessionConfig` and `FoirSessionUser` types — Foir-hosted first-party
256
+ sessions for backend-less SPAs, over the `fetchHostedSession` /
257
+ `hostedLoginUrl` hosted-login bridge that already shipped in 0.1.0 (EID-516).
258
+
259
+ ## [0.3.0] — 2026-07-12
260
+
261
+ ### Added
262
+
263
+ - **New subpath `@foir/sdk/react`** — drop-in React customer auth: `FoirProvider`,
264
+ `SignIn`, `OAuthButtons`, `UserButton`, `useFoirAuth`, `useFoirSession`,
265
+ `useFoirUser` (EID-515).
266
+ - **New subpath `@foir/sdk/next`** — Next.js App Router BFF auth adapter: cookie
267
+ session store, `createFoirMiddleware`, server-side sign-in/out actions and the
268
+ provider callback handler (EID-515).
269
+
270
+ ## [0.2.0] — 2026-07-10
271
+
272
+ ### Added
273
+
274
+ - **New subpath `@foir/sdk/push`** — native Web Push (VAPID) enrolment:
275
+ `enableWebPush`, `disableWebPush`, `isWebPushSupported`.
276
+ - `@foir/sdk/server`: `requireScopedToken` — a one-line ingress guard for
277
+ operation BFF routes, over the existing `verifyScopedToken` (EID-498).
278
+
279
+ ## [0.1.0] — 2026-06-23
280
+
281
+ First public release. Ten subpaths:
282
+
283
+ - `@foir/sdk` — editor iframe client: `EditorProvider`, `useEditor`, the
284
+ host↔editor message protocol, and `createPlatformClient` (EID-88, EID-91).
285
+ - `@foir/sdk/server` — server-side platform helpers for config API backends:
286
+ `createServerClient`, scoped-token verification, `verifyWebhookSignature`.
287
+ - `@foir/sdk/client` — React-free GraphQL transport for SSR/edge (`foirRequest`).
288
+ - `@foir/sdk/media` — progressive media components and adapters (EID-94).
289
+ - `@foir/sdk/content` — `createContentRenderer` and `RichTextRenderer` with
290
+ injected typography tokens (EID-93).
291
+ - `@foir/sdk/auth` — customer auth flows, token lifecycle, hosted-login bridge
292
+ (EID-95).
293
+ - `@foir/sdk/realtime` — `createCollabSession`, a thin y-websocket helper
294
+ (EID-103, EID-104).
295
+ - `@foir/sdk/codegen`, `@foir/sdk/codegen/block-registry`,
296
+ `@foir/sdk/codegen/block-scaffold` — the `foirCodegen` preset and the
297
+ graphql-codegen plugins (EID-92).
298
+
299
+ ---
300
+
301
+ ### A note on the backfilled entries
302
+
303
+ `@foir/sdk` shipped 0.1.0 through 0.5.0 with no changelog. The five sections
304
+ above were reconstructed after the fact from sources that can be re-checked:
305
+ the `sdk-v*` git tags, the `packages/sdk/package.json#exports` map read at each
306
+ of those tags, the commits between consecutive tags that touch `packages/sdk/`,
307
+ and the npm registry's publish timestamps (`npm view @foir/sdk time`), which is
308
+ where the dates come from.
309
+
310
+ They are **headline-level, not exhaustive**. They record subpath additions and
311
+ the named feature of each release; they do not claim to list every symbol added,
312
+ because no per-symbol record was kept at the time and reconstructing one would
313
+ mean guessing. Nothing above is asserted that could not be read out of the tags
314
+ and the commit history.
315
+
316
+ `Unreleased` and every section after it are authored at the time of the change
317
+ and are exhaustive for the public surface, which is what the export-surface
318
+ guard exists to keep true.
319
+
320
+ [Unreleased]: https://github.com/foir-io/clients/compare/sdk-v0.5.0...dev
321
+ [0.5.0]: https://github.com/foir-io/clients/compare/sdk-v0.4.0...sdk-v0.5.0
322
+ [0.4.0]: https://github.com/foir-io/clients/compare/sdk-v0.3.0...sdk-v0.4.0
323
+ [0.3.0]: https://github.com/foir-io/clients/compare/sdk-v0.2.0...sdk-v0.3.0
324
+ [0.2.0]: https://github.com/foir-io/clients/compare/sdk-v0.1.0...sdk-v0.2.0
325
+ [0.1.0]: https://github.com/foir-io/clients/releases/tag/sdk-v0.1.0
package/README.md CHANGED
@@ -24,6 +24,7 @@ pnpm add yjs y-websocket # for @foir/sdk/realtime
24
24
  | --- | --- | --- |
25
25
  | `@foir/sdk` | Editor iframe client — `EditorProvider`, `useEditor`, `useAutoResize`, `createPlatformClient`, host↔editor message protocol | `react` |
26
26
  | `@foir/sdk/client` | Client-agnostic GraphQL transport for SSR/edge loaders — `foirRequest`, `isFoirPreview`, `absolutiseFoirMediaUrl` | `graphql` |
27
+ | `@foir/sdk/data` | Typed data client over `POST /v1/query` — `createFoirClient`, per-key-shaped ops, `include`/`omit`, `fetchFoirManifest` | **none** |
27
28
  | `@foir/sdk/auth` | Framework-agnostic customer auth — login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `createFoirSessionClient` (Foir-hosted cookie sessions), `authedRequest`, guards | `graphql` |
28
29
  | `@foir/sdk/react` | Drop-in React auth — `<FoirProvider>` (token or `hostedSession` mode), `useFoirSession`/`useFoirUser`/`useFoirAuth`, `<SignIn>`, `<OAuthButtons>`, `<UserButton>` | `react`, `graphql` |
29
30
  | `@foir/sdk/next` | Next.js App Router BFF auth — `createFoirCookieAdapter`, `getFoirSession`, `foirSignIn*`, `completeFoirProviderLogin`, `createFoirMiddleware` | `graphql` |
@@ -35,6 +36,68 @@ pnpm add yjs y-websocket # for @foir/sdk/realtime
35
36
 
36
37
  ## Usage
37
38
 
39
+ ### Typed data client (no GraphQL, no codegen step)
40
+
41
+ `@foir/sdk/data` is a second app-runtime transport. You write ops instead of
42
+ `.graphql` documents, and the package needs **no `graphql` dependency at all** —
43
+ no `graphql`, no `@graphql-typed-document-node/core`, no `@graphql-codegen/*`,
44
+ and no new peer. The server compiles your op to a GraphQL document, so both
45
+ transports run through the same executor, scope checks and response cache.
46
+
47
+ ```ts
48
+ import { createFoirClient } from '@foir/sdk/data';
49
+ // generated by `foir types` from GET /v1/manifest
50
+ import { type FoirSchema, foirSchemaVersion } from './foir.generated';
51
+
52
+ const foir = createFoirClient<FoirSchema>({
53
+ apiUrl: process.env.PLATFORM_API_URL!, // a bare origin or the /graphql URL
54
+ apiKey: process.env.PLATFORM_API_KEY!,
55
+ schemaVersion: foirSchemaVersion, // warns once if the server has moved on
56
+ });
57
+
58
+ const post = await foir.post.findUnique({
59
+ naturalKey: 'hello',
60
+ include: { author: true }, // relations opt in
61
+ omit: ['bodyRichtext'], // drop the heavy field
62
+ });
63
+
64
+ const posts = await foir.post.findMany({ first: 10 }); // a Relay connection
65
+ const hits = await foir.ns.search.searchRecords({ query: 'shoes' });
66
+ const other = await foir.raw({ model: 'post', op: 'findMany', args: { first: 1 } });
67
+ ```
68
+
69
+ **Scalars are always complete**, so `post.title` is typed present and *is*
70
+ present — there is no partial-`select` footgun. `include` adds a relation key,
71
+ `omit` removes a scalar, and polymorphic fields are discriminated unions on
72
+ `__typename` that narrow under an exhaustive `switch` with no casts.
73
+
74
+ **The client is shaped by your credential.** The generated `FoirSchema` contains
75
+ only what that key's manifest lists, so a read-only `pk_` key has no `create` to
76
+ call:
77
+
78
+ ```ts
79
+ foir.post.create({ data }); // ✗ compile error under a pk_ key — not in its manifest
80
+ ```
81
+
82
+ Namespaces (`search`, `customers`, `notifications`) live under `foir.ns` because
83
+ `/v1/query` takes `model` **or** `namespace` and the client carries no runtime
84
+ schema to tell them apart. `ns` and `raw` are the only reserved names.
85
+
86
+ Session-bound reads take a `bearerToken`, as a string or a function resolved per
87
+ request:
88
+
89
+ ```ts
90
+ const foir = createFoirClient<FoirSchema>({
91
+ apiUrl,
92
+ apiKey,
93
+ bearerToken: () => session.get('foir_access_token'),
94
+ });
95
+ ```
96
+
97
+ Already have `PLATFORM_API_URL` / `PLATFORM_API_KEY` / `FOIR_PREVIEW` wired for
98
+ the GraphQL path? `createFoirClientFromEnv(env)` reuses them, including the
99
+ preview `sk_` key and the draft schema channel.
100
+
38
101
  ### Typed GraphQL from a server loader
39
102
 
40
103
  `foirRequest` is a typed `fetch` wrapper, not a GraphQL client: it carries the
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Foir customer auth — framework-agnostic flows, token lifecycle, and guards.
3
+ *
4
+ * Session I/O is injected via {@link SessionAdapter} (no Hydrogen/React-Router
5
+ * coupling). Pre-login flows authenticate with the project `x-api-key`;
6
+ * post-login reads add the customer bearer. The project-specific profile (e.g.
7
+ * `isAdmin`) is resolved by a consumer-supplied `resolveUser` — the SDK never
8
+ * hardcodes the per-project customer profile shape.
9
+ */
10
+ import { type FoirEnv } from '../graphql/foir-request';
11
+ import { type CustomerAuthResult, type ProviderLoginResult, type AuthProviderPublic, type ProviderLoginInput, type ProviderCallbackInput, type ProviderOTPVerifyInput, type SignupContext, type WaitlistJoinResult } from './operations';
12
+ import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
13
+ /** An access token + its rotating refresh token. */
14
+ export interface FoirAuthTokens {
15
+ token: string;
16
+ refreshToken: string;
17
+ }
18
+ /** The customer identity carried by an auth result. */
19
+ export interface FoirCustomer {
20
+ id: string;
21
+ email: string;
22
+ }
23
+ /** The resolved customer for a guarded surface. */
24
+ export interface FoirUser {
25
+ id: string;
26
+ email: string;
27
+ isAdmin: boolean;
28
+ }
29
+ /**
30
+ * Injected session store. Methods may be sync or async so consumers can plug
31
+ * in a cookie session (Hydrogen/Remix), a KV store, etc.
32
+ */
33
+ export interface SessionAdapter {
34
+ get(key: string): string | null | undefined | Promise<string | null | undefined>;
35
+ set(key: string, value: string): void | Promise<void>;
36
+ unset(key: string): void | Promise<void>;
37
+ }
38
+ /** Login with email + password. Requires the project to have passwords enabled. */
39
+ export declare function customerLogin(env: FoirEnv, vars: {
40
+ email: string;
41
+ password: string;
42
+ }): Promise<CustomerAuthResult>;
43
+ /** Register a new customer with email + password. */
44
+ export declare function customerRegister(env: FoirEnv, vars: {
45
+ email: string;
46
+ password: string;
47
+ }): Promise<CustomerAuthResult>;
48
+ /** Request a one-time password be emailed to the customer. */
49
+ export declare function customerRequestOTP(env: FoirEnv, email: string): Promise<boolean>;
50
+ /** Complete an OTP login. */
51
+ export declare function customerLoginOTP(env: FoirEnv, vars: {
52
+ email: string;
53
+ otp: string;
54
+ }): Promise<CustomerAuthResult>;
55
+ /** Rotate an access token using a refresh token. */
56
+ export declare function customerRefreshToken(env: FoirEnv, refreshToken: string): Promise<CustomerAuthResult>;
57
+ /**
58
+ * Initiate provider (OAuth/OIDC) login — returns a `redirectUrl` to send the
59
+ * user to, or an inline `token` for non-redirect providers.
60
+ */
61
+ export declare function customerLoginWithProvider(env: FoirEnv, input: ProviderLoginInput): Promise<ProviderLoginResult>;
62
+ /** Complete a provider login from the OAuth callback (`code`/`state`). */
63
+ export declare function customerProviderCallback(env: FoirEnv, input: ProviderCallbackInput): Promise<CustomerAuthResult>;
64
+ /** Verify an OTP issued by an external provider. */
65
+ export declare function customerProviderVerifyOTP(env: FoirEnv, input: ProviderOTPVerifyInput): Promise<CustomerAuthResult>;
66
+ /** List the project's configured auth providers (for a login UI). */
67
+ export declare function getAuthProviders(env: FoirEnv): Promise<AuthProviderPublic[]>;
68
+ /**
69
+ * Read the project's customer signup mode (public/restricted/waitlist/closed)
70
+ * so a self-hosted frontend can render the right signup surface proactively.
71
+ */
72
+ export declare function customerSignupContext(env: FoirEnv): Promise<SignupContext>;
73
+ /** Join the project's signup waitlist. The project must be in waitlist mode. */
74
+ export declare function customerJoinWaitlist(env: FoirEnv, email: string): Promise<WaitlistJoinResult>;
75
+ /** Persist an auth result to the session; returns the customer identity. */
76
+ export declare function saveSession(adapter: SessionAdapter, result: CustomerAuthResult): Promise<FoirCustomer>;
77
+ /** Clear all Foir auth keys from the session. */
78
+ export declare function clearSession(adapter: SessionAdapter): Promise<void>;
79
+ /** Read the stored access token (null when signed out). */
80
+ export declare function getSessionToken(adapter: SessionAdapter): Promise<string | null>;
81
+ /** Read the stored customer identity (null when signed out). */
82
+ export declare function getSessionCustomer(adapter: SessionAdapter): Promise<FoirCustomer | null>;
83
+ /** Login with email + password and persist the session. */
84
+ export declare function loginWithPassword(adapter: SessionAdapter, env: FoirEnv, vars: {
85
+ email: string;
86
+ password: string;
87
+ }): Promise<FoirCustomer>;
88
+ /** Register with email + password and persist the session. */
89
+ export declare function registerWithPassword(adapter: SessionAdapter, env: FoirEnv, vars: {
90
+ email: string;
91
+ password: string;
92
+ }): Promise<FoirCustomer>;
93
+ /** Complete an OTP login and persist the session. */
94
+ export declare function loginWithOTP(adapter: SessionAdapter, env: FoirEnv, vars: {
95
+ email: string;
96
+ otp: string;
97
+ }): Promise<FoirCustomer>;
98
+ /**
99
+ * Refresh the access token from the stored refresh token, persisting the
100
+ * rotated pair. Returns the new tokens, or null when there's no refresh token
101
+ * or the refresh fails (the session is cleared in that case).
102
+ */
103
+ export declare function refreshSession(adapter: SessionAdapter, env: FoirEnv): Promise<FoirAuthTokens | null>;
104
+ /**
105
+ * Run a bearer-authed GraphQL operation with refresh-with-retry: uses the
106
+ * stored access token, and on a 401-shaped failure (rejected/expired bearer)
107
+ * attempts a single token refresh and retries once. Any other error — network
108
+ * blips, server errors, validation — propagates without a retry so transient
109
+ * mutation failures can't be duplicated. Throws the original error if there's
110
+ * no refresh path.
111
+ */
112
+ export declare function authedRequest<TData, TVariables>(adapter: SessionAdapter, env: FoirEnv, document: TypedDocumentNode<TData, TVariables>, variables?: TVariables): Promise<TData>;
113
+ export interface RequireAuthOptions {
114
+ /**
115
+ * Resolve the full {@link FoirUser} (including the project-specific
116
+ * `isAdmin`) from the session identity. Typically runs the project's
117
+ * `myProfile` query via {@link authedRequest}. Defaults to `isAdmin: false`.
118
+ */
119
+ resolveUser?: (ctx: {
120
+ token: string;
121
+ customer: FoirCustomer;
122
+ }) => FoirUser | Promise<FoirUser>;
123
+ /**
124
+ * Called when there's no valid session — throw your framework's redirect
125
+ * (e.g. `throw redirect(loginUrl)`). Must not return.
126
+ */
127
+ onUnauthenticated: () => never | Promise<never>;
128
+ }
129
+ /**
130
+ * Require a signed-in customer. Resolves the {@link FoirUser} via
131
+ * `resolveUser` (or a minimal identity), or invokes `onUnauthenticated` when
132
+ * there's no session.
133
+ */
134
+ export declare function requireAuth(adapter: SessionAdapter, options: RequireAuthOptions): Promise<FoirUser>;
135
+ export interface RequireAdminOptions extends RequireAuthOptions {
136
+ /** Called when the user is authenticated but not an admin. Must not return. */
137
+ onForbidden: () => never | Promise<never>;
138
+ }
139
+ /** Require a signed-in customer who is also an admin. */
140
+ export declare function requireAdmin(adapter: SessionAdapter, options: RequireAdminOptions): Promise<FoirUser>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Hosted-login bridge — for storefronts that delegate sign-in to a Foir
3
+ * white-label front door (the `customerLoginWithFoir` flow) instead of
4
+ * rendering their own login UI.
5
+ *
6
+ * The front door sets a first-party session cookie on the registrable domain;
7
+ * because that domain is shared, the cookie reaches the storefront, which
8
+ * exchanges it server-side for a short-lived customer bearer + identity. The
9
+ * cookie is the durable credential (re-exchanged on bearer expiry) — there's
10
+ * no separate refresh token in this flow.
11
+ *
12
+ * Every URL/cookie/path is CONFIG, not a constant, so a domain restructure
13
+ * is a config change, not a code change.
14
+ */
15
+ export interface HostedLoginConfig {
16
+ /** White-label front-door origin, e.g. `https://login.example.com`. */
17
+ loginOrigin: string;
18
+ /** Session cookie name set on the registrable domain. Default `foir_customer_session`. */
19
+ cookieName?: string;
20
+ /** Session-exchange path on the front door. Default `/session/token`. */
21
+ sessionPath?: string;
22
+ }
23
+ /**
24
+ * Build the redirect URL to the hosted front door, forwarding a best-effort
25
+ * absolute `returnTo` (honoured if the front door supports it).
26
+ */
27
+ export declare function hostedLoginUrl(config: HostedLoginConfig, returnTo?: string): string;
28
+ /** The identity + bearer returned by a successful hosted-session exchange. */
29
+ export interface HostedSession {
30
+ token: string;
31
+ customerId: string;
32
+ email: string;
33
+ }
34
+ /**
35
+ * Exchange the hosted-login cookie on an incoming request for a customer
36
+ * bearer + identity (server-to-server: forwards the cookie to the front
37
+ * door's session endpoint with the CSRF header). Returns null when there's no
38
+ * cookie or it's expired/revoked.
39
+ */
40
+ export declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<HostedSession | null>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `@foir/sdk/auth` — framework-agnostic Foir customer auth.
3
+ *
4
+ * Platform-stable login/register/OTP/provider/refresh flows over api-public,
5
+ * token lifecycle with an injected `SessionAdapter`, guards, and the optional
6
+ * hosted-login bridge. `graphql` is an optional peer (the operation documents).
7
+ */
8
+ export { customerLogin, customerRegister, customerRequestOTP, customerLoginOTP, customerRefreshToken, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, getAuthProviders, customerSignupContext, customerJoinWaitlist, saveSession, clearSession, getSessionToken, getSessionCustomer, loginWithPassword, registerWithPassword, loginWithOTP, refreshSession, authedRequest, requireAuth, requireAdmin, } from './auth';
9
+ export type { FoirAuthTokens, FoirCustomer, FoirUser, SessionAdapter, RequireAuthOptions, RequireAdminOptions, } from './auth';
10
+ export { hostedLoginUrl, fetchHostedSession } from './hosted';
11
+ export type { HostedLoginConfig, HostedSession } from './hosted';
12
+ export { createFoirSessionClient } from './session-client';
13
+ export type { FoirSessionClient, FoirSessionConfig, FoirSessionUser, } from './session-client';
14
+ export type { CustomerAuthResult, ProviderLoginResult, AuthProviderPublic, ProviderLoginInput, ProviderCallbackInput, ProviderOTPVerifyInput, SignupContext, SignupMode, WaitlistJoinResult, } from './operations';
15
+ export { CustomerLoginDocument, CustomerRegisterDocument, CustomerRefreshTokenDocument, CustomerRequestOTPDocument, CustomerLoginOTPDocument, CustomerLoginWithProviderDocument, CustomerProviderCallbackDocument, CustomerProviderVerifyOTPDocument, AuthProvidersDocument, } from './operations';