@foir/cli 0.71.0 → 0.72.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.
@@ -129,6 +129,17 @@ interface FieldRelationshipInput {
129
129
  interface ReferenceFieldConfig {
130
130
  /** Models this reference may target. Empty ⇒ any record-bearing model. */
131
131
  referenceTypes?: string[];
132
+ /**
133
+ * The name of the GraphQL union a reference to two or more models returns.
134
+ * Omitted, the union is named from its models (`page` and `product` give
135
+ * `PageProductRef`), which moves when the models do and which a model key can
136
+ * land on. A name given here stays put.
137
+ *
138
+ * Only for two or more `referenceTypes`: a reference to one model returns that
139
+ * model's own type, and the platform refuses a name with nothing to name. Two
140
+ * fields may give one name only over the same models.
141
+ */
142
+ unionName?: string;
132
143
  /** Many-valued reference. Composition is invalid when true. */
133
144
  multiple?: boolean;
134
145
  relationship?: FieldRelationshipInput;
@@ -163,11 +174,12 @@ interface LookupDefinitionInput {
163
174
  name?: string;
164
175
  }
165
176
  /**
166
- * Per-model access policy — the unified resource-access primitive's BROAD
177
+ * Per-model access policy: the unified resource-access primitive's BROAD
167
178
  * layer. Mirrors the admin UI's model "Owner" plane control and the platform
168
179
  * `models.v1.AccessConfig`. Declared here, it is reconciled into the model's
169
- * config by `foir push`; omit `access` entirely to leave the model at the
170
- * platform default (project-owned, private).
180
+ * config by `foir push`, and takes effect for the published channel when the
181
+ * model is published; omit `access` entirely for a project-owned model, whose
182
+ * default visibility follows the rule on `defaultVisibility` below.
171
183
  */
172
184
  interface AccessInput {
173
185
  /**
@@ -184,7 +196,7 @@ interface AccessInput {
184
196
  * the row stays in the project plane, so the project credential finds
185
197
  * every organization's rows — which is how a multi-tenant app resolves
186
198
  * `<sub>.theapp.com` to an org before it knows which org the request is
187
- * for. Written by the project (never on behalf of a customer), owner
199
+ * for. Written by the project (never for a customer), owner
188
200
  * named by `ownerId` or by the org the caller acts in (`X-Org-Id`);
189
201
  * `singleton` means one record per organization. Refused on a project
190
202
  * with no organizations and on a shared (layered/group) model.
@@ -294,21 +306,20 @@ interface ApplyConfigOperationInput {
294
306
  /** Precondition that must be met before execution (e.g., segment membership). */
295
307
  precondition?: Precondition;
296
308
  /**
297
- * Execution mode the operation supports. `sync` blocks the caller on the
298
- * dispatch reaching a terminal state, bounded by the platform's
299
- * `SyncTimeout` of 60s. `async` dispatches with 202-ack semantics: the
300
- * extension returns 202 immediately and calls back via the typed GraphQL
301
- * mutation when work completes. Use `async` for long-running work that
302
- * exceeds the 60s wall clock.
309
+ * Execution mode the operation supports. `sync` is a direct call: the
310
+ * platform calls the endpoint and returns its answer to the caller, bounded
311
+ * by the operation's `timeoutMs`, and the answer is the result. `async`
312
+ * dispatches with 202-ack semantics: the extension returns 202 immediately
313
+ * and calls back via the typed GraphQL mutation when work completes. Use
314
+ * `async` for work nobody waits on, or that runs longer than a caller can.
303
315
  *
304
316
  * There is only one flavour of async on the platform: the callback-based
305
- * handshake. The prior "fire-and-forget" meaning of async was retired
306
- * in 2026-04 — every ASYNC dispatch now carries a scoped callback token
307
- * and the platform tracks the execution to terminal state.
317
+ * handshake. Every ASYNC dispatch carries a scoped callback token and the
318
+ * platform tracks the execution to terminal state.
308
319
  *
309
- * Default: `sync`. The CLI maps this to `supportsAsync` on the
310
- * platform Operation row — the operation can still be dispatched in SYNC
311
- * mode at runtime when callers want a blocking call.
320
+ * Default: `sync`. The CLI maps this to `supportsAsync` on the platform
321
+ * Operation row; the operation can still be called in SYNC mode at runtime
322
+ * when callers want a blocking call.
312
323
  */
313
324
  mode?: 'sync' | 'async';
314
325
  /**
@@ -344,9 +355,17 @@ interface ApplyConfigOperationInput {
344
355
  * touches; the unscoped two-part forms (`records:read` etc.) only
345
356
  * work for admin API keys and won't satisfy customer-scoped tokens.
346
357
  *
358
+ * Ownership, `records:attribute:<model_key>` (or `records:attribute` for
359
+ * every model), lets the operation name the customer a record belongs to
360
+ * when it creates one (`create<Model>(input, ownerId)`) and hand a record
361
+ * to another customer (`set<Model>Owner`). It is held only by an
362
+ * operation: no API key of any type can be minted with it.
363
+ *
347
364
  * Cross-cutting caps: `embeddings:read`, `embeddings:write`,
348
- * `files:read`, `files:write`,
349
- * `config:read`, `status:write`, and the secrets:* family. See
365
+ * `files:read`, `files:write`, `customers:write` (create a customer,
366
+ * such as the buyer at a guest checkout),
367
+ * `config:read`, `status:write`, and the secrets:* family. A capability
368
+ * outside this vocabulary is refused at push, by name. See
350
369
  * `services/internal/scopedtoken/capabilities.go` for the full enum.
351
370
  *
352
371
  * `operations:complete` is auto-injected on callback tokens — never
@@ -570,7 +589,12 @@ interface ApplyConfigRelyingPartyInput {
570
589
  * userinfo.
571
590
  */
572
591
  redirectUris: string[];
573
- /** OAuth scopes the RP can request (intersection with caller scopes wins). */
592
+ /**
593
+ * OAuth scopes the RP can request. A request is granted the scopes this list
594
+ * covers, narrowed forms included: `operations:execute` here grants a request
595
+ * for `operations:execute:checkout`. A scope wider than anything listed is
596
+ * not granted.
597
+ */
574
598
  allowedScopes?: string[];
575
599
  /**
576
600
  * Per-RP login method filter (customer-lane). Combines with the project-
@@ -636,9 +660,9 @@ interface ApplyConfigHookInput {
636
660
  isActive?: boolean;
637
661
  }
638
662
  interface ApplyConfigApiKeyInput {
639
- /** Name for this API key (e.g. "Tilly iOS", "Tilly BFF"). */
663
+ /** Name for this API key (e.g. "Tilly iOS", "Tilly CRM sync"). */
640
664
  name: string;
641
- /** Key type: 'public' for client apps, 'secret' for BFF/server. */
665
+ /** Key type: 'public' for your app, 'secret' for a third-party client's own server. */
642
666
  keyType: 'public' | 'secret';
643
667
  /** Environment variable name to write the key to in .env (e.g. "FOIR_PUBLIC_KEY"). */
644
668
  envVar: string;
@@ -667,6 +691,20 @@ interface ApplyConfigApiKeyInput {
667
691
  * credential to change which tenant it belongs to.
668
692
  */
669
693
  org?: string;
694
+ /**
695
+ * What a stranger may do with this key: the caller carrying it with nobody
696
+ * signed in, such as a guest at checkout or a contact form.
697
+ *
698
+ * Two forms, each naming one thing this config declares:
699
+ * `records:create:<model>` and `operations:execute:<operation>`. No wildcard,
700
+ * no update, delete or publish. A PUBLIC key only: a secret key is presented
701
+ * by a server that already knows who is calling.
702
+ *
703
+ * A push changes it on the existing key in place, as it does the key's
704
+ * scopes: the key keeps its id and its secret, and the new list is live the
705
+ * moment the push writes it, not at a release.
706
+ */
707
+ anonymousScopes?: string[];
670
708
  }
671
709
  /**
672
710
  * A customer RBAC role declared in foir.config.ts and reconciled by
@@ -692,6 +730,12 @@ interface ApplyConfigCustomerRoleInput {
692
730
  * records:{read,write}:<model> for every customer-writable model in the
693
731
  * project — but does NOT include operations:execute, so list that
694
732
  * explicitly when the app's customers run operations.
733
+ *
734
+ * `operations:execute` runs every operation the key exposes;
735
+ * `operations:execute:<operation_key>` runs that one alone, so a role can
736
+ * let every customer check out and keep rebooking for the customers whose
737
+ * role grants it. A push
738
+ * naming an operation the project does not have is refused, by name.
695
739
  */
696
740
  permissions: string[];
697
741
  /**
@@ -764,10 +808,10 @@ interface AppInput {
764
808
  };
765
809
  }
766
810
  /**
767
- * Project-level settings applied via UpdateProject at push time and
768
- * read via GetProject at pull time. Branding fields are empty-string-as-
769
- * clear on the platform side: `''` clears the column, `undefined` leaves
770
- * it untouched.
811
+ * Project-level settings applied by `foir push`, in the same server-side
812
+ * transaction as every other family, and read via GetProject at pull time.
813
+ * Branding fields are empty-string-as-clear on the platform side: `''` clears
814
+ * the column, `undefined` leaves it untouched.
771
815
  */
772
816
  interface ApplyConfigProjectSettingsInput {
773
817
  /** Display name in customer-facing emails / portal headers. */
@@ -863,7 +907,7 @@ interface SignupRestrictionsInput {
863
907
  /**
864
908
  * Project pin + declarative settings. The pin (id / tenantId) is a guard
865
909
  * against pushing one folder's config into another folder's project; the
866
- * settings are applied to the platform via UpdateProject.
910
+ * settings are applied by the push, on the server, with the rest of the config.
867
911
  */
868
912
  interface ApplyConfigProjectInput {
869
913
  /** Project id this folder is bound to. */
@@ -1017,8 +1061,80 @@ interface ApplyConfigProjectEmailInput {
1017
1061
  /** A vault ref, never a key. `foir secrets put` mints one. */
1018
1062
  resendKeySecretRef?: string;
1019
1063
  }
1020
- /** Define a complete config manifest. */
1064
+ /**
1065
+ * One value a named project supplies for a destination: a URL, an address, a
1066
+ * list of them, or an app's settings.
1067
+ */
1068
+ type DestinationValue = string | string[] | Record<string, unknown>;
1069
+ /** A named project's destinations, under the names the manifest reads them by. */
1070
+ type DestinationValues = Record<string, DestinationValue>;
1071
+ /**
1072
+ * One named project: an ordinary project this config pushes to when selected
1073
+ * with `--project <name>` or `FOIR_PROJECT=<name>`.
1074
+ *
1075
+ * The platform never learns that two named projects are related. What makes
1076
+ * them one config is that the CLI resolves the manifest once per project and
1077
+ * refuses a push when any two differ in a field that is not a destination.
1078
+ */
1079
+ interface NamedProjectInput<D extends DestinationValues = DestinationValues> {
1080
+ /** The project's id. */
1081
+ id: string;
1082
+ /** The id of the project's tenant. */
1083
+ tenantId: string;
1084
+ /** A human-friendly name for output. */
1085
+ name?: string;
1086
+ /**
1087
+ * The file this project's minted keys and signing secrets are written to,
1088
+ * relative to the config. Two named projects may not share one.
1089
+ */
1090
+ envFile: string;
1091
+ /**
1092
+ * The control plane this project lives on. Omit for the production control
1093
+ * plane. A command pointed anywhere else, or holding a session minted
1094
+ * anywhere else, is refused unless it passes `--allow-control-plane-mismatch`.
1095
+ */
1096
+ apiUrl?: string;
1097
+ /**
1098
+ * The values that may differ between named projects: endpoints, redirect
1099
+ * URIs, origins, sender addresses and the like. The manifest reads them from
1100
+ * the project it is resolved for. Every named project supplies the same
1101
+ * names.
1102
+ */
1103
+ destinations: D;
1104
+ }
1105
+ /** What the manifest function is given: the project it is resolved for. */
1106
+ interface SelectedNamedProject<D extends DestinationValues = DestinationValues> {
1107
+ /** The name this project is declared under in `projects`. */
1108
+ name: string;
1109
+ destinations: D;
1110
+ }
1111
+ /**
1112
+ * The manifest a named-projects config resolves to, for one project.
1113
+ *
1114
+ * The same shape as a single-project manifest, less the pin: a named project's
1115
+ * `id`, `tenantId` and `name` come from its entry in `projects`, so `project`
1116
+ * here carries only its `settings`.
1117
+ */
1118
+ interface NamedProjectManifest extends Omit<ApplyConfigInput, 'project'> {
1119
+ project?: {
1120
+ settings?: ApplyConfigProjectSettingsInput;
1121
+ };
1122
+ }
1123
+ /**
1124
+ * A config that declares several full projects by name.
1125
+ *
1126
+ * `manifest` is called once per declared project, with that project's
1127
+ * destinations. Only destinations may differ between the results: a push
1128
+ * refuses, before anything is sent, when two resolved manifests differ in any
1129
+ * other field, and names the field and both values.
1130
+ */
1131
+ interface FoirProjectsConfig<D extends DestinationValues = DestinationValues> {
1132
+ projects: Record<string, NamedProjectInput<D>>;
1133
+ manifest: (project: SelectedNamedProject<D>) => NamedProjectManifest | Promise<NamedProjectManifest>;
1134
+ }
1135
+ /** Define a complete config manifest, or several named projects built from one. */
1021
1136
  declare function defineConfig(config: ApplyConfigInput): ApplyConfigInput;
1137
+ declare function defineConfig<D extends DestinationValues>(config: FoirProjectsConfig<D>): FoirProjectsConfig<D>;
1022
1138
  /** Define a model with type-safe field definitions. */
1023
1139
  declare function defineModel(model: ApplyConfigModelInput): ApplyConfigModelInput;
1024
1140
  /** Define a field with type safety. */
@@ -1088,4 +1204,4 @@ interface FoirSecretsConfig {
1088
1204
  */
1089
1205
  declare function defineSecrets(config: FoirSecretsConfig): FoirSecretsConfig;
1090
1206
 
1091
- export { type AccessInput, type AppInput, type AppMappingDeclined, type AppPlacementFieldChoiceInput, type AppSinkMappingInput, type AppSourceMappingInput, type ApplyConfigApiKeyInput, type ApplyConfigAuthProviderInput, type ApplyConfigContextDimensionInput, type ApplyConfigCustomerRoleInput, type ApplyConfigDesignTokensInput, type ApplyConfigEmailCopyInput, type ApplyConfigHookInput, type ApplyConfigInput, type ApplyConfigLocaleInput, type ApplyConfigModelInput, type ApplyConfigOperationInput, type ApplyConfigPlacementInput, type ApplyConfigProjectEmailInput, type ApplyConfigProjectInput, type ApplyConfigProjectSettingsInput, type ApplyConfigRelyingPartyInput, type ApplyConfigRelyingPartyLoginMethods, type ApplyConfigScheduleInput, type ApplyConfigVariantCatalogEntryInput, type EnumFieldConfig, type EnumFieldDefinitionInput, type EnumFieldOption, type FieldAccessInput, type FieldDefinitionInput, type FieldPlacement, type FieldRelationshipInput, type FoirSecretsConfig, type LookupDefinitionInput, type QuotaRule, type ReferenceFieldConfig, type ReferenceFieldDefinitionInput, type SecretDeclaration, type SecretOwnerKind, type SelectFieldConfig, type SelectFieldDefinitionInput, type SignupModeName, type SignupRestrictionsInput, defineAuthProvider, defineConfig, defineContextDimension, defineDesignTokens, defineEmailCopy, defineEnumField, defineField, defineHook, defineLocale, defineModel, defineOperation, definePlacement, defineRelyingParty, defineSchedule, defineSecrets, defineSelectField, defineVariantCatalogEntry };
1207
+ export { type AccessInput, type AppInput, type AppMappingDeclined, type AppPlacementFieldChoiceInput, type AppSinkMappingInput, type AppSourceMappingInput, type ApplyConfigApiKeyInput, type ApplyConfigAuthProviderInput, type ApplyConfigContextDimensionInput, type ApplyConfigCustomerRoleInput, type ApplyConfigDesignTokensInput, type ApplyConfigEmailCopyInput, type ApplyConfigHookInput, type ApplyConfigInput, type ApplyConfigLocaleInput, type ApplyConfigModelInput, type ApplyConfigOperationInput, type ApplyConfigPlacementInput, type ApplyConfigProjectEmailInput, type ApplyConfigProjectInput, type ApplyConfigProjectSettingsInput, type ApplyConfigRelyingPartyInput, type ApplyConfigRelyingPartyLoginMethods, type ApplyConfigScheduleInput, type ApplyConfigVariantCatalogEntryInput, type DestinationValue, type DestinationValues, type EnumFieldConfig, type EnumFieldDefinitionInput, type EnumFieldOption, type FieldAccessInput, type FieldDefinitionInput, type FieldPlacement, type FieldRelationshipInput, type FoirProjectsConfig, type FoirSecretsConfig, type LookupDefinitionInput, type NamedProjectInput, type NamedProjectManifest, type QuotaRule, type ReferenceFieldConfig, type ReferenceFieldDefinitionInput, type SecretDeclaration, type SecretOwnerKind, type SelectFieldConfig, type SelectFieldDefinitionInput, type SelectedNamedProject, type SignupModeName, type SignupRestrictionsInput, defineAuthProvider, defineConfig, defineContextDimension, defineDesignTokens, defineEmailCopy, defineEnumField, defineField, defineHook, defineLocale, defineModel, defineOperation, definePlacement, defineRelyingParty, defineSchedule, defineSecrets, defineSelectField, defineVariantCatalogEntry };
@@ -71,8 +71,25 @@ type Hold = {
71
71
  hold: string;
72
72
  };
73
73
  type FieldDecision = RenameTo | Retype | RemapValues | SplitInto | JoinFrom | FillFrom | Drop | Hold;
74
- /** One model's decisions, keyed by the field being removed. */
75
- type ModelDecisions = Record<string, FieldDecision>;
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.
79
+ */
80
+ type OwnerDecision = {
81
+ assignFrom: string;
82
+ } | {
83
+ hold: string;
84
+ };
85
+ /**
86
+ * One model's decisions, keyed by the field being removed, and under `$owner`
87
+ * the decision about records that name no owner.
88
+ */
89
+ type ModelDecisions = {
90
+ [fieldKey: string]: FieldDecision | OwnerDecision | undefined;
91
+ $owner?: OwnerDecision;
92
+ };
76
93
  /** Every model this file speaks for. */
77
94
  type MigrationDecisions = Record<string, ModelDecisions>;
78
95
  /** What a decision file default-exports. */
@@ -89,7 +106,8 @@ interface MigrationFile {
89
106
  decisions: MigrationDecisions;
90
107
  }
91
108
  /**
92
- * Declare what happens to the data under fields this push removes.
109
+ * 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.
93
111
  *
94
112
  * Returns a plain object rather than doing anything: a decision file is a
95
113
  * declaration that gets committed and reviewed, and code that acted at import
@@ -97,4 +115,4 @@ interface MigrationFile {
97
115
  */
98
116
  declare function migration(id: string, decisions: MigrationDecisions): MigrationFile;
99
117
 
100
- export { type Drop, type FieldDecision, type FillFrom, type Hold, type JoinFrom, type MigrationDecisions, type MigrationFile, type ModelDecisions, type RemapValues, type RenameTo, type Retype, type SplitInto, migration };
118
+ export { type Drop, type FieldDecision, type FillFrom, type Hold, type JoinFrom, type MigrationDecisions, type MigrationFile, type ModelDecisions, type OwnerDecision, type RemapValues, type RenameTo, type Retype, type SplitInto, migration };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/cli",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
4
4
  "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -65,7 +65,7 @@
65
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.10.0"
68
+ "@foir/sdk": "0.11.0"
69
69
  },
70
70
  "engines": {
71
71
  "node": ">=18.0.0"