@foir/cli 0.73.0 → 0.77.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.
@@ -293,11 +293,13 @@ interface ApplyConfigOperationInput {
293
293
  /** Retry policy for failed executions. */
294
294
  retryPolicy?: Record<string, unknown>;
295
295
  /**
296
- * Admin permission verbs required to execute this operation.
296
+ * Permissions a console user must hold to execute this operation.
297
297
  *
298
- * CONJUNCTIVE: the caller must hold EVERY verb listed, so adding one always
299
- * narrows who may execute. Omit or pass `[]` for no gate. Entries are verbs
300
- * from the admin permission vocabulary, e.g. `['content:write']`.
298
+ * CONJUNCTIVE: the caller must hold EVERY permission listed, so adding one
299
+ * always narrows who may execute. Omit or pass `[]` for no gate. Each entry
300
+ * is a permission as the role editor names it, with its reach where it has
301
+ * one, e.g. `['records:write.all', 'models:write']`. An entry that is not a
302
+ * permission a console role can hold is refused when the config is pushed.
301
303
  *
302
304
  * Replaced `allowedRoles`, which named role KEYS. Role keys are
303
305
  * yours to choose, so renaming a role silently changed who could execute.
@@ -345,15 +347,15 @@ interface ApplyConfigOperationInput {
345
347
  maxRetries?: number;
346
348
  };
347
349
  /**
348
- * Capability strings stamped onto the scoped dispatch token (and its
349
- * callback-window successor) so the extension can call back into
350
- * api-public with the precise authorities it needs — nothing more.
350
+ * What the operation may do when its endpoint calls back into Foir, as
351
+ * grants in the permission catalogue's names. Its dispatch token carries
352
+ * them, and so does the token for its callback window: nothing more.
351
353
  *
352
- * Per-model record access is three-part: `records:read:<model_key>`,
353
- * `records:write:<model_key>`, `records:delete:<model_key>`,
354
- * `records:publish:<model_key>`. List every model the operation
355
- * touches; the unscoped two-part forms (`records:read` etc.) only
356
- * work for admin API keys and won't satisfy customer-scoped tokens.
354
+ * A record permission names its model: `records:read:<model_key>`,
355
+ * `records:write:<model_key>`, `records:create:<model_key>`,
356
+ * `records:delete:<model_key>`, `records:publish:<model_key>`. The write
357
+ * changes a record that is there and the create makes one, so an operation
358
+ * that does both declares both. List every model the operation touches.
357
359
  *
358
360
  * Ownership, `records:attribute:<model_key>` (or `records:attribute` for
359
361
  * every model), lets the operation name the customer a record belongs to
@@ -361,23 +363,24 @@ interface ApplyConfigOperationInput {
361
363
  * to another customer (`set<Model>Owner`). It is held only by an
362
364
  * operation: no API key of any type can be minted with it.
363
365
  *
364
- * Cross-cutting caps: `embeddings:read`, `embeddings:write`,
365
- * `files:read`, `files:write`, `customers:write` (create a customer,
366
- * such as the buyer at a guest checkout), `customers:read:named` (read one
367
- * customer the operation names, by id or by their exact email address),
368
- * `customers:read` (that, and the list), `notifications:write` (notify the
369
- * project's customers; an operation acting for a customer notifies that
370
- * customer alone, and one at an organisation reaches only its customers),
371
- * `config:read`, `status:write`, and the secrets:* family. A capability
372
- * outside this vocabulary is refused at push, by name. See
373
- * `services/internal/scopedtoken/capabilities.go` for the full enum.
366
+ * The rest are written as the catalogue writes them: `embeddings:read`,
367
+ * `embeddings:write`, `files:read`, `files:write`, `users:create` (create
368
+ * a customer, such as the buyer at a guest checkout), `users:read.named`
369
+ * (read one customer the operation names, by id or by their exact email
370
+ * address), `users:read.all` (that, and the list),
371
+ * `notifications:write.named` (notify the project's customers; an
372
+ * operation acting for a customer notifies that customer alone, and one
373
+ * at an organisation reaches only its customers), `status:write`, and
374
+ * `secrets:read`. A grant an operation may not declare is refused at
375
+ * push, by name. The whole list is at
376
+ * https://docs.foir.dev/api/operation-permissions.
374
377
  *
375
- * `operations:complete` is auto-injected on callback tokens — never
376
- * declare it here. Omitting `capabilities` (or passing an empty array)
377
- * mints an authentic-but-unprivileged token; useful for operations
378
- * whose extension makes no back-channel calls.
378
+ * `operations:complete` is added to a callback token by the platform, so
379
+ * never declare it here. Omitting `grants` (or passing an empty array)
380
+ * mints a token that carries nothing, which suits an operation whose
381
+ * endpoint never calls back.
379
382
  */
380
- capabilities?: string[];
383
+ grants?: string[];
381
384
  /**
382
385
  * How the operation surfaces in the public GraphQL schema and how it is
383
386
  * dispatched. `mutation` (default) is the existing behaviour: an
@@ -395,8 +398,9 @@ interface ApplyConfigOperationInput {
395
398
  */
396
399
  attachedToModel?: string;
397
400
  /**
398
- * Field path(s) under `attachedToModel` that this resolver populates.
399
- * FIELD only.
401
+ * Fields of the `attachedToModel` record sent to the resolver, where the
402
+ * request template reads them as `{{parent.X}}`. Only the fields named here
403
+ * are sent. FIELD only.
400
404
  */
401
405
  parentFields?: string[];
402
406
  /**
@@ -445,7 +449,9 @@ interface ApplyConfigOperationInput {
445
449
  * A context dimension — the home for segments. `sourceType: 'segment'`
446
450
  * carries an `evaluationRules` RuleExpression (evaluated fresh at request time
447
451
  * into segment.<key>); `sourceType: 'model'` designates a customer-context
448
- * source model via `sourceModelKey`. Reconciled config-scoped by `foir push`
452
+ * source model via `sourceModelKey`; `sourceType: 'request'` is a value the
453
+ * application sends in the contexts argument of a read, one of the records of
454
+ * the model `sourceModelKey` names. Reconciled config-scoped by `foir push`
449
455
  * (owned via config_id): dimensions this config declares are created / updated,
450
456
  * and config-owned dimensions dropped from the config are deleted.
451
457
  */
@@ -453,11 +459,18 @@ interface ApplyConfigContextDimensionInput {
453
459
  key: string;
454
460
  name: string;
455
461
  description?: string;
456
- /** 'segment' (rule-based) | 'model' (customer-context source) | custom. */
462
+ /**
463
+ * 'segment' (rule-based) | 'model' (customer-context source) | 'request'
464
+ * (a value sent with the read) | custom.
465
+ */
457
466
  sourceType: string;
458
467
  /** Segment rule (sourceType 'segment') — a RuleExpression object. */
459
468
  evaluationRules?: Record<string, unknown>;
460
- /** Customer-owned singleton model key (sourceType 'model'). */
469
+ /**
470
+ * The model the dimension is backed by. For sourceType 'model', a
471
+ * customer-owned singleton. For sourceType 'request', a model whose records
472
+ * belong to the project as a whole: each record's natural key is one value.
473
+ */
461
474
  sourceModelKey?: string;
462
475
  defaultRecordKey?: string;
463
476
  }
@@ -530,6 +543,15 @@ interface ApplyConfigScheduleInput {
530
543
  cron: string;
531
544
  timezone?: string;
532
545
  enabled?: boolean;
546
+ /**
547
+ * What one fire starts a run for. Left out, a fire starts one run, at the
548
+ * project's own level. `'organization'` starts one run in each active
549
+ * organization of the project, at every depth: each run is started in that
550
+ * organization, is confined to it, and holds what the operation declares.
551
+ * An organization that is suspended or scheduled for deletion gets no run.
552
+ * The operation must be one of the project's own, not an app's.
553
+ */
554
+ each?: 'organization';
533
555
  }
534
556
  interface ApplyConfigAuthProviderInput {
535
557
  key: string;
@@ -600,12 +622,22 @@ interface ApplyConfigRelyingPartyInput {
600
622
  */
601
623
  redirectUris: string[];
602
624
  /**
603
- * OAuth scopes the RP can request. A request is granted the scopes this list
604
- * covers, narrowed forms included: `operations:execute` here grants a request
605
- * for `operations:execute:checkout`. A scope wider than anything listed is
606
- * not granted.
625
+ * The most a sign-in through this app may carry of what the person's roles
626
+ * give them, in the permission catalogue's names: the same names a customer
627
+ * role is declared with (https://docs.foir.dev/api/role-permissions).
628
+ *
629
+ * The list gives nobody anything. A person holds what their roles give, and
630
+ * the list decides how much of it a token minted for this app carries. So it
631
+ * may hold a wildcard, which a role may not: `['*']` is an app that is not
632
+ * narrowed, and `'records:*'` carries whatever its users hold over records.
633
+ * An entry covers its narrower forms: `operations:execute` here covers a
634
+ * person's `operations:execute:checkout`.
635
+ *
636
+ * Required, and nothing is read into an absence: `[]` is an app whose
637
+ * sign-ins carry nothing of what its users' roles give. A sign-in may ask
638
+ * for less with `scope`, in the same names.
607
639
  */
608
- allowedScopes?: string[];
640
+ grants: string[];
609
641
  /**
610
642
  * Per-RP login method filter (customer-lane). Combines with the project-
611
643
  * level customer auth settings (project may have password OFF entirely; an
@@ -676,8 +708,35 @@ interface ApplyConfigApiKeyInput {
676
708
  keyType: 'public' | 'secret';
677
709
  /** Environment variable name to write the key to in .env (e.g. "FOIR_PUBLIC_KEY"). */
678
710
  envVar: string;
679
- /** Scopes to restrict the key (e.g. ["configs:read", "records:write"]). Use ["*"] for full access. */
680
- scopes?: string[];
711
+ /**
712
+ * What the key holds: the most anyone may do through it, in the permission
713
+ * catalogue's names (e.g. ["records:read", "records:write"]). A secret key
714
+ * may hold ["*"].
715
+ *
716
+ * Declared for every key. Nothing is filled in, so a key declared without
717
+ * it would be minted holding nothing, and a push refuses one.
718
+ *
719
+ * A push changes it on the existing key in place: the key keeps its id and
720
+ * its secret, and the new list is live the moment the push writes it.
721
+ */
722
+ grants: string[];
723
+ /**
724
+ * What the key gives whoever comes through it, in the same names: a visitor
725
+ * with no sign-in, on a public key, and a signed-in user beneath their role,
726
+ * on either kind.
727
+ *
728
+ * NOTHING IS FILLED IN. A key declared without this list gives a visitor
729
+ * nothing, whatever it holds: a public key holding `records:read` and no
730
+ * list here is a key no visitor can read with. Name each read a visitor
731
+ * needs. A visitor may also be given one model to submit into
732
+ * (`records:create:<model>`) and one operation to run
733
+ * (`operations:execute:<operation>`), each naming something this config
734
+ * declares.
735
+ *
736
+ * Declared, the list is the key's whole second list and a push sets it in
737
+ * place. Left out, a push leaves what the key gives as it is.
738
+ */
739
+ baselineGrants?: string[];
681
740
  /** Restrict the key to specific model keys (e.g. ["tilly_note", "tilly_block"]). */
682
741
  allowedModels?: string[];
683
742
  /** Restrict file uploads: a MIME type ("image/png"), a whole type ("image/*"), or IMAGE, VIDEO, FILE. */
@@ -701,32 +760,18 @@ interface ApplyConfigApiKeyInput {
701
760
  * credential to change which tenant it belongs to.
702
761
  */
703
762
  org?: string;
704
- /**
705
- * What a stranger may do with this key: the caller carrying it with nobody
706
- * signed in, such as a guest at checkout or a contact form.
707
- *
708
- * Two forms, each naming one thing this config declares:
709
- * `records:create:<model>` and `operations:execute:<operation>`. No wildcard,
710
- * no update, delete or publish. A PUBLIC key only: a secret key is presented
711
- * by a server that already knows who is calling.
712
- *
713
- * A push changes it on the existing key in place, as it does the key's
714
- * scopes: the key keeps its id and its secret, and the new list is live the
715
- * moment the push writes it, not at a release.
716
- */
717
- anonymousScopes?: string[];
718
763
  }
719
764
  /**
720
- * A customer RBAC role declared in foir.config.ts and reconciled by
721
- * `foir push`. The role's `permissions` become a customer access token's
722
- * scope set at mint time. Roles are matched by `key` within the
723
- * project — a declared key that already exists is updated in place; a new
724
- * key is created. (Roles present on the platform but absent from the config
725
- * are left untouched — never auto-disabled.)
765
+ * A customer role declared in foir.config.ts and reconciled by `foir push`.
766
+ * The role's `grants` are what a customer who holds it may do once signed in:
767
+ * their access token carries them. Roles are matched by `key` within the
768
+ * project: a declared key that already exists is updated in place, and a new
769
+ * key is created. A role on the platform that the config leaves out is left
770
+ * as it is, never disabled.
726
771
  *
727
- * Without a role, a consumer app's customers get read-only tokens after the
728
- * Model B auth change: the public `pk_` key can no longer grant
729
- * write/execute, so the only source of those scopes is the customer's role.
772
+ * Without a role, a signed-in customer may do only what the key gives
773
+ * everyone who comes through it (its `baselineGrants`). Anything a customer
774
+ * may do beyond that comes from a role they hold.
730
775
  */
731
776
  interface ApplyConfigCustomerRoleInput {
732
777
  /** Stable key, unique per project (e.g. "default", "member"). */
@@ -734,20 +779,29 @@ interface ApplyConfigCustomerRoleInput {
734
779
  /** Human-readable name (e.g. "Default Customer"). */
735
780
  name: string;
736
781
  /**
737
- * Scopes granted to customers holding this role. Reuses the scoped-token
738
- * grammar (records:read:<model>, records:write:<model>, operations:execute,
739
- * files:read, …). The `self:*` macro expands at mint time to
740
- * records:{read,write}:<model> for every customer-writable model in the
741
- * project — but does NOT include operations:execute, so list that
742
- * explicitly when the app's customers run operations.
782
+ * What the role gives customers who hold it, as grants in the permission
783
+ * catalogue's names: records:read:<model>, records:write:<model>,
784
+ * records:create:<model>, operations:execute, files:read and the rest of
785
+ * the list at https://docs.foir.dev/api/role-permissions.
786
+ *
787
+ * Each grant stands for itself. Changing a record and creating one are two
788
+ * grants, so a role that should do both lists `records:write:<model>` and
789
+ * `records:create:<model>`.
790
+ *
791
+ * The `self:*` shorthand is worked out when a customer signs in, into the
792
+ * read, the write and the create of every model the project lets a customer
793
+ * write. It does NOT include operations:execute, so list that explicitly
794
+ * when the app's customers run operations.
743
795
  *
744
796
  * `operations:execute` runs every operation the key exposes;
745
797
  * `operations:execute:<operation_key>` runs that one alone, so a role can
746
798
  * let every customer check out and keep rebooking for the customers whose
747
- * role grants it. A push
748
- * naming an operation the project does not have is refused, by name.
799
+ * role gives it. A push naming an operation the project does not have is
800
+ * refused, by name.
801
+ *
802
+ * A push puts on a role only what the person pushing holds themselves.
749
803
  */
750
- permissions: string[];
804
+ grants: string[];
751
805
  /**
752
806
  * When true, every customer in the project inherits this role with no
753
807
  * per-customer assignment. An app's baseline customer grant should be a
@@ -72,19 +72,29 @@ type Hold = {
72
72
  };
73
73
  type FieldDecision = RenameTo | Retype | RemapValues | SplitInto | JoinFrom | FillFrom | Drop | Hold;
74
74
  /**
75
- * Who owns the records that name no owner when the model moves onto an owned
76
- * plane: each takes the owner one of its fields names, or they are left as
77
- * they are while you handle it. A release carrying the move refuses until this
78
- * is answered, and an answered assignment runs inside that release.
75
+ * What happens to the records whose owner does not fit the plane their model is
76
+ * released on.
77
+ *
78
+ * A model that moves onto an owned plane with records that name no owner: each
79
+ * takes the owner one of its fields names (`assignFrom`), or they are left as
80
+ * they are while you handle it (`hold`).
81
+ *
82
+ * A model on the project's plane with records that name an owner: each becomes
83
+ * the project's (`toProject`), or they are left their owners' (`hold`).
84
+ *
85
+ * A release that carries such a move refuses until this is answered, and an
86
+ * answered decision runs inside that release.
79
87
  */
80
88
  type OwnerDecision = {
81
89
  assignFrom: string;
82
90
  } | {
83
91
  hold: string;
92
+ } | {
93
+ toProject: true;
84
94
  };
85
95
  /**
86
96
  * One model's decisions, keyed by the field being removed, and under `$owner`
87
- * the decision about records that name no owner.
97
+ * the decision about records whose owner does not fit the model's plane.
88
98
  */
89
99
  type ModelDecisions = {
90
100
  [fieldKey: string]: FieldDecision | OwnerDecision | undefined;
@@ -107,7 +117,7 @@ interface MigrationFile {
107
117
  }
108
118
  /**
109
119
  * Declare what happens to the data under fields this push removes, and to
110
- * records that name no owner when a model moves onto an owned plane.
120
+ * records whose owner does not fit the plane their model is released on.
111
121
  *
112
122
  * Returns a plain object rather than doing anything: a decision file is a
113
123
  * declaration that gets committed and reviewed, and code that acted at import
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/cli",
3
- "version": "0.73.0",
3
+ "version": "0.77.0",
4
4
  "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -61,11 +61,12 @@
61
61
  "tsx": "^4.20.0",
62
62
  "typescript": "5.9.2",
63
63
  "vitest": "^3.2.4",
64
+ "@foir/access-vocab": "0.0.0",
64
65
  "@foir/assistant": "0.0.0",
65
- "@foir/console-routes": "0.0.0",
66
66
  "@foir/proto-ts": "0.109.0",
67
67
  "@foir/rpc-node": "0.0.0",
68
- "@foir/sdk": "0.18.0"
68
+ "@foir/sdk": "0.20.0",
69
+ "@foir/console-routes": "0.0.0"
69
70
  },
70
71
  "engines": {
71
72
  "node": ">=18.0.0"