@foir/cli 0.69.0 → 0.70.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.
@@ -179,8 +179,17 @@ interface AccessInput {
179
179
  * (other operators can't see them unless shared).
180
180
  * - `'project'` — no individual owner; records belong to the project. The
181
181
  * platform default.
182
+ * - `'organization'` — owned by one of the project's ORGANIZATIONS (the
183
+ * tenants of a SaaS built on this project). Ownership, not containment:
184
+ * the row stays in the project plane, so the project credential finds
185
+ * every organization's rows — which is how a multi-tenant app resolves
186
+ * `<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
188
+ * named by `ownerId` or by the org the caller acts in (`X-Org-Id`);
189
+ * `singleton` means one record per organization. Refused on a project
190
+ * with no organizations and on a shared (layered/group) model.
182
191
  */
183
- ownerPlane: 'customer' | 'admin' | 'project';
192
+ ownerPlane: 'customer' | 'admin' | 'project' | 'organization';
184
193
  /**
185
194
  * Visibility new records start at (the record descriptor modes):
186
195
  * - `'private'` — only the owner and explicit grants.
@@ -285,11 +294,12 @@ interface ApplyConfigOperationInput {
285
294
  /** Precondition that must be met before execution (e.g., segment membership). */
286
295
  precondition?: Precondition;
287
296
  /**
288
- * Execution mode the operation supports. `sync` blocks the platform worker
289
- * on the HTTP call (≤30s realistically). `async` dispatches with
290
- * 202-ack semantics — the extension returns 202 immediately and calls
291
- * back via the typed GraphQL mutation when work completes. Use `async`
292
- * for long-running work that exceeds the 30s wall clock.
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.
293
303
  *
294
304
  * There is only one flavour of async on the platform: the callback-based
295
305
  * handshake. The prior "fire-and-forget" meaning of async was retired
@@ -303,9 +313,9 @@ interface ApplyConfigOperationInput {
303
313
  mode?: 'sync' | 'async';
304
314
  /**
305
315
  * How long the platform waits for a callback before marking the execution
306
- * `timed_out` and (per retry policy) re-queueing or sending to DLQ. Falls
307
- * back to project setting `operations.callback_default_ttl`, then 24h.
308
- * Clamped to [5m, 7d]. Only meaningful when `mode === 'async'`.
316
+ * `timed_out` and (per retry policy) re-queueing or sending to DLQ. Omit
317
+ * for the platform default of 24h. Clamped to [5m, 7d]. Only meaningful
318
+ * when `mode === 'async'`.
309
319
  */
310
320
  callbackTtlSeconds?: number;
311
321
  /**
@@ -548,7 +558,17 @@ interface ApplyConfigRelyingPartyInput {
548
558
  clientId: string;
549
559
  /** Display name shown on the hosted login. */
550
560
  name: string;
551
- /** Allow-listed exact redirect URIs the platform will 302 to with the code. */
561
+ /**
562
+ * Allow-listed redirect URIs the platform will 302 to with the code, e.g.
563
+ * `['https://app.eide.clothing/auth/callback']`.
564
+ *
565
+ * Matched EXACTLY, including the path and query, so each deployment is listed
566
+ * in full. Wildcards are rejected rather than stored: a pattern would match
567
+ * only its own literal text, which reads like an allow-list entry and covers
568
+ * nothing. https, except on a loopback host (`http://127.0.0.1:8976/callback`)
569
+ * where a CLI or native app cannot hold a certificate. No fragment, no
570
+ * userinfo.
571
+ */
552
572
  redirectUris: string[];
553
573
  /** OAuth scopes the RP can request (intersection with caller scopes wins). */
554
574
  allowedScopes?: string[];
@@ -626,8 +646,27 @@ interface ApplyConfigApiKeyInput {
626
646
  scopes?: string[];
627
647
  /** Restrict the key to specific model keys (e.g. ["tilly_note", "tilly_block"]). */
628
648
  allowedModels?: string[];
629
- /** Restrict file uploads to specific MIME types (e.g. ["image/*", "video/*"]). */
649
+ /** Restrict file uploads: a MIME type ("image/png"), a whole type ("image/*"), or IMAGE, VIDEO, FILE. */
630
650
  allowedFileTypes?: string[];
651
+ /**
652
+ * Confine the key to one ORGANISATION of this project, by id — the same value
653
+ * `foir api-keys create --confine-to-org` takes. An organisation is
654
+ * identified by a minted id, the way a project and a tenant are.
655
+ *
656
+ * A confined key reads and writes only that organisation's rows plus the
657
+ * project-shared tier, and the confinement travels on the credential rather
658
+ * than on any request, so nothing downstream can widen it.
659
+ *
660
+ * It is also part of the key's IDENTITY here, not just its configuration.
661
+ * Two organisations may each have a key named "Storefront", so the reconciler
662
+ * matches on name AND organisation; a manifest that declared one and omitted
663
+ * this field would be matched against the other one's key and rotate it.
664
+ *
665
+ * Set on CREATE only, like `configId` beside it: a key's organisation cannot
666
+ * be changed afterwards, and a manifest that moved one would be asking for a
667
+ * credential to change which tenant it belongs to.
668
+ */
669
+ org?: string;
631
670
  }
632
671
  /**
633
672
  * A customer RBAC role declared in foir.config.ts and reconciled by
@@ -662,6 +701,25 @@ interface ApplyConfigCustomerRoleInput {
662
701
  */
663
702
  isDefault?: boolean;
664
703
  }
704
+ /**
705
+ * Saying "I do not want this one" about a source or sink the manifest marks
706
+ * optional.
707
+ *
708
+ * EXPLICIT, BECAUSE SILENCE IS THE WRONG DEFAULT HERE. An optional source left
709
+ * out of the config could be a decision or an oversight, and the two have
710
+ * opposite consequences: one is an operator who does not want their customers
711
+ * synced, the other is an operator who thinks they are and is wrong. So an
712
+ * optional source that is neither mapped nor declined is refused at push, with
713
+ * both spellings named. Nothing is submitted for a declined one, so the platform
714
+ * drops the operations that would have carried it and grants no capability for
715
+ * it.
716
+ *
717
+ * sources: {
718
+ * product: { toModel: 'product', naturalKey: 'shopifyId', fields: { … } },
719
+ * customer: false, // this project does not sync customers
720
+ * }
721
+ */
722
+ type AppMappingDeclined = false;
665
723
  /**
666
724
  * Source-type mapping entry for an app install. Maps a manifest-declared
667
725
  * source type onto a project model. See docs/platform/apps.md §Mapping step.
@@ -698,8 +756,10 @@ interface AppInput {
698
756
  /** Opaque settings forwarded to the app's middleware. */
699
757
  settings?: Record<string, unknown>;
700
758
  mappings?: {
701
- sources?: Record<string, AppSourceMappingInput>;
702
- sinks?: Record<string, AppSinkMappingInput>;
759
+ /** `false` declines an OPTIONAL source. See AppMappingDeclined. */
760
+ sources?: Record<string, AppSourceMappingInput | AppMappingDeclined>;
761
+ /** `false` declines an OPTIONAL sink. See AppMappingDeclined. */
762
+ sinks?: Record<string, AppSinkMappingInput | AppMappingDeclined>;
703
763
  placementFields?: Record<string, AppPlacementFieldChoiceInput>;
704
764
  };
705
765
  }
@@ -758,6 +818,38 @@ interface ApplyConfigProjectSettingsInput {
758
818
  * Whether email one-time-code customer login is exposed. Default false.
759
819
  */
760
820
  customerOtpEnabled?: boolean;
821
+ /**
822
+ * Whether a signed-in customer may register an organization of their own
823
+ * through `registerOperatorOrganization`. Default false. Needs
824
+ * `operatorSignupRole` to name the role the founder is given, or the
825
+ * platform refuses every registration.
826
+ */
827
+ operatorSignupEnabled?: boolean;
828
+ /**
829
+ * The key of the customer role a self-registered organization's founder
830
+ * holds in it (a role of this project, e.g. `'operator'`). No default.
831
+ */
832
+ operatorSignupRole?: string;
833
+ /**
834
+ * The customer roles an organization's own staffer may hand out, by key.
835
+ *
836
+ * THE CEILING ON WHAT AN ORGANIZATION CAN BECOME, set by the party that is
837
+ * not inside it. An organization's staffer may add a member with a role only
838
+ * if the PROJECT declared that role assignable, which is why this is
839
+ * manifest-shaped and lives under project settings rather than anywhere an
840
+ * organization can reach.
841
+ *
842
+ * TRI-STATE, and the empty array is the point: omit it to leave the stored
843
+ * list alone, give a non-empty array to set it, and give an EMPTY array to
844
+ * take the capability away from every organization in the project. A bare
845
+ * "send nothing to clear" could not tell the last two apart.
846
+ *
847
+ * Every key must name a live customer role of this project. The platform
848
+ * refuses the whole list rather than dropping an entry it cannot resolve,
849
+ * because a list that quietly lost one is a capability an operator believes
850
+ * they granted and did not.
851
+ */
852
+ organizationAssignableRoles?: string[];
761
853
  }
762
854
  /** Self-serve signup mode, as authored in config-as-code. */
763
855
  type SignupModeName = 'public' | 'restricted' | 'waitlist' | 'closed';
@@ -808,11 +900,68 @@ interface ApplyConfigDesignTokensInput {
808
900
  /** Escape hatch for forward-compatible W3C groups (`spacing`, `border`, …). */
809
901
  [key: string]: unknown;
810
902
  }
903
+ /**
904
+ * A whole config manifest: every family `foir push` reconciles, plus the
905
+ * config's own identity and the project pin.
906
+ *
907
+ * ⚠️ NO INDEX SIGNATURE, deliberately, and it used to have one. `[key: string]:
908
+ * unknown` made every top-level key legal, so a block spelled `organisations`
909
+ * instead of `organizations` — or one from a release this CLI does not have —
910
+ * type-checked, serialised, was accepted by the server's equally tolerant parse
911
+ * and applied by nothing. Both sides agreed. Nothing errored. The block did
912
+ * nothing, for as long as nobody looked. Without the signature, TypeScript's
913
+ * excess-property check refuses the literal at the point somebody writes it,
914
+ * which is the cheapest place this can possibly be caught.
915
+ *
916
+ * The server reports what it did not recognise as well, because a type check is
917
+ * only one of the two sides and a manifest can reach it from a hand-written
918
+ * JSON, an older CLI, or a generator.
919
+ *
920
+ * ─────────────────────────────────────────────────────────────────────────────
921
+ * WHAT A PUSH DOES TO ORGANISATIONS
922
+ *
923
+ * A PUSH NEVER CREATES, MOVES, SUSPENDS OR DELETES AN ORGANISATION. Stated on
924
+ * the server too, beside the parser that would have to grow a family for it
925
+ * (`organisationPushRule` in the configs handler), because a rule held in one
926
+ * repository is a rule the other can contradict.
927
+ *
928
+ * Organisation POLICY is manifest-shaped and belongs here: whether self-serve
929
+ * sign-up is on, which role a founder is given, which roles an organisation may
930
+ * assign. Organisation INSTANCES are administrative state, like a customer or a
931
+ * credential's last use — created by an operator, by a customer signing up, or
932
+ * by a provisioning call, and suspended or deleted by deliberate acts with
933
+ * their own confirmations and their own audit trail.
934
+ *
935
+ * The reason it has to be said rather than assumed is this file's own prune
936
+ * rule: a family a manifest OMITS is a family whose resources this config owns
937
+ * and no longer wants. Apply that to organisations and a founder who pushed a
938
+ * config without an organisations block would lose a live tenant — every row,
939
+ * every customer, every credential — because a file did not mention it.
940
+ */
811
941
  interface ApplyConfigInput {
812
942
  key: string;
813
943
  name: string;
814
944
  configType?: string;
815
945
  force?: boolean;
946
+ /**
947
+ * Free text on the config row, for a reader rather than for the platform.
948
+ * Nothing resolves against it.
949
+ */
950
+ description?: string;
951
+ /**
952
+ * Which way content moves for this config: `'READ'` (the default),
953
+ * `'write'`, `'bidirectional'`. Recorded on the config row.
954
+ *
955
+ * A STRING. It was undeclared here until the manifest's top level gained a
956
+ * type at all, and one config in this repository had been sending the number
957
+ * `1` — which made the SERVER'S metadata decode fail outright, so that
958
+ * manifest's name, description and configType fell through to defaults on
959
+ * every push. One key of the wrong type, every metadata field silently
960
+ * dropped, nothing reported anywhere.
961
+ */
962
+ direction?: string;
963
+ /** The domain a connector-shaped config talks to. Recorded on the row. */
964
+ connectionDomain?: string;
816
965
  /** Base URL prepended to relative operation endpoints. */
817
966
  operationBaseUrl?: string;
818
967
  /**
@@ -854,7 +1003,19 @@ interface ApplyConfigInput {
854
1003
  * only — see ApplyConfigEmailCopyInput for why it never prunes).
855
1004
  */
856
1005
  emailCopy?: ApplyConfigEmailCopyInput[];
857
- [key: string]: unknown;
1006
+ /**
1007
+ * The project's bring-your-own transactional email sender: `fromEmail` and a
1008
+ * `resendKeySecretRef`. REF ONLY — the raw key is never in the committed
1009
+ * manifest; it is written out of band with `foir secrets put` and this
1010
+ * references the resulting vault ref. Omitted leaves the sender unchanged.
1011
+ */
1012
+ projectEmail?: ApplyConfigProjectEmailInput;
1013
+ }
1014
+ /** The project's own email sender, as authored in config-as-code. */
1015
+ interface ApplyConfigProjectEmailInput {
1016
+ fromEmail?: string;
1017
+ /** A vault ref, never a key. `foir secrets put` mints one. */
1018
+ resendKeySecretRef?: string;
858
1019
  }
859
1020
  /** Define a complete config manifest. */
860
1021
  declare function defineConfig(config: ApplyConfigInput): ApplyConfigInput;
@@ -927,4 +1088,4 @@ interface FoirSecretsConfig {
927
1088
  */
928
1089
  declare function defineSecrets(config: FoirSecretsConfig): FoirSecretsConfig;
929
1090
 
930
- export { type AccessInput, type AppInput, 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 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 };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/cli",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -62,10 +62,10 @@
62
62
  "typescript": "5.9.2",
63
63
  "vitest": "^3.2.4",
64
64
  "@foir/assistant": "0.0.0",
65
- "@foir/console-routes": "0.0.0",
66
- "@foir/proto-ts": "0.109.0",
65
+ "@foir/sdk": "0.10.0",
67
66
  "@foir/rpc-node": "0.0.0",
68
- "@foir/sdk": "0.9.0"
67
+ "@foir/proto-ts": "0.109.0",
68
+ "@foir/console-routes": "0.0.0"
69
69
  },
70
70
  "engines": {
71
71
  "node": ">=18.0.0"
@@ -75,7 +75,7 @@
75
75
  "url": "https://github.com/eidestudio/foir-cli.git"
76
76
  },
77
77
  "scripts": {
78
- "build": "tsup",
78
+ "build": "tsup && node scripts/smoke-bundle.mjs",
79
79
  "dev:cli": "tsx src/cli.ts",
80
80
  "check-types": "tsc --noEmit",
81
81
  "lint": "eslint src/",