@foir/sdk 0.23.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 +179 -154
- 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/auth/auth.d.ts +19 -12
- package/dist/auth/hosted.d.ts +5 -0
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/operations.d.ts +11 -12
- 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 +8 -9
- package/dist/components/EditorProvider.d.ts +1 -1
- package/dist/next.js +0 -1
- package/dist/push.js +0 -1
- package/dist/react.js +3 -3
- package/dist/server/events-client.d.ts +2 -2
- package/dist/server.js +2 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,33 @@ 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
|
+
|
|
111
138
|
## [0.23.0] — 2026-10-16
|
|
112
139
|
|
|
113
140
|
### Added
|
|
@@ -186,23 +213,23 @@ Changes on `dev` that have not been published to npm.
|
|
|
186
213
|
|
|
187
214
|
### Changed
|
|
188
215
|
|
|
189
|
-
- **Breaking: everyone who signs in is a user, and
|
|
190
|
-
|
|
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
|
|
191
218
|
names; an earlier platform refuses every call below by name.
|
|
192
219
|
- `@foir/sdk/auth`: the raw calls take the server's op names: `login`,
|
|
193
220
|
`register`, `requestOTP`, `loginWithOTP`, `refreshTokens` (the
|
|
194
221
|
`refreshToken` op), `logout`, `joinWaitlist`, `signupContext`,
|
|
195
222
|
`requestPasswordReset`, `resetPassword`, `updatePassword`, `verifyEmail`,
|
|
196
|
-
`loginWithProvider`, `providerCallback`, `providerVerifyOTP
|
|
197
|
-
|
|
198
|
-
organization over the
|
|
199
|
-
`
|
|
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
|
|
200
227
|
`signInWithPassword`, `signUpWithPassword` and `signInWithOTP` (were
|
|
201
228
|
`loginWithPassword`, `registerWithPassword`, `loginWithOTP`), beside
|
|
202
|
-
`signOut`.
|
|
203
|
-
`userAccess
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
206
233
|
earlier version reads as signed out once.
|
|
207
234
|
- `@foir/sdk/auth`: a provider sign-in that names an address somebody
|
|
208
235
|
already holds waits for the code sent to it: `providerSignInRefusalOf`
|
|
@@ -211,23 +238,23 @@ Changes on `dev` that have not been published to npm.
|
|
|
211
238
|
- `@foir/sdk/auth`: the hosted session answers `userId`
|
|
212
239
|
(`HostedSession.userId`, `FoirSessionUser.userId`), from the platform's
|
|
213
240
|
`user_id`, and its cookie is `foir_session`.
|
|
214
|
-
- `@foir/sdk/react`: `useFoirSession()` answers `user
|
|
215
|
-
- `@foir/sdk/app`: `records.user` and `users
|
|
216
|
-
`
|
|
241
|
+
- `@foir/sdk/react`: `useFoirSession()` answers `user`.
|
|
242
|
+
- `@foir/sdk/app`: `records.user` and `users`, over the platform's
|
|
243
|
+
`saveUserRecord`, `userRecordByIdentity`,
|
|
217
244
|
`eraseUserRecord`, `createUser`, `user`, `users`, `userByIdentity` and
|
|
218
245
|
`userIdentitiesById`. `users.list()` answers `{ users }`. `AppUser`,
|
|
219
246
|
`AppUserList`, `AppUserSaveResult` (`userCreated`), `AppUserEraseResult`
|
|
220
247
|
(`userDeleted`, `userKeptBecause`), `AppUserEmailUnavailableReason`,
|
|
221
|
-
`appUserUnlinkedOf` and `appUserEmailUnavailableOf`
|
|
222
|
-
|
|
248
|
+
`appUserUnlinkedOf` and `appUserEmailUnavailableOf` are the app client's
|
|
249
|
+
names for the people it reads. A notification, a consent change and an
|
|
223
250
|
organization's first member name `userId`.
|
|
224
251
|
- `@foir/sdk/server`: an operation's context names who started the run as
|
|
225
|
-
`actorId` and the user it acts for as `userId` (
|
|
226
|
-
|
|
252
|
+
`actorId` and the user it acts for as `userId` (`userId` used to name
|
|
253
|
+
who started it). `ScopedTokenIngress` names the user as `userId`.
|
|
227
254
|
- `@foir/sdk/data`: `FoirGranteeKind` is `'USER'`, a note's subject may be
|
|
228
|
-
`'user'`,
|
|
229
|
-
`
|
|
230
|
-
`
|
|
255
|
+
`'user'`, a note of a user's is `FoirUserNote` with
|
|
256
|
+
`FoirUserNoteAttachment`, and `FoirAuthDowngrade` names the dropped
|
|
257
|
+
token `userToken`.
|
|
231
258
|
|
|
232
259
|
### Removed
|
|
233
260
|
|
|
@@ -235,8 +262,9 @@ Changes on `dev` that have not been published to npm.
|
|
|
235
262
|
(`@foir/sdk/auth`). Nothing stores where a user stands: a request names the
|
|
236
263
|
organization it acts in (`orgId` or `orgUrl`), and the platform checks they
|
|
237
264
|
stand there on that request. `myOrganizations` still lists where they may.
|
|
238
|
-
- **Breaking:
|
|
239
|
-
`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:
|
|
240
268
|
guard on it with `requireRole` or `requireScope`. `requireAuth` takes the
|
|
241
269
|
type `resolveUser` answers, for a project that reads its own profile.
|
|
242
270
|
|
|
@@ -246,7 +274,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
246
274
|
|
|
247
275
|
- **An operation provisions an organization** (`@foir/sdk/app`).
|
|
248
276
|
`organizations.provision({ provisionId, name, parentId?, settings?, firstMember? })`
|
|
249
|
-
makes an organization with a
|
|
277
|
+
makes an organization with a user as its first member, in the role the
|
|
250
278
|
project names for a founder, and answers `{ organizationId }`. It needs
|
|
251
279
|
`organizations:create`. With `settings: { modelKey, data }` it writes the
|
|
252
280
|
organization's settings record too and answers `settingsRecordId`; that
|
|
@@ -261,7 +289,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
261
289
|
needs the platform release that carries `scheduleOrganizationDeletion`.
|
|
262
290
|
- **`FoirWriteNoteArgs`** (`@foir/sdk/data`): the arguments of
|
|
263
291
|
`foir.ns.notes.writeNote`, the one field that writes a note for a signed-in
|
|
264
|
-
|
|
292
|
+
user and for a visitor. It asks `notes:create` and needs the platform
|
|
265
293
|
release that carries `writeNote`. `foir.ns.notes` answers any field the
|
|
266
294
|
platform serves, so the call itself needs no new SDK.
|
|
267
295
|
|
|
@@ -288,7 +316,7 @@ Changes on `dev` that have not been published to npm.
|
|
|
288
316
|
itself uses `createServerClient` from `@foir/sdk/server`. A key minted
|
|
289
317
|
before a key had a kind (`sk_`) is not refused here.
|
|
290
318
|
- **A key's kind is read from its prefix in one place.** A key is publishable
|
|
291
|
-
(`foir_pub_`), server (`foir_srv_`) or
|
|
319
|
+
(`foir_pub_`), server (`foir_srv_`) or the console (`foir_adm_`), and a key minted
|
|
292
320
|
earlier is `pk_`, `sk_` or `tc_`. Only a publishable key, in either format,
|
|
293
321
|
is named in a URL and takes the shared cache lane. Earlier versions looked
|
|
294
322
|
for the letters `pk_` alone, so a `foir_pub_` key was sent to the origin and
|
|
@@ -352,7 +380,7 @@ to this version then.
|
|
|
352
380
|
reads it as what those were worth, exactly as the platform does.
|
|
353
381
|
|
|
354
382
|
A record grant reaches the operation's own records: the project's, or those
|
|
355
|
-
of the
|
|
383
|
+
of the user a run acts for. `records:read.all:<model>`, and the write,
|
|
356
384
|
delete and publish forms like it, reach every owner's records of the model.
|
|
357
385
|
An older token's write of a model was every owner's, so it is read as
|
|
358
386
|
`records:write.all:<model>` and `records:create:<model>`, and takes that
|
|
@@ -381,19 +409,19 @@ to this version then.
|
|
|
381
409
|
platform holds a running install's setting to a category the project has.
|
|
382
410
|
|
|
383
411
|
- **An app can say what connecting changes** (`@foir/sdk/app/contract`).
|
|
384
|
-
`Credentials.connectNotice` is the app's own words, shown to the
|
|
412
|
+
`Credentials.connectNotice` is the app's own words, shown to the user
|
|
385
413
|
beside the step that connects their account and, for an install the vendor
|
|
386
414
|
started, before the install is confirmed. A vendor that keeps one live
|
|
387
415
|
connection per account (Shopify, per store) says here that connecting moves
|
|
388
416
|
it from any other project. Empty when there is nothing to say.
|
|
389
417
|
|
|
390
|
-
- **The roles a signed-in
|
|
391
|
-
(`@foir/sdk/auth`, `@foir/sdk/react`). A storefront can now ask which
|
|
392
|
-
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
|
|
393
421
|
it had to treat one of a role's permissions as a stand-in for the role.
|
|
394
422
|
|
|
395
|
-
- `getSessionAccess(adapter, env)` and `
|
|
396
|
-
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
|
|
397
425
|
(`roles`), and everything they may do through the key (`scopes`). Plain
|
|
398
426
|
data, so a loader can return it. `null` when nobody is signed in.
|
|
399
427
|
- `hasRole(access, key)` and `hasScope(access, scope)` read it. `hasScope`
|
|
@@ -412,7 +440,7 @@ to this version then.
|
|
|
412
440
|
and a role taken away is gone the next time it is asked.
|
|
413
441
|
|
|
414
442
|
`roles` is `null`, not an empty list, when the platform did not say which
|
|
415
|
-
roles the
|
|
443
|
+
roles the user holds. `hasRole` answers false for it and `requireRole`
|
|
416
444
|
refuses with `roles_unknown`, so an outage is never read as "not staff". A
|
|
417
445
|
platform that does not serve the field yet reads the same way: the role
|
|
418
446
|
helpers need the platform release that carries it.
|
|
@@ -424,7 +452,7 @@ to this version then.
|
|
|
424
452
|
`accessStatus`, `loadAccess` and `ensureAccess`.
|
|
425
453
|
|
|
426
454
|
- **A save says when it made a record the operation may not read**
|
|
427
|
-
(`@foir/sdk/app`). `records.upsert` and `records.
|
|
455
|
+
(`@foir/sdk/app`). `records.upsert` and `records.user.save` answer
|
|
428
456
|
`fieldsWithheld`. It is true when the operation declared the create of a
|
|
429
457
|
model and not the read: the save made the record, and `record` carries its
|
|
430
458
|
`id` and `modelKey` and nothing else. The record is not empty.
|
|
@@ -440,13 +468,13 @@ to this version then.
|
|
|
440
468
|
app's operations are given the one they could use, so an installed app keeps
|
|
441
469
|
running; a manifest served from then on says it.
|
|
442
470
|
|
|
443
|
-
- **Breaking, with the platform release that carries it: erasing a
|
|
444
|
-
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
|
|
445
473
|
`users:create` beside the delete and the read of the model, and now needs
|
|
446
474
|
`identities:delete` there. `APP_GRANTS` carries it, with `files:delete`,
|
|
447
475
|
`notifications:read` and every owner's files (`files:read.all`,
|
|
448
476
|
`files:write.all`, `files:delete.all`), which a manifest may now declare.
|
|
449
|
-
`verifyScopedToken` reads a token minted before, that carried the
|
|
477
|
+
`verifyScopedToken` reads a token minted before, that carried the user
|
|
450
478
|
create beside the delete of a model, as holding `identities:delete`, as the
|
|
451
479
|
platform does.
|
|
452
480
|
|
|
@@ -464,17 +492,14 @@ to this version then.
|
|
|
464
492
|
| `config:read` | `appconfig:read` |
|
|
465
493
|
| `secrets:read.app`, `secrets:put.app`, `secrets:list.app`, `secrets:delete.app` | `appsecrets:read`, `appsecrets:put`, `appsecrets:list`, `appsecrets:delete` |
|
|
466
494
|
| `files:list` | `files:read` |
|
|
467
|
-
|
|
|
468
|
-
| `customers:read` | `users:read.all` |
|
|
469
|
-
| `customers:read:named` | `users:read.named` |
|
|
470
|
-
| `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` |
|
|
471
496
|
| `notifications:write` | `notifications:write.named` |
|
|
472
497
|
| `records:write:<model>` | `records:write:<model>` and `records:create:<model>` |
|
|
473
498
|
|
|
474
499
|
The last row is a change in what the name means. `records:write:<model>`
|
|
475
500
|
changes a record that is there, and making one is `records:create:<model>`.
|
|
476
501
|
An operation that does both declares both. `records.upsert` and
|
|
477
|
-
`records.
|
|
502
|
+
`records.user.save` are let through on either, and the platform asks for
|
|
478
503
|
the one the save turns out to need.
|
|
479
504
|
|
|
480
505
|
- **Breaking, with the platform release that carries it: a full-page placement
|
|
@@ -487,7 +512,7 @@ to this version then.
|
|
|
487
512
|
things change in what a placement writes:
|
|
488
513
|
|
|
489
514
|
- **Every owner's records are written `.all`.** `records:read:order` was
|
|
490
|
-
every order and is now the
|
|
515
|
+
every order and is now the user's own. Declare
|
|
491
516
|
`records:read.all:order`.
|
|
492
517
|
- **A write no longer includes its read.** `records:write:order` also read
|
|
493
518
|
and created, and `apps:write` also read apps. Declare each one the page
|
|
@@ -496,8 +521,8 @@ to this version then.
|
|
|
496
521
|
- **A wildcard and a permission that gives others are refused**, as `*`
|
|
497
522
|
always was.
|
|
498
523
|
|
|
499
|
-
The token then carries what the placement declares and the
|
|
500
|
-
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
|
|
501
526
|
given that part, where before the whole name was withheld.
|
|
502
527
|
|
|
503
528
|
- **`session.can` answers by what the token covers** (`@foir/sdk/placement`).
|
|
@@ -538,11 +563,11 @@ to this version then.
|
|
|
538
563
|
`records:read` for the model beside their own permission.
|
|
539
564
|
- An operation that declares the create of a model and not the read
|
|
540
565
|
(`@foir/sdk/app`) still makes its records. `records.upsert` landing on a
|
|
541
|
-
record already there, `records.
|
|
566
|
+
record already there, `records.user.save` likewise, `records.delete`,
|
|
542
567
|
`records.refresh`, `records.rename` and `records.updateMany` are refused
|
|
543
568
|
until it declares the read too.
|
|
544
569
|
|
|
545
|
-
- **Breaking, with the platform release that carries it: `records.
|
|
570
|
+
- **Breaking, with the platform release that carries it: `records.user.erase`
|
|
546
571
|
needs the delete of the model** (`@foir/sdk/app`). An erase removes a record,
|
|
547
572
|
so the operation declares `records:delete` and `records:read` for the model,
|
|
548
573
|
and `users:create`. Until now the grant to save the record was enough. An
|
|
@@ -554,14 +579,14 @@ to this version then.
|
|
|
554
579
|
`MyOrganizationRole.permissions` is gone and `MyOrganizationRole.grants`
|
|
555
580
|
takes its place: what the role gives, in the permission catalogue's names. A
|
|
556
581
|
grant stands for itself, so changing a record and creating one are two
|
|
557
|
-
names (`records:write`, `records:create`), reading every
|
|
582
|
+
names (`records:write`, `records:create`), reading every user's records
|
|
558
583
|
is `records:read.all`, and staffing an organization is `members:read` and
|
|
559
584
|
`members:write`. Code that looked for `organizations:staff` or
|
|
560
585
|
`records:list.all` in a role's list looks for the new names.
|
|
561
586
|
|
|
562
|
-
- **Breaking, with the platform release that carries it: `
|
|
563
|
-
and `
|
|
564
|
-
(`@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
|
|
565
590
|
includes them.
|
|
566
591
|
|
|
567
592
|
### Removed
|
|
@@ -632,29 +657,29 @@ the next promotion; `latest` moves to this version then.
|
|
|
632
657
|
(`@foir/sdk/data`). Code that reads a drift is unaffected. Code that builds
|
|
633
658
|
one by hand, for example to test a handler given to `onSchemaDrift` or made by
|
|
634
659
|
`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 `{
|
|
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
|
|
639
664
|
`AppConsentChange` no longer compiles: intersect with it instead.
|
|
640
665
|
|
|
641
666
|
### Added
|
|
642
667
|
|
|
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
|
|
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
|
|
646
671
|
at the install's level, or makes one, together with the change, so a change
|
|
647
672
|
it refuses (a category the project does not have, a confirmation email the
|
|
648
|
-
project cannot send) leaves no
|
|
649
|
-
|
|
650
|
-
`
|
|
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
|
|
651
676
|
exported.
|
|
652
677
|
|
|
653
678
|
- `FoirSchemaDrift.incompatible`: what the generated module describes that the
|
|
654
679
|
server no longer serves that way, as sorted paths into the manifest
|
|
655
680
|
(`models.order.selection.status`, `models.order.ops.update`,
|
|
656
681
|
`namespaces.search.ops.searchRecords.args.query`,
|
|
657
|
-
`models.order.relations.
|
|
682
|
+
`models.order.relations.buyer`, `selectionUnions.Block.HeroBlock.title`).
|
|
658
683
|
Empty when the surface has only grown. `changed` keeps its meaning: what
|
|
659
684
|
moved, not what broke.
|
|
660
685
|
|
|
@@ -665,26 +690,26 @@ platform serves from the next promotion; `latest` moves to it then.
|
|
|
665
690
|
|
|
666
691
|
### Changed
|
|
667
692
|
|
|
668
|
-
- **Breaking: an app finds a
|
|
669
|
-
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
|
|
670
695
|
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
|
|
696
|
+
walk it no longer name a model and have moved beside the other user
|
|
672
697
|
reads:
|
|
673
|
-
- `records.
|
|
674
|
-
`
|
|
675
|
-
- `
|
|
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
|
|
676
701
|
an identity at the app's own system, or `null`. It replaces reading a
|
|
677
|
-
|
|
702
|
+
user's record for its owner.
|
|
678
703
|
|
|
679
|
-
Both need the new capability `
|
|
704
|
+
Both need the new capability `identities:read` (or `users:read.all`),
|
|
680
705
|
declared by the operation, and are refused at an install that pairs no
|
|
681
706
|
sign-in. Reading or writing a model is no longer what admits them.
|
|
682
|
-
- `install.config()` asks the platform for `
|
|
707
|
+
- `install.config()` asks the platform for `userSignIn`, a field a platform
|
|
683
708
|
older than this release does not have.
|
|
684
709
|
|
|
685
710
|
### Added
|
|
686
711
|
|
|
687
|
-
- `install.config()` answers `
|
|
712
|
+
- `install.config()` answers `userSignIn`: the key of the user sign-in
|
|
688
713
|
the install pairs, or `null` when it pairs none.
|
|
689
714
|
|
|
690
715
|
## [0.17.0] — 2026-10-02
|
|
@@ -694,19 +719,19 @@ platform serves from the next promotion; `latest` moves to it then.
|
|
|
694
719
|
|
|
695
720
|
### Added
|
|
696
721
|
|
|
697
|
-
- **An app is told when a project cannot email its
|
|
722
|
+
- **An app is told when a project cannot email its users**
|
|
698
723
|
(`@foir/sdk/app`). A project with no App base URL used to accept a
|
|
699
724
|
notification's email and drop it. The platform now refuses a notification
|
|
700
725
|
that carries an email, and an opt-in to a category confirmed by email, with
|
|
701
|
-
the reason as data. `
|
|
726
|
+
the reason as data. `appUserEmailUnavailableOf(error)` reads it:
|
|
702
727
|
`{ 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. `
|
|
728
|
+
- `install.config()` answers `userEmailUnavailable`: the same reason ahead
|
|
729
|
+
of time, or `null` when the project can send. `AppUserEmailUnavailableReason`
|
|
705
730
|
is the type.
|
|
706
731
|
|
|
707
732
|
### Changed
|
|
708
733
|
|
|
709
|
-
- `install.config()` asks the platform for `
|
|
734
|
+
- `install.config()` asks the platform for `userEmailUnavailable`, a field
|
|
710
735
|
a platform older than this release does not have.
|
|
711
736
|
|
|
712
737
|
## [0.16.0] — 2026-10-02
|
|
@@ -717,11 +742,11 @@ it at the next promotion.
|
|
|
717
742
|
### Added
|
|
718
743
|
|
|
719
744
|
- **An embedded page is told how the console looks** (`@foir/sdk/placement`).
|
|
720
|
-
The console's theme is the
|
|
745
|
+
The console's theme is the user's own setting, which a framed page cannot
|
|
721
746
|
see, so a page that followed the system's preference could sit dark inside a
|
|
722
747
|
light console. `connectPlacement` takes `onAppearance`, called with
|
|
723
748
|
`{ theme: 'light' | 'dark' }` as soon as the console says (before the token
|
|
724
|
-
arrives) and again whenever the
|
|
749
|
+
arrives) and again whenever the user changes it; `session.appearance`
|
|
725
750
|
holds the current one. `PlacementAppearance` is the type, and
|
|
726
751
|
`PLACEMENT_MESSAGE.appearance` the message.
|
|
727
752
|
- A console older than this says nothing: `onAppearance` is not called and
|
|
@@ -739,7 +764,7 @@ then.
|
|
|
739
764
|
- **`notifications.setConsent` answers `confirmationPending`**, and takes
|
|
740
765
|
`givenElsewhere` and `locale` (`@foir/sdk/app`). A project can ask that an
|
|
741
766
|
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
|
|
767
|
+
In such a category an opt-in writes nothing: the platform sends the user
|
|
743
768
|
one confirmation email, in the page's `locale` when they have stated no
|
|
744
769
|
language of their own, and the answer says `confirmationPending`. The
|
|
745
770
|
preference turns on when they follow the link. Everywhere else an opt-in is
|
|
@@ -760,17 +785,17 @@ then.
|
|
|
760
785
|
|
|
761
786
|
### Added
|
|
762
787
|
|
|
763
|
-
- **`
|
|
764
|
-
client (`@foir/sdk/app`): an operation reads the
|
|
765
|
-
`get(id)` and `byEmail(email)` answer one
|
|
766
|
-
`
|
|
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
|
|
767
792
|
exact email address at the install's level, and nothing wider. `list` pages
|
|
768
|
-
through the
|
|
793
|
+
through the users at the install's level and needs `users:read.all`,
|
|
769
794
|
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
|
|
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
|
|
774
799
|
something that should wait for proof asks the flag first.
|
|
775
800
|
|
|
776
801
|
## [0.13.0] — 2026-10-01
|
|
@@ -797,7 +822,7 @@ then.
|
|
|
797
822
|
order, at most `first` of them, with `hasMore` saying the filter matched more.
|
|
798
823
|
For work an app hands out in turn, such as the next ten people waiting: list
|
|
799
824
|
them, then update the ones listed by id. A model's list is named by its
|
|
800
|
-
plural, which its
|
|
825
|
+
plural, which its user may set, so the first list a client makes reads
|
|
801
826
|
the data API's surface descriptor (`GET /v1/manifest`) as the same token,
|
|
802
827
|
once. `AppRecordList` is its query's type.
|
|
803
828
|
|
|
@@ -822,12 +847,12 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
822
847
|
`whoami`, `install.config`, `install.credentials`,
|
|
823
848
|
`install.writeCredentials`, `install.reportStatus`, `records.upsert`,
|
|
824
849
|
`read`, `delete`, `refresh`, `rename`, `updateMany` and `aggregate`,
|
|
825
|
-
`records.
|
|
826
|
-
`
|
|
850
|
+
`records.user.save`, `read`, `erase` and `identities`,
|
|
851
|
+
`users.ensure`, `notifications.send` and `notifications.setConsent`, and
|
|
827
852
|
`request` for a document of your own. Nothing is checked before a call: what
|
|
828
853
|
the token may do is the platform's to hold it to, and a refusal is a
|
|
829
854
|
`FoirRequestError` carrying the platform's answer.
|
|
830
|
-
- `appLookupConflictOf`, `
|
|
855
|
+
- `appLookupConflictOf`, `appUserUnlinkedOf` and `isAppFieldWithheld` read
|
|
831
856
|
the refusals an app acts on, as data.
|
|
832
857
|
- `readAppDispatch` and `readCredentialProbe` read the two bodies the platform
|
|
833
858
|
posts to an app, and `redeemExchangeTicket` trades an OAuth exchange ticket
|
|
@@ -855,7 +880,7 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
855
880
|
what it throws when it has no keys to judge a token by.
|
|
856
881
|
- **`requireScopedToken` answers more about who is calling.** The ingress it
|
|
857
882
|
resolves with gains `orgId` (the organisation level the token was minted at,
|
|
858
|
-
`null` when it states none), `
|
|
883
|
+
`null` when it states none), `userId`, `expiresAt` and `tokenId`.
|
|
859
884
|
`ScopedTokenError` gains `reason` (`missing`, `invalid`, `forbidden` or
|
|
860
885
|
`unavailable`), and its `status` may now be 503: keys that cannot be fetched
|
|
861
886
|
are this service's outage, which the platform retries, and not a verdict on
|
|
@@ -867,7 +892,7 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
867
892
|
passed. It takes `apiUrl`, used when a dispatch names no address to report
|
|
868
893
|
to, `retry`, and `signal`. A call the platform did not answer, or answered
|
|
869
894
|
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
|
|
895
|
+
- **`@foir/sdk/placement`**, a new subpath for an app's own console shown
|
|
871
896
|
inside Foir's as a full-page placement. `connectPlacement` tells the console
|
|
872
897
|
the page is ready, takes the token it posts into the frame (only from the
|
|
873
898
|
consoles you list), keeps it fresh, and gives you `request`, `can` and
|
|
@@ -926,8 +951,8 @@ production serves from the next promotion; `latest` moves to it then.
|
|
|
926
951
|
once and asks again, failing with `FoirSessionExpiredError` when the session
|
|
927
952
|
is over.
|
|
928
953
|
- A request that sent no bearer is untouched.
|
|
929
|
-
- **A refresh that names no
|
|
930
|
-
`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
|
|
931
956
|
refresh answered without them left a valid token beside an empty identity, and
|
|
932
957
|
`requireAuth` turned the session away. Given `env`, `requireAuth` now reads a
|
|
933
958
|
missing identity once from `currentUser` rather than failing.
|
|
@@ -985,10 +1010,10 @@ surface version instead of the schema counter.
|
|
|
985
1010
|
credential.
|
|
986
1011
|
|
|
987
1012
|
Two things a dispatch client does not do. It does not subscribe: the
|
|
988
|
-
subscription socket authenticates a key,
|
|
1013
|
+
subscription socket authenticates a key, a console user token or a user bearer
|
|
989
1014
|
and has no path for a dispatch token, so `onChange` and friends throw, naming
|
|
990
1015
|
why. And it takes no key-side option (`apiKey`, `bearerToken`, `orgId`,
|
|
991
|
-
`channel`, the socket options), because the token carries its own
|
|
1016
|
+
`channel`, the socket options), because the token carries its own user,
|
|
992
1017
|
organization and channel; each is a compile error and a runtime refusal.
|
|
993
1018
|
|
|
994
1019
|
- `readDispatch(request)` and `OperationPayloadError` on `@foir/sdk/server`.
|
|
@@ -1015,7 +1040,7 @@ surface version instead of the schema counter.
|
|
|
1015
1040
|
|
|
1016
1041
|
- `FoirRecordOwner` on `@foir/sdk/data`: `{ id, ownerId, ownerKind }`, what
|
|
1017
1042
|
`setOwner` answers with. An operation that declares `records:attribute:<model>`
|
|
1018
|
-
hands a record to another
|
|
1043
|
+
hands a record to another user with `foir.<model>.setOwner({ id, ownerId })`,
|
|
1019
1044
|
and names the owner of a record it creates with `create({ input, ownerId })`.
|
|
1020
1045
|
The answer carries the owner and not the record, because an operation acting
|
|
1021
1046
|
for the previous holder may no longer read it.
|
|
@@ -1259,14 +1284,14 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1259
1284
|
silent wrong answer for a thrown one that discards the answer. Nothing changes
|
|
1260
1285
|
for a caller that does not pass it.
|
|
1261
1286
|
|
|
1262
|
-
- `registerOrganization(env, { key, name }, { bearerToken })` on `@foir/sdk/auth`: a
|
|
1263
|
-
- **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`
|
|
1264
1289
|
and `selectOrganization`, plus the types `MyOrganization` and
|
|
1265
1290
|
`SelectedOrganization`, and two entries on `AUTH_OPS`.
|
|
1266
1291
|
|
|
1267
1292
|
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
|
|
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
|
|
1270
1295
|
own membership rows, so it is not the project's inventory and cannot be used as
|
|
1271
1296
|
one — and `selectOrganization({ orgId })` says which. An empty `orgId` clears
|
|
1272
1297
|
the selection.
|
|
@@ -1276,7 +1301,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1276
1301
|
caller's membership on every request. An application therefore cannot forget
|
|
1277
1302
|
to send it, cannot send another organization's, and cannot be made to cross
|
|
1278
1303
|
the boundary by a query it built wrong. These two functions are the entire
|
|
1279
|
-
per-
|
|
1304
|
+
per-user surface; everything downstream names no organization at all.
|
|
1280
1305
|
(The project credential's side of the same boundary is the client-level
|
|
1281
1306
|
`orgId` option below — a property of the client, still never of a call.)
|
|
1282
1307
|
|
|
@@ -1390,7 +1415,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1390
1415
|
Schema drift is a different problem with the same remedy. A deploy that
|
|
1391
1416
|
renames a field leaves every cached entry perfectly current and quietly wrong:
|
|
1392
1417
|
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
|
|
1418
|
+
that no longer exists — until the tab is reloaded, which on a long console
|
|
1394
1419
|
session can be days. `createFoirDriftHandler` wires the `FoirSchemaDrift` the
|
|
1395
1420
|
data client already reports to a full clear.
|
|
1396
1421
|
|
|
@@ -1511,7 +1536,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1511
1536
|
credential and the bearer is re-exchanged when it dies. A consumer that cannot
|
|
1512
1537
|
see the lifetime has to hardcode the platform's (coupling itself to a server
|
|
1513
1538
|
constant it cannot observe) or decode `exp` off the JWT — or, as happened,
|
|
1514
|
-
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
|
|
1515
1540
|
mid-session. `createFoirSessionClient` on the SPA path has read `expires_in`
|
|
1516
1541
|
and scheduled a proactive refresh all along; this closes the asymmetry.
|
|
1517
1542
|
|
|
@@ -1521,10 +1546,10 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1521
1546
|
- **`HostedSessionResult`** exported from `./auth`.
|
|
1522
1547
|
|
|
1523
1548
|
- **`FoirAuthDowngrade`** on `./data` and `./server`, plus `extensions` on the
|
|
1524
|
-
response envelope — Foir now reports when it DISCARDED a
|
|
1549
|
+
response envelope — Foir now reports when it DISCARDED a user token you
|
|
1525
1550
|
sent, and the SDK stops dropping that report on the floor.
|
|
1526
1551
|
|
|
1527
|
-
A
|
|
1552
|
+
A user JWT is optional: one that fails to verify is not refused, the read
|
|
1528
1553
|
is served anonymously and returns `200`. So a lapsed token and a genuinely
|
|
1529
1554
|
anonymous request were indistinguishable — nothing threw, no refresh armed,
|
|
1530
1555
|
and auth-targeted content quietly resolved to its anonymous variant. The
|
|
@@ -1533,7 +1558,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1533
1558
|
```json
|
|
1534
1559
|
{ "data": { "…": "…" },
|
|
1535
1560
|
"extensions": { "auth": {
|
|
1536
|
-
"
|
|
1561
|
+
"userToken": "rejected", "reason": "expired", "servedAs": "anonymous"
|
|
1537
1562
|
} } }
|
|
1538
1563
|
```
|
|
1539
1564
|
|
|
@@ -1553,7 +1578,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1553
1578
|
|
|
1554
1579
|
The GraphQL `events` subscription has been live on the public schema since the
|
|
1555
1580
|
events spine shipped, with no client able to reach it. It is an `sk_`-only
|
|
1556
|
-
|
|
1581
|
+
console surface, so it lives on `./server` beside the rest of the events
|
|
1557
1582
|
client and inherits its `pk_` and `Authorization` refusals unchanged.
|
|
1558
1583
|
|
|
1559
1584
|
New exports: `FoirEventsSubscribeParams`, plus `FoirSubscription` and
|
|
@@ -1575,7 +1600,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1575
1600
|
It collapsed every failure to `null`: no cookie, a `401`, a `502`, a rate
|
|
1576
1601
|
limit, a DNS failure. Consumers have to act on that `null`, and the only
|
|
1577
1602
|
safe-looking action is to clear the session and redirect to the front door —
|
|
1578
|
-
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
|
|
1579
1604
|
perfectly valid, and nothing recorded that it had happened. The route's own
|
|
1580
1605
|
rate limiter is deliberately fail-open for the opposite reason.
|
|
1581
1606
|
|
|
@@ -1594,7 +1619,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1594
1619
|
`outcome` says what to DO; `reason` says what happened. Only `401` and a
|
|
1595
1620
|
missing cookie are `signed-out`. A `403` reports as `unavailable` with reason
|
|
1596
1621
|
`forbidden` — it means the CSRF header did not arrive, which is a
|
|
1597
|
-
misconfiguration rather than a blip, but signing the
|
|
1622
|
+
misconfiguration rather than a blip, but signing the user out over it
|
|
1598
1623
|
would still be wrong.
|
|
1599
1624
|
|
|
1600
1625
|
- **`foir.ns.operations.onExecution` delivers `status` as the
|
|
@@ -1634,14 +1659,14 @@ Everything additive, so a MINOR either way under the pre-1.0 note.
|
|
|
1634
1659
|
### Added
|
|
1635
1660
|
|
|
1636
1661
|
- **`hostedLogoutUrl(config, clientId, returnTo?)` on `./auth`** — the mirror of
|
|
1637
|
-
`hostedLoginUrl`, and the only thing that actually signs a
|
|
1662
|
+
`hostedLoginUrl`, and the only thing that actually signs a user out on the
|
|
1638
1663
|
hosted-cookie flow.
|
|
1639
1664
|
|
|
1640
1665
|
Login on that flow is symmetric across browser and server; logout was not.
|
|
1641
|
-
The apex `
|
|
1666
|
+
The apex `foir_session` cookie is the durable credential, and
|
|
1642
1667
|
disposing of a bearer, clearing a local session, or calling the
|
|
1643
|
-
`
|
|
1644
|
-
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
|
|
1645
1670
|
`POST /session/logout`, cookie-authenticated and CSRF-header-gated, which only
|
|
1646
1671
|
a browser sending `credentials: 'include'` can reach.
|
|
1647
1672
|
|
|
@@ -2013,7 +2038,7 @@ note above.
|
|
|
2013
2038
|
|
|
2014
2039
|
Reads take the shared, cacheable lane where they can:
|
|
2015
2040
|
`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
|
|
2041
|
+
call is a query, on a `pk_` key, with no user bearer, on the published
|
|
2017
2042
|
channel — and `POST /graphql` otherwise. The server's gate refuses on five
|
|
2018
2043
|
conditions and the client decides all five before the request: the fifth,
|
|
2019
2044
|
"the credential is in the URL", it satisfies by construction. The server is
|
|
@@ -2035,7 +2060,7 @@ note above.
|
|
|
2035
2060
|
hand-written document may legitimately have several. As a document it reaches
|
|
2036
2061
|
everything the credential can reach — including the two dozen root fields no
|
|
2037
2062
|
model and no namespace claims (`designTokens`, `record`, `locales`, the
|
|
2038
|
-
|
|
2063
|
+
user-auth mutations), which is the point of an escape hatch and is more
|
|
2039
2064
|
than the old shape could address.
|
|
2040
2065
|
|
|
2041
2066
|
- **Every op prints, and there is one wire format rather than two.** The
|
|
@@ -2098,19 +2123,19 @@ note above.
|
|
|
2098
2123
|
schema knowledge the client is not allowed to hold. The manifest publishes
|
|
2099
2124
|
each op's result shape, so the table has nothing left to answer. Neither
|
|
2100
2125
|
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
|
|
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
|
|
2106
2131
|
have used. A caller got a success response every time and nothing was
|
|
2107
2132
|
delivered, which is worse than an error, because there is no signal to act on.
|
|
2108
2133
|
|
|
2109
|
-
**Use the OTP flow for mailbox proof:** `
|
|
2110
|
-
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
|
|
2111
2136
|
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; `
|
|
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
|
|
2114
2139
|
arrived by some other route.
|
|
2115
2140
|
|
|
2116
2141
|
Like the two entries above, this symbol has not appeared in a published
|
|
@@ -2143,11 +2168,11 @@ note above.
|
|
|
2143
2168
|
id are what the note is filed against and never appear on screen.
|
|
2144
2169
|
|
|
2145
2170
|
Opening the panel reads `noteConfig`, which the API charges `notes:write` and
|
|
2146
|
-
refuses to a caller with no
|
|
2171
|
+
refuses to a caller with no session, so one probe settles whether the
|
|
2147
2172
|
visitor is signed in, whether their role may write notes, and whether
|
|
2148
2173
|
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
|
|
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
|
|
2151
2176
|
carry.
|
|
2152
2177
|
|
|
2153
2178
|
Screenshot capture is offered only when the project allows it and the browser
|
|
@@ -2177,11 +2202,11 @@ note above.
|
|
|
2177
2202
|
New exports on `./media`: `captureFoirScreen`, `isFoirScreenCaptureSupported`,
|
|
2178
2203
|
`FoirScreenCapture`, `FoirScreenCaptureOptions`.
|
|
2179
2204
|
|
|
2180
|
-
- **Note attachments and note config on `./data`.** `
|
|
2205
|
+
- **Note attachments and note config on `./data`.** `FoirUserNote` gains
|
|
2181
2206
|
`attachments`, `FoirCreateMyNoteArgs` gains an optional `attachmentFileIds`,
|
|
2182
2207
|
and `FoirNoteConfig` is the shape `foir.ns.notes.noteConfig()` answers with.
|
|
2183
2208
|
|
|
2184
|
-
New exports on `./data`: `
|
|
2209
|
+
New exports on `./data`: `FoirUserNoteAttachment`, `FoirNoteConfig`.
|
|
2185
2210
|
|
|
2186
2211
|
- **A subject registry — the records a client fetched, so an annotation can
|
|
2187
2212
|
anchor to the record instead of to a URL.** `createFoirSubjectRegistry()`
|
|
@@ -2225,11 +2250,11 @@ note above.
|
|
|
2225
2250
|
`FoirSubject`, `FoirSubjectEntry`, `FoirSubjectOptions`, `FoirSubjectOrigin`,
|
|
2226
2251
|
`FoirSubjectRegistry`, `FoirRecordPath`.
|
|
2227
2252
|
|
|
2228
|
-
- **Types for the
|
|
2253
|
+
- **Types for the user notes surface.** `foir.ns.notes.myNotes()` and
|
|
2229
2254
|
`foir.ns.notes.createMyNote()` already worked — `notes` is a namespace the
|
|
2230
2255
|
manifest publishes like any other — but their shapes had to be written out by
|
|
2231
2256
|
hand at each call site. They are platform-owned and identical in every project,
|
|
2232
|
-
so they now ship: `
|
|
2257
|
+
so they now ship: `FoirUserNote`, `FoirMyNotesArgs`, `FoirMyNotesResult`,
|
|
2233
2258
|
`FoirCreateMyNoteArgs`, `FoirNoteSubjectType`.
|
|
2234
2259
|
|
|
2235
2260
|
```ts
|
|
@@ -2237,7 +2262,7 @@ note above.
|
|
|
2237
2262
|
```
|
|
2238
2263
|
|
|
2239
2264
|
`FoirNoteSubjectType` is the closed set the platform's write path enforces
|
|
2240
|
-
(`record`, `file`, `model`, `operation`, `schedule`, `
|
|
2265
|
+
(`record`, `file`, `model`, `operation`, `schedule`, `user`) and is never a
|
|
2241
2266
|
model key — a note on a record is `'record'` plus the key on
|
|
2242
2267
|
`subjectModelKey`. Like every mirrored platform table in `builtins.ts`, a kind
|
|
2243
2268
|
added upstream widens this union in a minor release.
|
|
@@ -2394,12 +2419,12 @@ note above.
|
|
|
2394
2419
|
`FoirEventsClientConfig`, `FoirEventsListParams`, `FoirEventsStreamParams`.
|
|
2395
2420
|
|
|
2396
2421
|
**On `@foir/sdk/server` and no other subpath, deliberately.** The event log is
|
|
2397
|
-
an
|
|
2398
|
-
|
|
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
|
|
2399
2424
|
mintable on a `pk_` key at all. `events.view` is plain tenant+project
|
|
2400
2425
|
containment while `record.view` carries owner/grantee/borrow terms on top, so a
|
|
2401
2426
|
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*
|
|
2427
|
+
could not read — and each row's `actor` names which *other* user acted.
|
|
2403
2428
|
`createEventsClient` therefore throws on a `pk_` key and on an `Authorization`
|
|
2404
2429
|
header rather than letting either reach the wire, and an import-graph test
|
|
2405
2430
|
fails if the module becomes reachable from a subpath that ships to a browser.
|
|
@@ -2420,12 +2445,12 @@ note above.
|
|
|
2420
2445
|
is the entire cost of GraphQL paid for one screen. Ten root fields closed:
|
|
2421
2446
|
|
|
2422
2447
|
`getAuthConfig`, `getDefaultAuthProvider`, `currentUser`, `myProviderToken`,
|
|
2423
|
-
`
|
|
2424
|
-
`
|
|
2448
|
+
`logout`, `requestPasswordReset`, `resetPassword`,
|
|
2449
|
+
`updatePassword`, `verifyEmail`, `deleteMyAccount`.
|
|
2425
2450
|
|
|
2426
2451
|
Plus the session-layer pairings for the flows that need one:
|
|
2427
2452
|
`resetPasswordAndSignIn` (redeem the emailed token and persist the session it
|
|
2428
|
-
mints — the peer of `loginWithPassword` for the branch where the
|
|
2453
|
+
mints — the peer of `loginWithPassword` for the branch where the user
|
|
2429
2454
|
cannot log in), `signOut` (revoke server-side, then clear locally, and clear
|
|
2430
2455
|
locally even if the revoke fails), and `authedOp` (`authedRequest`'s twin for
|
|
2431
2456
|
auth ops: refresh once, retry once, on a 401-shaped failure).
|
|
@@ -2442,10 +2467,10 @@ note above.
|
|
|
2442
2467
|
### Removed
|
|
2443
2468
|
|
|
2444
2469
|
- **The nine auth operation documents on `@foir/sdk/auth`** —
|
|
2445
|
-
`
|
|
2446
|
-
`
|
|
2447
|
-
`
|
|
2448
|
-
`
|
|
2470
|
+
`UserLoginDocument`, `UserRegisterDocument`,
|
|
2471
|
+
`UserRefreshTokenDocument`, `UserRequestOTPDocument`,
|
|
2472
|
+
`UserLoginOTPDocument`, `UserLoginWithProviderDocument`,
|
|
2473
|
+
`UserProviderCallbackDocument`, `UserProviderVerifyOTPDocument`,
|
|
2449
2474
|
`AuthProvidersDocument`.
|
|
2450
2475
|
|
|
2451
2476
|
**Breaking, so the next release is a MINOR** per the pre-1.0 note above.
|
|
@@ -2457,15 +2482,15 @@ note above.
|
|
|
2457
2482
|
eleven were unreachable. The flows now address the server's generated `auth`
|
|
2458
2483
|
namespace, so `@foir/sdk/auth` carries no transport of its own.
|
|
2459
2484
|
|
|
2460
|
-
*Migration.* Nothing that called a FUNCTION changes: `
|
|
2485
|
+
*Migration.* Nothing that called a FUNCTION changes: `login(env, …)`,
|
|
2461
2486
|
`getAuthProviders(env)`, `loginWithPassword(adapter, env, …)`, `refreshSession`,
|
|
2462
2487
|
`requireAuth`, `authedRequest` and every React hook in `@foir/sdk/react` keep
|
|
2463
2488
|
their exact signatures. Only code importing a `*Document` const is affected,
|
|
2464
2489
|
and only if it was executing the document itself through its own GraphQL
|
|
2465
2490
|
client. Two options:
|
|
2466
2491
|
|
|
2467
|
-
- call the SDK function instead (`
|
|
2468
|
-
`
|
|
2492
|
+
- call the SDK function instead (`login(env, vars)` rather than running
|
|
2493
|
+
`UserLoginDocument` through Apollo/urql), which is what the exports were
|
|
2469
2494
|
for; or
|
|
2470
2495
|
- keep your own client and write the document in your own codebase — these are
|
|
2471
2496
|
fixed-shape platform operations, so the text does not vary by project.
|
|
@@ -2529,7 +2554,7 @@ actually happened.
|
|
|
2529
2554
|
|
|
2530
2555
|
Purely additive: `./auth` keeps exporting all four, unchanged, and nothing is
|
|
2531
2556
|
moved or renamed. What changes is the cost of reaching them. `./auth` also
|
|
2532
|
-
exports the
|
|
2557
|
+
exports the user-auth GraphQL documents, which build their
|
|
2533
2558
|
TypedDocumentNodes with a top-level `parse()` that no bundler can treat as
|
|
2534
2559
|
pure — so a browser app importing only `createFoirSessionClient` was shipping
|
|
2535
2560
|
the whole `graphql` package to get it. `src/auth/session-client.ts` imports
|
|
@@ -2590,7 +2615,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2590
2615
|
|
|
2591
2616
|
### Added
|
|
2592
2617
|
|
|
2593
|
-
- `@foir/sdk/auth`: `
|
|
2618
|
+
- `@foir/sdk/auth`: `signupContext` and `joinWaitlist`, plus the
|
|
2594
2619
|
`SignupContext`, `SignupMode` and `WaitlistJoinResult` types — waitlist and
|
|
2595
2620
|
restricted-signup flows for projects that are not open-registration.
|
|
2596
2621
|
- A pinned public-export surface: `public-exports.snapshot.json` plus
|
|
@@ -2617,7 +2642,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2617
2642
|
|
|
2618
2643
|
### Added
|
|
2619
2644
|
|
|
2620
|
-
- `@foir/sdk/client`: `createFoirContentClient` — forwards the
|
|
2645
|
+
- `@foir/sdk/client`: `createFoirContentClient` — forwards the user bearer
|
|
2621
2646
|
token on content reads, so authenticated and segment-targeted content works
|
|
2622
2647
|
without hand-rolling the header.
|
|
2623
2648
|
|
|
@@ -2634,7 +2659,7 @@ on its own, which is the intended behaviour for a release that adds exports.
|
|
|
2634
2659
|
|
|
2635
2660
|
### Added
|
|
2636
2661
|
|
|
2637
|
-
- **New subpath `@foir/sdk/react`** — drop-in React
|
|
2662
|
+
- **New subpath `@foir/sdk/react`** — drop-in React user auth: `FoirProvider`,
|
|
2638
2663
|
`SignIn`, `OAuthButtons`, `UserButton`, `useFoirAuth`, `useFoirSession`,
|
|
2639
2664
|
`useFoirUser`.
|
|
2640
2665
|
- **New subpath `@foir/sdk/next`** — Next.js App Router BFF auth adapter: cookie
|
|
@@ -2662,7 +2687,7 @@ First public release. Ten subpaths:
|
|
|
2662
2687
|
- `@foir/sdk/media` — progressive media components and adapters.
|
|
2663
2688
|
- `@foir/sdk/content` — `createContentRenderer` and `RichTextRenderer` with
|
|
2664
2689
|
injected typography tokens.
|
|
2665
|
-
- `@foir/sdk/auth` —
|
|
2690
|
+
- `@foir/sdk/auth` — user auth flows, token lifecycle, hosted-login bridge.
|
|
2666
2691
|
- `@foir/sdk/realtime` — `createCollabSession`, a thin y-websocket helper.
|
|
2667
2692
|
- `@foir/sdk/codegen`, `@foir/sdk/codegen/block-registry`,
|
|
2668
2693
|
`@foir/sdk/codegen/block-scaffold` — the `foirCodegen` preset and the
|