@foir/sdk 0.19.0 → 0.21.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 +330 -0
- package/dist/app/client.d.ts +109 -21
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +223 -211
- package/dist/app/errors.d.ts +2 -2
- package/dist/app/index.d.ts +4 -4
- package/dist/app/manifest.d.ts +1 -1
- package/dist/app/vocabulary.generated.d.ts +13 -10
- package/dist/app-contract.js +27 -31
- package/dist/app.js +80 -23
- package/dist/auth/access.d.ts +106 -0
- package/dist/auth/grant-table.generated.d.ts +13 -0
- package/dist/auth/grants.d.ts +38 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/operations.d.ts +6 -1
- package/dist/auth/ops.d.ts +1 -0
- package/dist/auth.js +321 -7
- package/dist/client/platform-client.d.ts +1 -1
- package/dist/client.js +28 -1
- package/dist/codegen/index.js +1 -1
- package/dist/data/builtins.d.ts +57 -42
- package/dist/data/document.d.ts +14 -0
- package/dist/data/index.d.ts +2 -2
- package/dist/data/transport.d.ts +27 -20
- package/dist/data.js +91 -13
- package/dist/index.js +24 -0
- package/dist/next.js +47 -5
- package/dist/placement/index.d.ts +26 -11
- package/dist/placement.js +219 -2
- package/dist/push.js +55 -6
- package/dist/react/FoirProvider.d.ts +40 -1
- package/dist/react/feedback-notes.d.ts +1 -1
- package/dist/react/index.d.ts +5 -4
- package/dist/react.js +391 -8
- package/dist/server/events-client.d.ts +7 -7
- package/dist/server/index.d.ts +1 -1
- package/dist/server/require-scoped-token.d.ts +1 -1
- package/dist/server/server-client.d.ts +1 -1
- package/dist/server/verify-scoped-token.d.ts +18 -23
- package/dist/server.js +228 -14
- package/dist/transport/foir-http.d.ts +44 -5
- package/dist/transport/key-kind.d.ts +40 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,336 @@ 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
|
+
### Added
|
|
112
|
+
|
|
113
|
+
- **An operation provisions an organization** (`@foir/sdk/app`).
|
|
114
|
+
`organizations.provision({ provisionId, name, parentId?, settings?, firstMember? })`
|
|
115
|
+
makes an organization with a customer as its first member, in the role the
|
|
116
|
+
project names for a founder, and answers `{ organizationId }`. It needs
|
|
117
|
+
`organizations:create`. With `settings: { modelKey, data }` it writes the
|
|
118
|
+
organization's settings record too and answers `settingsRecordId`; that
|
|
119
|
+
takes `records:write:<modelKey>`. Sending the same `provisionId` again with
|
|
120
|
+
the same request answers the same organization. New type:
|
|
121
|
+
`AppOrganizationProvision`. It needs the platform release that carries
|
|
122
|
+
`provisionOrganization`.
|
|
123
|
+
- **An operation schedules the deletion of the organization it runs in**
|
|
124
|
+
(`@foir/sdk/app`). `organizations.scheduleDeletion()` takes nothing and
|
|
125
|
+
answers `{ organizationId, deletionScheduledAt, organizations }`. It needs
|
|
126
|
+
`organizations:delete`, reaches only the organization the run acts in, and
|
|
127
|
+
needs the platform release that carries `scheduleOrganizationDeletion`.
|
|
128
|
+
- **`FoirWriteNoteArgs`** (`@foir/sdk/data`): the arguments of
|
|
129
|
+
`foir.ns.notes.writeNote`, the one field that writes a note for a signed-in
|
|
130
|
+
customer and for a visitor. It asks `notes:create` and needs the platform
|
|
131
|
+
release that carries `writeNote`. `foir.ns.notes` answers any field the
|
|
132
|
+
platform serves, so the call itself needs no new SDK.
|
|
133
|
+
|
|
134
|
+
- **A server says where its visitor is** (`@foir/sdk/data`). `visitor` takes
|
|
135
|
+
an object as well as an address: `{ address, country, latitude, longitude }`,
|
|
136
|
+
or a function that yields one for each request. The address is sent as
|
|
137
|
+
`X-Foir-Visitor-IP`, as before. The country and the coordinates are attached
|
|
138
|
+
to every read, in its `contexts`, so location targeting is decided by where
|
|
139
|
+
the visitor is and not by where your server is. Coordinates are sent at two
|
|
140
|
+
decimal places, and latitude and longitude go together or not at all. The
|
|
141
|
+
visitor is resolved once for a request, so the header and the read name the
|
|
142
|
+
same one. A string is still the address alone. New type: `FoirVisitor`, and
|
|
143
|
+
`country` and `location` on `FoirResolveContext`. It needs a key holding
|
|
144
|
+
`visitors:forward`, and the platform release that takes `contexts.country`
|
|
145
|
+
and `contexts.location`: an earlier platform refuses a read that names them.
|
|
146
|
+
|
|
147
|
+
### Changed
|
|
148
|
+
|
|
149
|
+
- **Breaking: an app client refuses an admin key** (`@foir/sdk/data`).
|
|
150
|
+
`createFoirClient` given a key that begins `foir_adm_` throws before any
|
|
151
|
+
request. An admin key is a machine's own: nobody signs in through it, and an
|
|
152
|
+
app never holds one. Build the client from a publishable key, or from a
|
|
153
|
+
server key on the server that renders your pages. A machine that acts for
|
|
154
|
+
itself uses `createServerClient` from `@foir/sdk/server`. A key minted
|
|
155
|
+
before a key had a kind (`sk_`) is not refused here.
|
|
156
|
+
- **A key's kind is read from its prefix in one place.** A key is publishable
|
|
157
|
+
(`foir_pub_`), server (`foir_srv_`) or admin (`foir_adm_`), and a key minted
|
|
158
|
+
earlier is `pk_`, `sk_` or `tc_`. Only a publishable key, in either format,
|
|
159
|
+
is named in a URL and takes the shared cache lane. Earlier versions looked
|
|
160
|
+
for the letters `pk_` alone, so a `foir_pub_` key was sent to the origin and
|
|
161
|
+
missed the shared cache.
|
|
162
|
+
- **`visitor` takes a server key** (`@foir/sdk/data`). A client that names the
|
|
163
|
+
visitor it calls for is built from a server key (`foir_srv_`), or from a
|
|
164
|
+
secret key minted before a key had a kind. Earlier versions looked for the
|
|
165
|
+
letters `sk_` alone and refused a server key. A publishable key is still
|
|
166
|
+
refused `visitor`, in either format.
|
|
167
|
+
- **The events client takes an admin key** (`@foir/sdk/server`).
|
|
168
|
+
`createEventsClient` refuses a publishable key and a server key by name, and
|
|
169
|
+
takes an admin key (`foir_adm_`) or a secret key minted earlier.
|
|
170
|
+
- **`FOIR_EVENTS_SCOPE` is `events:read`** (`@foir/sdk/server`). It was
|
|
171
|
+
`events:list.project`, the name the event log's permission had when a key
|
|
172
|
+
was written with scopes. A key is made with the permission catalogue's
|
|
173
|
+
names, where it is `events:read`, and that is what a refusal names. A key
|
|
174
|
+
minted under the older name holds `events:read` and needs no change.
|
|
175
|
+
|
|
176
|
+
### Removed
|
|
177
|
+
|
|
178
|
+
- **Breaking: `ScopedTokenClaims` has no `cap`, `rrm` or `rwm`**
|
|
179
|
+
(`@foir/sdk/server`). The platform no longer writes the claims a token
|
|
180
|
+
carried before it said its grants. Read `grants`, or `recordsReadModels`,
|
|
181
|
+
`recordsWriteModels` and `recordsCreateModels`.
|
|
182
|
+
- **Breaking: `PlacementGrant` has no `grantedScopes` or `withheld`**
|
|
183
|
+
(`@foir/sdk/placement`). A console sends what a page's token carries as
|
|
184
|
+
`grants` and what it was refused as `withheldGrants`, and no longer sends
|
|
185
|
+
the same two lists under their older names. Read `session.grant.grants`, or
|
|
186
|
+
ask `session.can`.
|
|
187
|
+
- **Breaking: `verifyScopedToken` refuses a token that does not say its
|
|
188
|
+
grants** (`@foir/sdk/server`), with `the token says no grants`, as the
|
|
189
|
+
platform refuses it. It used to read such a token as what its older claims
|
|
190
|
+
were worth. `requireScopedToken` answers it 401 like any token that does not
|
|
191
|
+
verify.
|
|
192
|
+
|
|
193
|
+
### Deprecated
|
|
194
|
+
|
|
195
|
+
- **`FoirCreateMyNoteArgs`, `FoirSubmitAnonymousNoteArgs` and
|
|
196
|
+
`FoirAnonymousNoteReceipt`** (`@foir/sdk/data`). `createMyNote` and
|
|
197
|
+
`submitAnonymousNote` are the older names of `writeNote`. They still answer.
|
|
198
|
+
`<FoirFeedback>` writes through `createMyNote` in this release, and reads
|
|
199
|
+
whichever permission a refusal names, so it explains a role that is short
|
|
200
|
+
`notes:create` as it explained one short `notes:write`.
|
|
201
|
+
|
|
202
|
+
## [0.20.0] — 2026-10-08
|
|
203
|
+
|
|
204
|
+
An operation, and the token it is called with, say what they may do as
|
|
205
|
+
`grants`, in the permission catalogue's names. Everything marked "with the
|
|
206
|
+
platform release that carries it" needs the next promotion, and `latest` moves
|
|
207
|
+
to this version then.
|
|
208
|
+
|
|
209
|
+
### Added
|
|
210
|
+
|
|
211
|
+
- **A token says what it carries as grants** (`@foir/sdk/server`).
|
|
212
|
+
`ScopedTokenClaims.grants` is the whole of what a verified token may do:
|
|
213
|
+
`records:read:<model>`, `records:write:<model>`, `records:create:<model>`,
|
|
214
|
+
`users:create` and the rest, as the
|
|
215
|
+
[operation permissions](https://docs.foir.dev/api/operation-permissions) page
|
|
216
|
+
lists them. It is always set. A token minted before the platform release
|
|
217
|
+
that says grants carries only the older claims, and `verifyScopedToken`
|
|
218
|
+
reads it as what those were worth, exactly as the platform does.
|
|
219
|
+
|
|
220
|
+
A record grant reaches the operation's own records: the project's, or those
|
|
221
|
+
of the customer a run acts for. `records:read.all:<model>`, and the write,
|
|
222
|
+
delete and publish forms like it, reach every owner's records of the model.
|
|
223
|
+
An older token's write of a model was every owner's, so it is read as
|
|
224
|
+
`records:write.all:<model>` and `records:create:<model>`, and takes that
|
|
225
|
+
model's read, delete and publish to `.all` with it.
|
|
226
|
+
|
|
227
|
+
`recordsCreateModels(claims)` is the models a token may make records of.
|
|
228
|
+
|
|
229
|
+
- **The grants a manifest may declare** (`@foir/sdk/app`). `APP_GRANTS` and
|
|
230
|
+
`AppGrant` are the grants an operation declares as written, and
|
|
231
|
+
`APP_RECORD_GRANT_PREFIXES` and `AppRecordGrantPrefix` the ones it declares
|
|
232
|
+
once per model, the every-owner forms (`records:read.all:` and the like)
|
|
233
|
+
among them.
|
|
234
|
+
|
|
235
|
+
- **An embedded page reads what its token carries as grants**
|
|
236
|
+
(`@foir/sdk/placement`). `session.grant.grants` is what the token carries, in
|
|
237
|
+
the permission catalogue's names, and `session.grant.withheldGrants` is each
|
|
238
|
+
grant the placement declares that it does not carry whole, with why. Both
|
|
239
|
+
are always set: a console of the release before sends the two older lists
|
|
240
|
+
alone, and the session reads `grants` from those.
|
|
241
|
+
|
|
242
|
+
- **A feature can say which notification category it sends under**
|
|
243
|
+
(`@foir/sdk/app/contract`). `Feature.notificationCategory` names the string
|
|
244
|
+
setting that holds the category's key, and the category the app proposes
|
|
245
|
+
(`key`, `name`, `description`, `optIn`). The setup then offers the project's
|
|
246
|
+
categories, or makes the app's own, in place of a key typed by hand, and the
|
|
247
|
+
platform holds a running install's setting to a category the project has.
|
|
248
|
+
|
|
249
|
+
- **An app can say what connecting changes** (`@foir/sdk/app/contract`).
|
|
250
|
+
`Credentials.connectNotice` is the app's own words, shown to the operator
|
|
251
|
+
beside the step that connects their account and, for an install the vendor
|
|
252
|
+
started, before the install is confirmed. A vendor that keeps one live
|
|
253
|
+
connection per account (Shopify, per store) says here that connecting moves
|
|
254
|
+
it from any other project. Empty when there is nothing to say.
|
|
255
|
+
|
|
256
|
+
- **The roles a signed-in customer holds, and what they may do**
|
|
257
|
+
(`@foir/sdk/auth`, `@foir/sdk/react`). A storefront can now ask which customer
|
|
258
|
+
roles the signed-in customer holds and show the page to match, where before
|
|
259
|
+
it had to treat one of a role's permissions as a stand-in for the role.
|
|
260
|
+
|
|
261
|
+
- `getSessionAccess(adapter, env)` and `customerAccess(env, { bearerToken })`
|
|
262
|
+
return a `FoirAccess`: the customer's id, the keys of the roles they hold
|
|
263
|
+
(`roles`), and everything they may do through the key (`scopes`). Plain
|
|
264
|
+
data, so a loader can return it. `null` when nobody is signed in.
|
|
265
|
+
- `hasRole(access, key)` and `hasScope(access, scope)` read it. `hasScope`
|
|
266
|
+
applies the platform's own rule: `records:write` covers
|
|
267
|
+
`records:write:post`, `records:read.all` covers `records:read`, and never
|
|
268
|
+
the reverse.
|
|
269
|
+
- `requireRole(adapter, env, key)` and `requireScope(adapter, env, scope)`
|
|
270
|
+
guard a server route. They return the access, or throw
|
|
271
|
+
`FoirAccessDeniedError` with a `reason` of `signed_out`, `role_not_held`,
|
|
272
|
+
`roles_unknown` or `scope_not_held`.
|
|
273
|
+
- `useFoirAccess()` gives a component `hasRole(key)`, `hasScope(scope)`, a
|
|
274
|
+
`status` and `reload()`. It asks once however many components use it, and
|
|
275
|
+
again when the session refreshes.
|
|
276
|
+
|
|
277
|
+
The platform resolves the roles on every request, so nothing is cached here
|
|
278
|
+
and a role taken away is gone the next time it is asked.
|
|
279
|
+
|
|
280
|
+
`roles` is `null`, not an empty list, when the platform did not say which
|
|
281
|
+
roles the customer holds. `hasRole` answers false for it and `requireRole`
|
|
282
|
+
refuses with `roles_unknown`, so an outage is never read as "not staff". A
|
|
283
|
+
platform that does not serve the field yet reads the same way: the role
|
|
284
|
+
helpers need the platform release that carries it.
|
|
285
|
+
|
|
286
|
+
These decide what a page shows. They protect nothing: the platform checks
|
|
287
|
+
every request whatever the page offers.
|
|
288
|
+
|
|
289
|
+
`AUTH_OPS` gains `whoami`, and `FoirContextValue` gains `access`,
|
|
290
|
+
`accessStatus`, `loadAccess` and `ensureAccess`.
|
|
291
|
+
|
|
292
|
+
- **A save says when it made a record the operation may not read**
|
|
293
|
+
(`@foir/sdk/app`). `records.upsert` and `records.customer.save` answer
|
|
294
|
+
`fieldsWithheld`. It is true when the operation declared the create of a
|
|
295
|
+
model and not the read: the save made the record, and `record` carries its
|
|
296
|
+
`id` and `modelKey` and nothing else. The record is not empty.
|
|
297
|
+
|
|
298
|
+
### Changed
|
|
299
|
+
|
|
300
|
+
- **Breaking, with the platform release that carries it: an operation is
|
|
301
|
+
called with what it declares and nothing else** (`@foir/sdk/app`). The token
|
|
302
|
+
used to carry the read of its owner's secrets whatever the operation
|
|
303
|
+
declared. An operation whose handler reads the app's credentials
|
|
304
|
+
(`install.credentials()`) declares `appsecrets:read`, and a project's own
|
|
305
|
+
operation that reads a project secret declares `secrets:read`. An installed
|
|
306
|
+
app's operations are given the one they could use, so an installed app keeps
|
|
307
|
+
running; a manifest served from then on says it.
|
|
308
|
+
|
|
309
|
+
- **Breaking, with the platform release that carries it: erasing a customer
|
|
310
|
+
takes `identities:delete`** (`@foir/sdk/app`). `records.customer.erase` needed
|
|
311
|
+
`users:create` beside the delete and the read of the model, and now needs
|
|
312
|
+
`identities:delete` there. `APP_GRANTS` carries it, with `files:delete`,
|
|
313
|
+
`notifications:read` and every owner's files (`files:read.all`,
|
|
314
|
+
`files:write.all`, `files:delete.all`), which a manifest may now declare.
|
|
315
|
+
`verifyScopedToken` reads a token minted before, that carried the customer
|
|
316
|
+
create beside the delete of a model, as holding `identities:delete`, as the
|
|
317
|
+
platform does.
|
|
318
|
+
|
|
319
|
+
- **Breaking, with the platform release that carries it: an operation declares
|
|
320
|
+
`grants`** (`@foir/sdk/app`, `@foir/sdk/app/contract`). A manifest's
|
|
321
|
+
operation says what it may do as `grants`, in the permission catalogue's
|
|
322
|
+
names, and the platform refuses one that still declares `capabilities`,
|
|
323
|
+
saying what to declare. An installed app keeps running on what it was
|
|
324
|
+
installed with; it is installing and updating that need the new manifest.
|
|
325
|
+
|
|
326
|
+
Most names are written as they were. These changed:
|
|
327
|
+
|
|
328
|
+
| Before | Now |
|
|
329
|
+
|---|---|
|
|
330
|
+
| `config:read` | `appconfig:read` |
|
|
331
|
+
| `secrets:read.app`, `secrets:put.app`, `secrets:list.app`, `secrets:delete.app` | `appsecrets:read`, `appsecrets:put`, `appsecrets:list`, `appsecrets:delete` |
|
|
332
|
+
| `files:list` | `files:read` |
|
|
333
|
+
| `customers:write` | `users:create` |
|
|
334
|
+
| `customers:read` | `users:read.all` |
|
|
335
|
+
| `customers:read:named` | `users:read.named` |
|
|
336
|
+
| `customers:read:identity` | `identities:read` |
|
|
337
|
+
| `notifications:write` | `notifications:write.named` |
|
|
338
|
+
| `records:write:<model>` | `records:write:<model>` and `records:create:<model>` |
|
|
339
|
+
|
|
340
|
+
The last row is a change in what the name means. `records:write:<model>`
|
|
341
|
+
changes a record that is there, and making one is `records:create:<model>`.
|
|
342
|
+
An operation that does both declares both. `records.upsert` and
|
|
343
|
+
`records.customer.save` are let through on either, and the platform asks for
|
|
344
|
+
the one the save turns out to need.
|
|
345
|
+
|
|
346
|
+
- **Breaking, with the platform release that carries it: a full-page placement
|
|
347
|
+
declares `grants`** (`@foir/sdk/app/contract`, `@foir/sdk/placement`). A
|
|
348
|
+
placement in a manifest says what its page acts with as `grants`, and the
|
|
349
|
+
platform refuses one that still declares `scopes`, saying what to declare.
|
|
350
|
+
An installed app's page keeps working on what it was installed with.
|
|
351
|
+
|
|
352
|
+
The names are the ones a team member's role is composed from, so three
|
|
353
|
+
things change in what a placement writes:
|
|
354
|
+
|
|
355
|
+
- **Every owner's records are written `.all`.** `records:read:order` was
|
|
356
|
+
every order and is now the operator's own. Declare
|
|
357
|
+
`records:read.all:order`.
|
|
358
|
+
- **A write no longer includes its read.** `records:write:order` also read
|
|
359
|
+
and created, and `apps:write` also read apps. Declare each one the page
|
|
360
|
+
uses: `records:write.all:order`, `records:read.all:order`,
|
|
361
|
+
`records:create:order`.
|
|
362
|
+
- **A wildcard and a permission that gives others are refused**, as `*`
|
|
363
|
+
always was.
|
|
364
|
+
|
|
365
|
+
The token then carries what the placement declares and the operator holds,
|
|
366
|
+
permission by permission. An operator who holds part of a declared grant is
|
|
367
|
+
given that part, where before the whole name was withheld.
|
|
368
|
+
|
|
369
|
+
- **`session.can` answers by what the token covers** (`@foir/sdk/placement`).
|
|
370
|
+
It was an exact match on the spelling. `can('records:read.all:order')` is
|
|
371
|
+
now true for a token that carries `records:read.all`, as Foir's API would
|
|
372
|
+
answer. A name the token carries as it is written is still held.
|
|
373
|
+
|
|
374
|
+
- **`PlacementGrant.grantedScopes` and `PlacementGrant.withheld` are
|
|
375
|
+
deprecated** (`@foir/sdk/placement`). They are still set, and say what
|
|
376
|
+
`grants` and `withheldGrants` say.
|
|
377
|
+
|
|
378
|
+
- **Breaking, with the platform release that carries it: `whoami` answers
|
|
379
|
+
`grants`** (`@foir/sdk/app`). `AppWhoami.capabilities` is gone and
|
|
380
|
+
`AppWhoami.grants` takes its place, and `AppWhoami.models` gains `create`.
|
|
381
|
+
|
|
382
|
+
- **Breaking: the older claims on `ScopedTokenClaims` are optional**
|
|
383
|
+
(`@foir/sdk/server`). `cap`, `rrm` and `rwm` are still on a token the
|
|
384
|
+
platform mints, in the older names, and are marked deprecated: they never
|
|
385
|
+
say more than `grants` and can say less. `recordsReadModels` and
|
|
386
|
+
`recordsWriteModels` now read `grants`, so `recordsWriteModels` is the models
|
|
387
|
+
a token may change records of, and no longer the ones it may make them in.
|
|
388
|
+
Code that builds a `ScopedTokenClaims` by hand sets `grants`.
|
|
389
|
+
|
|
390
|
+
- **Breaking: `AppUpsertResult` has a new required field, `fieldsWithheld`.**
|
|
391
|
+
Code that builds one by hand, a test double for `records.upsert` for
|
|
392
|
+
instance, has to set it.
|
|
393
|
+
|
|
394
|
+
- **Breaking, with the platform release that carries it: only making a record
|
|
395
|
+
takes no read.** A write that finds a record already there is charged the
|
|
396
|
+
read of it as well. This is the platform's rule and reaches every caller,
|
|
397
|
+
whatever version of this package they run.
|
|
398
|
+
|
|
399
|
+
- `create` and `submit` (`@foir/sdk/data`) ask for the whole record by
|
|
400
|
+
default. A credential that may create records of a model and may not read
|
|
401
|
+
them is refused that, before anything is written, and told the read it
|
|
402
|
+
lacks. Pass `pick: ['_id']` to make the record and be answered with its id.
|
|
403
|
+
- `update`, `rename`, `delete`, `publish` and `unpublish` need
|
|
404
|
+
`records:read` for the model beside their own permission.
|
|
405
|
+
- An operation that declares the create of a model and not the read
|
|
406
|
+
(`@foir/sdk/app`) still makes its records. `records.upsert` landing on a
|
|
407
|
+
record already there, `records.customer.save` likewise, `records.delete`,
|
|
408
|
+
`records.refresh`, `records.rename` and `records.updateMany` are refused
|
|
409
|
+
until it declares the read too.
|
|
410
|
+
|
|
411
|
+
- **Breaking, with the platform release that carries it: `records.customer.erase`
|
|
412
|
+
needs the delete of the model** (`@foir/sdk/app`). An erase removes a record,
|
|
413
|
+
so the operation declares `records:delete` and `records:read` for the model,
|
|
414
|
+
and `users:create`. Until now the grant to save the record was enough. An
|
|
415
|
+
operation that erased on its write lane is refused until it declares the
|
|
416
|
+
delete. Nothing is taken from holding a write.
|
|
417
|
+
|
|
418
|
+
- **Breaking, with the platform release that carries it: a role in
|
|
419
|
+
`myOrganizations` carries `grants`** (`@foir/sdk/auth`).
|
|
420
|
+
`MyOrganizationRole.permissions` is gone and `MyOrganizationRole.grants`
|
|
421
|
+
takes its place: what the role gives, in the permission catalogue's names. A
|
|
422
|
+
grant stands for itself, so changing a record and creating one are two
|
|
423
|
+
names (`records:write`, `records:create`), reading every customer's records
|
|
424
|
+
is `records:read.all`, and staffing an organization is `members:read` and
|
|
425
|
+
`members:write`. Code that looked for `organizations:staff` or
|
|
426
|
+
`records:list.all` in a role's list looks for the new names.
|
|
427
|
+
|
|
428
|
+
- **Breaking, with the platform release that carries it: `customers.byIdentity`
|
|
429
|
+
and `customers.identities` need `identities:read`, declared by name**
|
|
430
|
+
(`@foir/sdk/app`). The read of every customer, `users:read.all`, no longer
|
|
431
|
+
includes them.
|
|
432
|
+
|
|
433
|
+
### Removed
|
|
434
|
+
|
|
435
|
+
- **Breaking: `APP_CAPABILITIES`, `APP_RECORD_CAPABILITY_PREFIXES`,
|
|
436
|
+
`AppCapability` and `AppRecordCapabilityPrefix`** (`@foir/sdk/app`). They
|
|
437
|
+
listed the names an operation was declared with before, which the platform
|
|
438
|
+
now refuses. `APP_GRANTS`, `APP_RECORD_GRANT_PREFIXES`, `AppGrant` and
|
|
439
|
+
`AppRecordGrantPrefix` list what it declares now.
|
|
440
|
+
|
|
111
441
|
## [0.19.0] — 2026-10-02
|
|
112
442
|
|
|
113
443
|
Published under npm's `next` tag from `dev`. The data client's change needs no
|
package/dist/app/client.d.ts
CHANGED
|
@@ -144,11 +144,22 @@ export interface AppRecord {
|
|
|
144
144
|
unreadable: string[];
|
|
145
145
|
}
|
|
146
146
|
export interface AppUpsertResult {
|
|
147
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* The record as the platform holds it now. When `fieldsWithheld` is true it
|
|
149
|
+
* is only which record it is, its `id` and `modelKey`: see there.
|
|
150
|
+
*/
|
|
148
151
|
record: Record<string, unknown>;
|
|
149
152
|
/** The platform's own id for it, when the answer carried one. */
|
|
150
153
|
id: string | null;
|
|
151
154
|
created: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* True when the save made a record the operation may not read: it declared
|
|
157
|
+
* the write for the model and not the read. Making a record takes no read, so
|
|
158
|
+
* the save worked, and its answer carries which record it made and nothing of
|
|
159
|
+
* it. The record is not empty. A save that lands on a record already there
|
|
160
|
+
* takes the read as well, and is refused without it.
|
|
161
|
+
*/
|
|
162
|
+
fieldsWithheld: boolean;
|
|
152
163
|
/** True when nothing was written because Foir already holds a newer copy (`sourceModifiedAt`). */
|
|
153
164
|
stale: boolean;
|
|
154
165
|
}
|
|
@@ -186,6 +197,38 @@ export interface AppNotification {
|
|
|
186
197
|
/** Makes the send safe to repeat: the same key from the same operation tells the customer once. */
|
|
187
198
|
idempotencyKey?: string;
|
|
188
199
|
}
|
|
200
|
+
/** An organisation an operation makes for one of its customers. */
|
|
201
|
+
export interface AppOrganizationProvision {
|
|
202
|
+
/**
|
|
203
|
+
* Yours to choose and to keep. Sending the same one again with the same
|
|
204
|
+
* request answers the same organisation and makes no second, so a run that
|
|
205
|
+
* stopped half way is finished by running it again. A different request
|
|
206
|
+
* under an id already used is refused.
|
|
207
|
+
*/
|
|
208
|
+
provisionId: string;
|
|
209
|
+
name: string;
|
|
210
|
+
/** The organisation this one is made beneath. Left out, it is made at the top of the project. */
|
|
211
|
+
parentId?: string;
|
|
212
|
+
/**
|
|
213
|
+
* The organisation's settings record, written with it: a record of the
|
|
214
|
+
* model the project keeps an organisation's settings in (a singleton on the
|
|
215
|
+
* organisation owner plane). The operation declares that model's write,
|
|
216
|
+
* `records:write:<modelKey>`, beside `organizations:create`. It is the one
|
|
217
|
+
* record of an organisation an operation writes without running in it.
|
|
218
|
+
*/
|
|
219
|
+
settings?: {
|
|
220
|
+
modelKey: string;
|
|
221
|
+
data: Record<string, unknown>;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* The customer who becomes its first member. They are given the role the
|
|
225
|
+
* project names for an organisation's founder, in its settings: the
|
|
226
|
+
* operation chooses who, and the project decides what they hold.
|
|
227
|
+
*/
|
|
228
|
+
firstMember?: {
|
|
229
|
+
customerId: string;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
189
232
|
/**
|
|
190
233
|
* Who a consent change is for: a customer whose id the app holds, or an
|
|
191
234
|
* address.
|
|
@@ -194,7 +237,7 @@ export interface AppNotification {
|
|
|
194
237
|
* found, or made, together with the change, so a change the platform refuses
|
|
195
238
|
* leaves no customer behind. It is for an opt-in from somebody who may have no
|
|
196
239
|
* customer yet (a sign-up form), and the operation must declare
|
|
197
|
-
* `
|
|
240
|
+
* `users:create` as well as `notifications:consent`.
|
|
198
241
|
*/
|
|
199
242
|
export type AppConsentSubject = {
|
|
200
243
|
customerId: string;
|
|
@@ -236,12 +279,13 @@ export type AppConsentChange = AppConsentSubject & {
|
|
|
236
279
|
export interface AppWhoami {
|
|
237
280
|
/** The app the token acts for, when it acts for one. */
|
|
238
281
|
app: string | null;
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
/** The models its record
|
|
282
|
+
/** What the token carries, as grants in the permission catalogue's names. */
|
|
283
|
+
grants: string[];
|
|
284
|
+
/** The models its record grants name, by permission. Changing a record and making one are two. */
|
|
242
285
|
models: {
|
|
243
286
|
read: string[];
|
|
244
287
|
write: string[];
|
|
288
|
+
create: string[];
|
|
245
289
|
delete: string[];
|
|
246
290
|
publish: string[];
|
|
247
291
|
};
|
|
@@ -264,17 +308,17 @@ export interface AppClient {
|
|
|
264
308
|
whoami(): Promise<AppWhoami>;
|
|
265
309
|
/** The install this token acts for: its configuration, its credential, its status. */
|
|
266
310
|
install: {
|
|
267
|
-
/** The operator's settings and mappings. Needs `
|
|
311
|
+
/** The operator's settings and mappings. Needs `appconfig:read`. */
|
|
268
312
|
config(): Promise<AppInstallConfig>;
|
|
269
313
|
/**
|
|
270
314
|
* The install's credential, by the manifest's credential field names.
|
|
271
|
-
* Values are always strings. Needs `
|
|
315
|
+
* Values are always strings. Needs `appsecrets:read`.
|
|
272
316
|
*/
|
|
273
317
|
credentials(): Promise<Record<string, string>>;
|
|
274
318
|
/**
|
|
275
319
|
* Write or rotate credential fields. An empty value is skipped, not
|
|
276
320
|
* cleared, and a call naming no non-empty field is refused. Needs
|
|
277
|
-
* `
|
|
321
|
+
* `appsecrets:put`.
|
|
278
322
|
*/
|
|
279
323
|
writeCredentials(values: Record<string, string>): Promise<void>;
|
|
280
324
|
/** Report the install's health. Needs `status:write`. See {@link AppStatusReport}. */
|
|
@@ -284,7 +328,9 @@ export interface AppClient {
|
|
|
284
328
|
records: {
|
|
285
329
|
/**
|
|
286
330
|
* Write the record at a natural key, making it when there is none.
|
|
287
|
-
*
|
|
331
|
+
* Changing the record there needs `records:write:<model>` and making one
|
|
332
|
+
* needs `records:create:<model>`, so an operation that does both declares
|
|
333
|
+
* both. `ownerId` names the customer it belongs to (needs
|
|
288
334
|
* `records:attribute:<model>`); `sourceModifiedAt` is when the object this
|
|
289
335
|
* mirrors last changed at its source, so an older copy arriving late is
|
|
290
336
|
* not written.
|
|
@@ -399,7 +445,9 @@ export interface AppClient {
|
|
|
399
445
|
/**
|
|
400
446
|
* Erase what the app holds for the customer, as a privacy request asks:
|
|
401
447
|
* their record and the identity's link, and the customer when nothing
|
|
402
|
-
* else is attached to them.
|
|
448
|
+
* else is attached to them. It removes a record, so it needs
|
|
449
|
+
* `records:delete` and `records:read` for the model, and
|
|
450
|
+
* `identities:delete`. The grant to save the record is not enough.
|
|
403
451
|
*/
|
|
404
452
|
erase(model: string, identity: string): Promise<AppCustomerEraseResult>;
|
|
405
453
|
};
|
|
@@ -408,7 +456,7 @@ export interface AppClient {
|
|
|
408
456
|
/**
|
|
409
457
|
* The customer who has this email at the install's level, made when there
|
|
410
458
|
* is none. The same customer every time for one email. Needs
|
|
411
|
-
* `
|
|
459
|
+
* `users:create`.
|
|
412
460
|
*
|
|
413
461
|
* `verified` says whether that address is one the customer has proved is
|
|
414
462
|
* theirs. A customer this call makes has not, and neither has one an
|
|
@@ -423,14 +471,14 @@ export interface AppClient {
|
|
|
423
471
|
}>;
|
|
424
472
|
/**
|
|
425
473
|
* One customer by id, or null when there is none at the install's level.
|
|
426
|
-
* Needs `
|
|
474
|
+
* Needs `users:read.named`, or `users:read.all`.
|
|
427
475
|
*/
|
|
428
476
|
get(id: string): Promise<AppCustomer | null>;
|
|
429
477
|
/**
|
|
430
478
|
* The customer who has this email at the install's level, or null. The
|
|
431
479
|
* exact address, as `ensure` matches it, and nothing wider: it finds one
|
|
432
|
-
* customer and never searches. Needs `
|
|
433
|
-
* `
|
|
480
|
+
* customer and never searches. Needs `users:read.named`, or
|
|
481
|
+
* `users:read.all`.
|
|
434
482
|
*/
|
|
435
483
|
byEmail(email: string): Promise<AppCustomer | null>;
|
|
436
484
|
/**
|
|
@@ -439,7 +487,8 @@ export interface AppClient {
|
|
|
439
487
|
* of its customers. The identity is the subject of the customer's sign-in
|
|
440
488
|
* with that system, and it is resolved through the customer sign-in the
|
|
441
489
|
* install pairs (`install.config().customerSignIn`) and no other. Never
|
|
442
|
-
* matched by email. Needs `
|
|
490
|
+
* matched by email. Needs `identities:read`, declared by name:
|
|
491
|
+
* `users:read.all` does not include it.
|
|
443
492
|
*/
|
|
444
493
|
byIdentity(identity: string): Promise<string | null>;
|
|
445
494
|
/**
|
|
@@ -447,13 +496,13 @@ export interface AppClient {
|
|
|
447
496
|
* holds at the app's own system, most recently seen first, through the
|
|
448
497
|
* same pairing. Empty when they have never signed in that way. For an app
|
|
449
498
|
* acting on an event that names the Foir customer. Needs
|
|
450
|
-
* `
|
|
499
|
+
* `identities:read`, declared by name.
|
|
451
500
|
*/
|
|
452
501
|
identities(customerId: string): Promise<string[]>;
|
|
453
502
|
/**
|
|
454
503
|
* The customers at the install's level, a page at a time, newest first.
|
|
455
|
-
* Needs `
|
|
456
|
-
* `
|
|
504
|
+
* Needs `users:read.all`: an operation declaring only
|
|
505
|
+
* `users:read.named` reads the customers it names and is refused the
|
|
457
506
|
* list.
|
|
458
507
|
*/
|
|
459
508
|
list(query?: AppCustomerList): Promise<{
|
|
@@ -462,8 +511,47 @@ export interface AppClient {
|
|
|
462
511
|
endCursor: string | null;
|
|
463
512
|
}>;
|
|
464
513
|
};
|
|
514
|
+
organizations: {
|
|
515
|
+
/**
|
|
516
|
+
* Make an organisation, with its settings record and a customer as its
|
|
517
|
+
* first member. Needs `organizations:create`, and for `settings` the write
|
|
518
|
+
* of the settings model. Sign-up is the use: one operation creates the
|
|
519
|
+
* customer (`customers.ensure`) and then the organisation that is theirs.
|
|
520
|
+
*
|
|
521
|
+
* The first member is given the role the project names for an
|
|
522
|
+
* organisation's founder. A project that names none refuses it, saying
|
|
523
|
+
* which setting to fill in.
|
|
524
|
+
*
|
|
525
|
+
* Safe to run again: the same `provisionId` with the same request answers
|
|
526
|
+
* the same organisation and the same settings record.
|
|
527
|
+
*/
|
|
528
|
+
provision(organization: AppOrganizationProvision): Promise<{
|
|
529
|
+
organizationId: string;
|
|
530
|
+
settingsRecordId?: string;
|
|
531
|
+
}>;
|
|
532
|
+
/**
|
|
533
|
+
* Schedule the deletion of the organisation this run acts in, with every
|
|
534
|
+
* organisation beneath it. Needs `organizations:delete`. It takes no
|
|
535
|
+
* organisation: the one whoever triggered the run stands in is the only
|
|
536
|
+
* one it reaches, and a run that acts in none is refused. So is a run
|
|
537
|
+
* that acts for nobody who is a member of that organisation.
|
|
538
|
+
*
|
|
539
|
+
* The organisation stops being active at once, so nothing that stands in
|
|
540
|
+
* it is served after this answers. Nothing is removed until
|
|
541
|
+
* `deletionScheduledAt`, and the project can resume it until then. This
|
|
542
|
+
* call cannot.
|
|
543
|
+
*/
|
|
544
|
+
scheduleDeletion(): Promise<{
|
|
545
|
+
organizationId: string;
|
|
546
|
+
deletionScheduledAt: string | null;
|
|
547
|
+
organizations: {
|
|
548
|
+
id: string;
|
|
549
|
+
name: string;
|
|
550
|
+
}[];
|
|
551
|
+
}>;
|
|
552
|
+
};
|
|
465
553
|
notifications: {
|
|
466
|
-
/** Tell a customer something. Needs `notifications:write`. */
|
|
554
|
+
/** Tell a customer something. Needs `notifications:write.named`. */
|
|
467
555
|
send(notification: AppNotification): Promise<void>;
|
|
468
556
|
/**
|
|
469
557
|
* Set a named customer's consent in an opt-in category. Needs
|
|
@@ -477,7 +565,7 @@ export interface AppClient {
|
|
|
477
565
|
*
|
|
478
566
|
* Name the customer by `customerId`, or by `email` for somebody who may
|
|
479
567
|
* have no customer yet: the platform finds or makes them with the change,
|
|
480
|
-
* and that also needs `
|
|
568
|
+
* and that also needs `users:create`.
|
|
481
569
|
*/
|
|
482
570
|
setConsent(change: AppConsentChange): Promise<{
|
|
483
571
|
stale: boolean;
|
|
@@ -497,7 +585,7 @@ export interface AppClient {
|
|
|
497
585
|
*
|
|
498
586
|
* Only the token a delivery arrived with may call it, for an operation
|
|
499
587
|
* that is active at the install. An operation's own token is refused: it
|
|
500
|
-
* carries that operation's
|
|
588
|
+
* carries that operation's grants and no other's.
|
|
501
589
|
*/
|
|
502
590
|
start(operationKey: string, options?: AppOperationStart): Promise<AppStartedOperation>;
|
|
503
591
|
};
|