@foir/sdk 0.18.0 → 0.20.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 CHANGED
@@ -108,6 +108,371 @@ 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
+ ### Changed
135
+
136
+ - **`FOIR_EVENTS_SCOPE` is `events:read`** (`@foir/sdk/server`). It was
137
+ `events:list.project`, the name the event log's permission had when a key
138
+ was written with scopes. A key is made with the permission catalogue's
139
+ names, where it is `events:read`, and that is what a refusal names. A key
140
+ minted under the older name holds `events:read` and needs no change.
141
+
142
+ ### Deprecated
143
+
144
+ - **`FoirCreateMyNoteArgs`, `FoirSubmitAnonymousNoteArgs` and
145
+ `FoirAnonymousNoteReceipt`** (`@foir/sdk/data`). `createMyNote` and
146
+ `submitAnonymousNote` are the older names of `writeNote`. They still answer.
147
+ `<FoirFeedback>` writes through `createMyNote` in this release, and reads
148
+ whichever permission a refusal names, so it explains a role that is short
149
+ `notes:create` as it explained one short `notes:write`.
150
+
151
+ ## [0.20.0] — 2026-10-08
152
+
153
+ An operation, and the token it is called with, say what they may do as
154
+ `grants`, in the permission catalogue's names. Everything marked "with the
155
+ platform release that carries it" needs the next promotion, and `latest` moves
156
+ to this version then.
157
+
158
+ ### Added
159
+
160
+ - **A token says what it carries as grants** (`@foir/sdk/server`).
161
+ `ScopedTokenClaims.grants` is the whole of what a verified token may do:
162
+ `records:read:<model>`, `records:write:<model>`, `records:create:<model>`,
163
+ `users:create` and the rest, as the
164
+ [operation permissions](https://docs.foir.dev/api/operation-permissions) page
165
+ lists them. It is always set. A token minted before the platform release
166
+ that says grants carries only the older claims, and `verifyScopedToken`
167
+ reads it as what those were worth, exactly as the platform does.
168
+
169
+ A record grant reaches the operation's own records: the project's, or those
170
+ of the customer a run acts for. `records:read.all:<model>`, and the write,
171
+ delete and publish forms like it, reach every owner's records of the model.
172
+ An older token's write of a model was every owner's, so it is read as
173
+ `records:write.all:<model>` and `records:create:<model>`, and takes that
174
+ model's read, delete and publish to `.all` with it.
175
+
176
+ `recordsCreateModels(claims)` is the models a token may make records of.
177
+
178
+ - **The grants a manifest may declare** (`@foir/sdk/app`). `APP_GRANTS` and
179
+ `AppGrant` are the grants an operation declares as written, and
180
+ `APP_RECORD_GRANT_PREFIXES` and `AppRecordGrantPrefix` the ones it declares
181
+ once per model, the every-owner forms (`records:read.all:` and the like)
182
+ among them.
183
+
184
+ - **An embedded page reads what its token carries as grants**
185
+ (`@foir/sdk/placement`). `session.grant.grants` is what the token carries, in
186
+ the permission catalogue's names, and `session.grant.withheldGrants` is each
187
+ grant the placement declares that it does not carry whole, with why. Both
188
+ are always set: a console of the release before sends the two older lists
189
+ alone, and the session reads `grants` from those.
190
+
191
+ - **A feature can say which notification category it sends under**
192
+ (`@foir/sdk/app/contract`). `Feature.notificationCategory` names the string
193
+ setting that holds the category's key, and the category the app proposes
194
+ (`key`, `name`, `description`, `optIn`). The setup then offers the project's
195
+ categories, or makes the app's own, in place of a key typed by hand, and the
196
+ platform holds a running install's setting to a category the project has.
197
+
198
+ - **An app can say what connecting changes** (`@foir/sdk/app/contract`).
199
+ `Credentials.connectNotice` is the app's own words, shown to the operator
200
+ beside the step that connects their account and, for an install the vendor
201
+ started, before the install is confirmed. A vendor that keeps one live
202
+ connection per account (Shopify, per store) says here that connecting moves
203
+ it from any other project. Empty when there is nothing to say.
204
+
205
+ - **The roles a signed-in customer holds, and what they may do**
206
+ (`@foir/sdk/auth`, `@foir/sdk/react`). A storefront can now ask which customer
207
+ roles the signed-in customer holds and show the page to match, where before
208
+ it had to treat one of a role's permissions as a stand-in for the role.
209
+
210
+ - `getSessionAccess(adapter, env)` and `customerAccess(env, { bearerToken })`
211
+ return a `FoirAccess`: the customer's id, the keys of the roles they hold
212
+ (`roles`), and everything they may do through the key (`scopes`). Plain
213
+ data, so a loader can return it. `null` when nobody is signed in.
214
+ - `hasRole(access, key)` and `hasScope(access, scope)` read it. `hasScope`
215
+ applies the platform's own rule: `records:write` covers
216
+ `records:write:post`, `records:read.all` covers `records:read`, and never
217
+ the reverse.
218
+ - `requireRole(adapter, env, key)` and `requireScope(adapter, env, scope)`
219
+ guard a server route. They return the access, or throw
220
+ `FoirAccessDeniedError` with a `reason` of `signed_out`, `role_not_held`,
221
+ `roles_unknown` or `scope_not_held`.
222
+ - `useFoirAccess()` gives a component `hasRole(key)`, `hasScope(scope)`, a
223
+ `status` and `reload()`. It asks once however many components use it, and
224
+ again when the session refreshes.
225
+
226
+ The platform resolves the roles on every request, so nothing is cached here
227
+ and a role taken away is gone the next time it is asked.
228
+
229
+ `roles` is `null`, not an empty list, when the platform did not say which
230
+ roles the customer holds. `hasRole` answers false for it and `requireRole`
231
+ refuses with `roles_unknown`, so an outage is never read as "not staff". A
232
+ platform that does not serve the field yet reads the same way: the role
233
+ helpers need the platform release that carries it.
234
+
235
+ These decide what a page shows. They protect nothing: the platform checks
236
+ every request whatever the page offers.
237
+
238
+ `AUTH_OPS` gains `whoami`, and `FoirContextValue` gains `access`,
239
+ `accessStatus`, `loadAccess` and `ensureAccess`.
240
+
241
+ - **A save says when it made a record the operation may not read**
242
+ (`@foir/sdk/app`). `records.upsert` and `records.customer.save` answer
243
+ `fieldsWithheld`. It is true when the operation declared the create of a
244
+ model and not the read: the save made the record, and `record` carries its
245
+ `id` and `modelKey` and nothing else. The record is not empty.
246
+
247
+ ### Changed
248
+
249
+ - **Breaking, with the platform release that carries it: an operation is
250
+ called with what it declares and nothing else** (`@foir/sdk/app`). The token
251
+ used to carry the read of its owner's secrets whatever the operation
252
+ declared. An operation whose handler reads the app's credentials
253
+ (`install.credentials()`) declares `appsecrets:read`, and a project's own
254
+ operation that reads a project secret declares `secrets:read`. An installed
255
+ app's operations are given the one they could use, so an installed app keeps
256
+ running; a manifest served from then on says it.
257
+
258
+ - **Breaking, with the platform release that carries it: erasing a customer
259
+ takes `identities:delete`** (`@foir/sdk/app`). `records.customer.erase` needed
260
+ `users:create` beside the delete and the read of the model, and now needs
261
+ `identities:delete` there. `APP_GRANTS` carries it, with `files:delete`,
262
+ `notifications:read` and every owner's files (`files:read.all`,
263
+ `files:write.all`, `files:delete.all`), which a manifest may now declare.
264
+ `verifyScopedToken` reads a token minted before, that carried the customer
265
+ create beside the delete of a model, as holding `identities:delete`, as the
266
+ platform does.
267
+
268
+ - **Breaking, with the platform release that carries it: an operation declares
269
+ `grants`** (`@foir/sdk/app`, `@foir/sdk/app/contract`). A manifest's
270
+ operation says what it may do as `grants`, in the permission catalogue's
271
+ names, and the platform refuses one that still declares `capabilities`,
272
+ saying what to declare. An installed app keeps running on what it was
273
+ installed with; it is installing and updating that need the new manifest.
274
+
275
+ Most names are written as they were. These changed:
276
+
277
+ | Before | Now |
278
+ |---|---|
279
+ | `config:read` | `appconfig:read` |
280
+ | `secrets:read.app`, `secrets:put.app`, `secrets:list.app`, `secrets:delete.app` | `appsecrets:read`, `appsecrets:put`, `appsecrets:list`, `appsecrets:delete` |
281
+ | `files:list` | `files:read` |
282
+ | `customers:write` | `users:create` |
283
+ | `customers:read` | `users:read.all` |
284
+ | `customers:read:named` | `users:read.named` |
285
+ | `customers:read:identity` | `identities:read` |
286
+ | `notifications:write` | `notifications:write.named` |
287
+ | `records:write:<model>` | `records:write:<model>` and `records:create:<model>` |
288
+
289
+ The last row is a change in what the name means. `records:write:<model>`
290
+ changes a record that is there, and making one is `records:create:<model>`.
291
+ An operation that does both declares both. `records.upsert` and
292
+ `records.customer.save` are let through on either, and the platform asks for
293
+ the one the save turns out to need.
294
+
295
+ - **Breaking, with the platform release that carries it: a full-page placement
296
+ declares `grants`** (`@foir/sdk/app/contract`, `@foir/sdk/placement`). A
297
+ placement in a manifest says what its page acts with as `grants`, and the
298
+ platform refuses one that still declares `scopes`, saying what to declare.
299
+ An installed app's page keeps working on what it was installed with.
300
+
301
+ The names are the ones a team member's role is composed from, so three
302
+ things change in what a placement writes:
303
+
304
+ - **Every owner's records are written `.all`.** `records:read:order` was
305
+ every order and is now the operator's own. Declare
306
+ `records:read.all:order`.
307
+ - **A write no longer includes its read.** `records:write:order` also read
308
+ and created, and `apps:write` also read apps. Declare each one the page
309
+ uses: `records:write.all:order`, `records:read.all:order`,
310
+ `records:create:order`.
311
+ - **A wildcard and a permission that gives others are refused**, as `*`
312
+ always was.
313
+
314
+ The token then carries what the placement declares and the operator holds,
315
+ permission by permission. An operator who holds part of a declared grant is
316
+ given that part, where before the whole name was withheld.
317
+
318
+ - **`session.can` answers by what the token covers** (`@foir/sdk/placement`).
319
+ It was an exact match on the spelling. `can('records:read.all:order')` is
320
+ now true for a token that carries `records:read.all`, as Foir's API would
321
+ answer. A name the token carries as it is written is still held.
322
+
323
+ - **`PlacementGrant.grantedScopes` and `PlacementGrant.withheld` are
324
+ deprecated** (`@foir/sdk/placement`). They are still set, and say what
325
+ `grants` and `withheldGrants` say.
326
+
327
+ - **Breaking, with the platform release that carries it: `whoami` answers
328
+ `grants`** (`@foir/sdk/app`). `AppWhoami.capabilities` is gone and
329
+ `AppWhoami.grants` takes its place, and `AppWhoami.models` gains `create`.
330
+
331
+ - **Breaking: the older claims on `ScopedTokenClaims` are optional**
332
+ (`@foir/sdk/server`). `cap`, `rrm` and `rwm` are still on a token the
333
+ platform mints, in the older names, and are marked deprecated: they never
334
+ say more than `grants` and can say less. `recordsReadModels` and
335
+ `recordsWriteModels` now read `grants`, so `recordsWriteModels` is the models
336
+ a token may change records of, and no longer the ones it may make them in.
337
+ Code that builds a `ScopedTokenClaims` by hand sets `grants`.
338
+
339
+ - **Breaking: `AppUpsertResult` has a new required field, `fieldsWithheld`.**
340
+ Code that builds one by hand, a test double for `records.upsert` for
341
+ instance, has to set it.
342
+
343
+ - **Breaking, with the platform release that carries it: only making a record
344
+ takes no read.** A write that finds a record already there is charged the
345
+ read of it as well. This is the platform's rule and reaches every caller,
346
+ whatever version of this package they run.
347
+
348
+ - `create` and `submit` (`@foir/sdk/data`) ask for the whole record by
349
+ default. A credential that may create records of a model and may not read
350
+ them is refused that, before anything is written, and told the read it
351
+ lacks. Pass `pick: ['_id']` to make the record and be answered with its id.
352
+ - `update`, `rename`, `delete`, `publish` and `unpublish` need
353
+ `records:read` for the model beside their own permission.
354
+ - An operation that declares the create of a model and not the read
355
+ (`@foir/sdk/app`) still makes its records. `records.upsert` landing on a
356
+ record already there, `records.customer.save` likewise, `records.delete`,
357
+ `records.refresh`, `records.rename` and `records.updateMany` are refused
358
+ until it declares the read too.
359
+
360
+ - **Breaking, with the platform release that carries it: `records.customer.erase`
361
+ needs the delete of the model** (`@foir/sdk/app`). An erase removes a record,
362
+ so the operation declares `records:delete` and `records:read` for the model,
363
+ and `users:create`. Until now the grant to save the record was enough. An
364
+ operation that erased on its write lane is refused until it declares the
365
+ delete. Nothing is taken from holding a write.
366
+
367
+ - **Breaking, with the platform release that carries it: a role in
368
+ `myOrganizations` carries `grants`** (`@foir/sdk/auth`).
369
+ `MyOrganizationRole.permissions` is gone and `MyOrganizationRole.grants`
370
+ takes its place: what the role gives, in the permission catalogue's names. A
371
+ grant stands for itself, so changing a record and creating one are two
372
+ names (`records:write`, `records:create`), reading every customer's records
373
+ is `records:read.all`, and staffing an organization is `members:read` and
374
+ `members:write`. Code that looked for `organizations:staff` or
375
+ `records:list.all` in a role's list looks for the new names.
376
+
377
+ - **Breaking, with the platform release that carries it: `customers.byIdentity`
378
+ and `customers.identities` need `identities:read`, declared by name**
379
+ (`@foir/sdk/app`). The read of every customer, `users:read.all`, no longer
380
+ includes them.
381
+
382
+ ### Removed
383
+
384
+ - **Breaking: `APP_CAPABILITIES`, `APP_RECORD_CAPABILITY_PREFIXES`,
385
+ `AppCapability` and `AppRecordCapabilityPrefix`** (`@foir/sdk/app`). They
386
+ listed the names an operation was declared with before, which the platform
387
+ now refuses. `APP_GRANTS`, `APP_RECORD_GRANT_PREFIXES`, `AppGrant` and
388
+ `AppRecordGrantPrefix` list what it declares now.
389
+
390
+ ## [0.19.0] — 2026-10-02
391
+
392
+ Published under npm's `next` tag from `dev`. The data client's change needs no
393
+ platform release. `setConsent` by email calls a field the platform serves from
394
+ the next promotion; `latest` moves to this version then.
395
+
396
+ ### Changed
397
+
398
+ - **Breaking: schema drift by itself no longer refuses a typed call**
399
+ (`@foir/sdk/data`). What `strictSchemaDrift` refuses has changed; its name
400
+ and its default have not.
401
+
402
+ Before, a client built with a generated module refused every typed call,
403
+ with `FoirSchemaDriftError`, from the moment it saw the server serving another
404
+ surface version. That version is a hash over everything `foir types` reads,
405
+ so it moves whenever the surface grows. A field published on a model, or an
406
+ op the platform added to every model, made every deployed client refuse its
407
+ second typed call, though nothing it prints had changed.
408
+
409
+ Now the drift is reported once, exactly as before (`onSchemaDrift`, or the
410
+ `console.warn`), and it refuses nothing by itself. When the manifest arrives
411
+ the client compares its generated trees with the served ones, and refuses a
412
+ typed call only when something that call depends on is no longer served the
413
+ way the module describes it:
414
+
415
+ - its op is gone, or the op's root field or result kind changed;
416
+ - an argument the module declares is gone, or has a type the generated one no
417
+ longer fits, or the op now requires an argument the module does not know;
418
+ - a field its selection names is gone or changed shape, in the model the op
419
+ returns or in a union that model embeds;
420
+ - a relation the call includes changed target, became a list or a
421
+ connection, or its target model lost a field.
422
+
423
+ Anything added refuses nothing: a model, a namespace, an op, an optional
424
+ argument, a field, a relation, a union member, a subscription. An argument
425
+ that went from required to optional refuses nothing either.
426
+
427
+ `FoirSchemaDriftError`'s message now names what the refused call depends on
428
+ (`models.order.selection.status (no longer served)`), and still names
429
+ `foir types` as the remedy. The warning for a drift that broke nothing says
430
+ the surface has grown and that every typed call keeps working.
431
+
432
+ Unchanged: `strictSchemaDrift: false` turns refusal off entirely, the first
433
+ call on a client is never blocked on the manifest, `raw()` is never refused,
434
+ and `checkSchemaDrift: false` skips the fetch. `createFoirDriftHandler` in
435
+ `@foir/sdk/live` still resyncs on every drift, whether or not anything is
436
+ incompatible.
437
+
438
+ A client wired by hand with `surfaceVersion` and no `selections` prints from
439
+ the served manifest, so it has nothing older to compare and is no longer
440
+ refused on drift.
441
+
442
+ Migration: none for an app that only catches `FoirSchemaDriftError`. An app
443
+ that used the refusal to learn that its module was stale in ANY way should
444
+ read `onSchemaDrift`, which still fires for every drift, or gate on
445
+ `foir types --check` in CI.
446
+ - **Breaking: `FoirSchemaDrift` has a new required field, `incompatible`**
447
+ (`@foir/sdk/data`). Code that reads a drift is unaffected. Code that builds
448
+ one by hand, for example to test a handler given to `onSchemaDrift` or made by
449
+ `createFoirDriftHandler`, must add `incompatible: []`.
450
+ - **Breaking (types): `AppConsentChange` names its customer one way**
451
+ (`@foir/sdk/app`). It was an interface with a required `customerId`. It is
452
+ now a type that holds a `customerId` or an `email`, never both. A change
453
+ written as `{ customerId, ... }` is unaffected. An interface that extended
454
+ `AppConsentChange` no longer compiles: intersect with it instead.
455
+
456
+ ### Added
457
+
458
+ - `notifications.setConsent` takes `email` in place of `customerId`
459
+ (`@foir/sdk/app`), for an opt-in from somebody who may have no customer yet,
460
+ such as a sign-up form. The platform finds the customer holding the address
461
+ at the install's level, or makes one, together with the change, so a change
462
+ it refuses (a category the project does not have, a confirmation email the
463
+ project cannot send) leaves no customer behind. Finding or making the
464
+ customer first with `customers.ensure` did. The operation must declare
465
+ `customers:write` as well as `notifications:consent`. `AppConsentSubject` is
466
+ exported.
467
+
468
+ - `FoirSchemaDrift.incompatible`: what the generated module describes that the
469
+ server no longer serves that way, as sorted paths into the manifest
470
+ (`models.order.selection.status`, `models.order.ops.update`,
471
+ `namespaces.search.ops.searchRecords.args.query`,
472
+ `models.order.relations.customer`, `selectionUnions.Block.HeroBlock.title`).
473
+ Empty when the surface has only grown. `changed` keeps its meaning: what
474
+ moved, not what broke.
475
+
111
476
  ## [0.18.0] — 2026-10-02
112
477
 
113
478
  Published under npm's `next` tag from `dev`, because it calls fields the
package/README.md CHANGED
@@ -88,7 +88,14 @@ was generated from: a hash over exactly what `foir types` reads, which moves onl
88
88
  when regenerating would write a different file. Pushing an operation, a role or
89
89
  a key moves the server's invalidation counter and not the surface, so it is not
90
90
  drift. Publishing a field on a model the types include is, and the client
91
- refuses the next typed call with a message naming the model. The remedy is
91
+ reports it once, naming the model.
92
+
93
+ **Drift does not stop a call.** A surface that has grown still answers every
94
+ document the module prints. A typed call is refused, with `FoirSchemaDriftError`,
95
+ only when something it depends on is no longer served as generated: a field its
96
+ selection names, its op, one of its arguments, or the target of a relation it
97
+ includes. The message names that part, and `drift.incompatible` lists every
98
+ such part as a path (`models.order.selection.status`). The remedy is
92
99
  `foir types`; `strictSchemaDrift: false` never is.
93
100
 
94
101
  **Scalars are always complete**, so `post.title` is typed present and *is*
@@ -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,8 +197,56 @@ 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
  }
189
- export interface AppConsentChange {
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
+ }
232
+ /**
233
+ * Who a consent change is for: a customer whose id the app holds, or an
234
+ * address.
235
+ *
236
+ * By `email`, the customer holding the address at the operation's level is
237
+ * found, or made, together with the change, so a change the platform refuses
238
+ * leaves no customer behind. It is for an opt-in from somebody who may have no
239
+ * customer yet (a sign-up form), and the operation must declare
240
+ * `users:create` as well as `notifications:consent`.
241
+ */
242
+ export type AppConsentSubject = {
190
243
  customerId: string;
244
+ email?: undefined;
245
+ } | {
246
+ email: string;
247
+ customerId?: undefined;
248
+ };
249
+ export type AppConsentChange = AppConsentSubject & {
191
250
  /** A notification category the project makes opt-in. */
192
251
  category: string;
193
252
  /** The channel the consent is for, as the project's preferences name it (for example `email`). */
@@ -215,17 +274,18 @@ export interface AppConsentChange {
215
274
  * email is written in it when the customer has stated none of their own.
216
275
  */
217
276
  locale?: string;
218
- }
277
+ };
219
278
  /** What the calling token is, and what it is served. Read from the platform, never off the token. */
220
279
  export interface AppWhoami {
221
280
  /** The app the token acts for, when it acts for one. */
222
281
  app: string | null;
223
- /** The capabilities the token carries. */
224
- capabilities: string[];
225
- /** 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. */
226
285
  models: {
227
286
  read: string[];
228
287
  write: string[];
288
+ create: string[];
229
289
  delete: string[];
230
290
  publish: string[];
231
291
  };
@@ -248,17 +308,17 @@ export interface AppClient {
248
308
  whoami(): Promise<AppWhoami>;
249
309
  /** The install this token acts for: its configuration, its credential, its status. */
250
310
  install: {
251
- /** The operator's settings and mappings. Needs `config:read`. */
311
+ /** The operator's settings and mappings. Needs `appconfig:read`. */
252
312
  config(): Promise<AppInstallConfig>;
253
313
  /**
254
314
  * The install's credential, by the manifest's credential field names.
255
- * Values are always strings. Needs `secrets:read.app`.
315
+ * Values are always strings. Needs `appsecrets:read`.
256
316
  */
257
317
  credentials(): Promise<Record<string, string>>;
258
318
  /**
259
319
  * Write or rotate credential fields. An empty value is skipped, not
260
320
  * cleared, and a call naming no non-empty field is refused. Needs
261
- * `secrets:put.app`.
321
+ * `appsecrets:put`.
262
322
  */
263
323
  writeCredentials(values: Record<string, string>): Promise<void>;
264
324
  /** Report the install's health. Needs `status:write`. See {@link AppStatusReport}. */
@@ -268,7 +328,9 @@ export interface AppClient {
268
328
  records: {
269
329
  /**
270
330
  * Write the record at a natural key, making it when there is none.
271
- * `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
272
334
  * `records:attribute:<model>`); `sourceModifiedAt` is when the object this
273
335
  * mirrors last changed at its source, so an older copy arriving late is
274
336
  * not written.
@@ -383,7 +445,9 @@ export interface AppClient {
383
445
  /**
384
446
  * Erase what the app holds for the customer, as a privacy request asks:
385
447
  * their record and the identity's link, and the customer when nothing
386
- * 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.
387
451
  */
388
452
  erase(model: string, identity: string): Promise<AppCustomerEraseResult>;
389
453
  };
@@ -392,7 +456,7 @@ export interface AppClient {
392
456
  /**
393
457
  * The customer who has this email at the install's level, made when there
394
458
  * is none. The same customer every time for one email. Needs
395
- * `customers:write`.
459
+ * `users:create`.
396
460
  *
397
461
  * `verified` says whether that address is one the customer has proved is
398
462
  * theirs. A customer this call makes has not, and neither has one an
@@ -407,14 +471,14 @@ export interface AppClient {
407
471
  }>;
408
472
  /**
409
473
  * One customer by id, or null when there is none at the install's level.
410
- * Needs `customers:read:named`, or `customers:read`.
474
+ * Needs `users:read.named`, or `users:read.all`.
411
475
  */
412
476
  get(id: string): Promise<AppCustomer | null>;
413
477
  /**
414
478
  * The customer who has this email at the install's level, or null. The
415
479
  * exact address, as `ensure` matches it, and nothing wider: it finds one
416
- * customer and never searches. Needs `customers:read:named`, or
417
- * `customers:read`.
480
+ * customer and never searches. Needs `users:read.named`, or
481
+ * `users:read.all`.
418
482
  */
419
483
  byEmail(email: string): Promise<AppCustomer | null>;
420
484
  /**
@@ -423,7 +487,8 @@ export interface AppClient {
423
487
  * of its customers. The identity is the subject of the customer's sign-in
424
488
  * with that system, and it is resolved through the customer sign-in the
425
489
  * install pairs (`install.config().customerSignIn`) and no other. Never
426
- * 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.
427
492
  */
428
493
  byIdentity(identity: string): Promise<string | null>;
429
494
  /**
@@ -431,13 +496,13 @@ export interface AppClient {
431
496
  * holds at the app's own system, most recently seen first, through the
432
497
  * same pairing. Empty when they have never signed in that way. For an app
433
498
  * acting on an event that names the Foir customer. Needs
434
- * `customers:read:identity`, or `customers:read`.
499
+ * `identities:read`, declared by name.
435
500
  */
436
501
  identities(customerId: string): Promise<string[]>;
437
502
  /**
438
503
  * The customers at the install's level, a page at a time, newest first.
439
- * Needs `customers:read`: an operation declaring only
440
- * `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
441
506
  * list.
442
507
  */
443
508
  list(query?: AppCustomerList): Promise<{
@@ -446,8 +511,47 @@ export interface AppClient {
446
511
  endCursor: string | null;
447
512
  }>;
448
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
+ };
449
553
  notifications: {
450
- /** Tell a customer something. Needs `notifications:write`. */
554
+ /** Tell a customer something. Needs `notifications:write.named`. */
451
555
  send(notification: AppNotification): Promise<void>;
452
556
  /**
453
557
  * Set a named customer's consent in an opt-in category. Needs
@@ -458,6 +562,10 @@ export interface AppClient {
458
562
  * been sent a link, or was sent one recently, and the preference turns on
459
563
  * when they follow it. The answer is the same whether or not the address
460
564
  * already had a customer.
565
+ *
566
+ * Name the customer by `customerId`, or by `email` for somebody who may
567
+ * have no customer yet: the platform finds or makes them with the change,
568
+ * and that also needs `users:create`.
461
569
  */
462
570
  setConsent(change: AppConsentChange): Promise<{
463
571
  stale: boolean;
@@ -477,7 +585,7 @@ export interface AppClient {
477
585
  *
478
586
  * Only the token a delivery arrived with may call it, for an operation
479
587
  * that is active at the install. An operation's own token is refused: it
480
- * carries that operation's capabilities and no other's.
588
+ * carries that operation's grants and no other's.
481
589
  */
482
590
  start(operationKey: string, options?: AppOperationStart): Promise<AppStartedOperation>;
483
591
  };