@foir/sdk 0.23.0 → 0.25.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 +223 -154
- package/dist/app/client.d.ts +52 -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/index.d.ts +1 -1
- package/dist/app.js +37 -3
- package/dist/auth/auth.d.ts +78 -15
- package/dist/auth/hosted.d.ts +5 -0
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/operations.d.ts +66 -12
- package/dist/auth/ops.d.ts +8 -1
- package/dist/auth/session-client.d.ts +5 -0
- package/dist/auth-session.js +3 -2
- package/dist/auth.js +51 -9
- package/dist/components/EditorProvider.d.ts +1 -1
- package/dist/next.js +11 -1
- package/dist/push.js +11 -1
- package/dist/react.js +14 -3
- package/dist/server/events-client.d.ts +2 -2
- package/dist/server.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,77 @@ 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.25.0] — 2026-10-18
|
|
112
|
+
|
|
113
|
+
### Changed
|
|
114
|
+
|
|
115
|
+
- **`deleteMyAccount` for an account with no password needs a recent sign-in**
|
|
116
|
+
(`@foir/sdk/auth`). `confirm` alone used to be enough. Now it's refused
|
|
117
|
+
unless the user signed in within the last ten minutes, so a stolen session
|
|
118
|
+
can't delete an account. Migration: when it's refused, send the user through
|
|
119
|
+
sign-in again, then call it once more.
|
|
120
|
+
|
|
121
|
+
### Added
|
|
122
|
+
|
|
123
|
+
- **`organizations.pool`, `organizations.startPool` and `organizations.dropPool`**
|
|
124
|
+
(`@foir/sdk/app`). In an operation, read which pool the people of the
|
|
125
|
+
organisation the run acts in sign in to, start its own pool, or drop it.
|
|
126
|
+
Reading needs `organizations:read`, and the other two `organizations:write`.
|
|
127
|
+
Starting one is refused while anybody of the pool it signs in to now stands
|
|
128
|
+
in it or beneath it, and dropping one while it holds anybody; each refusal
|
|
129
|
+
says how many. Each answers an `AppOrganizationPool`.
|
|
130
|
+
- **`AppOrganizationPool`** (`@foir/sdk/app`, type).
|
|
131
|
+
- **`organizations.provision` makes its first member by address**
|
|
132
|
+
(`@foir/sdk/app`): `firstMember: { email, firstName?, lastName? }`, made in
|
|
133
|
+
the pool that serves the new organisation (its own with `ownPool`). It also
|
|
134
|
+
needs `users:create`.
|
|
135
|
+
- **`leaveMembership`** (`@foir/sdk/auth`). The signed-in person ends their own
|
|
136
|
+
membership where the request stands: the organisation `orgId` names, or the
|
|
137
|
+
project's own. It asks no permission. It's refused where it would leave an
|
|
138
|
+
organisation with nobody who can staff it, or leave them nowhere to sign in,
|
|
139
|
+
since a project's people sign in only where they stand.
|
|
140
|
+
- **`myAddresses`, `addMyAddress`, `proveMyAddress`, `makeMyAddressPrimary`
|
|
141
|
+
and `removeMyAddress`** (`@foir/sdk/auth`), and the **`MyAddress`** type. A
|
|
142
|
+
signed-in person adds an email address to their own account. It's kept only
|
|
143
|
+
once the code it was sent proves it. Adding one asks for their current
|
|
144
|
+
password, or, for an account with none, a sign-in in the last ten minutes.
|
|
145
|
+
An address another account in the project holds isn't added. The address
|
|
146
|
+
the account uses moves only to a proven one, and it can't be removed.
|
|
147
|
+
- **`exportMyData`** (`@foir/sdk/auth`), and the **`MyDataExport`** type. A
|
|
148
|
+
signed-in person takes a copy of their own data: their account, and
|
|
149
|
+
everything deleting it would erase (the records and files they own). It
|
|
150
|
+
holds no password, code or token value.
|
|
151
|
+
- **`renameFactor`** (`@foir/sdk/auth`). A signed-in person renames one of
|
|
152
|
+
their own security keys or passkeys. A name proves nothing, so it asks for
|
|
153
|
+
no step-up.
|
|
154
|
+
|
|
155
|
+
## [0.24.0] — 2026-10-18
|
|
156
|
+
|
|
157
|
+
### Changed
|
|
158
|
+
|
|
159
|
+
- **`joinWaitlist` resolves `true` for every address** (`@foir/sdk/auth`). It
|
|
160
|
+
used to resolve the waitlist entry, whose status told anyone who asked
|
|
161
|
+
whether an address had been approved. The server now answers every address
|
|
162
|
+
alike, whether it was queued, was already let in, or already has an account,
|
|
163
|
+
and what differs is only the email the address is sent. Migration: stop
|
|
164
|
+
reading `id`, `email` or `status` from the result; a resolved call means the
|
|
165
|
+
address was not refused.
|
|
166
|
+
- **`myProviderToken` and `logout` act for one sign-in** (`@foir/sdk/auth`). A
|
|
167
|
+
provider's token belongs to the sign-in that obtained it, so `logout` ends
|
|
168
|
+
this device's and no other device's, and a sign-in with nothing kept for it
|
|
169
|
+
resolves `linked: false` with `reauthenticate: true`. Migration: where
|
|
170
|
+
`reauthenticate` is true, show the user the way to sign in through the
|
|
171
|
+
provider again rather than an empty or failed page.
|
|
172
|
+
|
|
173
|
+
### Removed
|
|
174
|
+
|
|
175
|
+
- **`WaitlistJoinResult`** (`@foir/sdk/auth`, type). `joinWaitlist` resolves a
|
|
176
|
+
boolean.
|
|
177
|
+
- **`verifyEmail`** (`@foir/sdk/auth`). It redeemed verification tokens that
|
|
178
|
+
only the console's own sign-up minted, and that lane is gone. An address is
|
|
179
|
+
proven by the code it is sent: `requestOTP`, then `loginWithOTP`, which marks
|
|
180
|
+
the address proven and lets in a person who was waiting to prove it.
|
|
181
|
+
|
|
111
182
|
## [0.23.0] — 2026-10-16
|
|
112
183
|
|
|
113
184
|
### Added
|
|
@@ -186,23 +257,23 @@ Changes on `dev` that have not been published to npm.
|
|
|
186
257
|
|
|
187
258
|
### Changed
|
|
188
259
|
|
|
189
|
-
- **Breaking: everyone who signs in is a user, and
|
|
190
|
-
|
|
260
|
+
- **Breaking: everyone who signs in is a user, and no name says what kind
|
|
261
|
+
of person somebody is.** It needs the platform release that serves the same
|
|
191
262
|
names; an earlier platform refuses every call below by name.
|
|
192
263
|
- `@foir/sdk/auth`: the raw calls take the server's op names: `login`,
|
|
193
264
|
`register`, `requestOTP`, `loginWithOTP`, `refreshTokens` (the
|
|
194
265
|
`refreshToken` op), `logout`, `joinWaitlist`, `signupContext`,
|
|
195
266
|
`requestPasswordReset`, `resetPassword`, `updatePassword`, `verifyEmail`,
|
|
196
|
-
`loginWithProvider`, `providerCallback`, `providerVerifyOTP
|
|
197
|
-
|
|
198
|
-
organization over the
|
|
199
|
-
`
|
|
267
|
+
`loginWithProvider`, `providerCallback`, `providerVerifyOTP`, each
|
|
268
|
+
renamed from a name that said what kind of person called it, and
|
|
269
|
+
`registerOrganization` founds an organization over the
|
|
270
|
+
`registerOrganization` op. The helpers that save a session are
|
|
200
271
|
`signInWithPassword`, `signUpWithPassword` and `signInWithOTP` (were
|
|
201
272
|
`loginWithPassword`, `registerWithPassword`, `loginWithOTP`), beside
|
|
202
|
-
`signOut`.
|
|
203
|
-
`userAccess
|
|
204
|
-
|
|
205
|
-
|
|
273
|
+
`signOut`. The session, access and result helpers are `getSessionUser`,
|
|
274
|
+
`userAccess` and `AuthResult` (which carries `userId`), the signed-in
|
|
275
|
+
person is `FoirUser` (`{ id, email }`), and `FoirAccess` names them as
|
|
276
|
+
`userId`. A session is stored under `foir:userId`, so one saved by an
|
|
206
277
|
earlier version reads as signed out once.
|
|
207
278
|
- `@foir/sdk/auth`: a provider sign-in that names an address somebody
|
|
208
279
|
already holds waits for the code sent to it: `providerSignInRefusalOf`
|
|
@@ -211,23 +282,23 @@ Changes on `dev` that have not been published to npm.
|
|
|
211
282
|
- `@foir/sdk/auth`: the hosted session answers `userId`
|
|
212
283
|
(`HostedSession.userId`, `FoirSessionUser.userId`), from the platform's
|
|
213
284
|
`user_id`, and its cookie is `foir_session`.
|
|
214
|
-
- `@foir/sdk/react`: `useFoirSession()` answers `user
|
|
215
|
-
- `@foir/sdk/app`: `records.user` and `users
|
|
216
|
-
`
|
|
285
|
+
- `@foir/sdk/react`: `useFoirSession()` answers `user`.
|
|
286
|
+
- `@foir/sdk/app`: `records.user` and `users`, over the platform's
|
|
287
|
+
`saveUserRecord`, `userRecordByIdentity`,
|
|
217
288
|
`eraseUserRecord`, `createUser`, `user`, `users`, `userByIdentity` and
|
|
218
289
|
`userIdentitiesById`. `users.list()` answers `{ users }`. `AppUser`,
|
|
219
290
|
`AppUserList`, `AppUserSaveResult` (`userCreated`), `AppUserEraseResult`
|
|
220
291
|
(`userDeleted`, `userKeptBecause`), `AppUserEmailUnavailableReason`,
|
|
221
|
-
`appUserUnlinkedOf` and `appUserEmailUnavailableOf`
|
|
222
|
-
|
|
292
|
+
`appUserUnlinkedOf` and `appUserEmailUnavailableOf` are the app client's
|
|
293
|
+
names for the people it reads. A notification, a consent change and an
|
|
223
294
|
organization's first member name `userId`.
|
|
224
295
|
- `@foir/sdk/server`: an operation's context names who started the run as
|
|
225
|
-
`actorId` and the user it acts for as `userId` (
|
|
226
|
-
|
|
296
|
+
`actorId` and the user it acts for as `userId` (`userId` used to name
|
|
297
|
+
who started it). `ScopedTokenIngress` names the user as `userId`.
|
|
227
298
|
- `@foir/sdk/data`: `FoirGranteeKind` is `'USER'`, a note's subject may be
|
|
228
|
-
`'user'`,
|
|
229
|
-
`
|
|
230
|
-
`
|
|
299
|
+
`'user'`, a note of a user's is `FoirUserNote` with
|
|
300
|
+
`FoirUserNoteAttachment`, and `FoirAuthDowngrade` names the dropped
|
|
301
|
+
token `userToken`.
|
|
231
302
|
|
|
232
303
|
### Removed
|
|
233
304
|
|
|
@@ -235,8 +306,9 @@ Changes on `dev` that have not been published to npm.
|
|
|
235
306
|
(`@foir/sdk/auth`). Nothing stores where a user stands: a request names the
|
|
236
307
|
organization it acts in (`orgId` or `orgUrl`), and the platform checks they
|
|
237
308
|
stand there on that request. `myOrganizations` still lists where they may.
|
|
238
|
-
- **Breaking:
|
|
239
|
-
`FoirUser
|
|
309
|
+
- **Breaking: the helpers that guarded on a person being of the managing
|
|
310
|
+
kind, and the flag on `FoirUser` that said so** (`@foir/sdk/auth`). What a
|
|
311
|
+
user may do is a role or a scope:
|
|
240
312
|
guard on it with `requireRole` or `requireScope`. `requireAuth` takes the
|
|
241
313
|
type `resolveUser` answers, for a project that reads its own profile.
|
|
242
314
|
|
|
@@ -246,7 +318,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
246
318
|
|
|
247
319
|
- **An operation provisions an organization** (`@foir/sdk/app`).
|
|
248
320
|
`organizations.provision({ provisionId, name, parentId?, settings?, firstMember? })`
|
|
249
|
-
makes an organization with a
|
|
321
|
+
makes an organization with a user as its first member, in the role the
|
|
250
322
|
project names for a founder, and answers `{ organizationId }`. It needs
|
|
251
323
|
`organizations:create`. With `settings: { modelKey, data }` it writes the
|
|
252
324
|
organization's settings record too and answers `settingsRecordId`; that
|
|
@@ -261,7 +333,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
261
333
|
needs the platform release that carries `scheduleOrganizationDeletion`.
|
|
262
334
|
- **`FoirWriteNoteArgs`** (`@foir/sdk/data`): the arguments of
|
|
263
335
|
`foir.ns.notes.writeNote`, the one field that writes a note for a signed-in
|
|
264
|
-
|
|
336
|
+
user and for a visitor. It asks `notes:create` and needs the platform
|
|
265
337
|
release that carries `writeNote`. `foir.ns.notes` answers any field the
|
|
266
338
|
platform serves, so the call itself needs no new SDK.
|
|
267
339
|
|
|
@@ -288,7 +360,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
288
360
|
itself uses `createServerClient` from `@foir/sdk/server`. A key minted
|
|
289
361
|
before a key had a kind (`sk_`) is not refused here.
|
|
290
362
|
- **A key's kind is read from its prefix in one place.** A key is publishable
|
|
291
|
-
(`foir_pub_`), server (`foir_srv_`) or
|
|
363
|
+
(`foir_pub_`), server (`foir_srv_`) or the console (`foir_adm_`), and a key minted
|
|
292
364
|
earlier is `pk_`, `sk_` or `tc_`. Only a publishable key, in either format,
|
|
293
365
|
is named in a URL and takes the shared cache lane. Earlier versions looked
|
|
294
366
|
for the letters `pk_` alone, so a `foir_pub_` key was sent to the origin and
|
|
@@ -352,7 +424,7 @@ to this version then.
|
|
|
352
424
|
reads it as what those were worth, exactly as the platform does.
|
|
353
425
|
|
|
354
426
|
A record grant reaches the operation's own records: the project's, or those
|
|
355
|
-
of the
|
|
427
|
+
of the user a run acts for. `records:read.all:<model>`, and the write,
|
|
356
428
|
delete and publish forms like it, reach every owner's records of the model.
|
|
357
429
|
An older token's write of a model was every owner's, so it is read as
|
|
358
430
|
`records:write.all:<model>` and `records:create:<model>`, and takes that
|
|
@@ -381,19 +453,19 @@ to this version then.
|
|
|
381
453
|
platform holds a running install's setting to a category the project has.
|
|
382
454
|
|
|
383
455
|
- **An app can say what connecting changes** (`@foir/sdk/app/contract`).
|
|
384
|
-
`Credentials.connectNotice` is the app's own words, shown to the
|
|
456
|
+
`Credentials.connectNotice` is the app's own words, shown to the user
|
|
385
457
|
beside the step that connects their account and, for an install the vendor
|
|
386
458
|
started, before the install is confirmed. A vendor that keeps one live
|
|
387
459
|
connection per account (Shopify, per store) says here that connecting moves
|
|
388
460
|
it from any other project. Empty when there is nothing to say.
|
|
389
461
|
|
|
390
|
-
- **The roles a signed-in
|
|
391
|
-
(`@foir/sdk/auth`, `@foir/sdk/react`). A storefront can now ask which
|
|
392
|
-
roles the signed-in
|
|
462
|
+
- **The roles a signed-in user holds, and what they may do**
|
|
463
|
+
(`@foir/sdk/auth`, `@foir/sdk/react`). A storefront can now ask which user
|
|
464
|
+
roles the signed-in user holds and show the page to match, where before
|
|
393
465
|
it had to treat one of a role's permissions as a stand-in for the role.
|
|
394
466
|
|
|
395
|
-
- `getSessionAccess(adapter, env)` and `
|
|
396
|
-
return a `FoirAccess`: the
|
|
467
|
+
- `getSessionAccess(adapter, env)` and `userAccess(env, { bearerToken })`
|
|
468
|
+
return a `FoirAccess`: the user's id, the keys of the roles they hold
|
|
397
469
|
(`roles`), and everything they may do through the key (`scopes`). Plain
|
|
398
470
|
data, so a loader can return it. `null` when nobody is signed in.
|
|
399
471
|
- `hasRole(access, key)` and `hasScope(access, scope)` read it. `hasScope`
|
|
@@ -412,7 +484,7 @@ to this version then.
|
|
|
412
484
|
and a role taken away is gone the next time it is asked.
|
|
413
485
|
|
|
414
486
|
`roles` is `null`, not an empty list, when the platform did not say which
|
|
415
|
-
roles the
|
|
487
|
+
roles the user holds. `hasRole` answers false for it and `requireRole`
|
|
416
488
|
refuses with `roles_unknown`, so an outage is never read as "not staff". A
|
|
417
489
|
platform that does not serve the field yet reads the same way: the role
|
|
418
490
|
helpers need the platform release that carries it.
|
|
@@ -424,7 +496,7 @@ to this version then.
|
|
|
424
496
|
`accessStatus`, `loadAccess` and `ensureAccess`.
|
|
425
497
|
|
|
426
498
|
- **A save says when it made a record the operation may not read**
|
|
427
|
-
(`@foir/sdk/app`). `records.upsert` and `records.
|
|
499
|
+
(`@foir/sdk/app`). `records.upsert` and `records.user.save` answer
|
|
428
500
|
`fieldsWithheld`. It is true when the operation declared the create of a
|
|
429
501
|
model and not the read: the save made the record, and `record` carries its
|
|
430
502
|
`id` and `modelKey` and nothing else. The record is not empty.
|
|
@@ -440,13 +512,13 @@ to this version then.
|
|
|
440
512
|
app's operations are given the one they could use, so an installed app keeps
|
|
441
513
|
running; a manifest served from then on says it.
|
|
442
514
|
|
|
443
|
-
- **Breaking, with the platform release that carries it: erasing a
|
|
444
|
-
takes `identities:delete`** (`@foir/sdk/app`). `records.
|
|
515
|
+
- **Breaking, with the platform release that carries it: erasing a user
|
|
516
|
+
takes `identities:delete`** (`@foir/sdk/app`). `records.user.erase` needed
|
|
445
517
|
`users:create` beside the delete and the read of the model, and now needs
|
|
446
518
|
`identities:delete` there. `APP_GRANTS` carries it, with `files:delete`,
|
|
447
519
|
`notifications:read` and every owner's files (`files:read.all`,
|
|
448
520
|
`files:write.all`, `files:delete.all`), which a manifest may now declare.
|
|
449
|
-
`verifyScopedToken` reads a token minted before, that carried the
|
|
521
|
+
`verifyScopedToken` reads a token minted before, that carried the user
|
|
450
522
|
create beside the delete of a model, as holding `identities:delete`, as the
|
|
451
523
|
platform does.
|
|
452
524
|
|
|
@@ -464,17 +536,14 @@ to this version then.
|
|
|
464
536
|
| `config:read` | `appconfig:read` |
|
|
465
537
|
| `secrets:read.app`, `secrets:put.app`, `secrets:list.app`, `secrets:delete.app` | `appsecrets:read`, `appsecrets:put`, `appsecrets:list`, `appsecrets:delete` |
|
|
466
538
|
| `files:list` | `files:read` |
|
|
467
|
-
|
|
|
468
|
-
| `customers:read` | `users:read.all` |
|
|
469
|
-
| `customers:read:named` | `users:read.named` |
|
|
470
|
-
| `customers:read:identity` | `identities:read` |
|
|
539
|
+
| The names for writing and reading a project's users | `users:create`, `users:read.all`, `users:read.named`, `identities:read` |
|
|
471
540
|
| `notifications:write` | `notifications:write.named` |
|
|
472
541
|
| `records:write:<model>` | `records:write:<model>` and `records:create:<model>` |
|
|
473
542
|
|
|
474
543
|
The last row is a change in what the name means. `records:write:<model>`
|
|
475
544
|
changes a record that is there, and making one is `records:create:<model>`.
|
|
476
545
|
An operation that does both declares both. `records.upsert` and
|
|
477
|
-
`records.
|
|
546
|
+
`records.user.save` are let through on either, and the platform asks for
|
|
478
547
|
the one the save turns out to need.
|
|
479
548
|
|
|
480
549
|
- **Breaking, with the platform release that carries it: a full-page placement
|
|
@@ -487,7 +556,7 @@ to this version then.
|
|
|
487
556
|
things change in what a placement writes:
|
|
488
557
|
|
|
489
558
|
- **Every owner's records are written `.all`.** `records:read:order` was
|
|
490
|
-
every order and is now the
|
|
559
|
+
every order and is now the user's own. Declare
|
|
491
560
|
`records:read.all:order`.
|
|
492
561
|
- **A write no longer includes its read.** `records:write:order` also read
|
|
493
562
|
and created, and `apps:write` also read apps. Declare each one the page
|
|
@@ -496,8 +565,8 @@ to this version then.
|
|
|
496
565
|
- **A wildcard and a permission that gives others are refused**, as `*`
|
|
497
566
|
always was.
|
|
498
567
|
|
|
499
|
-
The token then carries what the placement declares and the
|
|
500
|
-
permission by permission.
|
|
568
|
+
The token then carries what the placement declares and the user holds,
|
|
569
|
+
permission by permission. A user who holds part of a declared grant is
|
|
501
570
|
given that part, where before the whole name was withheld.
|
|
502
571
|
|
|
503
572
|
- **`session.can` answers by what the token covers** (`@foir/sdk/placement`).
|
|
@@ -538,11 +607,11 @@ to this version then.
|
|
|
538
607
|
`records:read` for the model beside their own permission.
|
|
539
608
|
- An operation that declares the create of a model and not the read
|
|
540
609
|
(`@foir/sdk/app`) still makes its records. `records.upsert` landing on a
|
|
541
|
-
record already there, `records.
|
|
610
|
+
record already there, `records.user.save` likewise, `records.delete`,
|
|
542
611
|
`records.refresh`, `records.rename` and `records.updateMany` are refused
|
|
543
612
|
until it declares the read too.
|
|
544
613
|
|
|
545
|
-
- **Breaking, with the platform release that carries it: `records.
|
|
614
|
+
- **Breaking, with the platform release that carries it: `records.user.erase`
|
|
546
615
|
needs the delete of the model** (`@foir/sdk/app`). An erase removes a record,
|
|
547
616
|
so the operation declares `records:delete` and `records:read` for the model,
|
|
548
617
|
and `users:create`. Until now the grant to save the record was enough. An
|
|
@@ -554,14 +623,14 @@ to this version then.
|
|
|
554
623
|
`MyOrganizationRole.permissions` is gone and `MyOrganizationRole.grants`
|
|
555
624
|
takes its place: what the role gives, in the permission catalogue's names. A
|
|
556
625
|
grant stands for itself, so changing a record and creating one are two
|
|
557
|
-
names (`records:write`, `records:create`), reading every
|
|
626
|
+
names (`records:write`, `records:create`), reading every user's records
|
|
558
627
|
is `records:read.all`, and staffing an organization is `members:read` and
|
|
559
628
|
`members:write`. Code that looked for `organizations:staff` or
|
|
560
629
|
`records:list.all` in a role's list looks for the new names.
|
|
561
630
|
|
|
562
|
-
- **Breaking, with the platform release that carries it: `
|
|
563
|
-
and `
|
|
564
|
-
(`@foir/sdk/app`). The read of every
|
|
631
|
+
- **Breaking, with the platform release that carries it: `users.byIdentity`
|
|
632
|
+
and `users.identities` need `identities:read`, declared by name**
|
|
633
|
+
(`@foir/sdk/app`). The read of every user, `users:read.all`, no longer
|
|
565
634
|
includes them.
|
|
566
635
|
|
|
567
636
|
### Removed
|
|
@@ -632,29 +701,29 @@ the next promotion; `latest` moves to this version then.
|
|
|
632
701
|
(`@foir/sdk/data`). Code that reads a drift is unaffected. Code that builds
|
|
633
702
|
one by hand, for example to test a handler given to `onSchemaDrift` or made by
|
|
634
703
|
`createFoirDriftHandler`, must add `incompatible: []`.
|
|
635
|
-
- **Breaking (types): `AppConsentChange` names its
|
|
636
|
-
(`@foir/sdk/app`). It was an interface with a required `
|
|
637
|
-
now a type that holds a `
|
|
638
|
-
written as `{
|
|
704
|
+
- **Breaking (types): `AppConsentChange` names its user one way**
|
|
705
|
+
(`@foir/sdk/app`). It was an interface with a required `userId`. It is
|
|
706
|
+
now a type that holds a `userId` or an `email`, never both. A change
|
|
707
|
+
written as `{ userId, ... }` is unaffected. An interface that extended
|
|
639
708
|
`AppConsentChange` no longer compiles: intersect with it instead.
|
|
640
709
|
|
|
641
710
|
### Added
|
|
642
711
|
|
|
643
|
-
- `notifications.setConsent` takes `email` in place of `
|
|
644
|
-
(`@foir/sdk/app`), for an opt-in from somebody who may have no
|
|
645
|
-
such as a sign-up form. The platform finds the
|
|
712
|
+
- `notifications.setConsent` takes `email` in place of `userId`
|
|
713
|
+
(`@foir/sdk/app`), for an opt-in from somebody who may have no user yet,
|
|
714
|
+
such as a sign-up form. The platform finds the user holding the address
|
|
646
715
|
at the install's level, or makes one, together with the change, so a change
|
|
647
716
|
it refuses (a category the project does not have, a confirmation email the
|
|
648
|
-
project cannot send) leaves no
|
|
649
|
-
|
|
650
|
-
`
|
|
717
|
+
project cannot send) leaves no user behind. Finding or making the
|
|
718
|
+
user first with `users.ensure` did. The operation must declare
|
|
719
|
+
`users:create` as well as `notifications:consent`. `AppConsentSubject` is
|
|
651
720
|
exported.
|
|
652
721
|
|
|
653
722
|
- `FoirSchemaDrift.incompatible`: what the generated module describes that the
|
|
654
723
|
server no longer serves that way, as sorted paths into the manifest
|
|
655
724
|
(`models.order.selection.status`, `models.order.ops.update`,
|
|
656
725
|
`namespaces.search.ops.searchRecords.args.query`,
|
|
657
|
-
`models.order.relations.
|
|
726
|
+
`models.order.relations.buyer`, `selectionUnions.Block.HeroBlock.title`).
|
|
658
727
|
Empty when the surface has only grown. `changed` keeps its meaning: what
|
|
659
728
|
moved, not what broke.
|
|
660
729
|
|
|
@@ -665,26 +734,26 @@ platform serves from the next promotion; `latest` moves to it then.
|
|
|
665
734
|
|
|
666
735
|
### Changed
|
|
667
736
|
|
|
668
|
-
- **Breaking: an app finds a
|
|
669
|
-
through a model** (`@foir/sdk/app`). The
|
|
737
|
+
- **Breaking: an app finds a user by identity through its install, not
|
|
738
|
+
through a model** (`@foir/sdk/app`). The user sign-in an app's users
|
|
670
739
|
come through is paired with the install as a whole, so the two reads that
|
|
671
|
-
walk it no longer name a model and have moved beside the other
|
|
740
|
+
walk it no longer name a model and have moved beside the other user
|
|
672
741
|
reads:
|
|
673
|
-
- `records.
|
|
674
|
-
`
|
|
675
|
-
- `
|
|
742
|
+
- `records.user.identities(model, userId)` is now
|
|
743
|
+
`users.identities(userId)`.
|
|
744
|
+
- `users.byIdentity(identity)` is new: the id of the user who holds
|
|
676
745
|
an identity at the app's own system, or `null`. It replaces reading a
|
|
677
|
-
|
|
746
|
+
user's record for its owner.
|
|
678
747
|
|
|
679
|
-
Both need the new capability `
|
|
748
|
+
Both need the new capability `identities:read` (or `users:read.all`),
|
|
680
749
|
declared by the operation, and are refused at an install that pairs no
|
|
681
750
|
sign-in. Reading or writing a model is no longer what admits them.
|
|
682
|
-
- `install.config()` asks the platform for `
|
|
751
|
+
- `install.config()` asks the platform for `userSignIn`, a field a platform
|
|
683
752
|
older than this release does not have.
|
|
684
753
|
|
|
685
754
|
### Added
|
|
686
755
|
|
|
687
|
-
- `install.config()` answers `
|
|
756
|
+
- `install.config()` answers `userSignIn`: the key of the user sign-in
|
|
688
757
|
the install pairs, or `null` when it pairs none.
|
|
689
758
|
|
|
690
759
|
## [0.17.0] — 2026-10-02
|
|
@@ -694,19 +763,19 @@ platform serves from the next promotion; `latest` moves to it then.
|
|
|
694
763
|
|
|
695
764
|
### Added
|
|
696
765
|
|
|
697
|
-
- **An app is told when a project cannot email its
|
|
766
|
+
- **An app is told when a project cannot email its users**
|
|
698
767
|
(`@foir/sdk/app`). A project with no App base URL used to accept a
|
|
699
768
|
notification's email and drop it. The platform now refuses a notification
|
|
700
769
|
that carries an email, and an opt-in to a category confirmed by email, with
|
|
701
|
-
the reason as data. `
|
|
770
|
+
the reason as data. `appUserEmailUnavailableOf(error)` reads it:
|
|
702
771
|
`{ reason: 'NO_APP_BASE_URL' }`, or `null` for any other failure.
|
|
703
|
-
- `install.config()` answers `
|
|
704
|
-
of time, or `null` when the project can send. `
|
|
772
|
+
- `install.config()` answers `userEmailUnavailable`: the same reason ahead
|
|
773
|
+
of time, or `null` when the project can send. `AppUserEmailUnavailableReason`
|
|
705
774
|
is the type.
|
|
706
775
|
|
|
707
776
|
### Changed
|
|
708
777
|
|
|
709
|
-
- `install.config()` asks the platform for `
|
|
778
|
+
- `install.config()` asks the platform for `userEmailUnavailable`, a field
|
|
710
779
|
a platform older than this release does not have.
|
|
711
780
|
|
|
712
781
|
## [0.16.0] — 2026-10-02
|
|
@@ -717,11 +786,11 @@ it at the next promotion.
|
|
|
717
786
|
### Added
|
|
718
787
|
|
|
719
788
|
- **An embedded page is told how the console looks** (`@foir/sdk/placement`).
|
|
720
|
-
The console's theme is the
|
|
789
|
+
The console's theme is the user's own setting, which a framed page cannot
|
|
721
790
|
see, so a page that followed the system's preference could sit dark inside a
|
|
722
791
|
light console. `connectPlacement` takes `onAppearance`, called with
|
|
723
792
|
`{ theme: 'light' | 'dark' }` as soon as the console says (before the token
|
|
724
|
-
arrives) and again whenever the
|
|
793
|
+
arrives) and again whenever the user changes it; `session.appearance`
|
|
725
794
|
holds the current one. `PlacementAppearance` is the type, and
|
|
726
795
|
`PLACEMENT_MESSAGE.appearance` the message.
|
|
727
796
|
- A console older than this says nothing: `onAppearance` is not called and
|
|
@@ -739,7 +808,7 @@ then.
|
|
|
739
808
|
- **`notifications.setConsent` answers `confirmationPending`**, and takes
|
|
740
809
|
`givenElsewhere` and `locale` (`@foir/sdk/app`). A project can ask that an
|
|
741
810
|
opt-in an app collects in a category is confirmed by email before it counts.
|
|
742
|
-
In such a category an opt-in writes nothing: the platform sends the
|
|
811
|
+
In such a category an opt-in writes nothing: the platform sends the user
|
|
743
812
|
one confirmation email, in the page's `locale` when they have stated no
|
|
744
813
|
language of their own, and the answer says `confirmationPending`. The
|
|
745
814
|
preference turns on when they follow the link. Everywhere else an opt-in is
|
|
@@ -760,17 +829,17 @@ then.
|
|
|
760
829
|
|
|
761
830
|
### Added
|
|
762
831
|
|
|
763
|
-
- **`
|
|
764
|
-
client (`@foir/sdk/app`): an operation reads the
|
|
765
|
-
`get(id)` and `byEmail(email)` answer one
|
|
766
|
-
`
|
|
832
|
+
- **`users.get`, `users.byEmail` and `users.list`** on the app
|
|
833
|
+
client (`@foir/sdk/app`): an operation reads the users its work is about.
|
|
834
|
+
`get(id)` and `byEmail(email)` answer one user or null, and need
|
|
835
|
+
`users:read.named`: the user the operation names, by id or by their
|
|
767
836
|
exact email address at the install's level, and nothing wider. `list` pages
|
|
768
|
-
through the
|
|
837
|
+
through the users at the install's level and needs `users:read.all`,
|
|
769
838
|
which covers the two lookups as well. An operation declaring only the named
|
|
770
|
-
read is refused the list. `
|
|
771
|
-
- **`
|
|
772
|
-
made has proved the address is theirs. It finds the
|
|
773
|
-
email either way, so one shopper asking twice is one
|
|
839
|
+
read is refused the list. `AppUser` and `AppUserList` are their types.
|
|
840
|
+
- **`users.ensure` answers `verified`**: whether the user it found or
|
|
841
|
+
made has proved the address is theirs. It finds the user holding the
|
|
842
|
+
email either way, so one shopper asking twice is one user; a caller with
|
|
774
843
|
something that should wait for proof asks the flag first.
|
|
775
844
|
|
|
776
845
|
## [0.13.0] — 2026-10-01
|
|
@@ -797,7 +866,7 @@ then.
|
|
|
797
866
|
order, at most `first` of them, with `hasMore` saying the filter matched more.
|
|
798
867
|
For work an app hands out in turn, such as the next ten people waiting: list
|
|
799
868
|
them, then update the ones listed by id. A model's list is named by its
|
|
800
|
-
plural, which its
|
|
869
|
+
plural, which its user may set, so the first list a client makes reads
|
|
801
870
|
the data API's surface descriptor (`GET /v1/manifest`) as the same token,
|
|
802
871
|
once. `AppRecordList` is its query's type.
|
|
803
872
|
|
|
@@ -822,12 +891,12 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
822
891
|
`whoami`, `install.config`, `install.credentials`,
|
|
823
892
|
`install.writeCredentials`, `install.reportStatus`, `records.upsert`,
|
|
824
893
|
`read`, `delete`, `refresh`, `rename`, `updateMany` and `aggregate`,
|
|
825
|
-
`records.
|
|
826
|
-
`
|
|
894
|
+
`records.user.save`, `read`, `erase` and `identities`,
|
|
895
|
+
`users.ensure`, `notifications.send` and `notifications.setConsent`, and
|
|
827
896
|
`request` for a document of your own. Nothing is checked before a call: what
|
|
828
897
|
the token may do is the platform's to hold it to, and a refusal is a
|
|
829
898
|
`FoirRequestError` carrying the platform's answer.
|
|
830
|
-
- `appLookupConflictOf`, `
|
|
899
|
+
- `appLookupConflictOf`, `appUserUnlinkedOf` and `isAppFieldWithheld` read
|
|
831
900
|
the refusals an app acts on, as data.
|
|
832
901
|
- `readAppDispatch` and `readCredentialProbe` read the two bodies the platform
|
|
833
902
|
posts to an app, and `redeemExchangeTicket` trades an OAuth exchange ticket
|
|
@@ -855,7 +924,7 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
855
924
|
what it throws when it has no keys to judge a token by.
|
|
856
925
|
- **`requireScopedToken` answers more about who is calling.** The ingress it
|
|
857
926
|
resolves with gains `orgId` (the organisation level the token was minted at,
|
|
858
|
-
`null` when it states none), `
|
|
927
|
+
`null` when it states none), `userId`, `expiresAt` and `tokenId`.
|
|
859
928
|
`ScopedTokenError` gains `reason` (`missing`, `invalid`, `forbidden` or
|
|
860
929
|
`unavailable`), and its `status` may now be 503: keys that cannot be fetched
|
|
861
930
|
are this service's outage, which the platform retries, and not a verdict on
|
|
@@ -867,7 +936,7 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
867
936
|
passed. It takes `apiUrl`, used when a dispatch names no address to report
|
|
868
937
|
to, `retry`, and `signal`. A call the platform did not answer, or answered
|
|
869
938
|
with a 5xx or a 429, is tried again, with the same signed bytes.
|
|
870
|
-
- **`@foir/sdk/placement`**, a new subpath for an app's own
|
|
939
|
+
- **`@foir/sdk/placement`**, a new subpath for an app's own console shown
|
|
871
940
|
inside Foir's as a full-page placement. `connectPlacement` tells the console
|
|
872
941
|
the page is ready, takes the token it posts into the frame (only from the
|
|
873
942
|
consoles you list), keeps it fresh, and gives you `request`, `can` and
|
|
@@ -926,8 +995,8 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
926
995
|
once and asks again, failing with `FoirSessionExpiredError` when the session
|
|
927
996
|
is over.
|
|
928
997
|
- A request that sent no bearer is untouched.
|
|
929
|
-
- **A refresh that names no
|
|
930
|
-
`saveSession` used to write the
|
|
998
|
+
- **A refresh that names no user keeps the session's identity.**
|
|
999
|
+
`saveSession` used to write the user id and email unconditionally, so a
|
|
931
1000
|
refresh answered without them left a valid token beside an empty identity, and
|
|
932
1001
|
`requireAuth` turned the session away. Given `env`, `requireAuth` now reads a
|
|
933
1002
|
missing identity once from `currentUser` rather than failing.
|
|
@@ -985,10 +1054,10 @@ surface version instead of the schema counter.
|
|
|
985
1054
|
credential.
|
|
986
1055
|
|
|
987
1056
|
Two things a dispatch client does not do. It does not subscribe: the
|
|
988
|
-
subscription socket authenticates a key,
|
|
1057
|
+
subscription socket authenticates a key, a console user token or a user bearer
|
|
989
1058
|
and has no path for a dispatch token, so `onChange` and friends throw, naming
|
|
990
1059
|
why. And it takes no key-side option (`apiKey`, `bearerToken`, `orgId`,
|
|
991
|
-
`channel`, the socket options), because the token carries its own
|
|
1060
|
+
`channel`, the socket options), because the token carries its own user,
|
|
992
1061
|
organization and channel; each is a compile error and a runtime refusal.
|
|
993
1062
|
|
|
994
1063
|
- `readDispatch(request)` and `OperationPayloadError` on `@foir/sdk/server`.
|
|
@@ -1015,7 +1084,7 @@ surface version instead of the schema counter.
|
|
|
1015
1084
|
|
|
1016
1085
|
- `FoirRecordOwner` on `@foir/sdk/data`: `{ id, ownerId, ownerKind }`, what
|
|
1017
1086
|
`setOwner` answers with. An operation that declares `records:attribute:<model>`
|
|
1018
|
-
hands a record to another
|
|
1087
|
+
hands a record to another user with `foir.<model>.setOwner({ id, ownerId })`,
|
|
1019
1088
|
and names the owner of a record it creates with `create({ input, ownerId })`.
|
|
1020
1089
|
The answer carries the owner and not the record, because an operation acting
|
|
1021
1090
|
for the previous holder may no longer read it.
|
|
@@ -1259,14 +1328,14 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1259
1328
|
silent wrong answer for a thrown one that discards the answer. Nothing changes
|
|
1260
1329
|
for a caller that does not pass it.
|
|
1261
1330
|
|
|
1262
|
-
- `registerOrganization(env, { key, name }, { bearerToken })` on `@foir/sdk/auth`: a
|
|
1263
|
-
- **The
|
|
1331
|
+
- `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.
|
|
1332
|
+
- **The user's organization context, on `@foir/sdk/auth`** — `myOrganizations`
|
|
1264
1333
|
and `selectOrganization`, plus the types `MyOrganization` and
|
|
1265
1334
|
`SelectedOrganization`, and two entries on `AUTH_OPS`.
|
|
1266
1335
|
|
|
1267
1336
|
A Fóir project can be partitioned into organizations, one per tenant of a SaaS
|
|
1268
|
-
built on Fóir. An *
|
|
1269
|
-
`myOrganizations()` answers which ones this
|
|
1337
|
+
built on Fóir. An *user* is a user who runs one or more of them.
|
|
1338
|
+
`myOrganizations()` answers which ones this user may stand in — from their
|
|
1270
1339
|
own membership rows, so it is not the project's inventory and cannot be used as
|
|
1271
1340
|
one — and `selectOrganization({ orgId })` says which. An empty `orgId` clears
|
|
1272
1341
|
the selection.
|
|
@@ -1276,7 +1345,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1276
1345
|
caller's membership on every request. An application therefore cannot forget
|
|
1277
1346
|
to send it, cannot send another organization's, and cannot be made to cross
|
|
1278
1347
|
the boundary by a query it built wrong. These two functions are the entire
|
|
1279
|
-
per-
|
|
1348
|
+
per-user surface; everything downstream names no organization at all.
|
|
1280
1349
|
(The project credential's side of the same boundary is the client-level
|
|
1281
1350
|
`orgId` option below — a property of the client, still never of a call.)
|
|
1282
1351
|
|
|
@@ -1390,7 +1459,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1390
1459
|
Schema drift is a different problem with the same remedy. A deploy that
|
|
1391
1460
|
renames a field leaves every cached entry perfectly current and quietly wrong:
|
|
1392
1461
|
no record changed, nothing is out of order, and the bytes describe a schema
|
|
1393
|
-
that no longer exists — until the tab is reloaded, which on a long
|
|
1462
|
+
that no longer exists — until the tab is reloaded, which on a long console
|
|
1394
1463
|
session can be days. `createFoirDriftHandler` wires the `FoirSchemaDrift` the
|
|
1395
1464
|
data client already reports to a full clear.
|
|
1396
1465
|
|
|
@@ -1511,7 +1580,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1511
1580
|
credential and the bearer is re-exchanged when it dies. A consumer that cannot
|
|
1512
1581
|
see the lifetime has to hardcode the platform's (coupling itself to a server
|
|
1513
1582
|
constant it cannot observe) or decode `exp` off the JWT — or, as happened,
|
|
1514
|
-
treat a 15-minute bearer as good forever and have
|
|
1583
|
+
treat a 15-minute bearer as good forever and have users appear signed out
|
|
1515
1584
|
mid-session. `createFoirSessionClient` on the SPA path has read `expires_in`
|
|
1516
1585
|
and scheduled a proactive refresh all along; this closes the asymmetry.
|
|
1517
1586
|
|
|
@@ -1521,10 +1590,10 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1521
1590
|
- **`HostedSessionResult`** exported from `./auth`.
|
|
1522
1591
|
|
|
1523
1592
|
- **`FoirAuthDowngrade`** on `./data` and `./server`, plus `extensions` on the
|
|
1524
|
-
response envelope — Foir now reports when it DISCARDED a
|
|
1593
|
+
response envelope — Foir now reports when it DISCARDED a user token you
|
|
1525
1594
|
sent, and the SDK stops dropping that report on the floor.
|
|
1526
1595
|
|
|
1527
|
-
A
|
|
1596
|
+
A user JWT is optional: one that fails to verify is not refused, the read
|
|
1528
1597
|
is served anonymously and returns `200`. So a lapsed token and a genuinely
|
|
1529
1598
|
anonymous request were indistinguishable — nothing threw, no refresh armed,
|
|
1530
1599
|
and auth-targeted content quietly resolved to its anonymous variant. The
|
|
@@ -1533,7 +1602,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1533
1602
|
```json
|
|
1534
1603
|
{ "data": { "…": "…" },
|
|
1535
1604
|
"extensions": { "auth": {
|
|
1536
|
-
"
|
|
1605
|
+
"userToken": "rejected", "reason": "expired", "servedAs": "anonymous"
|
|
1537
1606
|
} } }
|
|
1538
1607
|
```
|
|
1539
1608
|
|
|
@@ -1553,7 +1622,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1553
1622
|
|
|
1554
1623
|
The GraphQL `events` subscription has been live on the public schema since the
|
|
1555
1624
|
events spine shipped, with no client able to reach it. It is an `sk_`-only
|
|
1556
|
-
|
|
1625
|
+
console surface, so it lives on `./server` beside the rest of the events
|
|
1557
1626
|
client and inherits its `pk_` and `Authorization` refusals unchanged.
|
|
1558
1627
|
|
|
1559
1628
|
New exports: `FoirEventsSubscribeParams`, plus `FoirSubscription` and
|
|
@@ -1575,7 +1644,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1575
1644
|
It collapsed every failure to `null`: no cookie, a `401`, a `502`, a rate
|
|
1576
1645
|
limit, a DNS failure. Consumers have to act on that `null`, and the only
|
|
1577
1646
|
safe-looking action is to clear the session and redirect to the front door —
|
|
1578
|
-
so one blip on `/session/token` signed out a
|
|
1647
|
+
so one blip on `/session/token` signed out a user whose 30-day cookie was
|
|
1579
1648
|
perfectly valid, and nothing recorded that it had happened. The route's own
|
|
1580
1649
|
rate limiter is deliberately fail-open for the opposite reason.
|
|
1581
1650
|
|
|
@@ -1594,7 +1663,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1594
1663
|
`outcome` says what to DO; `reason` says what happened. Only `401` and a
|
|
1595
1664
|
missing cookie are `signed-out`. A `403` reports as `unavailable` with reason
|
|
1596
1665
|
`forbidden` — it means the CSRF header did not arrive, which is a
|
|
1597
|
-
misconfiguration rather than a blip, but signing the
|
|
1666
|
+
misconfiguration rather than a blip, but signing the user out over it
|
|
1598
1667
|
would still be wrong.
|
|
1599
1668
|
|
|
1600
1669
|
- **`foir.ns.operations.onExecution` delivers `status` as the
|
|
@@ -1634,14 +1703,14 @@ Everything additive, so a MINOR either way under the pre-1.0 note.
|
|
|
1634
1703
|
### Added
|
|
1635
1704
|
|
|
1636
1705
|
- **`hostedLogoutUrl(config, clientId, returnTo?)` on `./auth`** — the mirror of
|
|
1637
|
-
`hostedLoginUrl`, and the only thing that actually signs a
|
|
1706
|
+
`hostedLoginUrl`, and the only thing that actually signs a user out on the
|
|
1638
1707
|
hosted-cookie flow.
|
|
1639
1708
|
|
|
1640
1709
|
Login on that flow is symmetric across browser and server; logout was not.
|
|
1641
|
-
The apex `
|
|
1710
|
+
The apex `foir_session` cookie is the durable credential, and
|
|
1642
1711
|
disposing of a bearer, clearing a local session, or calling the
|
|
1643
|
-
`
|
|
1644
|
-
re-establishes the
|
|
1712
|
+
`logout` mutation all leave it in place — the next request
|
|
1713
|
+
re-establishes the user from it. The only surface that ended a session was
|
|
1645
1714
|
`POST /session/logout`, cookie-authenticated and CSRF-header-gated, which only
|
|
1646
1715
|
a browser sending `credentials: 'include'` can reach.
|
|
1647
1716
|
|
|
@@ -2013,7 +2082,7 @@ note above.
|
|
|
2013
2082
|
|
|
2014
2083
|
Reads take the shared, cacheable lane where they can:
|
|
2015
2084
|
`GET /cdn/graphql?key=pk_…` with the persisted-query hash in the URL when the
|
|
2016
|
-
call is a query, on a `pk_` key, with no
|
|
2085
|
+
call is a query, on a `pk_` key, with no user bearer, on the published
|
|
2017
2086
|
channel — and `POST /graphql` otherwise. The server's gate refuses on five
|
|
2018
2087
|
conditions and the client decides all five before the request: the fifth,
|
|
2019
2088
|
"the credential is in the URL", it satisfies by construction. The server is
|
|
@@ -2035,7 +2104,7 @@ note above.
|
|
|
2035
2104
|
hand-written document may legitimately have several. As a document it reaches
|
|
2036
2105
|
everything the credential can reach — including the two dozen root fields no
|
|
2037
2106
|
model and no namespace claims (`designTokens`, `record`, `locales`, the
|
|
2038
|
-
|
|
2107
|
+
user-auth mutations), which is the point of an escape hatch and is more
|
|
2039
2108
|
than the old shape could address.
|
|
2040
2109
|
|
|
2041
2110
|
- **Every op prints, and there is one wire format rather than two.** The
|
|
@@ -2098,19 +2167,19 @@ note above.
|
|
|
2098
2167
|
schema knowledge the client is not allowed to hold. The manifest publishes
|
|
2099
2168
|
each op's result shape, so the table has nothing left to answer. Neither
|
|
2100
2169
|
symbol has appeared in a published release.
|
|
2101
|
-
- **`
|
|
2102
|
-
`AUTH_OPS.
|
|
2103
|
-
email.** The resolver behind it checked that the request carried a
|
|
2104
|
-
returned `true`, and called no RPC; there is no
|
|
2105
|
-
column and no
|
|
2170
|
+
- **`resendVerificationEmail` from `@foir/sdk/auth`** — and the
|
|
2171
|
+
`AUTH_OPS.resendVerificationEmail` entry with it. **It never sent an
|
|
2172
|
+
email.** The resolver behind it checked that the request carried a user id,
|
|
2173
|
+
returned `true`, and called no RPC; there is no user verification-code
|
|
2174
|
+
column and no user verification email in the action catalogue for it to
|
|
2106
2175
|
have used. A caller got a success response every time and nothing was
|
|
2107
2176
|
delivered, which is worse than an error, because there is no signal to act on.
|
|
2108
2177
|
|
|
2109
|
-
**Use the OTP flow for mailbox proof:** `
|
|
2110
|
-
a code, and `
|
|
2178
|
+
**Use the OTP flow for mailbox proof:** `requestOTP(env, email)` emails
|
|
2179
|
+
a code, and `loginWithOTP(env, { email, otp })` redeems it — activating a
|
|
2111
2180
|
pending account on redemption and returning a session, so possession of the
|
|
2112
|
-
mailbox is proven by the same call that signs the
|
|
2113
|
-
delivered path today; `
|
|
2181
|
+
mailbox is proven by the same call that signs the user in. That is a live,
|
|
2182
|
+
delivered path today; `verifyEmail` remains for redeeming a token that
|
|
2114
2183
|
arrived by some other route.
|
|
2115
2184
|
|
|
2116
2185
|
Like the two entries above, this symbol has not appeared in a published
|
|
@@ -2143,11 +2212,11 @@ note above.
|
|
|
2143
2212
|
id are what the note is filed against and never appear on screen.
|
|
2144
2213
|
|
|
2145
2214
|
Opening the panel reads `noteConfig`, which the API charges `notes:write` and
|
|
2146
|
-
refuses to a caller with no
|
|
2215
|
+
refuses to a caller with no session, so one probe settles whether the
|
|
2147
2216
|
visitor is signed in, whether their role may write notes, and whether
|
|
2148
2217
|
screenshot capture may be offered. A missing scope reads like a bad API key and
|
|
2149
|
-
is not one, so it is reported as what it is: the signed-in
|
|
2150
|
-
short `notes:write`, which a new project's default
|
|
2218
|
+
is not one, so it is reported as what it is: the signed-in user's ROLE is
|
|
2219
|
+
short `notes:write`, which a new project's default user role does not
|
|
2151
2220
|
carry.
|
|
2152
2221
|
|
|
2153
2222
|
Screenshot capture is offered only when the project allows it and the browser
|
|
@@ -2177,11 +2246,11 @@ note above.
|
|
|
2177
2246
|
New exports on `./media`: `captureFoirScreen`, `isFoirScreenCaptureSupported`,
|
|
2178
2247
|
`FoirScreenCapture`, `FoirScreenCaptureOptions`.
|
|
2179
2248
|
|
|
2180
|
-
- **Note attachments and note config on `./data`.** `
|
|
2249
|
+
- **Note attachments and note config on `./data`.** `FoirUserNote` gains
|
|
2181
2250
|
`attachments`, `FoirCreateMyNoteArgs` gains an optional `attachmentFileIds`,
|
|
2182
2251
|
and `FoirNoteConfig` is the shape `foir.ns.notes.noteConfig()` answers with.
|
|
2183
2252
|
|
|
2184
|
-
New exports on `./data`: `
|
|
2253
|
+
New exports on `./data`: `FoirUserNoteAttachment`, `FoirNoteConfig`.
|
|
2185
2254
|
|
|
2186
2255
|
- **A subject registry — the records a client fetched, so an annotation can
|
|
2187
2256
|
anchor to the record instead of to a URL.** `createFoirSubjectRegistry()`
|
|
@@ -2225,11 +2294,11 @@ note above.
|
|
|
2225
2294
|
`FoirSubject`, `FoirSubjectEntry`, `FoirSubjectOptions`, `FoirSubjectOrigin`,
|
|
2226
2295
|
`FoirSubjectRegistry`, `FoirRecordPath`.
|
|
2227
2296
|
|
|
2228
|
-
- **Types for the
|
|
2297
|
+
- **Types for the user notes surface.** `foir.ns.notes.myNotes()` and
|
|
2229
2298
|
`foir.ns.notes.createMyNote()` already worked — `notes` is a namespace the
|
|
2230
2299
|
manifest publishes like any other — but their shapes had to be written out by
|
|
2231
2300
|
hand at each call site. They are platform-owned and identical in every project,
|
|
2232
|
-
so they now ship: `
|
|
2301
|
+
so they now ship: `FoirUserNote`, `FoirMyNotesArgs`, `FoirMyNotesResult`,
|
|
2233
2302
|
`FoirCreateMyNoteArgs`, `FoirNoteSubjectType`.
|
|
2234
2303
|
|
|
2235
2304
|
```ts
|
|
@@ -2237,7 +2306,7 @@ note above.
|
|
|
2237
2306
|
```
|
|
2238
2307
|
|
|
2239
2308
|
`FoirNoteSubjectType` is the closed set the platform's write path enforces
|
|
2240
|
-
(`record`, `file`, `model`, `operation`, `schedule`, `
|
|
2309
|
+
(`record`, `file`, `model`, `operation`, `schedule`, `user`) and is never a
|
|
2241
2310
|
model key — a note on a record is `'record'` plus the key on
|
|
2242
2311
|
`subjectModelKey`. Like every mirrored platform table in `builtins.ts`, a kind
|
|
2243
2312
|
added upstream widens this union in a minor release.
|
|
@@ -2394,12 +2463,12 @@ note above.
|
|
|
2394
2463
|
`FoirEventsClientConfig`, `FoirEventsListParams`, `FoirEventsStreamParams`.
|
|
2395
2464
|
|
|
2396
2465
|
**On `@foir/sdk/server` and no other subpath, deliberately.** The event log is
|
|
2397
|
-
an
|
|
2398
|
-
|
|
2466
|
+
an USER surface (DECIDED 2026-08-04): reachable by an `sk_` key or
|
|
2467
|
+
a console session, never by a user token, and `events:list.project` is not
|
|
2399
2468
|
mintable on a `pk_` key at all. `events.view` is plain tenant+project
|
|
2400
2469
|
containment while `record.view` carries owner/grantee/borrow terms on top, so a
|
|
2401
2470
|
containment-only read over the log describes rows whose records the same caller
|
|
2402
|
-
could not read — and each row's `actor` names which *other*
|
|
2471
|
+
could not read — and each row's `actor` names which *other* user acted.
|
|
2403
2472
|
`createEventsClient` therefore throws on a `pk_` key and on an `Authorization`
|
|
2404
2473
|
header rather than letting either reach the wire, and an import-graph test
|
|
2405
2474
|
fails if the module becomes reachable from a subpath that ships to a browser.
|
|
@@ -2420,12 +2489,12 @@ note above.
|
|
|
2420
2489
|
is the entire cost of GraphQL paid for one screen. Ten root fields closed:
|
|
2421
2490
|
|
|
2422
2491
|
`getAuthConfig`, `getDefaultAuthProvider`, `currentUser`, `myProviderToken`,
|
|
2423
|
-
`
|
|
2424
|
-
`
|
|
2492
|
+
`logout`, `requestPasswordReset`, `resetPassword`,
|
|
2493
|
+
`updatePassword`, `verifyEmail`, `deleteMyAccount`.
|
|
2425
2494
|
|
|
2426
2495
|
Plus the session-layer pairings for the flows that need one:
|
|
2427
2496
|
`resetPasswordAndSignIn` (redeem the emailed token and persist the session it
|
|
2428
|
-
mints — the peer of `loginWithPassword` for the branch where the
|
|
2497
|
+
mints — the peer of `loginWithPassword` for the branch where the user
|
|
2429
2498
|
cannot log in), `signOut` (revoke server-side, then clear locally, and clear
|
|
2430
2499
|
locally even if the revoke fails), and `authedOp` (`authedRequest`'s twin for
|
|
2431
2500
|
auth ops: refresh once, retry once, on a 401-shaped failure).
|
|
@@ -2442,10 +2511,10 @@ note above.
|
|
|
2442
2511
|
### Removed
|
|
2443
2512
|
|
|
2444
2513
|
- **The nine auth operation documents on `@foir/sdk/auth`** —
|
|
2445
|
-
`
|
|
2446
|
-
`
|
|
2447
|
-
`
|
|
2448
|
-
`
|
|
2514
|
+
`UserLoginDocument`, `UserRegisterDocument`,
|
|
2515
|
+
`UserRefreshTokenDocument`, `UserRequestOTPDocument`,
|
|
2516
|
+
`UserLoginOTPDocument`, `UserLoginWithProviderDocument`,
|
|
2517
|
+
`UserProviderCallbackDocument`, `UserProviderVerifyOTPDocument`,
|
|
2449
2518
|
`AuthProvidersDocument`.
|
|
2450
2519
|
|
|
2451
2520
|
**Breaking, so the next release is a MINOR** per the pre-1.0 note above.
|
|
@@ -2457,15 +2526,15 @@ note above.
|
|
|
2457
2526
|
eleven were unreachable. The flows now address the server's generated `auth`
|
|
2458
2527
|
namespace, so `@foir/sdk/auth` carries no transport of its own.
|
|
2459
2528
|
|
|
2460
|
-
*Migration.* Nothing that called a FUNCTION changes: `
|
|
2529
|
+
*Migration.* Nothing that called a FUNCTION changes: `login(env, …)`,
|
|
2461
2530
|
`getAuthProviders(env)`, `loginWithPassword(adapter, env, …)`, `refreshSession`,
|
|
2462
2531
|
`requireAuth`, `authedRequest` and every React hook in `@foir/sdk/react` keep
|
|
2463
2532
|
their exact signatures. Only code importing a `*Document` const is affected,
|
|
2464
2533
|
and only if it was executing the document itself through its own GraphQL
|
|
2465
2534
|
client. Two options:
|
|
2466
2535
|
|
|
2467
|
-
- call the SDK function instead (`
|
|
2468
|
-
`
|
|
2536
|
+
- call the SDK function instead (`login(env, vars)` rather than running
|
|
2537
|
+
`UserLoginDocument` through Apollo/urql), which is what the exports were
|
|
2469
2538
|
for; or
|
|
2470
2539
|
- keep your own client and write the document in your own codebase — these are
|
|
2471
2540
|
fixed-shape platform operations, so the text does not vary by project.
|
|
@@ -2529,7 +2598,7 @@ actually happened.
|
|
|
2529
2598
|
|
|
2530
2599
|
Purely additive: `./auth` keeps exporting all four, unchanged, and nothing is
|
|
2531
2600
|
moved or renamed. What changes is the cost of reaching them. `./auth` also
|
|
2532
|
-
exports the
|
|
2601
|
+
exports the user-auth GraphQL documents, which build their
|
|
2533
2602
|
TypedDocumentNodes with a top-level `parse()` that no bundler can treat as
|
|
2534
2603
|
pure — so a browser app importing only `createFoirSessionClient` was shipping
|
|
2535
2604
|
the whole `graphql` package to get it. `src/auth/session-client.ts` imports
|
|
@@ -2590,7 +2659,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2590
2659
|
|
|
2591
2660
|
### Added
|
|
2592
2661
|
|
|
2593
|
-
- `@foir/sdk/auth`: `
|
|
2662
|
+
- `@foir/sdk/auth`: `signupContext` and `joinWaitlist`, plus the
|
|
2594
2663
|
`SignupContext`, `SignupMode` and `WaitlistJoinResult` types — waitlist and
|
|
2595
2664
|
restricted-signup flows for projects that are not open-registration.
|
|
2596
2665
|
- A pinned public-export surface: `public-exports.snapshot.json` plus
|
|
@@ -2617,7 +2686,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2617
2686
|
|
|
2618
2687
|
### Added
|
|
2619
2688
|
|
|
2620
|
-
- `@foir/sdk/client`: `createFoirContentClient` — forwards the
|
|
2689
|
+
- `@foir/sdk/client`: `createFoirContentClient` — forwards the user bearer
|
|
2621
2690
|
token on content reads, so authenticated and segment-targeted content works
|
|
2622
2691
|
without hand-rolling the header.
|
|
2623
2692
|
|
|
@@ -2634,7 +2703,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2634
2703
|
|
|
2635
2704
|
### Added
|
|
2636
2705
|
|
|
2637
|
-
- **New subpath `@foir/sdk/react`** — drop-in React
|
|
2706
|
+
- **New subpath `@foir/sdk/react`** — drop-in React user auth: `FoirProvider`,
|
|
2638
2707
|
`SignIn`, `OAuthButtons`, `UserButton`, `useFoirAuth`, `useFoirSession`,
|
|
2639
2708
|
`useFoirUser`.
|
|
2640
2709
|
- **New subpath `@foir/sdk/next`** — Next.js App Router BFF auth adapter: cookie
|
|
@@ -2662,7 +2731,7 @@ First public release. Ten subpaths:
|
|
|
2662
2731
|
- `@foir/sdk/media` — progressive media components and adapters.
|
|
2663
2732
|
- `@foir/sdk/content` — `createContentRenderer` and `RichTextRenderer` with
|
|
2664
2733
|
injected typography tokens.
|
|
2665
|
-
- `@foir/sdk/auth` —
|
|
2734
|
+
- `@foir/sdk/auth` — user auth flows, token lifecycle, hosted-login bridge.
|
|
2666
2735
|
- `@foir/sdk/realtime` — `createCollabSession`, a thin y-websocket helper.
|
|
2667
2736
|
- `@foir/sdk/codegen`, `@foir/sdk/codegen/block-registry`,
|
|
2668
2737
|
`@foir/sdk/codegen/block-scaffold` — the `foirCodegen` preset and the
|