@foir/sdk 0.21.0 → 0.23.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 +135 -1
- package/README.md +15 -15
- package/dist/app/client.d.ts +93 -86
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +14 -13
- package/dist/app/dispatch.d.ts +5 -5
- package/dist/app/errors.d.ts +17 -15
- package/dist/app/index.d.ts +4 -4
- package/dist/app/naming.d.ts +1 -1
- package/dist/app/ticket.d.ts +2 -2
- package/dist/app/vocabulary.generated.d.ts +2 -2
- package/dist/app-contract.js +3 -3
- package/dist/app.js +92 -90
- package/dist/auth/access.d.ts +19 -19
- package/dist/auth/auth.d.ts +251 -117
- package/dist/auth/grant-table.generated.d.ts +5 -4
- package/dist/auth/hosted.d.ts +38 -14
- package/dist/auth/index.d.ts +9 -9
- package/dist/auth/operations.d.ts +153 -33
- package/dist/auth/ops.d.ts +39 -21
- package/dist/auth/session-client.d.ts +3 -3
- package/dist/auth-session.js +3 -3
- package/dist/auth.js +400 -250
- package/dist/client.js +5 -8
- package/dist/codegen/preset.d.ts +1 -1
- package/dist/data/builtins.d.ts +26 -26
- package/dist/data/client.d.ts +3 -3
- package/dist/data/decimal.d.ts +1 -1
- package/dist/data/index.d.ts +1 -1
- package/dist/data/manifest.d.ts +1 -1
- package/dist/data/schema.d.ts +2 -2
- package/dist/data/transport.d.ts +14 -13
- package/dist/data/ws-transport.d.ts +1 -1
- package/dist/data.js +8 -11
- package/dist/graphql/content-client.d.ts +13 -13
- package/dist/index.js +1 -4
- package/dist/live/drift.d.ts +1 -1
- package/dist/media/screen-capture.d.ts +1 -1
- package/dist/next/index.d.ts +2 -2
- package/dist/next/session.d.ts +10 -10
- package/dist/next.js +78 -60
- package/dist/placement/index.d.ts +13 -13
- package/dist/placement.js +127 -116
- package/dist/push.js +62 -44
- package/dist/react/FoirFeedback.d.ts +2 -2
- package/dist/react/FoirProvider.d.ts +24 -24
- package/dist/react/SignIn.d.ts +4 -4
- package/dist/react/UserButton.d.ts +1 -1
- package/dist/react/feedback-notes.d.ts +8 -6
- package/dist/react/index.d.ts +3 -3
- package/dist/react.js +238 -209
- package/dist/realtime/index.d.ts +1 -1
- package/dist/server/callback-client.d.ts +4 -4
- package/dist/server/events-client.d.ts +21 -21
- package/dist/server/operation-types.d.ts +8 -8
- package/dist/server/require-scoped-token.d.ts +3 -3
- package/dist/server/types.d.ts +2 -2
- package/dist/server/verify-scoped-token.d.ts +3 -3
- package/dist/server.js +130 -126
- package/dist/transport/foir-http.d.ts +14 -14
- package/dist/transport/key-kind.d.ts +1 -12
- package/dist/types/index.d.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,140 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
|
|
|
108
108
|
|
|
109
109
|
Changes on `dev` that have not been published to npm.
|
|
110
110
|
|
|
111
|
+
## [0.23.0] — 2026-10-16
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
|
|
115
|
+
- **`signInRefusalOf(error)`** (`@foir/sdk/auth`) reads why a sign-in your app's
|
|
116
|
+
own form sent was refused, as a stable code: `password_sign_in_off` from
|
|
117
|
+
`login` or `register` where the project, or the app the sign-in came through,
|
|
118
|
+
takes no password, and `code_sign_in_off` from `requestOTP` or `loginWithOTP`
|
|
119
|
+
where it takes no code. Null for any other failure.
|
|
120
|
+
- **`hostedSignInNoticeOf` reads what the hosted login sent back** (`@foir/sdk/auth`).
|
|
121
|
+
A refused sign-in's `foir_error` code, or the provider a sign-in connected to
|
|
122
|
+
the account (`foir_connected`), each with a sentence your app can show as it
|
|
123
|
+
is. A code it has no words for reads as a general sentence, never the code.
|
|
124
|
+
- **`myProviderToken` says what it did about the token** (`@foir/sdk/auth`).
|
|
125
|
+
`refresh` is `REFRESHED`, `FAILED` (with `refreshFailure` saying where),
|
|
126
|
+
`HELD_BACK` or `NOT_ATTEMPTED`; `reauthenticate` is true when only signing in
|
|
127
|
+
with the provider again mends the connection; and `linked: false` answers a
|
|
128
|
+
signed-in user with nothing stored for the provider. Pass `forceRefresh`
|
|
129
|
+
after the provider refused the token it was given. A token whose provider
|
|
130
|
+
stated no expiry is now renewed when read, and two reads at once renew it
|
|
131
|
+
once. New type: `ProviderTokenRefresh`.
|
|
132
|
+
- **`requestPasswordReset` takes a `clientId`** (`@foir/sdk/auth`). Where the
|
|
133
|
+
project has no App base URL, the reset email links to Foir's hosted reset
|
|
134
|
+
page, and naming your app sends the user to its initiate login URL to sign in
|
|
135
|
+
once the password is changed.
|
|
136
|
+
- **`registerOrganization` takes an `appBaseUrl`** (`@foir/sdk/auth`), so an
|
|
137
|
+
organization a user founds has its address from the first request and an app
|
|
138
|
+
that finds its organization by `orgUrl` finds this one at once. The address
|
|
139
|
+
sits under the project's own app address or a domain it verified, and one
|
|
140
|
+
another organization holds is refused.
|
|
141
|
+
|
|
142
|
+
### Changed
|
|
143
|
+
|
|
144
|
+
- **Breaking: `myOrganizations` pages from an entry's `cursor`** (`@foir/sdk/auth`).
|
|
145
|
+
Each `MyOrganization` it lists carries `cursor`; pass the last one's as
|
|
146
|
+
`after` for the next page. `after` no longer takes an organization's id. A
|
|
147
|
+
user whose role holds `organizations:read` in the project is listed every
|
|
148
|
+
organization of it, suspended ones included.
|
|
149
|
+
|
|
150
|
+
- **`ProviderToken.accessToken` may be `null`**, when `linked` is false.
|
|
151
|
+
- **`holds` reads a grant that names one field** (`@foir/sdk/auth`), as
|
|
152
|
+
`records:read:<model>.<field>` or `records:write:<model>.<field>`, the way the
|
|
153
|
+
platform does. A guarded field is covered by the grant that names it and by
|
|
154
|
+
one over every owner's records of every model, and not by its model's grant,
|
|
155
|
+
so `holds(['records:read'], 'records:read:order.taxId')` is `false`.
|
|
156
|
+
- **Breaking: a key in an older format is refused.** Foir answers `401` to a
|
|
157
|
+
key that begins `pk_`, `sk_` or `tc_` and says it no longer reads that format,
|
|
158
|
+
and the SDK reads a kind only from `foir_pub_`, `foir_srv_` and `foir_adm_`.
|
|
159
|
+
Make a key of its kind in its place: see
|
|
160
|
+
[Credentials](https://docs.foir.dev/credentials).
|
|
161
|
+
|
|
162
|
+
## [0.22.0] — 2026-10-15
|
|
163
|
+
|
|
164
|
+
### Added
|
|
165
|
+
|
|
166
|
+
- **A signed-in user manages their own account** (`@foir/sdk/auth`), in any
|
|
167
|
+
pool. `updateMyProfile` changes their name, picture or language and asks no
|
|
168
|
+
permission. `mySessions`, `endMySession` and `signOutEverywhere` list and end
|
|
169
|
+
their sessions. `createPersonalAccessToken` makes a token that begins
|
|
170
|
+
`foir_pat_`, answered once (`NewPersonalAccessToken`). `mySignInMethods`, `connectSignInMethod` and
|
|
171
|
+
`disconnectSignInMethod` list, add and remove the ways they sign in.
|
|
172
|
+
`myAuthorizedApps` and `revokeAuthorizedApp` list the apps they have let in
|
|
173
|
+
and take one back. New types: `MyProfile`, `MyProfileChange`, `MySession`,
|
|
174
|
+
`MySignInMethod`, `AuthorizedApp`.
|
|
175
|
+
- **Second factors and recovery codes** (`@foir/sdk/auth`). `myFactors` lists
|
|
176
|
+
what a user holds. `beginStepUp` and `completeStepUp` prove a factor and
|
|
177
|
+
answer an elevated handle for one change in the next ten minutes, which
|
|
178
|
+
`beginFactorEnrollment`, `completeFactorEnrollment`, `removeFactor` and
|
|
179
|
+
`makeRecoveryCodes` take. A factor is `totp`, `webauthn` or `passkey`. New
|
|
180
|
+
types: `MyFactor`, `FactorEnrollment`, `StepUp`.
|
|
181
|
+
- **An organisation that signs its own people in** (`@foir/sdk/app`).
|
|
182
|
+
`organizations.provision({ ..., ownPool: true })` starts a pool of its own
|
|
183
|
+
with the organisation, apart from its parent's and the project's; its
|
|
184
|
+
descendants share it unless one starts another.
|
|
185
|
+
- All of the above need the platform release that serves them.
|
|
186
|
+
|
|
187
|
+
### Changed
|
|
188
|
+
|
|
189
|
+
- **Breaking: everyone who signs in is a user, and nothing says customer or
|
|
190
|
+
admin as a kind.** It needs the platform release that serves the same
|
|
191
|
+
names; an earlier platform refuses every call below by name.
|
|
192
|
+
- `@foir/sdk/auth`: the raw calls take the server's op names: `login`,
|
|
193
|
+
`register`, `requestOTP`, `loginWithOTP`, `refreshTokens` (the
|
|
194
|
+
`refreshToken` op), `logout`, `joinWaitlist`, `signupContext`,
|
|
195
|
+
`requestPasswordReset`, `resetPassword`, `updatePassword`, `verifyEmail`,
|
|
196
|
+
`loginWithProvider`, `providerCallback`, `providerVerifyOTP` (were
|
|
197
|
+
`customerLogin` and the rest), and `registerOrganization` founds an
|
|
198
|
+
organization over the `registerOrganization` op (was
|
|
199
|
+
`registerOperatorOrganization`). The helpers that save a session are
|
|
200
|
+
`signInWithPassword`, `signUpWithPassword` and `signInWithOTP` (were
|
|
201
|
+
`loginWithPassword`, `registerWithPassword`, `loginWithOTP`), beside
|
|
202
|
+
`signOut`. `getSessionCustomer` is `getSessionUser`, `customerAccess` is
|
|
203
|
+
`userAccess`, `CustomerAuthResult` is `AuthResult` and carries `userId`,
|
|
204
|
+
`FoirCustomer` is `FoirUser` (`{ id, email }`), and `FoirAccess.customerId`
|
|
205
|
+
is `userId`. A session is stored under `foir:userId`, so one saved by an
|
|
206
|
+
earlier version reads as signed out once.
|
|
207
|
+
- `@foir/sdk/auth`: a provider sign-in that names an address somebody
|
|
208
|
+
already holds waits for the code sent to it: `providerSignInRefusalOf`
|
|
209
|
+
reads the refusal (`link_code_sent` with its `pending`), and
|
|
210
|
+
`confirmProviderLink` and `resendProviderLinkCode` finish it.
|
|
211
|
+
- `@foir/sdk/auth`: the hosted session answers `userId`
|
|
212
|
+
(`HostedSession.userId`, `FoirSessionUser.userId`), from the platform's
|
|
213
|
+
`user_id`, and its cookie is `foir_session`.
|
|
214
|
+
- `@foir/sdk/react`: `useFoirSession()` answers `user` (was `customer`).
|
|
215
|
+
- `@foir/sdk/app`: `records.user` and `users` (were `records.customer` and
|
|
216
|
+
`customers`), over the platform's `saveUserRecord`, `userRecordByIdentity`,
|
|
217
|
+
`eraseUserRecord`, `createUser`, `user`, `users`, `userByIdentity` and
|
|
218
|
+
`userIdentitiesById`. `users.list()` answers `{ users }`. `AppUser`,
|
|
219
|
+
`AppUserList`, `AppUserSaveResult` (`userCreated`), `AppUserEraseResult`
|
|
220
|
+
(`userDeleted`, `userKeptBecause`), `AppUserEmailUnavailableReason`,
|
|
221
|
+
`appUserUnlinkedOf` and `appUserEmailUnavailableOf` replace the
|
|
222
|
+
`AppCustomer` names. A notification, a consent change and an
|
|
223
|
+
organization's first member name `userId`.
|
|
224
|
+
- `@foir/sdk/server`: an operation's context names who started the run as
|
|
225
|
+
`actorId` and the user it acts for as `userId` (was `userId` and
|
|
226
|
+
`customerId`). `ScopedTokenIngress.userId` was `customerId`.
|
|
227
|
+
- `@foir/sdk/data`: `FoirGranteeKind` is `'USER'`, a note's subject may be
|
|
228
|
+
`'user'`, `FoirUserNote` and `FoirUserNoteAttachment` replace the
|
|
229
|
+
`FoirCustomerNote` names, and `FoirAuthDowngrade.userToken` was
|
|
230
|
+
`customerToken`.
|
|
231
|
+
|
|
232
|
+
### Removed
|
|
233
|
+
|
|
234
|
+
- **Breaking: `selectOrganization` and `SelectedOrganization`**
|
|
235
|
+
(`@foir/sdk/auth`). Nothing stores where a user stands: a request names the
|
|
236
|
+
organization it acts in (`orgId` or `orgUrl`), and the platform checks they
|
|
237
|
+
stand there on that request. `myOrganizations` still lists where they may.
|
|
238
|
+
- **Breaking: `requireAdmin` and `RequireAdminOptions`, and `isAdmin` on
|
|
239
|
+
`FoirUser`** (`@foir/sdk/auth`). What a user may do is a role or a scope:
|
|
240
|
+
guard on it with `requireRole` or `requireScope`. `requireAuth` takes the
|
|
241
|
+
type `resolveUser` answers, for a project that reads its own profile.
|
|
242
|
+
|
|
243
|
+
## [0.21.0] — 2026-10-13
|
|
244
|
+
|
|
111
245
|
### Added
|
|
112
246
|
|
|
113
247
|
- **An operation provisions an organization** (`@foir/sdk/app`).
|
|
@@ -1125,7 +1259,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1125
1259
|
silent wrong answer for a thrown one that discards the answer. Nothing changes
|
|
1126
1260
|
for a caller that does not pass it.
|
|
1127
1261
|
|
|
1128
|
-
- `registerOrganization(env, { key, name }, { bearerToken })` on `@foir/sdk/auth`: a customer founds an organization of their own and becomes its first member, holding the role the project's `
|
|
1262
|
+
- `registerOrganization(env, { key, name }, { bearerToken })` on `@foir/sdk/auth`: a customer founds an organization of their own and becomes its first member, holding the role the project's `organizationSignupRole` setting names. Returns the same `MyOrganization` entry `myOrganizations` lists. Present on the wire only while the project's `organizationSignupEnabled` setting is on.
|
|
1129
1263
|
- **The operator's organization context, on `@foir/sdk/auth`** — `myOrganizations`
|
|
1130
1264
|
and `selectOrganization`, plus the types `MyOrganization` and
|
|
1131
1265
|
`SelectedOrganization`, and two entries on `AUTH_OPS`.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @foir/sdk
|
|
2
2
|
|
|
3
|
-
Official Foir SDK — client-agnostic typed GraphQL transport,
|
|
3
|
+
Official Foir SDK — client-agnostic typed GraphQL transport, sign-in for your app's users,
|
|
4
4
|
progressive media, content rendering, realtime collaboration, and editor/server
|
|
5
5
|
helpers for building apps on the [Foir](https://foir.dev) platform.
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ pnpm add yjs y-websocket # for @foir/sdk/realtime
|
|
|
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
27
|
| `@foir/sdk/data` | Typed data client over `/graphql` — `createFoirClient`, per-key-shaped ops, `include`/`omit`, `fetchFoirManifest`; prints the document for you, so still no `graphql` | **none** |
|
|
28
|
-
| `@foir/sdk/auth` | Framework-agnostic
|
|
28
|
+
| `@foir/sdk/auth` | Framework-agnostic sign-in for your app's users: login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `createFoirSessionClient` (Foir-hosted cookie sessions), `authedRequest`, guards | `graphql` |
|
|
29
29
|
| `@foir/sdk/react` | Drop-in React auth — `<FoirProvider>` (token or `hostedSession` mode), `useFoirSession`/`useFoirUser`/`useFoirAuth`, `<SignIn>`, `<OAuthButtons>`, `<UserButton>` | `react`, `graphql` |
|
|
30
30
|
| `@foir/sdk/next` | Next.js App Router BFF auth — `createFoirCookieAdapter`, `getFoirSession`, `foirSignIn*`, `completeFoirProviderLogin`, `createFoirMiddleware` | `graphql` |
|
|
31
31
|
| `@foir/sdk/server` | Server-side helpers for extension/config API backends — `createServerClient`, `verifyWebhookSignature`, `verifyScopedToken`, callback client | — |
|
|
@@ -104,11 +104,11 @@ present — there is no partial-`select` footgun. `include` adds a relation key,
|
|
|
104
104
|
`__typename` that narrow under an exhaustive `switch` with no casts.
|
|
105
105
|
|
|
106
106
|
**The client is shaped by your credential.** The generated `FoirSchema` contains
|
|
107
|
-
only what that key's manifest lists, so a read-only
|
|
107
|
+
only what that key's manifest lists, so a read-only publishable key has no `create` to
|
|
108
108
|
call:
|
|
109
109
|
|
|
110
110
|
```ts
|
|
111
|
-
foir.post.create({ data }); // ✗ compile error under a
|
|
111
|
+
foir.post.create({ data }); // ✗ compile error under a publishable key: not in its manifest
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
`generated` is optional. Without it — and without `selections` — the client
|
|
@@ -119,14 +119,14 @@ that way; silence it with `announceManifestFallback: false`.
|
|
|
119
119
|
|
|
120
120
|
Reads that can be shared are sent to `GET /cdn/graphql` with only the
|
|
121
121
|
persisted-query hash in the URL, so a CDN in front of you can serve the next
|
|
122
|
-
visitor without reaching origin. That is a query, on a
|
|
123
|
-
|
|
122
|
+
visitor without reaching origin. That is a query, on a publishable key, with no
|
|
123
|
+
user bearer, on the published channel; everything else is a `POST /graphql`.
|
|
124
124
|
|
|
125
125
|
`foir.raw()` is the escape hatch, and it takes a **GraphQL document** — it
|
|
126
126
|
reaches anything the credential can, including root fields no model or namespace
|
|
127
127
|
claims. It returns the whole `data` object.
|
|
128
128
|
|
|
129
|
-
Namespaces (`search`, `
|
|
129
|
+
Namespaces (`search`, `users`, `notifications`) live under `foir.ns` because
|
|
130
130
|
the client carries no runtime schema to tell a namespace from a model. `ns` and
|
|
131
131
|
`raw` are the only reserved names.
|
|
132
132
|
|
|
@@ -144,7 +144,7 @@ const foir = createFoirClient({
|
|
|
144
144
|
|
|
145
145
|
Already have `PLATFORM_API_URL` / `PLATFORM_API_KEY` / `FOIR_PREVIEW` wired for
|
|
146
146
|
the GraphQL path? `createFoirClientFromEnv(env, { generated })` reuses them,
|
|
147
|
-
including the preview
|
|
147
|
+
including the preview key and the draft schema channel.
|
|
148
148
|
|
|
149
149
|
A product built on organizations names the one a request acts in with `orgId`,
|
|
150
150
|
which the client sends as `X-Org-Id` on every request. This is for the PROJECT
|
|
@@ -178,18 +178,18 @@ export async function loader({ context }: LoaderArgs) {
|
|
|
178
178
|
}
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
###
|
|
181
|
+
### Signing your users in
|
|
182
182
|
|
|
183
183
|
```ts
|
|
184
184
|
import {
|
|
185
|
-
|
|
185
|
+
signInWithPassword,
|
|
186
186
|
authedRequest,
|
|
187
187
|
type SessionAdapter,
|
|
188
188
|
} from '@foir/sdk/auth';
|
|
189
189
|
import { MyProfileDocument } from './generated/graphql';
|
|
190
190
|
|
|
191
191
|
// SessionAdapter = { get, set, delete } over your session store (cookie, KV…).
|
|
192
|
-
const
|
|
192
|
+
const user = await signInWithPassword(adapter, env, { email, password });
|
|
193
193
|
|
|
194
194
|
// Bearer-authed request with automatic refresh-and-retry on expired sessions.
|
|
195
195
|
const { myProfile } = await authedRequest(adapter, env, MyProfileDocument);
|
|
@@ -207,7 +207,7 @@ import { FoirProvider, SignIn, UserButton, useFoirSession } from '@foir/sdk/reac
|
|
|
207
207
|
|
|
208
208
|
const env = {
|
|
209
209
|
PLATFORM_API_URL: process.env.NEXT_PUBLIC_PLATFORM_API_URL,
|
|
210
|
-
PLATFORM_API_KEY: process.env.NEXT_PUBLIC_PLATFORM_API_KEY, // a publishable
|
|
210
|
+
PLATFORM_API_KEY: process.env.NEXT_PUBLIC_PLATFORM_API_KEY, // a publishable key
|
|
211
211
|
};
|
|
212
212
|
|
|
213
213
|
export function App() {
|
|
@@ -241,7 +241,7 @@ backend.
|
|
|
241
241
|
|
|
242
242
|
```tsx
|
|
243
243
|
<FoirProvider
|
|
244
|
-
env={{ PLATFORM_API_URL: 'https://api.foir.dev/graphql', PLATFORM_API_KEY: '
|
|
244
|
+
env={{ PLATFORM_API_URL: 'https://api.foir.dev/graphql', PLATFORM_API_KEY: 'foir_pub_…' }}
|
|
245
245
|
hostedSession={{ loginOrigin: 'https://login.acme.com' }}
|
|
246
246
|
>
|
|
247
247
|
<App />
|
|
@@ -282,8 +282,8 @@ import { cookies } from 'next/headers';
|
|
|
282
282
|
import { getFoirSession } from '@foir/sdk/next';
|
|
283
283
|
|
|
284
284
|
export default async function Dashboard() {
|
|
285
|
-
const
|
|
286
|
-
return <p>Signed in as {
|
|
285
|
+
const user = await getFoirSession(await cookies());
|
|
286
|
+
return <p>Signed in as {user?.email}</p>;
|
|
287
287
|
}
|
|
288
288
|
```
|
|
289
289
|
|