@foir/sdk 0.22.0 → 0.24.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 +230 -154
- package/README.md +6 -6
- package/dist/app/client.d.ts +5 -4
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +104 -99
- package/dist/app/contract/proto/apps/v1/discovery_pb.d.ts +10 -10
- package/dist/app/errors.d.ts +8 -6
- package/dist/auth/auth.d.ts +56 -17
- package/dist/auth/grant-table.generated.d.ts +5 -4
- package/dist/auth/hosted.d.ts +29 -0
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/operations.d.ts +34 -11
- package/dist/auth/ops.d.ts +0 -1
- package/dist/auth/session-client.d.ts +5 -0
- package/dist/auth-session.js +3 -2
- package/dist/auth.js +182 -132
- package/dist/client.js +1 -4
- package/dist/codegen/preset.d.ts +1 -1
- package/dist/components/EditorProvider.d.ts +1 -1
- package/dist/data/client.d.ts +1 -1
- package/dist/data/schema.d.ts +1 -1
- package/dist/data/transport.d.ts +7 -6
- package/dist/data.js +2 -5
- package/dist/index.js +1 -4
- package/dist/next.js +1 -5
- package/dist/placement.js +124 -116
- package/dist/push.js +1 -5
- package/dist/react/FoirProvider.d.ts +1 -1
- package/dist/react/feedback-notes.d.ts +4 -2
- package/dist/react.js +128 -123
- package/dist/server/events-client.d.ts +9 -9
- package/dist/server/types.d.ts +1 -1
- package/dist/server.js +123 -122
- package/dist/transport/foir-http.d.ts +1 -1
- package/dist/transport/key-kind.d.ts +1 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,84 @@ 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.24.0] — 2026-10-18
|
|
112
|
+
|
|
113
|
+
### Changed
|
|
114
|
+
|
|
115
|
+
- **`joinWaitlist` resolves `true` for every address** (`@foir/sdk/auth`). It
|
|
116
|
+
used to resolve the waitlist entry, whose status told anyone who asked
|
|
117
|
+
whether an address had been approved. The server now answers every address
|
|
118
|
+
alike, whether it was queued, was already let in, or already has an account,
|
|
119
|
+
and what differs is only the email the address is sent. Migration: stop
|
|
120
|
+
reading `id`, `email` or `status` from the result; a resolved call means the
|
|
121
|
+
address was not refused.
|
|
122
|
+
- **`myProviderToken` and `logout` act for one sign-in** (`@foir/sdk/auth`). A
|
|
123
|
+
provider's token belongs to the sign-in that obtained it, so `logout` ends
|
|
124
|
+
this device's and no other device's, and a sign-in with nothing kept for it
|
|
125
|
+
resolves `linked: false` with `reauthenticate: true`. Migration: where
|
|
126
|
+
`reauthenticate` is true, show the user the way to sign in through the
|
|
127
|
+
provider again rather than an empty or failed page.
|
|
128
|
+
|
|
129
|
+
### Removed
|
|
130
|
+
|
|
131
|
+
- **`WaitlistJoinResult`** (`@foir/sdk/auth`, type). `joinWaitlist` resolves a
|
|
132
|
+
boolean.
|
|
133
|
+
- **`verifyEmail`** (`@foir/sdk/auth`). It redeemed verification tokens that
|
|
134
|
+
only the console's own sign-up minted, and that lane is gone. An address is
|
|
135
|
+
proven by the code it is sent: `requestOTP`, then `loginWithOTP`, which marks
|
|
136
|
+
the address proven and lets in a person who was waiting to prove it.
|
|
137
|
+
|
|
138
|
+
## [0.23.0] — 2026-10-16
|
|
139
|
+
|
|
140
|
+
### Added
|
|
141
|
+
|
|
142
|
+
- **`signInRefusalOf(error)`** (`@foir/sdk/auth`) reads why a sign-in your app's
|
|
143
|
+
own form sent was refused, as a stable code: `password_sign_in_off` from
|
|
144
|
+
`login` or `register` where the project, or the app the sign-in came through,
|
|
145
|
+
takes no password, and `code_sign_in_off` from `requestOTP` or `loginWithOTP`
|
|
146
|
+
where it takes no code. Null for any other failure.
|
|
147
|
+
- **`hostedSignInNoticeOf` reads what the hosted login sent back** (`@foir/sdk/auth`).
|
|
148
|
+
A refused sign-in's `foir_error` code, or the provider a sign-in connected to
|
|
149
|
+
the account (`foir_connected`), each with a sentence your app can show as it
|
|
150
|
+
is. A code it has no words for reads as a general sentence, never the code.
|
|
151
|
+
- **`myProviderToken` says what it did about the token** (`@foir/sdk/auth`).
|
|
152
|
+
`refresh` is `REFRESHED`, `FAILED` (with `refreshFailure` saying where),
|
|
153
|
+
`HELD_BACK` or `NOT_ATTEMPTED`; `reauthenticate` is true when only signing in
|
|
154
|
+
with the provider again mends the connection; and `linked: false` answers a
|
|
155
|
+
signed-in user with nothing stored for the provider. Pass `forceRefresh`
|
|
156
|
+
after the provider refused the token it was given. A token whose provider
|
|
157
|
+
stated no expiry is now renewed when read, and two reads at once renew it
|
|
158
|
+
once. New type: `ProviderTokenRefresh`.
|
|
159
|
+
- **`requestPasswordReset` takes a `clientId`** (`@foir/sdk/auth`). Where the
|
|
160
|
+
project has no App base URL, the reset email links to Foir's hosted reset
|
|
161
|
+
page, and naming your app sends the user to its initiate login URL to sign in
|
|
162
|
+
once the password is changed.
|
|
163
|
+
- **`registerOrganization` takes an `appBaseUrl`** (`@foir/sdk/auth`), so an
|
|
164
|
+
organization a user founds has its address from the first request and an app
|
|
165
|
+
that finds its organization by `orgUrl` finds this one at once. The address
|
|
166
|
+
sits under the project's own app address or a domain it verified, and one
|
|
167
|
+
another organization holds is refused.
|
|
168
|
+
|
|
169
|
+
### Changed
|
|
170
|
+
|
|
171
|
+
- **Breaking: `myOrganizations` pages from an entry's `cursor`** (`@foir/sdk/auth`).
|
|
172
|
+
Each `MyOrganization` it lists carries `cursor`; pass the last one's as
|
|
173
|
+
`after` for the next page. `after` no longer takes an organization's id. A
|
|
174
|
+
user whose role holds `organizations:read` in the project is listed every
|
|
175
|
+
organization of it, suspended ones included.
|
|
176
|
+
|
|
177
|
+
- **`ProviderToken.accessToken` may be `null`**, when `linked` is false.
|
|
178
|
+
- **`holds` reads a grant that names one field** (`@foir/sdk/auth`), as
|
|
179
|
+
`records:read:<model>.<field>` or `records:write:<model>.<field>`, the way the
|
|
180
|
+
platform does. A guarded field is covered by the grant that names it and by
|
|
181
|
+
one over every owner's records of every model, and not by its model's grant,
|
|
182
|
+
so `holds(['records:read'], 'records:read:order.taxId')` is `false`.
|
|
183
|
+
- **Breaking: a key in an older format is refused.** Foir answers `401` to a
|
|
184
|
+
key that begins `pk_`, `sk_` or `tc_` and says it no longer reads that format,
|
|
185
|
+
and the SDK reads a kind only from `foir_pub_`, `foir_srv_` and `foir_adm_`.
|
|
186
|
+
Make a key of its kind in its place: see
|
|
187
|
+
[Credentials](https://docs.foir.dev/credentials).
|
|
188
|
+
|
|
111
189
|
## [0.22.0] — 2026-10-15
|
|
112
190
|
|
|
113
191
|
### Added
|
|
@@ -135,23 +213,23 @@ Changes on `dev` that have not been published to npm.
|
|
|
135
213
|
|
|
136
214
|
### Changed
|
|
137
215
|
|
|
138
|
-
- **Breaking: everyone who signs in is a user, and
|
|
139
|
-
|
|
216
|
+
- **Breaking: everyone who signs in is a user, and no name says what kind
|
|
217
|
+
of person somebody is.** It needs the platform release that serves the same
|
|
140
218
|
names; an earlier platform refuses every call below by name.
|
|
141
219
|
- `@foir/sdk/auth`: the raw calls take the server's op names: `login`,
|
|
142
220
|
`register`, `requestOTP`, `loginWithOTP`, `refreshTokens` (the
|
|
143
221
|
`refreshToken` op), `logout`, `joinWaitlist`, `signupContext`,
|
|
144
222
|
`requestPasswordReset`, `resetPassword`, `updatePassword`, `verifyEmail`,
|
|
145
|
-
`loginWithProvider`, `providerCallback`, `providerVerifyOTP
|
|
146
|
-
|
|
147
|
-
organization over the
|
|
148
|
-
`
|
|
223
|
+
`loginWithProvider`, `providerCallback`, `providerVerifyOTP`, each
|
|
224
|
+
renamed from a name that said what kind of person called it, and
|
|
225
|
+
`registerOrganization` founds an organization over the
|
|
226
|
+
`registerOrganization` op. The helpers that save a session are
|
|
149
227
|
`signInWithPassword`, `signUpWithPassword` and `signInWithOTP` (were
|
|
150
228
|
`loginWithPassword`, `registerWithPassword`, `loginWithOTP`), beside
|
|
151
|
-
`signOut`.
|
|
152
|
-
`userAccess
|
|
153
|
-
|
|
154
|
-
|
|
229
|
+
`signOut`. The session, access and result helpers are `getSessionUser`,
|
|
230
|
+
`userAccess` and `AuthResult` (which carries `userId`), the signed-in
|
|
231
|
+
person is `FoirUser` (`{ id, email }`), and `FoirAccess` names them as
|
|
232
|
+
`userId`. A session is stored under `foir:userId`, so one saved by an
|
|
155
233
|
earlier version reads as signed out once.
|
|
156
234
|
- `@foir/sdk/auth`: a provider sign-in that names an address somebody
|
|
157
235
|
already holds waits for the code sent to it: `providerSignInRefusalOf`
|
|
@@ -160,23 +238,23 @@ Changes on `dev` that have not been published to npm.
|
|
|
160
238
|
- `@foir/sdk/auth`: the hosted session answers `userId`
|
|
161
239
|
(`HostedSession.userId`, `FoirSessionUser.userId`), from the platform's
|
|
162
240
|
`user_id`, and its cookie is `foir_session`.
|
|
163
|
-
- `@foir/sdk/react`: `useFoirSession()` answers `user
|
|
164
|
-
- `@foir/sdk/app`: `records.user` and `users
|
|
165
|
-
`
|
|
241
|
+
- `@foir/sdk/react`: `useFoirSession()` answers `user`.
|
|
242
|
+
- `@foir/sdk/app`: `records.user` and `users`, over the platform's
|
|
243
|
+
`saveUserRecord`, `userRecordByIdentity`,
|
|
166
244
|
`eraseUserRecord`, `createUser`, `user`, `users`, `userByIdentity` and
|
|
167
245
|
`userIdentitiesById`. `users.list()` answers `{ users }`. `AppUser`,
|
|
168
246
|
`AppUserList`, `AppUserSaveResult` (`userCreated`), `AppUserEraseResult`
|
|
169
247
|
(`userDeleted`, `userKeptBecause`), `AppUserEmailUnavailableReason`,
|
|
170
|
-
`appUserUnlinkedOf` and `appUserEmailUnavailableOf`
|
|
171
|
-
|
|
248
|
+
`appUserUnlinkedOf` and `appUserEmailUnavailableOf` are the app client's
|
|
249
|
+
names for the people it reads. A notification, a consent change and an
|
|
172
250
|
organization's first member name `userId`.
|
|
173
251
|
- `@foir/sdk/server`: an operation's context names who started the run as
|
|
174
|
-
`actorId` and the user it acts for as `userId` (
|
|
175
|
-
|
|
252
|
+
`actorId` and the user it acts for as `userId` (`userId` used to name
|
|
253
|
+
who started it). `ScopedTokenIngress` names the user as `userId`.
|
|
176
254
|
- `@foir/sdk/data`: `FoirGranteeKind` is `'USER'`, a note's subject may be
|
|
177
|
-
`'user'`,
|
|
178
|
-
`
|
|
179
|
-
`
|
|
255
|
+
`'user'`, a note of a user's is `FoirUserNote` with
|
|
256
|
+
`FoirUserNoteAttachment`, and `FoirAuthDowngrade` names the dropped
|
|
257
|
+
token `userToken`.
|
|
180
258
|
|
|
181
259
|
### Removed
|
|
182
260
|
|
|
@@ -184,8 +262,9 @@ Changes on `dev` that have not been published to npm.
|
|
|
184
262
|
(`@foir/sdk/auth`). Nothing stores where a user stands: a request names the
|
|
185
263
|
organization it acts in (`orgId` or `orgUrl`), and the platform checks they
|
|
186
264
|
stand there on that request. `myOrganizations` still lists where they may.
|
|
187
|
-
- **Breaking:
|
|
188
|
-
`FoirUser
|
|
265
|
+
- **Breaking: the helpers that guarded on a person being of the managing
|
|
266
|
+
kind, and the flag on `FoirUser` that said so** (`@foir/sdk/auth`). What a
|
|
267
|
+
user may do is a role or a scope:
|
|
189
268
|
guard on it with `requireRole` or `requireScope`. `requireAuth` takes the
|
|
190
269
|
type `resolveUser` answers, for a project that reads its own profile.
|
|
191
270
|
|
|
@@ -195,7 +274,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
195
274
|
|
|
196
275
|
- **An operation provisions an organization** (`@foir/sdk/app`).
|
|
197
276
|
`organizations.provision({ provisionId, name, parentId?, settings?, firstMember? })`
|
|
198
|
-
makes an organization with a
|
|
277
|
+
makes an organization with a user as its first member, in the role the
|
|
199
278
|
project names for a founder, and answers `{ organizationId }`. It needs
|
|
200
279
|
`organizations:create`. With `settings: { modelKey, data }` it writes the
|
|
201
280
|
organization's settings record too and answers `settingsRecordId`; that
|
|
@@ -210,7 +289,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
210
289
|
needs the platform release that carries `scheduleOrganizationDeletion`.
|
|
211
290
|
- **`FoirWriteNoteArgs`** (`@foir/sdk/data`): the arguments of
|
|
212
291
|
`foir.ns.notes.writeNote`, the one field that writes a note for a signed-in
|
|
213
|
-
|
|
292
|
+
user and for a visitor. It asks `notes:create` and needs the platform
|
|
214
293
|
release that carries `writeNote`. `foir.ns.notes` answers any field the
|
|
215
294
|
platform serves, so the call itself needs no new SDK.
|
|
216
295
|
|
|
@@ -237,7 +316,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
237
316
|
itself uses `createServerClient` from `@foir/sdk/server`. A key minted
|
|
238
317
|
before a key had a kind (`sk_`) is not refused here.
|
|
239
318
|
- **A key's kind is read from its prefix in one place.** A key is publishable
|
|
240
|
-
(`foir_pub_`), server (`foir_srv_`) or
|
|
319
|
+
(`foir_pub_`), server (`foir_srv_`) or the console (`foir_adm_`), and a key minted
|
|
241
320
|
earlier is `pk_`, `sk_` or `tc_`. Only a publishable key, in either format,
|
|
242
321
|
is named in a URL and takes the shared cache lane. Earlier versions looked
|
|
243
322
|
for the letters `pk_` alone, so a `foir_pub_` key was sent to the origin and
|
|
@@ -301,7 +380,7 @@ to this version then.
|
|
|
301
380
|
reads it as what those were worth, exactly as the platform does.
|
|
302
381
|
|
|
303
382
|
A record grant reaches the operation's own records: the project's, or those
|
|
304
|
-
of the
|
|
383
|
+
of the user a run acts for. `records:read.all:<model>`, and the write,
|
|
305
384
|
delete and publish forms like it, reach every owner's records of the model.
|
|
306
385
|
An older token's write of a model was every owner's, so it is read as
|
|
307
386
|
`records:write.all:<model>` and `records:create:<model>`, and takes that
|
|
@@ -330,19 +409,19 @@ to this version then.
|
|
|
330
409
|
platform holds a running install's setting to a category the project has.
|
|
331
410
|
|
|
332
411
|
- **An app can say what connecting changes** (`@foir/sdk/app/contract`).
|
|
333
|
-
`Credentials.connectNotice` is the app's own words, shown to the
|
|
412
|
+
`Credentials.connectNotice` is the app's own words, shown to the user
|
|
334
413
|
beside the step that connects their account and, for an install the vendor
|
|
335
414
|
started, before the install is confirmed. A vendor that keeps one live
|
|
336
415
|
connection per account (Shopify, per store) says here that connecting moves
|
|
337
416
|
it from any other project. Empty when there is nothing to say.
|
|
338
417
|
|
|
339
|
-
- **The roles a signed-in
|
|
340
|
-
(`@foir/sdk/auth`, `@foir/sdk/react`). A storefront can now ask which
|
|
341
|
-
roles the signed-in
|
|
418
|
+
- **The roles a signed-in user holds, and what they may do**
|
|
419
|
+
(`@foir/sdk/auth`, `@foir/sdk/react`). A storefront can now ask which user
|
|
420
|
+
roles the signed-in user holds and show the page to match, where before
|
|
342
421
|
it had to treat one of a role's permissions as a stand-in for the role.
|
|
343
422
|
|
|
344
|
-
- `getSessionAccess(adapter, env)` and `
|
|
345
|
-
return a `FoirAccess`: the
|
|
423
|
+
- `getSessionAccess(adapter, env)` and `userAccess(env, { bearerToken })`
|
|
424
|
+
return a `FoirAccess`: the user's id, the keys of the roles they hold
|
|
346
425
|
(`roles`), and everything they may do through the key (`scopes`). Plain
|
|
347
426
|
data, so a loader can return it. `null` when nobody is signed in.
|
|
348
427
|
- `hasRole(access, key)` and `hasScope(access, scope)` read it. `hasScope`
|
|
@@ -361,7 +440,7 @@ to this version then.
|
|
|
361
440
|
and a role taken away is gone the next time it is asked.
|
|
362
441
|
|
|
363
442
|
`roles` is `null`, not an empty list, when the platform did not say which
|
|
364
|
-
roles the
|
|
443
|
+
roles the user holds. `hasRole` answers false for it and `requireRole`
|
|
365
444
|
refuses with `roles_unknown`, so an outage is never read as "not staff". A
|
|
366
445
|
platform that does not serve the field yet reads the same way: the role
|
|
367
446
|
helpers need the platform release that carries it.
|
|
@@ -373,7 +452,7 @@ to this version then.
|
|
|
373
452
|
`accessStatus`, `loadAccess` and `ensureAccess`.
|
|
374
453
|
|
|
375
454
|
- **A save says when it made a record the operation may not read**
|
|
376
|
-
(`@foir/sdk/app`). `records.upsert` and `records.
|
|
455
|
+
(`@foir/sdk/app`). `records.upsert` and `records.user.save` answer
|
|
377
456
|
`fieldsWithheld`. It is true when the operation declared the create of a
|
|
378
457
|
model and not the read: the save made the record, and `record` carries its
|
|
379
458
|
`id` and `modelKey` and nothing else. The record is not empty.
|
|
@@ -389,13 +468,13 @@ to this version then.
|
|
|
389
468
|
app's operations are given the one they could use, so an installed app keeps
|
|
390
469
|
running; a manifest served from then on says it.
|
|
391
470
|
|
|
392
|
-
- **Breaking, with the platform release that carries it: erasing a
|
|
393
|
-
takes `identities:delete`** (`@foir/sdk/app`). `records.
|
|
471
|
+
- **Breaking, with the platform release that carries it: erasing a user
|
|
472
|
+
takes `identities:delete`** (`@foir/sdk/app`). `records.user.erase` needed
|
|
394
473
|
`users:create` beside the delete and the read of the model, and now needs
|
|
395
474
|
`identities:delete` there. `APP_GRANTS` carries it, with `files:delete`,
|
|
396
475
|
`notifications:read` and every owner's files (`files:read.all`,
|
|
397
476
|
`files:write.all`, `files:delete.all`), which a manifest may now declare.
|
|
398
|
-
`verifyScopedToken` reads a token minted before, that carried the
|
|
477
|
+
`verifyScopedToken` reads a token minted before, that carried the user
|
|
399
478
|
create beside the delete of a model, as holding `identities:delete`, as the
|
|
400
479
|
platform does.
|
|
401
480
|
|
|
@@ -413,17 +492,14 @@ to this version then.
|
|
|
413
492
|
| `config:read` | `appconfig:read` |
|
|
414
493
|
| `secrets:read.app`, `secrets:put.app`, `secrets:list.app`, `secrets:delete.app` | `appsecrets:read`, `appsecrets:put`, `appsecrets:list`, `appsecrets:delete` |
|
|
415
494
|
| `files:list` | `files:read` |
|
|
416
|
-
|
|
|
417
|
-
| `customers:read` | `users:read.all` |
|
|
418
|
-
| `customers:read:named` | `users:read.named` |
|
|
419
|
-
| `customers:read:identity` | `identities:read` |
|
|
495
|
+
| The names for writing and reading a project's users | `users:create`, `users:read.all`, `users:read.named`, `identities:read` |
|
|
420
496
|
| `notifications:write` | `notifications:write.named` |
|
|
421
497
|
| `records:write:<model>` | `records:write:<model>` and `records:create:<model>` |
|
|
422
498
|
|
|
423
499
|
The last row is a change in what the name means. `records:write:<model>`
|
|
424
500
|
changes a record that is there, and making one is `records:create:<model>`.
|
|
425
501
|
An operation that does both declares both. `records.upsert` and
|
|
426
|
-
`records.
|
|
502
|
+
`records.user.save` are let through on either, and the platform asks for
|
|
427
503
|
the one the save turns out to need.
|
|
428
504
|
|
|
429
505
|
- **Breaking, with the platform release that carries it: a full-page placement
|
|
@@ -436,7 +512,7 @@ to this version then.
|
|
|
436
512
|
things change in what a placement writes:
|
|
437
513
|
|
|
438
514
|
- **Every owner's records are written `.all`.** `records:read:order` was
|
|
439
|
-
every order and is now the
|
|
515
|
+
every order and is now the user's own. Declare
|
|
440
516
|
`records:read.all:order`.
|
|
441
517
|
- **A write no longer includes its read.** `records:write:order` also read
|
|
442
518
|
and created, and `apps:write` also read apps. Declare each one the page
|
|
@@ -445,8 +521,8 @@ to this version then.
|
|
|
445
521
|
- **A wildcard and a permission that gives others are refused**, as `*`
|
|
446
522
|
always was.
|
|
447
523
|
|
|
448
|
-
The token then carries what the placement declares and the
|
|
449
|
-
permission by permission.
|
|
524
|
+
The token then carries what the placement declares and the user holds,
|
|
525
|
+
permission by permission. A user who holds part of a declared grant is
|
|
450
526
|
given that part, where before the whole name was withheld.
|
|
451
527
|
|
|
452
528
|
- **`session.can` answers by what the token covers** (`@foir/sdk/placement`).
|
|
@@ -487,11 +563,11 @@ to this version then.
|
|
|
487
563
|
`records:read` for the model beside their own permission.
|
|
488
564
|
- An operation that declares the create of a model and not the read
|
|
489
565
|
(`@foir/sdk/app`) still makes its records. `records.upsert` landing on a
|
|
490
|
-
record already there, `records.
|
|
566
|
+
record already there, `records.user.save` likewise, `records.delete`,
|
|
491
567
|
`records.refresh`, `records.rename` and `records.updateMany` are refused
|
|
492
568
|
until it declares the read too.
|
|
493
569
|
|
|
494
|
-
- **Breaking, with the platform release that carries it: `records.
|
|
570
|
+
- **Breaking, with the platform release that carries it: `records.user.erase`
|
|
495
571
|
needs the delete of the model** (`@foir/sdk/app`). An erase removes a record,
|
|
496
572
|
so the operation declares `records:delete` and `records:read` for the model,
|
|
497
573
|
and `users:create`. Until now the grant to save the record was enough. An
|
|
@@ -503,14 +579,14 @@ to this version then.
|
|
|
503
579
|
`MyOrganizationRole.permissions` is gone and `MyOrganizationRole.grants`
|
|
504
580
|
takes its place: what the role gives, in the permission catalogue's names. A
|
|
505
581
|
grant stands for itself, so changing a record and creating one are two
|
|
506
|
-
names (`records:write`, `records:create`), reading every
|
|
582
|
+
names (`records:write`, `records:create`), reading every user's records
|
|
507
583
|
is `records:read.all`, and staffing an organization is `members:read` and
|
|
508
584
|
`members:write`. Code that looked for `organizations:staff` or
|
|
509
585
|
`records:list.all` in a role's list looks for the new names.
|
|
510
586
|
|
|
511
|
-
- **Breaking, with the platform release that carries it: `
|
|
512
|
-
and `
|
|
513
|
-
(`@foir/sdk/app`). The read of every
|
|
587
|
+
- **Breaking, with the platform release that carries it: `users.byIdentity`
|
|
588
|
+
and `users.identities` need `identities:read`, declared by name**
|
|
589
|
+
(`@foir/sdk/app`). The read of every user, `users:read.all`, no longer
|
|
514
590
|
includes them.
|
|
515
591
|
|
|
516
592
|
### Removed
|
|
@@ -581,29 +657,29 @@ the next promotion; `latest` moves to this version then.
|
|
|
581
657
|
(`@foir/sdk/data`). Code that reads a drift is unaffected. Code that builds
|
|
582
658
|
one by hand, for example to test a handler given to `onSchemaDrift` or made by
|
|
583
659
|
`createFoirDriftHandler`, must add `incompatible: []`.
|
|
584
|
-
- **Breaking (types): `AppConsentChange` names its
|
|
585
|
-
(`@foir/sdk/app`). It was an interface with a required `
|
|
586
|
-
now a type that holds a `
|
|
587
|
-
written as `{
|
|
660
|
+
- **Breaking (types): `AppConsentChange` names its user one way**
|
|
661
|
+
(`@foir/sdk/app`). It was an interface with a required `userId`. It is
|
|
662
|
+
now a type that holds a `userId` or an `email`, never both. A change
|
|
663
|
+
written as `{ userId, ... }` is unaffected. An interface that extended
|
|
588
664
|
`AppConsentChange` no longer compiles: intersect with it instead.
|
|
589
665
|
|
|
590
666
|
### Added
|
|
591
667
|
|
|
592
|
-
- `notifications.setConsent` takes `email` in place of `
|
|
593
|
-
(`@foir/sdk/app`), for an opt-in from somebody who may have no
|
|
594
|
-
such as a sign-up form. The platform finds the
|
|
668
|
+
- `notifications.setConsent` takes `email` in place of `userId`
|
|
669
|
+
(`@foir/sdk/app`), for an opt-in from somebody who may have no user yet,
|
|
670
|
+
such as a sign-up form. The platform finds the user holding the address
|
|
595
671
|
at the install's level, or makes one, together with the change, so a change
|
|
596
672
|
it refuses (a category the project does not have, a confirmation email the
|
|
597
|
-
project cannot send) leaves no
|
|
598
|
-
|
|
599
|
-
`
|
|
673
|
+
project cannot send) leaves no user behind. Finding or making the
|
|
674
|
+
user first with `users.ensure` did. The operation must declare
|
|
675
|
+
`users:create` as well as `notifications:consent`. `AppConsentSubject` is
|
|
600
676
|
exported.
|
|
601
677
|
|
|
602
678
|
- `FoirSchemaDrift.incompatible`: what the generated module describes that the
|
|
603
679
|
server no longer serves that way, as sorted paths into the manifest
|
|
604
680
|
(`models.order.selection.status`, `models.order.ops.update`,
|
|
605
681
|
`namespaces.search.ops.searchRecords.args.query`,
|
|
606
|
-
`models.order.relations.
|
|
682
|
+
`models.order.relations.buyer`, `selectionUnions.Block.HeroBlock.title`).
|
|
607
683
|
Empty when the surface has only grown. `changed` keeps its meaning: what
|
|
608
684
|
moved, not what broke.
|
|
609
685
|
|
|
@@ -614,26 +690,26 @@ platform serves from the next promotion; `latest` moves to it then.
|
|
|
614
690
|
|
|
615
691
|
### Changed
|
|
616
692
|
|
|
617
|
-
- **Breaking: an app finds a
|
|
618
|
-
through a model** (`@foir/sdk/app`). The
|
|
693
|
+
- **Breaking: an app finds a user by identity through its install, not
|
|
694
|
+
through a model** (`@foir/sdk/app`). The user sign-in an app's users
|
|
619
695
|
come through is paired with the install as a whole, so the two reads that
|
|
620
|
-
walk it no longer name a model and have moved beside the other
|
|
696
|
+
walk it no longer name a model and have moved beside the other user
|
|
621
697
|
reads:
|
|
622
|
-
- `records.
|
|
623
|
-
`
|
|
624
|
-
- `
|
|
698
|
+
- `records.user.identities(model, userId)` is now
|
|
699
|
+
`users.identities(userId)`.
|
|
700
|
+
- `users.byIdentity(identity)` is new: the id of the user who holds
|
|
625
701
|
an identity at the app's own system, or `null`. It replaces reading a
|
|
626
|
-
|
|
702
|
+
user's record for its owner.
|
|
627
703
|
|
|
628
|
-
Both need the new capability `
|
|
704
|
+
Both need the new capability `identities:read` (or `users:read.all`),
|
|
629
705
|
declared by the operation, and are refused at an install that pairs no
|
|
630
706
|
sign-in. Reading or writing a model is no longer what admits them.
|
|
631
|
-
- `install.config()` asks the platform for `
|
|
707
|
+
- `install.config()` asks the platform for `userSignIn`, a field a platform
|
|
632
708
|
older than this release does not have.
|
|
633
709
|
|
|
634
710
|
### Added
|
|
635
711
|
|
|
636
|
-
- `install.config()` answers `
|
|
712
|
+
- `install.config()` answers `userSignIn`: the key of the user sign-in
|
|
637
713
|
the install pairs, or `null` when it pairs none.
|
|
638
714
|
|
|
639
715
|
## [0.17.0] — 2026-10-02
|
|
@@ -643,19 +719,19 @@ platform serves from the next promotion; `latest` moves to it then.
|
|
|
643
719
|
|
|
644
720
|
### Added
|
|
645
721
|
|
|
646
|
-
- **An app is told when a project cannot email its
|
|
722
|
+
- **An app is told when a project cannot email its users**
|
|
647
723
|
(`@foir/sdk/app`). A project with no App base URL used to accept a
|
|
648
724
|
notification's email and drop it. The platform now refuses a notification
|
|
649
725
|
that carries an email, and an opt-in to a category confirmed by email, with
|
|
650
|
-
the reason as data. `
|
|
726
|
+
the reason as data. `appUserEmailUnavailableOf(error)` reads it:
|
|
651
727
|
`{ reason: 'NO_APP_BASE_URL' }`, or `null` for any other failure.
|
|
652
|
-
- `install.config()` answers `
|
|
653
|
-
of time, or `null` when the project can send. `
|
|
728
|
+
- `install.config()` answers `userEmailUnavailable`: the same reason ahead
|
|
729
|
+
of time, or `null` when the project can send. `AppUserEmailUnavailableReason`
|
|
654
730
|
is the type.
|
|
655
731
|
|
|
656
732
|
### Changed
|
|
657
733
|
|
|
658
|
-
- `install.config()` asks the platform for `
|
|
734
|
+
- `install.config()` asks the platform for `userEmailUnavailable`, a field
|
|
659
735
|
a platform older than this release does not have.
|
|
660
736
|
|
|
661
737
|
## [0.16.0] — 2026-10-02
|
|
@@ -666,11 +742,11 @@ it at the next promotion.
|
|
|
666
742
|
### Added
|
|
667
743
|
|
|
668
744
|
- **An embedded page is told how the console looks** (`@foir/sdk/placement`).
|
|
669
|
-
The console's theme is the
|
|
745
|
+
The console's theme is the user's own setting, which a framed page cannot
|
|
670
746
|
see, so a page that followed the system's preference could sit dark inside a
|
|
671
747
|
light console. `connectPlacement` takes `onAppearance`, called with
|
|
672
748
|
`{ theme: 'light' | 'dark' }` as soon as the console says (before the token
|
|
673
|
-
arrives) and again whenever the
|
|
749
|
+
arrives) and again whenever the user changes it; `session.appearance`
|
|
674
750
|
holds the current one. `PlacementAppearance` is the type, and
|
|
675
751
|
`PLACEMENT_MESSAGE.appearance` the message.
|
|
676
752
|
- A console older than this says nothing: `onAppearance` is not called and
|
|
@@ -688,7 +764,7 @@ then.
|
|
|
688
764
|
- **`notifications.setConsent` answers `confirmationPending`**, and takes
|
|
689
765
|
`givenElsewhere` and `locale` (`@foir/sdk/app`). A project can ask that an
|
|
690
766
|
opt-in an app collects in a category is confirmed by email before it counts.
|
|
691
|
-
In such a category an opt-in writes nothing: the platform sends the
|
|
767
|
+
In such a category an opt-in writes nothing: the platform sends the user
|
|
692
768
|
one confirmation email, in the page's `locale` when they have stated no
|
|
693
769
|
language of their own, and the answer says `confirmationPending`. The
|
|
694
770
|
preference turns on when they follow the link. Everywhere else an opt-in is
|
|
@@ -709,17 +785,17 @@ then.
|
|
|
709
785
|
|
|
710
786
|
### Added
|
|
711
787
|
|
|
712
|
-
- **`
|
|
713
|
-
client (`@foir/sdk/app`): an operation reads the
|
|
714
|
-
`get(id)` and `byEmail(email)` answer one
|
|
715
|
-
`
|
|
788
|
+
- **`users.get`, `users.byEmail` and `users.list`** on the app
|
|
789
|
+
client (`@foir/sdk/app`): an operation reads the users its work is about.
|
|
790
|
+
`get(id)` and `byEmail(email)` answer one user or null, and need
|
|
791
|
+
`users:read.named`: the user the operation names, by id or by their
|
|
716
792
|
exact email address at the install's level, and nothing wider. `list` pages
|
|
717
|
-
through the
|
|
793
|
+
through the users at the install's level and needs `users:read.all`,
|
|
718
794
|
which covers the two lookups as well. An operation declaring only the named
|
|
719
|
-
read is refused the list. `
|
|
720
|
-
- **`
|
|
721
|
-
made has proved the address is theirs. It finds the
|
|
722
|
-
email either way, so one shopper asking twice is one
|
|
795
|
+
read is refused the list. `AppUser` and `AppUserList` are their types.
|
|
796
|
+
- **`users.ensure` answers `verified`**: whether the user it found or
|
|
797
|
+
made has proved the address is theirs. It finds the user holding the
|
|
798
|
+
email either way, so one shopper asking twice is one user; a caller with
|
|
723
799
|
something that should wait for proof asks the flag first.
|
|
724
800
|
|
|
725
801
|
## [0.13.0] — 2026-10-01
|
|
@@ -746,7 +822,7 @@ then.
|
|
|
746
822
|
order, at most `first` of them, with `hasMore` saying the filter matched more.
|
|
747
823
|
For work an app hands out in turn, such as the next ten people waiting: list
|
|
748
824
|
them, then update the ones listed by id. A model's list is named by its
|
|
749
|
-
plural, which its
|
|
825
|
+
plural, which its user may set, so the first list a client makes reads
|
|
750
826
|
the data API's surface descriptor (`GET /v1/manifest`) as the same token,
|
|
751
827
|
once. `AppRecordList` is its query's type.
|
|
752
828
|
|
|
@@ -771,12 +847,12 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
771
847
|
`whoami`, `install.config`, `install.credentials`,
|
|
772
848
|
`install.writeCredentials`, `install.reportStatus`, `records.upsert`,
|
|
773
849
|
`read`, `delete`, `refresh`, `rename`, `updateMany` and `aggregate`,
|
|
774
|
-
`records.
|
|
775
|
-
`
|
|
850
|
+
`records.user.save`, `read`, `erase` and `identities`,
|
|
851
|
+
`users.ensure`, `notifications.send` and `notifications.setConsent`, and
|
|
776
852
|
`request` for a document of your own. Nothing is checked before a call: what
|
|
777
853
|
the token may do is the platform's to hold it to, and a refusal is a
|
|
778
854
|
`FoirRequestError` carrying the platform's answer.
|
|
779
|
-
- `appLookupConflictOf`, `
|
|
855
|
+
- `appLookupConflictOf`, `appUserUnlinkedOf` and `isAppFieldWithheld` read
|
|
780
856
|
the refusals an app acts on, as data.
|
|
781
857
|
- `readAppDispatch` and `readCredentialProbe` read the two bodies the platform
|
|
782
858
|
posts to an app, and `redeemExchangeTicket` trades an OAuth exchange ticket
|
|
@@ -804,7 +880,7 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
804
880
|
what it throws when it has no keys to judge a token by.
|
|
805
881
|
- **`requireScopedToken` answers more about who is calling.** The ingress it
|
|
806
882
|
resolves with gains `orgId` (the organisation level the token was minted at,
|
|
807
|
-
`null` when it states none), `
|
|
883
|
+
`null` when it states none), `userId`, `expiresAt` and `tokenId`.
|
|
808
884
|
`ScopedTokenError` gains `reason` (`missing`, `invalid`, `forbidden` or
|
|
809
885
|
`unavailable`), and its `status` may now be 503: keys that cannot be fetched
|
|
810
886
|
are this service's outage, which the platform retries, and not a verdict on
|
|
@@ -816,7 +892,7 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
816
892
|
passed. It takes `apiUrl`, used when a dispatch names no address to report
|
|
817
893
|
to, `retry`, and `signal`. A call the platform did not answer, or answered
|
|
818
894
|
with a 5xx or a 429, is tried again, with the same signed bytes.
|
|
819
|
-
- **`@foir/sdk/placement`**, a new subpath for an app's own
|
|
895
|
+
- **`@foir/sdk/placement`**, a new subpath for an app's own console shown
|
|
820
896
|
inside Foir's as a full-page placement. `connectPlacement` tells the console
|
|
821
897
|
the page is ready, takes the token it posts into the frame (only from the
|
|
822
898
|
consoles you list), keeps it fresh, and gives you `request`, `can` and
|
|
@@ -875,8 +951,8 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
875
951
|
once and asks again, failing with `FoirSessionExpiredError` when the session
|
|
876
952
|
is over.
|
|
877
953
|
- A request that sent no bearer is untouched.
|
|
878
|
-
- **A refresh that names no
|
|
879
|
-
`saveSession` used to write the
|
|
954
|
+
- **A refresh that names no user keeps the session's identity.**
|
|
955
|
+
`saveSession` used to write the user id and email unconditionally, so a
|
|
880
956
|
refresh answered without them left a valid token beside an empty identity, and
|
|
881
957
|
`requireAuth` turned the session away. Given `env`, `requireAuth` now reads a
|
|
882
958
|
missing identity once from `currentUser` rather than failing.
|
|
@@ -934,10 +1010,10 @@ surface version instead of the schema counter.
|
|
|
934
1010
|
credential.
|
|
935
1011
|
|
|
936
1012
|
Two things a dispatch client does not do. It does not subscribe: the
|
|
937
|
-
subscription socket authenticates a key,
|
|
1013
|
+
subscription socket authenticates a key, a console user token or a user bearer
|
|
938
1014
|
and has no path for a dispatch token, so `onChange` and friends throw, naming
|
|
939
1015
|
why. And it takes no key-side option (`apiKey`, `bearerToken`, `orgId`,
|
|
940
|
-
`channel`, the socket options), because the token carries its own
|
|
1016
|
+
`channel`, the socket options), because the token carries its own user,
|
|
941
1017
|
organization and channel; each is a compile error and a runtime refusal.
|
|
942
1018
|
|
|
943
1019
|
- `readDispatch(request)` and `OperationPayloadError` on `@foir/sdk/server`.
|
|
@@ -964,7 +1040,7 @@ surface version instead of the schema counter.
|
|
|
964
1040
|
|
|
965
1041
|
- `FoirRecordOwner` on `@foir/sdk/data`: `{ id, ownerId, ownerKind }`, what
|
|
966
1042
|
`setOwner` answers with. An operation that declares `records:attribute:<model>`
|
|
967
|
-
hands a record to another
|
|
1043
|
+
hands a record to another user with `foir.<model>.setOwner({ id, ownerId })`,
|
|
968
1044
|
and names the owner of a record it creates with `create({ input, ownerId })`.
|
|
969
1045
|
The answer carries the owner and not the record, because an operation acting
|
|
970
1046
|
for the previous holder may no longer read it.
|
|
@@ -1208,14 +1284,14 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1208
1284
|
silent wrong answer for a thrown one that discards the answer. Nothing changes
|
|
1209
1285
|
for a caller that does not pass it.
|
|
1210
1286
|
|
|
1211
|
-
- `registerOrganization(env, { key, name }, { bearerToken })` on `@foir/sdk/auth`: a
|
|
1212
|
-
- **The
|
|
1287
|
+
- `registerOrganization(env, { key, name }, { bearerToken })` on `@foir/sdk/auth`: a user 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.
|
|
1288
|
+
- **The user's organization context, on `@foir/sdk/auth`** — `myOrganizations`
|
|
1213
1289
|
and `selectOrganization`, plus the types `MyOrganization` and
|
|
1214
1290
|
`SelectedOrganization`, and two entries on `AUTH_OPS`.
|
|
1215
1291
|
|
|
1216
1292
|
A Fóir project can be partitioned into organizations, one per tenant of a SaaS
|
|
1217
|
-
built on Fóir. An *
|
|
1218
|
-
`myOrganizations()` answers which ones this
|
|
1293
|
+
built on Fóir. An *user* is a user who runs one or more of them.
|
|
1294
|
+
`myOrganizations()` answers which ones this user may stand in — from their
|
|
1219
1295
|
own membership rows, so it is not the project's inventory and cannot be used as
|
|
1220
1296
|
one — and `selectOrganization({ orgId })` says which. An empty `orgId` clears
|
|
1221
1297
|
the selection.
|
|
@@ -1225,7 +1301,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1225
1301
|
caller's membership on every request. An application therefore cannot forget
|
|
1226
1302
|
to send it, cannot send another organization's, and cannot be made to cross
|
|
1227
1303
|
the boundary by a query it built wrong. These two functions are the entire
|
|
1228
|
-
per-
|
|
1304
|
+
per-user surface; everything downstream names no organization at all.
|
|
1229
1305
|
(The project credential's side of the same boundary is the client-level
|
|
1230
1306
|
`orgId` option below — a property of the client, still never of a call.)
|
|
1231
1307
|
|
|
@@ -1339,7 +1415,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1339
1415
|
Schema drift is a different problem with the same remedy. A deploy that
|
|
1340
1416
|
renames a field leaves every cached entry perfectly current and quietly wrong:
|
|
1341
1417
|
no record changed, nothing is out of order, and the bytes describe a schema
|
|
1342
|
-
that no longer exists — until the tab is reloaded, which on a long
|
|
1418
|
+
that no longer exists — until the tab is reloaded, which on a long console
|
|
1343
1419
|
session can be days. `createFoirDriftHandler` wires the `FoirSchemaDrift` the
|
|
1344
1420
|
data client already reports to a full clear.
|
|
1345
1421
|
|
|
@@ -1460,7 +1536,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1460
1536
|
credential and the bearer is re-exchanged when it dies. A consumer that cannot
|
|
1461
1537
|
see the lifetime has to hardcode the platform's (coupling itself to a server
|
|
1462
1538
|
constant it cannot observe) or decode `exp` off the JWT — or, as happened,
|
|
1463
|
-
treat a 15-minute bearer as good forever and have
|
|
1539
|
+
treat a 15-minute bearer as good forever and have users appear signed out
|
|
1464
1540
|
mid-session. `createFoirSessionClient` on the SPA path has read `expires_in`
|
|
1465
1541
|
and scheduled a proactive refresh all along; this closes the asymmetry.
|
|
1466
1542
|
|
|
@@ -1470,10 +1546,10 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1470
1546
|
- **`HostedSessionResult`** exported from `./auth`.
|
|
1471
1547
|
|
|
1472
1548
|
- **`FoirAuthDowngrade`** on `./data` and `./server`, plus `extensions` on the
|
|
1473
|
-
response envelope — Foir now reports when it DISCARDED a
|
|
1549
|
+
response envelope — Foir now reports when it DISCARDED a user token you
|
|
1474
1550
|
sent, and the SDK stops dropping that report on the floor.
|
|
1475
1551
|
|
|
1476
|
-
A
|
|
1552
|
+
A user JWT is optional: one that fails to verify is not refused, the read
|
|
1477
1553
|
is served anonymously and returns `200`. So a lapsed token and a genuinely
|
|
1478
1554
|
anonymous request were indistinguishable — nothing threw, no refresh armed,
|
|
1479
1555
|
and auth-targeted content quietly resolved to its anonymous variant. The
|
|
@@ -1482,7 +1558,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1482
1558
|
```json
|
|
1483
1559
|
{ "data": { "…": "…" },
|
|
1484
1560
|
"extensions": { "auth": {
|
|
1485
|
-
"
|
|
1561
|
+
"userToken": "rejected", "reason": "expired", "servedAs": "anonymous"
|
|
1486
1562
|
} } }
|
|
1487
1563
|
```
|
|
1488
1564
|
|
|
@@ -1502,7 +1578,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1502
1578
|
|
|
1503
1579
|
The GraphQL `events` subscription has been live on the public schema since the
|
|
1504
1580
|
events spine shipped, with no client able to reach it. It is an `sk_`-only
|
|
1505
|
-
|
|
1581
|
+
console surface, so it lives on `./server` beside the rest of the events
|
|
1506
1582
|
client and inherits its `pk_` and `Authorization` refusals unchanged.
|
|
1507
1583
|
|
|
1508
1584
|
New exports: `FoirEventsSubscribeParams`, plus `FoirSubscription` and
|
|
@@ -1524,7 +1600,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1524
1600
|
It collapsed every failure to `null`: no cookie, a `401`, a `502`, a rate
|
|
1525
1601
|
limit, a DNS failure. Consumers have to act on that `null`, and the only
|
|
1526
1602
|
safe-looking action is to clear the session and redirect to the front door —
|
|
1527
|
-
so one blip on `/session/token` signed out a
|
|
1603
|
+
so one blip on `/session/token` signed out a user whose 30-day cookie was
|
|
1528
1604
|
perfectly valid, and nothing recorded that it had happened. The route's own
|
|
1529
1605
|
rate limiter is deliberately fail-open for the opposite reason.
|
|
1530
1606
|
|
|
@@ -1543,7 +1619,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1543
1619
|
`outcome` says what to DO; `reason` says what happened. Only `401` and a
|
|
1544
1620
|
missing cookie are `signed-out`. A `403` reports as `unavailable` with reason
|
|
1545
1621
|
`forbidden` — it means the CSRF header did not arrive, which is a
|
|
1546
|
-
misconfiguration rather than a blip, but signing the
|
|
1622
|
+
misconfiguration rather than a blip, but signing the user out over it
|
|
1547
1623
|
would still be wrong.
|
|
1548
1624
|
|
|
1549
1625
|
- **`foir.ns.operations.onExecution` delivers `status` as the
|
|
@@ -1583,14 +1659,14 @@ Everything additive, so a MINOR either way under the pre-1.0 note.
|
|
|
1583
1659
|
### Added
|
|
1584
1660
|
|
|
1585
1661
|
- **`hostedLogoutUrl(config, clientId, returnTo?)` on `./auth`** — the mirror of
|
|
1586
|
-
`hostedLoginUrl`, and the only thing that actually signs a
|
|
1662
|
+
`hostedLoginUrl`, and the only thing that actually signs a user out on the
|
|
1587
1663
|
hosted-cookie flow.
|
|
1588
1664
|
|
|
1589
1665
|
Login on that flow is symmetric across browser and server; logout was not.
|
|
1590
|
-
The apex `
|
|
1666
|
+
The apex `foir_session` cookie is the durable credential, and
|
|
1591
1667
|
disposing of a bearer, clearing a local session, or calling the
|
|
1592
|
-
`
|
|
1593
|
-
re-establishes the
|
|
1668
|
+
`logout` mutation all leave it in place — the next request
|
|
1669
|
+
re-establishes the user from it. The only surface that ended a session was
|
|
1594
1670
|
`POST /session/logout`, cookie-authenticated and CSRF-header-gated, which only
|
|
1595
1671
|
a browser sending `credentials: 'include'` can reach.
|
|
1596
1672
|
|
|
@@ -1962,7 +2038,7 @@ note above.
|
|
|
1962
2038
|
|
|
1963
2039
|
Reads take the shared, cacheable lane where they can:
|
|
1964
2040
|
`GET /cdn/graphql?key=pk_…` with the persisted-query hash in the URL when the
|
|
1965
|
-
call is a query, on a `pk_` key, with no
|
|
2041
|
+
call is a query, on a `pk_` key, with no user bearer, on the published
|
|
1966
2042
|
channel — and `POST /graphql` otherwise. The server's gate refuses on five
|
|
1967
2043
|
conditions and the client decides all five before the request: the fifth,
|
|
1968
2044
|
"the credential is in the URL", it satisfies by construction. The server is
|
|
@@ -1984,7 +2060,7 @@ note above.
|
|
|
1984
2060
|
hand-written document may legitimately have several. As a document it reaches
|
|
1985
2061
|
everything the credential can reach — including the two dozen root fields no
|
|
1986
2062
|
model and no namespace claims (`designTokens`, `record`, `locales`, the
|
|
1987
|
-
|
|
2063
|
+
user-auth mutations), which is the point of an escape hatch and is more
|
|
1988
2064
|
than the old shape could address.
|
|
1989
2065
|
|
|
1990
2066
|
- **Every op prints, and there is one wire format rather than two.** The
|
|
@@ -2047,19 +2123,19 @@ note above.
|
|
|
2047
2123
|
schema knowledge the client is not allowed to hold. The manifest publishes
|
|
2048
2124
|
each op's result shape, so the table has nothing left to answer. Neither
|
|
2049
2125
|
symbol has appeared in a published release.
|
|
2050
|
-
- **`
|
|
2051
|
-
`AUTH_OPS.
|
|
2052
|
-
email.** The resolver behind it checked that the request carried a
|
|
2053
|
-
returned `true`, and called no RPC; there is no
|
|
2054
|
-
column and no
|
|
2126
|
+
- **`resendVerificationEmail` from `@foir/sdk/auth`** — and the
|
|
2127
|
+
`AUTH_OPS.resendVerificationEmail` entry with it. **It never sent an
|
|
2128
|
+
email.** The resolver behind it checked that the request carried a user id,
|
|
2129
|
+
returned `true`, and called no RPC; there is no user verification-code
|
|
2130
|
+
column and no user verification email in the action catalogue for it to
|
|
2055
2131
|
have used. A caller got a success response every time and nothing was
|
|
2056
2132
|
delivered, which is worse than an error, because there is no signal to act on.
|
|
2057
2133
|
|
|
2058
|
-
**Use the OTP flow for mailbox proof:** `
|
|
2059
|
-
a code, and `
|
|
2134
|
+
**Use the OTP flow for mailbox proof:** `requestOTP(env, email)` emails
|
|
2135
|
+
a code, and `loginWithOTP(env, { email, otp })` redeems it — activating a
|
|
2060
2136
|
pending account on redemption and returning a session, so possession of the
|
|
2061
|
-
mailbox is proven by the same call that signs the
|
|
2062
|
-
delivered path today; `
|
|
2137
|
+
mailbox is proven by the same call that signs the user in. That is a live,
|
|
2138
|
+
delivered path today; `verifyEmail` remains for redeeming a token that
|
|
2063
2139
|
arrived by some other route.
|
|
2064
2140
|
|
|
2065
2141
|
Like the two entries above, this symbol has not appeared in a published
|
|
@@ -2092,11 +2168,11 @@ note above.
|
|
|
2092
2168
|
id are what the note is filed against and never appear on screen.
|
|
2093
2169
|
|
|
2094
2170
|
Opening the panel reads `noteConfig`, which the API charges `notes:write` and
|
|
2095
|
-
refuses to a caller with no
|
|
2171
|
+
refuses to a caller with no session, so one probe settles whether the
|
|
2096
2172
|
visitor is signed in, whether their role may write notes, and whether
|
|
2097
2173
|
screenshot capture may be offered. A missing scope reads like a bad API key and
|
|
2098
|
-
is not one, so it is reported as what it is: the signed-in
|
|
2099
|
-
short `notes:write`, which a new project's default
|
|
2174
|
+
is not one, so it is reported as what it is: the signed-in user's ROLE is
|
|
2175
|
+
short `notes:write`, which a new project's default user role does not
|
|
2100
2176
|
carry.
|
|
2101
2177
|
|
|
2102
2178
|
Screenshot capture is offered only when the project allows it and the browser
|
|
@@ -2126,11 +2202,11 @@ note above.
|
|
|
2126
2202
|
New exports on `./media`: `captureFoirScreen`, `isFoirScreenCaptureSupported`,
|
|
2127
2203
|
`FoirScreenCapture`, `FoirScreenCaptureOptions`.
|
|
2128
2204
|
|
|
2129
|
-
- **Note attachments and note config on `./data`.** `
|
|
2205
|
+
- **Note attachments and note config on `./data`.** `FoirUserNote` gains
|
|
2130
2206
|
`attachments`, `FoirCreateMyNoteArgs` gains an optional `attachmentFileIds`,
|
|
2131
2207
|
and `FoirNoteConfig` is the shape `foir.ns.notes.noteConfig()` answers with.
|
|
2132
2208
|
|
|
2133
|
-
New exports on `./data`: `
|
|
2209
|
+
New exports on `./data`: `FoirUserNoteAttachment`, `FoirNoteConfig`.
|
|
2134
2210
|
|
|
2135
2211
|
- **A subject registry — the records a client fetched, so an annotation can
|
|
2136
2212
|
anchor to the record instead of to a URL.** `createFoirSubjectRegistry()`
|
|
@@ -2174,11 +2250,11 @@ note above.
|
|
|
2174
2250
|
`FoirSubject`, `FoirSubjectEntry`, `FoirSubjectOptions`, `FoirSubjectOrigin`,
|
|
2175
2251
|
`FoirSubjectRegistry`, `FoirRecordPath`.
|
|
2176
2252
|
|
|
2177
|
-
- **Types for the
|
|
2253
|
+
- **Types for the user notes surface.** `foir.ns.notes.myNotes()` and
|
|
2178
2254
|
`foir.ns.notes.createMyNote()` already worked — `notes` is a namespace the
|
|
2179
2255
|
manifest publishes like any other — but their shapes had to be written out by
|
|
2180
2256
|
hand at each call site. They are platform-owned and identical in every project,
|
|
2181
|
-
so they now ship: `
|
|
2257
|
+
so they now ship: `FoirUserNote`, `FoirMyNotesArgs`, `FoirMyNotesResult`,
|
|
2182
2258
|
`FoirCreateMyNoteArgs`, `FoirNoteSubjectType`.
|
|
2183
2259
|
|
|
2184
2260
|
```ts
|
|
@@ -2186,7 +2262,7 @@ note above.
|
|
|
2186
2262
|
```
|
|
2187
2263
|
|
|
2188
2264
|
`FoirNoteSubjectType` is the closed set the platform's write path enforces
|
|
2189
|
-
(`record`, `file`, `model`, `operation`, `schedule`, `
|
|
2265
|
+
(`record`, `file`, `model`, `operation`, `schedule`, `user`) and is never a
|
|
2190
2266
|
model key — a note on a record is `'record'` plus the key on
|
|
2191
2267
|
`subjectModelKey`. Like every mirrored platform table in `builtins.ts`, a kind
|
|
2192
2268
|
added upstream widens this union in a minor release.
|
|
@@ -2343,12 +2419,12 @@ note above.
|
|
|
2343
2419
|
`FoirEventsClientConfig`, `FoirEventsListParams`, `FoirEventsStreamParams`.
|
|
2344
2420
|
|
|
2345
2421
|
**On `@foir/sdk/server` and no other subpath, deliberately.** The event log is
|
|
2346
|
-
an
|
|
2347
|
-
|
|
2422
|
+
an USER surface (DECIDED 2026-08-04): reachable by an `sk_` key or
|
|
2423
|
+
a console session, never by a user token, and `events:list.project` is not
|
|
2348
2424
|
mintable on a `pk_` key at all. `events.view` is plain tenant+project
|
|
2349
2425
|
containment while `record.view` carries owner/grantee/borrow terms on top, so a
|
|
2350
2426
|
containment-only read over the log describes rows whose records the same caller
|
|
2351
|
-
could not read — and each row's `actor` names which *other*
|
|
2427
|
+
could not read — and each row's `actor` names which *other* user acted.
|
|
2352
2428
|
`createEventsClient` therefore throws on a `pk_` key and on an `Authorization`
|
|
2353
2429
|
header rather than letting either reach the wire, and an import-graph test
|
|
2354
2430
|
fails if the module becomes reachable from a subpath that ships to a browser.
|
|
@@ -2369,12 +2445,12 @@ note above.
|
|
|
2369
2445
|
is the entire cost of GraphQL paid for one screen. Ten root fields closed:
|
|
2370
2446
|
|
|
2371
2447
|
`getAuthConfig`, `getDefaultAuthProvider`, `currentUser`, `myProviderToken`,
|
|
2372
|
-
`
|
|
2373
|
-
`
|
|
2448
|
+
`logout`, `requestPasswordReset`, `resetPassword`,
|
|
2449
|
+
`updatePassword`, `verifyEmail`, `deleteMyAccount`.
|
|
2374
2450
|
|
|
2375
2451
|
Plus the session-layer pairings for the flows that need one:
|
|
2376
2452
|
`resetPasswordAndSignIn` (redeem the emailed token and persist the session it
|
|
2377
|
-
mints — the peer of `loginWithPassword` for the branch where the
|
|
2453
|
+
mints — the peer of `loginWithPassword` for the branch where the user
|
|
2378
2454
|
cannot log in), `signOut` (revoke server-side, then clear locally, and clear
|
|
2379
2455
|
locally even if the revoke fails), and `authedOp` (`authedRequest`'s twin for
|
|
2380
2456
|
auth ops: refresh once, retry once, on a 401-shaped failure).
|
|
@@ -2391,10 +2467,10 @@ note above.
|
|
|
2391
2467
|
### Removed
|
|
2392
2468
|
|
|
2393
2469
|
- **The nine auth operation documents on `@foir/sdk/auth`** —
|
|
2394
|
-
`
|
|
2395
|
-
`
|
|
2396
|
-
`
|
|
2397
|
-
`
|
|
2470
|
+
`UserLoginDocument`, `UserRegisterDocument`,
|
|
2471
|
+
`UserRefreshTokenDocument`, `UserRequestOTPDocument`,
|
|
2472
|
+
`UserLoginOTPDocument`, `UserLoginWithProviderDocument`,
|
|
2473
|
+
`UserProviderCallbackDocument`, `UserProviderVerifyOTPDocument`,
|
|
2398
2474
|
`AuthProvidersDocument`.
|
|
2399
2475
|
|
|
2400
2476
|
**Breaking, so the next release is a MINOR** per the pre-1.0 note above.
|
|
@@ -2406,15 +2482,15 @@ note above.
|
|
|
2406
2482
|
eleven were unreachable. The flows now address the server's generated `auth`
|
|
2407
2483
|
namespace, so `@foir/sdk/auth` carries no transport of its own.
|
|
2408
2484
|
|
|
2409
|
-
*Migration.* Nothing that called a FUNCTION changes: `
|
|
2485
|
+
*Migration.* Nothing that called a FUNCTION changes: `login(env, …)`,
|
|
2410
2486
|
`getAuthProviders(env)`, `loginWithPassword(adapter, env, …)`, `refreshSession`,
|
|
2411
2487
|
`requireAuth`, `authedRequest` and every React hook in `@foir/sdk/react` keep
|
|
2412
2488
|
their exact signatures. Only code importing a `*Document` const is affected,
|
|
2413
2489
|
and only if it was executing the document itself through its own GraphQL
|
|
2414
2490
|
client. Two options:
|
|
2415
2491
|
|
|
2416
|
-
- call the SDK function instead (`
|
|
2417
|
-
`
|
|
2492
|
+
- call the SDK function instead (`login(env, vars)` rather than running
|
|
2493
|
+
`UserLoginDocument` through Apollo/urql), which is what the exports were
|
|
2418
2494
|
for; or
|
|
2419
2495
|
- keep your own client and write the document in your own codebase — these are
|
|
2420
2496
|
fixed-shape platform operations, so the text does not vary by project.
|
|
@@ -2478,7 +2554,7 @@ actually happened.
|
|
|
2478
2554
|
|
|
2479
2555
|
Purely additive: `./auth` keeps exporting all four, unchanged, and nothing is
|
|
2480
2556
|
moved or renamed. What changes is the cost of reaching them. `./auth` also
|
|
2481
|
-
exports the
|
|
2557
|
+
exports the user-auth GraphQL documents, which build their
|
|
2482
2558
|
TypedDocumentNodes with a top-level `parse()` that no bundler can treat as
|
|
2483
2559
|
pure — so a browser app importing only `createFoirSessionClient` was shipping
|
|
2484
2560
|
the whole `graphql` package to get it. `src/auth/session-client.ts` imports
|
|
@@ -2539,7 +2615,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2539
2615
|
|
|
2540
2616
|
### Added
|
|
2541
2617
|
|
|
2542
|
-
- `@foir/sdk/auth`: `
|
|
2618
|
+
- `@foir/sdk/auth`: `signupContext` and `joinWaitlist`, plus the
|
|
2543
2619
|
`SignupContext`, `SignupMode` and `WaitlistJoinResult` types — waitlist and
|
|
2544
2620
|
restricted-signup flows for projects that are not open-registration.
|
|
2545
2621
|
- A pinned public-export surface: `public-exports.snapshot.json` plus
|
|
@@ -2566,7 +2642,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2566
2642
|
|
|
2567
2643
|
### Added
|
|
2568
2644
|
|
|
2569
|
-
- `@foir/sdk/client`: `createFoirContentClient` — forwards the
|
|
2645
|
+
- `@foir/sdk/client`: `createFoirContentClient` — forwards the user bearer
|
|
2570
2646
|
token on content reads, so authenticated and segment-targeted content works
|
|
2571
2647
|
without hand-rolling the header.
|
|
2572
2648
|
|
|
@@ -2583,7 +2659,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2583
2659
|
|
|
2584
2660
|
### Added
|
|
2585
2661
|
|
|
2586
|
-
- **New subpath `@foir/sdk/react`** — drop-in React
|
|
2662
|
+
- **New subpath `@foir/sdk/react`** — drop-in React user auth: `FoirProvider`,
|
|
2587
2663
|
`SignIn`, `OAuthButtons`, `UserButton`, `useFoirAuth`, `useFoirSession`,
|
|
2588
2664
|
`useFoirUser`.
|
|
2589
2665
|
- **New subpath `@foir/sdk/next`** — Next.js App Router BFF auth adapter: cookie
|
|
@@ -2611,7 +2687,7 @@ First public release. Ten subpaths:
|
|
|
2611
2687
|
- `@foir/sdk/media` — progressive media components and adapters.
|
|
2612
2688
|
- `@foir/sdk/content` — `createContentRenderer` and `RichTextRenderer` with
|
|
2613
2689
|
injected typography tokens.
|
|
2614
|
-
- `@foir/sdk/auth` —
|
|
2690
|
+
- `@foir/sdk/auth` — user auth flows, token lifecycle, hosted-login bridge.
|
|
2615
2691
|
- `@foir/sdk/realtime` — `createCollabSession`, a thin y-websocket helper.
|
|
2616
2692
|
- `@foir/sdk/codegen`, `@foir/sdk/codegen/block-registry`,
|
|
2617
2693
|
`@foir/sdk/codegen/block-scaffold` — the `foirCodegen` preset and the
|