@foir/cli 0.74.0 → 0.78.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
@@ -672,12 +704,67 @@ interface ApplyConfigHookInput {
672
704
  interface ApplyConfigApiKeyInput {
673
705
  /** Name for this API key (e.g. "Tilly iOS", "Tilly CRM sync"). */
674
706
  name: string;
675
- /** Key type: 'public' for your app, 'secret' for a third-party client's own server. */
676
- keyType: 'public' | 'secret';
707
+ /**
708
+ * What the key is for. You choose it, and the platform holds the key to what
709
+ * that kind may hold and give:
710
+ *
711
+ * - `'publishable'` sits in a web page or an app, where anyone can read it.
712
+ * It gives a visitor only what is safe to give everyone.
713
+ * - `'server'` is held by the server that renders your pages. It is a
714
+ * publishable key that may also say who and where its visitor is
715
+ * (`visitors:forward`), and it never goes to a browser.
716
+ * - `'admin'` is a machine's own: a sync job, a pipeline, a tool. It does
717
+ * what it holds, gives nobody anything, and nobody signs in through it.
718
+ * It expires.
719
+ *
720
+ * A key's kind never changes. A config that declares another kind for a key
721
+ * that already exists is refused: declare a new key under a new name.
722
+ */
723
+ kind: 'publishable' | 'server' | 'admin';
677
724
  /** Environment variable name to write the key to in .env (e.g. "FOIR_PUBLIC_KEY"). */
678
725
  envVar: string;
679
- /** Scopes to restrict the key (e.g. ["configs:read", "records:write"]). Use ["*"] for full access. */
680
- scopes?: string[];
726
+ /**
727
+ * What the key holds, in the permission catalogue's names (e.g.
728
+ * ["records:read", "records:write"]): for a publishable or a server key the
729
+ * most a signed-in user may do through it, and for an admin key what it
730
+ * does. An admin key may hold ["*"].
731
+ *
732
+ * Declared for every key. Nothing is filled in, so a key declared without
733
+ * it would be minted holding nothing, and a push refuses one.
734
+ *
735
+ * A push changes it on the existing key in place: the key keeps its id and
736
+ * its secret, and the new list is live the moment the push writes it.
737
+ */
738
+ grants: string[];
739
+ /**
740
+ * What a publishable or a server key gives whoever comes through it, in the
741
+ * same names: a visitor with no sign-in, and a signed-in user beneath their
742
+ * role. An admin key gives nobody anything, and one declared with this list
743
+ * is refused.
744
+ *
745
+ * NOTHING IS FILLED IN. A key declared without this list gives a visitor
746
+ * nothing, whatever it holds: a publishable key holding `records:read` and
747
+ * no list here is a key no visitor can read with. Name each read a visitor
748
+ * needs. A visitor may also be given one model to submit into
749
+ * (`records:create:<model>`) and one operation to run
750
+ * (`operations:execute:<operation>`), each naming something this config
751
+ * declares.
752
+ *
753
+ * Declared, the list is the key's whole second list and a push sets it in
754
+ * place. Left out, a push leaves what the key gives as it is.
755
+ */
756
+ baselineGrants?: string[];
757
+ /**
758
+ * An admin key only: how long it lives from the push that makes it, in days
759
+ * (e.g. "90d", which is what it is given when nothing is said, up to
760
+ * "365d"). A publishable or a server key never expires, and one declared
761
+ * with this is refused.
762
+ *
763
+ * It is read when the key is made. A push never renews a key that exists:
764
+ * renew it with `foir api-keys renew`, so that a key is kept alive by
765
+ * somebody deciding to and not by a pipeline that happens to run.
766
+ */
767
+ expiresIn?: string;
681
768
  /** Restrict the key to specific model keys (e.g. ["tilly_note", "tilly_block"]). */
682
769
  allowedModels?: string[];
683
770
  /** Restrict file uploads: a MIME type ("image/png"), a whole type ("image/*"), or IMAGE, VIDEO, FILE. */
@@ -701,32 +788,18 @@ interface ApplyConfigApiKeyInput {
701
788
  * credential to change which tenant it belongs to.
702
789
  */
703
790
  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
791
  }
719
792
  /**
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.)
793
+ * A customer role declared in foir.config.ts and reconciled by `foir push`.
794
+ * The role's `grants` are what a customer who holds it may do once signed in:
795
+ * their access token carries them. Roles are matched by `key` within the
796
+ * project: a declared key that already exists is updated in place, and a new
797
+ * key is created. A role on the platform that the config leaves out is left
798
+ * as it is, never disabled.
726
799
  *
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.
800
+ * Without a role, a signed-in customer may do only what the key gives
801
+ * everyone who comes through it (its `baselineGrants`). Anything a customer
802
+ * may do beyond that comes from a role they hold.
730
803
  */
731
804
  interface ApplyConfigCustomerRoleInput {
732
805
  /** Stable key, unique per project (e.g. "default", "member"). */
@@ -734,20 +807,29 @@ interface ApplyConfigCustomerRoleInput {
734
807
  /** Human-readable name (e.g. "Default Customer"). */
735
808
  name: string;
736
809
  /**
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.
810
+ * What the role gives customers who hold it, as grants in the permission
811
+ * catalogue's names: records:read:<model>, records:write:<model>,
812
+ * records:create:<model>, operations:execute, files:read and the rest of
813
+ * the list at https://docs.foir.dev/api/role-permissions.
814
+ *
815
+ * Each grant stands for itself. Changing a record and creating one are two
816
+ * grants, so a role that should do both lists `records:write:<model>` and
817
+ * `records:create:<model>`.
818
+ *
819
+ * The `self:*` shorthand is worked out when a customer signs in, into the
820
+ * read, the write and the create of every model the project lets a customer
821
+ * write. It does NOT include operations:execute, so list that explicitly
822
+ * when the app's customers run operations.
743
823
  *
744
824
  * `operations:execute` runs every operation the key exposes;
745
825
  * `operations:execute:<operation_key>` runs that one alone, so a role can
746
826
  * 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.
827
+ * role gives it. A push naming an operation the project does not have is
828
+ * refused, by name.
829
+ *
830
+ * A push puts on a role only what the person pushing holds themselves.
749
831
  */
750
- permissions: string[];
832
+ grants: string[];
751
833
  /**
752
834
  * When true, every customer in the project inherits this role with no
753
835
  * 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.74.0",
3
+ "version": "0.78.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.21.0",
69
+ "@foir/console-routes": "0.0.0"
69
70
  },
70
71
  "engines": {
71
72
  "node": ">=18.0.0"