@foir/cli 0.61.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.
@@ -0,0 +1,749 @@
1
+ import { Precondition } from '@foir/rpc-node/rpc/operations';
2
+ export { ExpressionPrecondition, Precondition, SegmentPrecondition } from '@foir/rpc-node/rpc/operations';
3
+
4
+ /**
5
+ * Config definition helpers.
6
+ *
7
+ * Type-safe helpers for building `foir.config.ts` files.
8
+ * Every function is a pass-through that provides IntelliSense
9
+ * and compile-time validation.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { defineConfig } from '@foir/cli/configs';
14
+ *
15
+ * export default defineConfig({
16
+ * key: 'my-app',
17
+ * name: 'My App',
18
+ * models: [...]
19
+ * });
20
+ * ```
21
+ */
22
+ /**
23
+ * `select` is record-backed: options come from records of `optionModelKey`.
24
+ * For inline `{label, value}` choices, declare `type: 'enum'` instead — the
25
+ * `options?: never` here forbids the legacy shape at the type level.
26
+ */
27
+ interface SelectFieldConfig {
28
+ optionModelKey: string;
29
+ multiple?: boolean;
30
+ options?: never;
31
+ }
32
+ interface EnumFieldOption {
33
+ label: string;
34
+ value: string;
35
+ }
36
+ /**
37
+ * `enum` is config-only: the storefront receives a typed string-literal
38
+ * union from inline `{label, value}` options. Variant overrides apply;
39
+ * locale overrides do not.
40
+ */
41
+ interface EnumFieldConfig {
42
+ options: EnumFieldOption[];
43
+ multiple?: boolean;
44
+ default?: string | string[];
45
+ }
46
+ /**
47
+ * Per-field {read, write} principal allow-lists. A non-empty `write` names
48
+ * the only principals permitted to write the field — "public" | "self" |
49
+ * "scoped" | "service" | "admin" — and any other principal is rejected by the
50
+ * platform on every write path. Omit for the default (writable by any
51
+ * authenticated principal). `read` governs client API exposure only.
52
+ */
53
+ interface FieldAccessInput {
54
+ read?: string[];
55
+ write?: string[];
56
+ }
57
+ interface BaseFieldDefinitionInput {
58
+ key: string;
59
+ label?: string;
60
+ required?: boolean;
61
+ /**
62
+ * Declares the field for the typed-filter side table so it can be used in
63
+ * where/orderBy on records. Without it the platform rejects sort by it.
64
+ */
65
+ queryable?: boolean;
66
+ helpText?: string;
67
+ placeholder?: string;
68
+ defaultValue?: unknown;
69
+ itemType?: string;
70
+ storage?: string;
71
+ templateZone?: string;
72
+ zoneOrder?: number;
73
+ access?: FieldAccessInput;
74
+ }
75
+ type SelectFieldDefinitionInput = BaseFieldDefinitionInput & {
76
+ type: 'select';
77
+ config: SelectFieldConfig;
78
+ };
79
+ type EnumFieldDefinitionInput = BaseFieldDefinitionInput & {
80
+ type: 'enum';
81
+ config: EnumFieldConfig;
82
+ };
83
+ /**
84
+ * Relationship declaration on a reference field.
85
+ *
86
+ * `composition` makes records of this model OWNED BY their referenced parent — a
87
+ * single-valued foreign key with ON DELETE CASCADE: sharing and deletes (and
88
+ * later ownership) cascade parent → child. Only valid on a single
89
+ * (non-`multiple`) reference. `association` (the default) is a plain reference;
90
+ * the target keeps its own access + lifecycle. `inverse` names the reverse
91
+ * traversal (parent → children, e.g. a note's `blocks`).
92
+ *
93
+ * The platform validates these at push (e.g. composition on a `multiple`
94
+ * reference is rejected).
95
+ */
96
+ interface FieldRelationshipInput {
97
+ kind: 'composition' | 'association';
98
+ inverse?: string;
99
+ }
100
+ interface ReferenceFieldConfig {
101
+ /** Models this reference may target. Empty ⇒ any record-bearing model. */
102
+ referenceTypes?: string[];
103
+ /** Many-valued reference. Composition is invalid when true. */
104
+ multiple?: boolean;
105
+ relationship?: FieldRelationshipInput;
106
+ }
107
+ type ReferenceFieldDefinitionInput = BaseFieldDefinitionInput & {
108
+ type: 'reference';
109
+ config?: ReferenceFieldConfig;
110
+ };
111
+ type GenericFieldDefinitionInput = BaseFieldDefinitionInput & {
112
+ type: string;
113
+ config?: Record<string, unknown> & {
114
+ options?: never;
115
+ };
116
+ };
117
+ type FieldDefinitionInput = SelectFieldDefinitionInput | EnumFieldDefinitionInput | ReferenceFieldDefinitionInput | GenericFieldDefinitionInput;
118
+ /**
119
+ * Lookup definition — declares one indexed access path on a model.
120
+ *
121
+ * `keyBy` is the ordered list of top-level scalar field keys that make
122
+ * up the lookup key. Single-field lookups are common; composites are
123
+ * first-class (e.g. `['fromHost', 'fromPath']` for a redirect model).
124
+ * `name` overrides the generated GraphQL query field name; omit it to
125
+ * use `<typeLowerCamel>By<PascalCaseKeyFields>`.
126
+ *
127
+ * Validation runs server-side via the platform's config-write RPC —
128
+ * see RFC §Data model → Constraints. The CLI does no local validation
129
+ * here; the server's structured error surfaces verbatim on failure.
130
+ */
131
+ interface LookupDefinitionInput {
132
+ keyBy: string[];
133
+ name?: string;
134
+ }
135
+ /**
136
+ * Per-model access policy — the unified resource-access primitive's BROAD
137
+ * layer. Mirrors the admin UI's model "Owner" plane control and the platform
138
+ * `models.v1.AccessConfig`. Declared here, it is reconciled into the model's
139
+ * config by `foir push`; omit `access` entirely to leave the model at the
140
+ * platform default (project-owned, private).
141
+ */
142
+ interface AccessInput {
143
+ /**
144
+ * Which plane owns records of this model:
145
+ * - `'customer'` — owned by the customer who created them. Per-customer RLS
146
+ * isolation, private response-cache, per-owner lookups. Required for a
147
+ * customer-context singleton.
148
+ * - `'admin'` — owned by the operator who created them; operator-private
149
+ * (other operators can't see them unless shared).
150
+ * - `'project'` — no individual owner; records belong to the project. The
151
+ * platform default.
152
+ */
153
+ ownerPlane: 'customer' | 'admin' | 'project';
154
+ /**
155
+ * Visibility new records start at (the record descriptor modes):
156
+ * - `'private'` — only the owner and explicit grants. The default.
157
+ * - `'public_project'` — readable by anyone in the project.
158
+ * - `'public_world'` — readable by anyone (public API).
159
+ * Omit to use the platform default (private).
160
+ */
161
+ defaultVisibility?: 'private' | 'public_project' | 'public_world';
162
+ }
163
+ interface ApplyConfigModelInput {
164
+ key: string;
165
+ name: string;
166
+ pluralName?: string;
167
+ pluralKey?: string;
168
+ description?: string;
169
+ fields?: FieldDefinitionInput[];
170
+ config?: Record<string, unknown>;
171
+ /**
172
+ * Per-model access policy (owner plane + default visibility). Reconciled into
173
+ * the model's config by `foir push`, matching the admin UI's "Owner" plane
174
+ * control. Omit to leave the model at the platform default (project-owned).
175
+ */
176
+ access?: AccessInput;
177
+ /**
178
+ * Lookup definitions for this model. Server validates: existence /
179
+ * scalar-type / top-level-only / caps (4 lookups, 4 fields) / GraphQL
180
+ * field-name regex for `name`. See RFC §Data model (config surface).
181
+ */
182
+ lookups?: LookupDefinitionInput[];
183
+ }
184
+ interface QuotaRule {
185
+ /** Segment key to target (empty string or omitted = default/fallback). */
186
+ segmentKey?: string;
187
+ /** Max requests allowed in the window (0 = unlimited). */
188
+ points: number;
189
+ /** Window duration in seconds (3600=1hr, 86400=1day, 604800=7day, 2592000=30day). */
190
+ duration: number;
191
+ /** Scope for tracking: per customer, per user, or per tenant. */
192
+ scope: 'customer' | 'user' | 'tenant';
193
+ /** Optional rule expression condition. */
194
+ condition?: Record<string, unknown>;
195
+ /** Optional expiry — rule is ignored after this date (ISO 8601). */
196
+ expiresAt?: string;
197
+ /** Custom message returned when this rule's quota is exhausted (e.g., upgrade CTA). */
198
+ message?: string;
199
+ }
200
+
201
+ interface ApplyConfigOperationInput {
202
+ key: string;
203
+ name: string;
204
+ description?: string;
205
+ icon?: string;
206
+ category?: string;
207
+ /** HTTP endpoint URL that the platform calls when this operation is triggered. */
208
+ endpoint?: string;
209
+ isActive?: boolean;
210
+ timeoutMs?: number;
211
+ inputSchema?: Record<string, unknown>;
212
+ outputSchema?: Record<string, unknown>;
213
+ /** Usage quota rules (rate limits per customer/user/tenant). */
214
+ quotas?: {
215
+ rules: QuotaRule[];
216
+ };
217
+ /** Retry policy for failed executions. */
218
+ retryPolicy?: Record<string, unknown>;
219
+ /** Roles allowed to execute this operation. */
220
+ allowedRoles?: string[];
221
+ /** Precondition that must be met before execution (e.g., segment membership). */
222
+ precondition?: Precondition;
223
+ /**
224
+ * Execution mode the operation supports. `sync` blocks the platform worker
225
+ * on the HTTP call (≤30s realistically). `async` dispatches with
226
+ * 202-ack semantics — the extension returns 202 immediately and calls
227
+ * back via the typed GraphQL mutation when work completes. Use `async`
228
+ * for long-running work that exceeds the 30s wall clock.
229
+ *
230
+ * There is only one flavour of async on the platform: the callback-based
231
+ * handshake. The prior "fire-and-forget" meaning of async was retired
232
+ * in 2026-04 — every ASYNC dispatch now carries a scoped callback token
233
+ * and the platform tracks the execution to terminal state.
234
+ *
235
+ * Default: `sync`. The CLI maps this to `supportsAsync` on the
236
+ * platform Operation row — the operation can still be dispatched in SYNC
237
+ * mode at runtime when callers want a blocking call.
238
+ */
239
+ mode?: 'sync' | 'async';
240
+ /**
241
+ * How long the platform waits for a callback before marking the execution
242
+ * `timed_out` and (per retry policy) re-queueing or sending to DLQ. Falls
243
+ * back to project setting `operations.callback_default_ttl`, then 24h.
244
+ * Clamped to [5m, 7d]. Only meaningful when `mode === 'async'`.
245
+ */
246
+ callbackTtlSeconds?: number;
247
+ /**
248
+ * Retry policy applied specifically when the callback times out. Distinct
249
+ * from `retryPolicy`, which governs HTTP dispatch failures (network, 5xx).
250
+ * Splitting these lets operators retry dispatch failures aggressively while
251
+ * being conservative about callback timeouts — re-running an hour-long job
252
+ * that timed out because the upstream was slow tends to burn another hour
253
+ * and hit the same slowness.
254
+ *
255
+ * Shape mirrors `retryPolicy` (`{ maxRetries }`). Falls back to
256
+ * `retryPolicy` when unset, then to the watchdog default (3).
257
+ * Set `{ maxRetries: 0 }` to opt out of retry on timeout entirely.
258
+ */
259
+ callbackTimeoutRetryPolicy?: {
260
+ maxRetries?: number;
261
+ };
262
+ /**
263
+ * Capability strings stamped onto the scoped dispatch token (and its
264
+ * callback-window successor) so the extension can call back into
265
+ * api-public with the precise authorities it needs — nothing more.
266
+ *
267
+ * Per-model record access is three-part: `records:read:<model_key>`,
268
+ * `records:write:<model_key>`, `records:delete:<model_key>`,
269
+ * `records:publish:<model_key>`. List every model the operation
270
+ * touches; the unscoped two-part forms (`records:read` etc.) only
271
+ * work for admin API keys and won't satisfy customer-scoped tokens.
272
+ *
273
+ * Cross-cutting caps: `embeddings:read`, `embeddings:write`,
274
+ * `files:read`, `files:write`,
275
+ * `config:read`, `status:write`, and the secrets:* family. See
276
+ * `services/internal/scopedtoken/capabilities.go` for the full enum.
277
+ *
278
+ * `operations:complete` is auto-injected on callback tokens — never
279
+ * declare it here. Omitting `capabilities` (or passing an empty array)
280
+ * mints an authentic-but-unprivileged token; useful for operations
281
+ * whose extension makes no back-channel calls.
282
+ */
283
+ capabilities?: string[];
284
+ /**
285
+ * How the operation surfaces in the public GraphQL schema and how it is
286
+ * dispatched. `mutation` (default) is the existing behaviour: an
287
+ * `executeXxx` root mutation dispatched via the worker queue. `query`
288
+ * registers a root Query field resolved synchronously in-process.
289
+ * `field` attaches a resolver field to `attachedToModel`, batched per
290
+ * request. QUERY/FIELD operations are the new "resolver" kind and use
291
+ * `requestTemplate` / `responseTransform` to call an upstream endpoint.
292
+ */
293
+ kind?: 'mutation' | 'query' | 'field';
294
+ /**
295
+ * Model key the resolver field attaches to. Required (and only meaningful)
296
+ * when `kind === 'field'`.
297
+ */
298
+ attachedToModel?: string;
299
+ /**
300
+ * Field path(s) under `attachedToModel` that this resolver populates.
301
+ * FIELD only.
302
+ */
303
+ parentFields?: string[];
304
+ /**
305
+ * Cache-control for QUERY / FIELD resolver results. `scope` selects the
306
+ * cache audience; `maxAge` is the freshness window in seconds.
307
+ */
308
+ cacheControl?: {
309
+ scope: 'PUBLIC' | 'PRIVATE' | 'NO_CACHE';
310
+ maxAge?: number;
311
+ };
312
+ /**
313
+ * Maps `{{secret.NAME}}` placeholders in the request template to
314
+ * PROJECT-owned secrets. The dispatcher resolves plaintext at call time;
315
+ * it is never stored on the operation row.
316
+ */
317
+ secretBindings?: Array<{
318
+ name: string;
319
+ secretRef: string;
320
+ }>;
321
+ /**
322
+ * Shapes the outbound HTTP request for QUERY / FIELD operations. Template
323
+ * variables `{{input.X}}`, `{{parent.X}}`, `{{secret.NAME}}`,
324
+ * `{{context.X}}` are interpolated at dispatch time. Set `batchCapable`
325
+ * to send one upstream request for the whole deduped input set.
326
+ */
327
+ requestTemplate?: {
328
+ method?: string;
329
+ pathTemplate?: string;
330
+ query?: Record<string, string>;
331
+ headers?: Record<string, string>;
332
+ bodyTemplate?: string;
333
+ batchCapable?: boolean;
334
+ };
335
+ /**
336
+ * jq-style transform applied to the upstream response before it is
337
+ * returned to the caller. QUERY / FIELD only.
338
+ */
339
+ responseTransform?: string;
340
+ }
341
+ interface ApplyConfigSegmentInput {
342
+ key: string;
343
+ name: string;
344
+ description?: string;
345
+ rules?: Record<string, unknown>;
346
+ evaluationMode?: string;
347
+ isActive?: boolean;
348
+ }
349
+ interface ApplyConfigScheduleInput {
350
+ /** Unique key for this schedule. Defaults to operationKey if not provided. */
351
+ key?: string;
352
+ /** Display name. Defaults to key if not provided. */
353
+ name?: string;
354
+ description?: string;
355
+ operationKey: string;
356
+ cron: string;
357
+ timezone?: string;
358
+ enabled?: boolean;
359
+ }
360
+ interface ApplyConfigAuthProviderInput {
361
+ key: string;
362
+ name: string;
363
+ type: string;
364
+ config?: Record<string, unknown>;
365
+ enabled?: boolean;
366
+ isDefault?: boolean;
367
+ priority?: number;
368
+ }
369
+ /**
370
+ * Login-with-Foir customer-lane relying party. Each entry registers a
371
+ * storefront or third-party app that can launch the hosted customer
372
+ * login at auth.foir.dev/authorize?client_id=… and exchange the auth
373
+ * code at /customer/oauth/token.
374
+ *
375
+ * The reconciler is project-scoped (tenant + project come from the
376
+ * push context), so consumers only declare the per-RP fields. Pass to
377
+ * `relyingParties` on the manifest.
378
+ */
379
+ /**
380
+ * Per-RP login method config (login-with-foir-full-workstream-plan.md
381
+ * §Per-RP login method toggle). All fields optional — missing keys
382
+ * default to enabled. Lets a project's storefront opt to only Sign in
383
+ * with Shopify (disable password/OTP + don't list Apple) without
384
+ * touching the project-level provider catalogue.
385
+ */
386
+ interface ApplyConfigRelyingPartyLoginMethods {
387
+ /**
388
+ * Show email & password sign-in on this RP's hosted login? Defaults to
389
+ * true. Only narrows the project capability — can't enable a method the
390
+ * project (`customerPasswordEnabled`) has turned off.
391
+ */
392
+ password?: boolean;
393
+ /**
394
+ * Show one-time code sign-in on this RP's hosted login? Defaults to true.
395
+ * Only narrows the project capability (`customerOtpEnabled`).
396
+ */
397
+ otp?: boolean;
398
+ /**
399
+ * Provider keys to HIDE on this RP's login page. Empty/missing = show
400
+ * every provider configured at the project level. List a key (e.g.
401
+ * "google") to omit it from this RP only.
402
+ */
403
+ disabledProviders?: string[];
404
+ }
405
+ interface ApplyConfigRelyingPartyInput {
406
+ /**
407
+ * Which lane this RP authenticates. `foir push` is project-scoped, so
408
+ * RPs declared in config are customer-lane (the default); admin RPs are
409
+ * platform-managed and not part of project config.
410
+ */
411
+ kind?: 'customer' | 'admin';
412
+ /** Public RP slug, unique per project (e.g. `acme-storefront`). */
413
+ clientId: string;
414
+ /** Display name shown on the hosted login. */
415
+ name: string;
416
+ /** Allow-listed exact redirect URIs the platform will 302 to with the code. */
417
+ redirectUris: string[];
418
+ /** OAuth scopes the RP can request (intersection with caller scopes wins). */
419
+ allowedScopes?: string[];
420
+ /**
421
+ * Per-RP login method filter (customer-lane). Combines with the project-
422
+ * level customer auth settings (project may have password OFF entirely; an
423
+ * RP can't override-up, only override-down).
424
+ */
425
+ loginMethods?: ApplyConfigRelyingPartyLoginMethods;
426
+ /**
427
+ * White-label custom login domains for this RP (EID-46), e.g.
428
+ * `['login.eide.clothing']`. `foir push` registers each (creating a pending
429
+ * row + a DNS-TXT challenge) and prunes any not listed. Verification is a
430
+ * separate step — add the CNAME + `_foir-challenge` TXT records the push
431
+ * output prints, then verify. Customer-lane only.
432
+ */
433
+ customDomains?: string[];
434
+ /**
435
+ * Where a bare custom-domain (front-door) login lands after Foir sets the
436
+ * first-party session cookie (EID-108) — the app's landing, not an OAuth
437
+ * callback. Empty falls back to the redirect URI. Customer-lane only.
438
+ */
439
+ postLoginUrl?: string;
440
+ }
441
+ interface ApplyConfigPlacementInput {
442
+ type: string;
443
+ url: string;
444
+ allowedOrigin?: string;
445
+ height?: number;
446
+ tabName?: string;
447
+ hideContentTab?: boolean;
448
+ modelKeys?: string[];
449
+ }
450
+ interface ApplyConfigHookInput {
451
+ /** Unique key for this hook. */
452
+ key?: string;
453
+ /** Display name. */
454
+ name?: string;
455
+ /** Description of what this hook does. */
456
+ description?: string;
457
+ /** Lifecycle event that triggers this hook. */
458
+ event: string;
459
+ /** Target type — defaults to 'operation'. */
460
+ type?: string;
461
+ /** Key of the operation to execute. */
462
+ operationKey?: string;
463
+ /** Filter to scope the hook (e.g., `{ modelKey: 'redirect' }`). */
464
+ filter?: Record<string, unknown>;
465
+ /** Notification config for notification-type hooks. */
466
+ notificationConfig?: Record<string, unknown>;
467
+ /** Whether the hook is active. Defaults to true. */
468
+ isActive?: boolean;
469
+ }
470
+ interface ApplyConfigApiKeyInput {
471
+ /** Name for this API key (e.g. "Tilly iOS", "Tilly BFF"). */
472
+ name: string;
473
+ /** Key type: 'public' for client apps, 'secret' for BFF/server. */
474
+ keyType: 'public' | 'secret';
475
+ /** Environment variable name to write the key to in .env (e.g. "FOIR_PUBLIC_KEY"). */
476
+ envVar: string;
477
+ /** Scopes to restrict the key (e.g. ["configs:read", "records:write"]). Use ["*"] for full access. */
478
+ scopes?: string[];
479
+ /** Restrict the key to specific model keys (e.g. ["tilly_note", "tilly_block"]). */
480
+ allowedModels?: string[];
481
+ /** Restrict file uploads to specific MIME types (e.g. ["image/*", "video/*"]). */
482
+ allowedFileTypes?: string[];
483
+ }
484
+ /**
485
+ * A customer RBAC role declared in foir.config.ts and reconciled by
486
+ * `foir push`. The role's `permissions` become a customer access token's
487
+ * scope set at mint time (EID-125). Roles are matched by `key` within the
488
+ * project — a declared key that already exists is updated in place; a new
489
+ * key is created. (Roles present on the platform but absent from the config
490
+ * are left untouched — never auto-disabled.)
491
+ *
492
+ * Without a role, a consumer app's customers get read-only tokens after the
493
+ * Model B auth change (EID-17): the public `pk_` key can no longer grant
494
+ * write/execute, so the only source of those scopes is the customer's role.
495
+ */
496
+ interface ApplyConfigCustomerRoleInput {
497
+ /** Stable key, unique per project (e.g. "default", "member"). */
498
+ key: string;
499
+ /** Human-readable name (e.g. "Default Customer"). */
500
+ name: string;
501
+ /**
502
+ * Scopes granted to customers holding this role. Reuses the scoped-token
503
+ * grammar (records:read:<model>, records:write:<model>, operations:execute,
504
+ * files:read, …). The `self:*` macro expands at mint time to
505
+ * records:{read,write}:<model> for every customer-writable model in the
506
+ * project — but does NOT include operations:execute, so list that
507
+ * explicitly when the app's customers run operations.
508
+ */
509
+ permissions: string[];
510
+ /**
511
+ * When true, every customer in the project inherits this role with no
512
+ * per-customer assignment. An app's baseline customer grant should be a
513
+ * single `isDefault: true` role. Defaults to false.
514
+ */
515
+ isDefault?: boolean;
516
+ }
517
+ /**
518
+ * Source-type mapping entry for an app install. Maps a manifest-declared
519
+ * source type onto a project model. See docs/platform/apps.md §Mapping step.
520
+ */
521
+ interface AppSourceMappingInput {
522
+ toModel: string;
523
+ naturalKey: string;
524
+ fields: Record<string, string>;
525
+ }
526
+ /**
527
+ * Sink-contract mapping entry for an app install.
528
+ */
529
+ interface AppSinkMappingInput {
530
+ toModel: string;
531
+ naturalKey: string;
532
+ fields: Record<string, string>;
533
+ }
534
+ /**
535
+ * Placement field choice for TARGET_FIELD placements with
536
+ * field_selected_at_install=true. Keyed by placement.key.
537
+ */
538
+ interface AppPlacementFieldChoiceInput {
539
+ model: string;
540
+ field: string;
541
+ }
542
+ /**
543
+ * Per-project app installation declared in foir.config.ts under apps.<name>.
544
+ * Platform state is reconciled against this at `foir push` time; the
545
+ * manifest is fetched from `source` each push.
546
+ */
547
+ interface AppInput {
548
+ /** https URL to the manifest JSON. */
549
+ source: string;
550
+ /** Opaque settings forwarded to the app's middleware. */
551
+ settings?: Record<string, unknown>;
552
+ mappings?: {
553
+ sources?: Record<string, AppSourceMappingInput>;
554
+ sinks?: Record<string, AppSinkMappingInput>;
555
+ placementFields?: Record<string, AppPlacementFieldChoiceInput>;
556
+ };
557
+ }
558
+ /**
559
+ * Project-level settings applied via UpdateProject at push time and
560
+ * read via GetProject at pull time. Branding fields are empty-string-as-
561
+ * clear on the platform side: `''` clears the column, `undefined` leaves
562
+ * it untouched.
563
+ */
564
+ interface ApplyConfigProjectSettingsInput {
565
+ /** Display name in customer-facing emails / portal headers. */
566
+ displayName?: string;
567
+ /** Public URL of the project logo (rendered in customer emails). */
568
+ logoUrl?: string;
569
+ /** Hex color used as accent in customer-facing emails. */
570
+ primaryColor?: string;
571
+ /** "From" name on outbound customer emails. */
572
+ fromName?: string;
573
+ /** "Reply-To" address on outbound customer emails. */
574
+ replyTo?: string;
575
+ /** Support email surfaced in email footers. */
576
+ supportEmail?: string;
577
+ /**
578
+ * Public base URL of the consumer-facing app (storefront, portal,
579
+ * internal tool, …). Reset / verify / invitation links in customer
580
+ * emails are built from this — required before the platform will mint
581
+ * customer-email links.
582
+ */
583
+ appBaseUrl?: string;
584
+ /**
585
+ * Whether the platform sends a welcome email on customer registration.
586
+ * Default true. Disable for invite-style flows.
587
+ */
588
+ customerWelcomeEmailEnabled?: boolean;
589
+ /**
590
+ * Whether the public CustomerRegister mutation is allowed. Default true.
591
+ * When false, customers can only be created via secret-key admin paths
592
+ * (CreateCustomer, CreateCustomerInvitation).
593
+ */
594
+ customerSignupEnabled?: boolean;
595
+ /**
596
+ * Whether email/password customer login is exposed. Default true. Disable
597
+ * for a passwordless project (one-time codes or single sign-on only).
598
+ */
599
+ customerPasswordEnabled?: boolean;
600
+ /**
601
+ * Whether email one-time-code customer login is exposed. Default false.
602
+ */
603
+ customerOtpEnabled?: boolean;
604
+ }
605
+ /**
606
+ * Project pin + declarative settings. The pin (id / tenantId) is a guard
607
+ * against pushing one folder's config into another folder's project; the
608
+ * settings are applied to the platform via UpdateProject.
609
+ */
610
+ interface ApplyConfigProjectInput {
611
+ /** Project id this folder is bound to. */
612
+ id: string;
613
+ /** Tenant id of the bound project. */
614
+ tenantId: string;
615
+ /** Optional human-friendly name for diff readability. */
616
+ name?: string;
617
+ /**
618
+ * Project-level settings reconciled by `foir push`. Omit to leave the
619
+ * platform settings untouched. `foir pull` populates this block from
620
+ * the live project state so the local config can be version-controlled.
621
+ */
622
+ settings?: ApplyConfigProjectSettingsInput;
623
+ }
624
+ /**
625
+ * W3C Design Tokens Format Module document. The CLI applies this
626
+ * verbatim — references like "{font.size.display1}" are preserved on
627
+ * disk so renames in one place propagate to consumers. The platform's
628
+ * resolver runs at write time and rejects cycles or unknown references.
629
+ *
630
+ * See foir-docs/content/platform/design-tokens.md for the full shape.
631
+ */
632
+ interface ApplyConfigDesignTokensInput {
633
+ color?: Record<string, unknown>;
634
+ font?: {
635
+ family?: Record<string, unknown>;
636
+ weight?: Record<string, unknown>;
637
+ size?: Record<string, unknown>;
638
+ lineHeight?: Record<string, unknown>;
639
+ letterSpacing?: Record<string, unknown>;
640
+ };
641
+ typography?: Record<string, unknown>;
642
+ /** Escape hatch for forward-compatible W3C groups (`spacing`, `border`, …). */
643
+ [key: string]: unknown;
644
+ }
645
+ interface ApplyConfigInput {
646
+ key: string;
647
+ name: string;
648
+ configType?: string;
649
+ force?: boolean;
650
+ /** Base URL prepended to relative operation endpoints. */
651
+ operationBaseUrl?: string;
652
+ /**
653
+ * Project pin + declarative project-level settings. The pin guards
654
+ * against cross-folder mis-pushes; the settings (when present) are
655
+ * applied to the platform alongside the model / operation reconcile.
656
+ */
657
+ project?: ApplyConfigProjectInput;
658
+ models?: ApplyConfigModelInput[];
659
+ operations?: ApplyConfigOperationInput[];
660
+ segments?: ApplyConfigSegmentInput[];
661
+ schedules?: ApplyConfigScheduleInput[];
662
+ hooks?: ApplyConfigHookInput[];
663
+ authProviders?: ApplyConfigAuthProviderInput[];
664
+ relyingParties?: ApplyConfigRelyingPartyInput[];
665
+ placements?: ApplyConfigPlacementInput[];
666
+ apiKeys?: ApplyConfigApiKeyInput[];
667
+ /**
668
+ * Customer RBAC roles, reconciled by `foir push`. Declare an
669
+ * `isDefault: true` role to grant every customer the write/execute scopes
670
+ * their tokens need (EID-125) — without one, customers get read-only
671
+ * tokens after the Model B auth change (EID-17).
672
+ */
673
+ customerRoles?: ApplyConfigCustomerRoleInput[];
674
+ /** Per-project app declarations, keyed by app name. */
675
+ apps?: Record<string, AppInput>;
676
+ /**
677
+ * W3C-formatted design tokens document for this project. Reconciled by
678
+ * `foir push` — the document is applied verbatim to the draft channel.
679
+ * Add `--publish` to promote draft → published in the same push.
680
+ */
681
+ designTokens?: ApplyConfigDesignTokensInput;
682
+ [key: string]: unknown;
683
+ }
684
+ /** Define a complete config manifest. */
685
+ declare function defineConfig(config: ApplyConfigInput): ApplyConfigInput;
686
+ /** Define a model with type-safe field definitions. */
687
+ declare function defineModel(model: ApplyConfigModelInput): ApplyConfigModelInput;
688
+ /** Define a field with type safety. */
689
+ declare function defineField(field: FieldDefinitionInput): FieldDefinitionInput;
690
+ /** Define a select field — requires `optionModelKey` pointing to a model whose records become options. */
691
+ declare function defineSelectField(field: SelectFieldDefinitionInput): FieldDefinitionInput;
692
+ /**
693
+ * Define a W3C design tokens document. Pure pass-through that gives
694
+ * IntelliSense for the supported groups (`color`, `font.*`,
695
+ * `typography`); the type permits arbitrary additional groups for
696
+ * forward compatibility.
697
+ */
698
+ declare function defineDesignTokens(tokens: ApplyConfigDesignTokensInput): ApplyConfigDesignTokensInput;
699
+ /** Define an enum field — config-only choice from inline `{label, value}` options. */
700
+ declare function defineEnumField(field: EnumFieldDefinitionInput): FieldDefinitionInput;
701
+ /** Define an operation with type safety. */
702
+ declare function defineOperation(operation: ApplyConfigOperationInput): ApplyConfigOperationInput;
703
+ /** Define a segment with type safety. */
704
+ declare function defineSegment(segment: ApplyConfigSegmentInput): ApplyConfigSegmentInput;
705
+ /** Define a cron schedule for an operation. */
706
+ declare function defineSchedule(schedule: ApplyConfigScheduleInput): ApplyConfigScheduleInput;
707
+ /** Define an auth provider. */
708
+ declare function defineAuthProvider(provider: ApplyConfigAuthProviderInput): ApplyConfigAuthProviderInput;
709
+ /** Define a Login-with-Foir relying party (storefront / external app). */
710
+ declare function defineRelyingParty(client: ApplyConfigRelyingPartyInput): ApplyConfigRelyingPartyInput;
711
+ /** Define a lifecycle hook. */
712
+ declare function defineHook(hook: ApplyConfigHookInput): ApplyConfigHookInput;
713
+ /** Define an editor placement (sidebar or main-editor tab). */
714
+ declare function definePlacement(placement: ApplyConfigPlacementInput): ApplyConfigPlacementInput;
715
+ type SecretOwnerKind = 'project' | 'app';
716
+ /**
717
+ * One declared secret. `label` is the human-readable handle the
718
+ * reconciler uses to look the secret up in the vault — no opaque ref
719
+ * is committed. `ownerId` is required for app-owned secrets and ignored
720
+ * for project-owned ones.
721
+ */
722
+ interface SecretDeclaration {
723
+ ownerKind: SecretOwnerKind;
724
+ ownerId?: string;
725
+ label: string;
726
+ }
727
+ /** Shape of a `foir.secrets.ts` default export. */
728
+ interface FoirSecretsConfig {
729
+ secrets: SecretDeclaration[];
730
+ }
731
+ /**
732
+ * Type-safe identity helper for `foir.secrets.ts`:
733
+ *
734
+ * ```ts
735
+ * import { defineSecrets } from '@foir/cli/configs';
736
+ * export default defineSecrets({
737
+ * secrets: [
738
+ * { ownerKind: 'project', label: 'deepl_api_key' },
739
+ * ],
740
+ * });
741
+ * ```
742
+ *
743
+ * Plaintext lives in a sibling `local.foir.secrets.ts` (gitignored)
744
+ * keyed by label, or in env vars. Production never runs the
745
+ * reconciler — operators set production secrets through the admin UI.
746
+ */
747
+ declare function defineSecrets(config: FoirSecretsConfig): FoirSecretsConfig;
748
+
749
+ export { type AccessInput, type AppInput, type AppPlacementFieldChoiceInput, type AppSinkMappingInput, type AppSourceMappingInput, type ApplyConfigApiKeyInput, type ApplyConfigAuthProviderInput, type ApplyConfigCustomerRoleInput, type ApplyConfigDesignTokensInput, type ApplyConfigHookInput, type ApplyConfigInput, type ApplyConfigModelInput, type ApplyConfigOperationInput, type ApplyConfigPlacementInput, type ApplyConfigProjectInput, type ApplyConfigProjectSettingsInput, type ApplyConfigRelyingPartyInput, type ApplyConfigRelyingPartyLoginMethods, type ApplyConfigScheduleInput, type ApplyConfigSegmentInput, type EnumFieldConfig, type EnumFieldDefinitionInput, type EnumFieldOption, type FieldAccessInput, type FieldDefinitionInput, type FieldRelationshipInput, type FoirSecretsConfig, type LookupDefinitionInput, type QuotaRule, type ReferenceFieldConfig, type ReferenceFieldDefinitionInput, type SecretDeclaration, type SecretOwnerKind, type SelectFieldConfig, type SelectFieldDefinitionInput, defineAuthProvider, defineConfig, defineDesignTokens, defineEnumField, defineField, defineHook, defineModel, defineOperation, definePlacement, defineRelyingParty, defineSchedule, defineSecrets, defineSegment, defineSelectField };