@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.
- 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 +290 -32
- 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 -83
- 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
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';
|