@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +135 -1
  2. package/README.md +15 -15
  3. package/dist/app/client.d.ts +93 -86
  4. package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +14 -13
  5. package/dist/app/dispatch.d.ts +5 -5
  6. package/dist/app/errors.d.ts +17 -15
  7. package/dist/app/index.d.ts +4 -4
  8. package/dist/app/naming.d.ts +1 -1
  9. package/dist/app/ticket.d.ts +2 -2
  10. package/dist/app/vocabulary.generated.d.ts +2 -2
  11. package/dist/app-contract.js +3 -3
  12. package/dist/app.js +92 -90
  13. package/dist/auth/access.d.ts +19 -19
  14. package/dist/auth/auth.d.ts +251 -117
  15. package/dist/auth/grant-table.generated.d.ts +5 -4
  16. package/dist/auth/hosted.d.ts +38 -14
  17. package/dist/auth/index.d.ts +9 -9
  18. package/dist/auth/operations.d.ts +153 -33
  19. package/dist/auth/ops.d.ts +39 -21
  20. package/dist/auth/session-client.d.ts +3 -3
  21. package/dist/auth-session.js +3 -3
  22. package/dist/auth.js +400 -250
  23. package/dist/client.js +5 -8
  24. package/dist/codegen/preset.d.ts +1 -1
  25. package/dist/data/builtins.d.ts +26 -26
  26. package/dist/data/client.d.ts +3 -3
  27. package/dist/data/decimal.d.ts +1 -1
  28. package/dist/data/index.d.ts +1 -1
  29. package/dist/data/manifest.d.ts +1 -1
  30. package/dist/data/schema.d.ts +2 -2
  31. package/dist/data/transport.d.ts +14 -13
  32. package/dist/data/ws-transport.d.ts +1 -1
  33. package/dist/data.js +8 -11
  34. package/dist/graphql/content-client.d.ts +13 -13
  35. package/dist/index.js +1 -4
  36. package/dist/live/drift.d.ts +1 -1
  37. package/dist/media/screen-capture.d.ts +1 -1
  38. package/dist/next/index.d.ts +2 -2
  39. package/dist/next/session.d.ts +10 -10
  40. package/dist/next.js +78 -60
  41. package/dist/placement/index.d.ts +13 -13
  42. package/dist/placement.js +127 -116
  43. package/dist/push.js +62 -44
  44. package/dist/react/FoirFeedback.d.ts +2 -2
  45. package/dist/react/FoirProvider.d.ts +24 -24
  46. package/dist/react/SignIn.d.ts +4 -4
  47. package/dist/react/UserButton.d.ts +1 -1
  48. package/dist/react/feedback-notes.d.ts +8 -6
  49. package/dist/react/index.d.ts +3 -3
  50. package/dist/react.js +238 -209
  51. package/dist/realtime/index.d.ts +1 -1
  52. package/dist/server/callback-client.d.ts +4 -4
  53. package/dist/server/events-client.d.ts +21 -21
  54. package/dist/server/operation-types.d.ts +8 -8
  55. package/dist/server/require-scoped-token.d.ts +3 -3
  56. package/dist/server/types.d.ts +2 -2
  57. package/dist/server/verify-scoped-token.d.ts +3 -3
  58. package/dist/server.js +130 -126
  59. package/dist/transport/foir-http.d.ts +14 -14
  60. package/dist/transport/key-kind.d.ts +1 -12
  61. package/dist/types/index.d.ts +2 -2
  62. 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 `operatorSignupRole` setting names. Returns the same `MyOrganization` entry `myOrganizations` lists. Present on the wire only while the project's `operatorSignupEnabled` setting is on.
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, customer auth,
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 customer auth — login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `createFoirSessionClient` (Foir-hosted cookie sessions), `authedRequest`, guards | `graphql` |
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 `pk_` key has no `create` to
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 pk_ key — not in its manifest
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 `pk_` key, with no
123
- customer bearer, on the published channel; everything else is a `POST /graphql`.
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`, `customers`, `notifications`) live under `foir.ns` because
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 `sk_` key and the draft schema channel.
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
- ### Customer auth
181
+ ### Signing your users in
182
182
 
183
183
  ```ts
184
184
  import {
185
- loginWithPassword,
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 customer = await loginWithPassword(adapter, env, { email, password });
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 pk_ key
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: 'pk_…' }}
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 customer = await getFoirSession(await cookies());
286
- return <p>Signed in as {customer?.email}</p>;
285
+ const user = await getFoirSession(await cookies());
286
+ return <p>Signed in as {user?.email}</p>;
287
287
  }
288
288
  ```
289
289