@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/dist/app/client.d.ts +109 -21
  3. package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +223 -211
  4. package/dist/app/errors.d.ts +2 -2
  5. package/dist/app/index.d.ts +4 -4
  6. package/dist/app/manifest.d.ts +1 -1
  7. package/dist/app/vocabulary.generated.d.ts +13 -10
  8. package/dist/app-contract.js +27 -31
  9. package/dist/app.js +80 -23
  10. package/dist/auth/access.d.ts +106 -0
  11. package/dist/auth/grant-table.generated.d.ts +13 -0
  12. package/dist/auth/grants.d.ts +38 -0
  13. package/dist/auth/index.d.ts +2 -0
  14. package/dist/auth/operations.d.ts +6 -1
  15. package/dist/auth/ops.d.ts +1 -0
  16. package/dist/auth.js +321 -7
  17. package/dist/client/platform-client.d.ts +1 -1
  18. package/dist/client.js +28 -1
  19. package/dist/codegen/index.js +1 -1
  20. package/dist/data/builtins.d.ts +57 -42
  21. package/dist/data/document.d.ts +14 -0
  22. package/dist/data/index.d.ts +2 -2
  23. package/dist/data/transport.d.ts +27 -20
  24. package/dist/data.js +91 -13
  25. package/dist/index.js +24 -0
  26. package/dist/next.js +47 -5
  27. package/dist/placement/index.d.ts +26 -11
  28. package/dist/placement.js +219 -2
  29. package/dist/push.js +55 -6
  30. package/dist/react/FoirProvider.d.ts +40 -1
  31. package/dist/react/feedback-notes.d.ts +1 -1
  32. package/dist/react/index.d.ts +5 -4
  33. package/dist/react.js +391 -8
  34. package/dist/server/events-client.d.ts +7 -7
  35. package/dist/server/index.d.ts +1 -1
  36. package/dist/server/require-scoped-token.d.ts +1 -1
  37. package/dist/server/server-client.d.ts +1 -1
  38. package/dist/server/verify-scoped-token.d.ts +18 -23
  39. package/dist/server.js +228 -14
  40. package/dist/transport/foir-http.d.ts +44 -5
  41. package/dist/transport/key-kind.d.ts +40 -0
  42. 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
@@ -144,11 +144,22 @@ export interface AppRecord {
144
144
  unreadable: string[];
145
145
  }
146
146
  export interface AppUpsertResult {
147
- /** The record as the platform holds it now. */
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
- * `customers:write` as well as `notifications:consent`.
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
- /** The capabilities the token carries. */
240
- capabilities: string[];
241
- /** The models its record capabilities name, by verb. */
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 `config:read`. */
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 `secrets:read.app`.
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
- * `secrets:put.app`.
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
- * `ownerId` names the customer it belongs to (needs
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. Needs `customers:write` as well.
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
- * `customers:write`.
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 `customers:read:named`, or `customers:read`.
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 `customers:read:named`, or
433
- * `customers:read`.
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 `customers:read:identity`, or `customers:read`.
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
- * `customers:read:identity`, or `customers:read`.
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 `customers:read`: an operation declaring only
456
- * `customers:read:named` reads the customers it names and is refused the
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 `customers:write`.
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 capabilities and no other's.
588
+ * carries that operation's grants and no other's.
501
589
  */
502
590
  start(operationKey: string, options?: AppOperationStart): Promise<AppStartedOperation>;
503
591
  };