@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.
- package/dist/cli.js +12970 -0
- package/dist/lib/config-helpers.d.ts +749 -0
- package/dist/lib/config-helpers.js +59 -0
- package/package.json +80 -0
|
@@ -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 };
|