@vxil/cli 0.2.1 → 0.4.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/README.md CHANGED
@@ -7,6 +7,8 @@ npx @vxil/cli quickstart # create a tenant + scaffold vxil.config.ts
7
7
  npx @vxil/cli push # apply your config (features, cms collections, functions)
8
8
  npx @vxil/cli gen # generate a typed client + MCP manifest for YOUR backend
9
9
  npx @vxil/cli functions dev fn # run a tenant function locally against the remote edge
10
+ npx @vxil/cli listen --forward-to localhost:3000/webhook # forward inbound webhook events to your local server
11
+ npx @vxil/cli mcp install --client cursor # wire your backend's MCP server into your editor's agent
10
12
  npx @vxil/cli doctor # sanity-check keys, URLs, reachability
11
13
  ```
12
14
 
@@ -24,7 +26,9 @@ export default defineConfig({
24
26
  });
25
27
  ```
26
28
 
27
- Verbs: `init quickstart push pull diff gen secrets env seed export import functions cms dev doctor versions rollback link apply migrate try`.
29
+ Verbs: `init quickstart push pull diff gen secrets env seed export import functions listen mcp cms dev doctor versions rollback link apply migrate try`.
30
+
31
+ `vxil listen --forward-to <url>` polls your tenant's inbound webhook events and re-POSTs each new one to your local server under the exact delivery contract production uses — same body shape, same signed `X-Vxil-Jobs-Signature` header (your real signing secret) — so the handler you test locally is the handler you ship. `--replay-last <n>` re-forwards recent events; `--raw` posts the stored provider payload alone.
28
32
 
29
33
  Docs: [vxil.com](https://vxil.com) · Dashboard: [vxil.com/dashboard](https://vxil.com/dashboard)
30
34
 
@@ -5,6 +5,32 @@ export declare const RESERVED_CREDIT_TYPES: ReadonlySet<string>;
5
5
  /** True when a credit_type is a vxil-COGS reserved type (grant/consume/config
6
6
  * of which is restricted to internal platform machinery). */
7
7
  export declare function isReservedCreditType(creditType: string): boolean;
8
+ export declare const NOTIFICATION_TEMPLATE_PLACEHOLDERS: Readonly<Record<string, readonly string[]>>;
9
+ /** Per-override caps (D4). Bodies are emails, not documents. */
10
+ export declare const NOTIF_OVERRIDE_SUBJECT_MAX = 500;
11
+ export declare const NOTIF_OVERRIDE_BODY_MAX = 20000;
12
+ /** Whole-map caps: keeps one manifest (and the KV row every send reads) small. */
13
+ export declare const NOTIF_OVERRIDE_MAX_LOCALES_PER_TEMPLATE = 20;
14
+ export declare const NOTIF_OVERRIDE_MAX_BYTES: number;
15
+ /**
16
+ * D4 — validate `templates.overrides` at CONFIG-WRITE time. Returns [] when the
17
+ * bag is absent or clean. Every rule fails LOUD rather than shipping something
18
+ * that silently renders wrong in a customer's inbox:
19
+ * • overrides present with `allowOverride:false` → rejected (never inert);
20
+ * • an unknown template id / locale tag → rejected;
21
+ * • an unknown `{{placeholder}}` in subject/html/text → rejected (the class of
22
+ * typo that renders as an empty string in a live email);
23
+ * • size caps (per-locale count, whole-map bytes) → rejected.
24
+ * Pure, so it is unit-tested without a control plane.
25
+ */
26
+ export declare function validateNotificationOverrides(templates: {
27
+ allowOverride?: boolean;
28
+ overrides?: Record<string, Record<string, {
29
+ subject?: string;
30
+ html?: string;
31
+ text?: string;
32
+ }>>;
33
+ } | undefined): string[];
8
34
  export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObject<{
9
35
  enabled: import("@sinclair/typebox").TBoolean;
10
36
  fromEmail: import("@sinclair/typebox").TString;
@@ -25,9 +51,14 @@ export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObj
25
51
  perDay: import("@sinclair/typebox").TInteger;
26
52
  perTenantSec: import("@sinclair/typebox").TInteger;
27
53
  }>;
28
- templates: import("@sinclair/typebox").TObject<{
54
+ templates: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
29
55
  allowOverride: import("@sinclair/typebox").TBoolean;
30
- }>;
56
+ overrides: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
57
+ subject: import("@sinclair/typebox").TString;
58
+ html: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
59
+ text: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
60
+ }>>>>;
61
+ }>>;
31
62
  /** in-app inbox channel (send with channel: 'inbox' | 'both') */
32
63
  inboxEnabled: import("@sinclair/typebox").TBoolean;
33
64
  /** §11b.5 broadcast campaigns channel. Optional bag (= 1 leaf): absent means
@@ -100,16 +131,29 @@ export declare const JobsConfigSchema: import("@sinclair/typebox").TObject<{
100
131
  }>;
101
132
  }>;
102
133
  export type JobsConfig = Static<typeof JobsConfigSchema>;
134
+ declare const OidcProviderSchema: import("@sinclair/typebox").TObject<{
135
+ issuer: import("@sinclair/typebox").TString;
136
+ clientId: import("@sinclair/typebox").TString;
137
+ clientSecretRef: import("@sinclair/typebox").TString;
138
+ scopes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
139
+ claims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
140
+ email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
141
+ name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
142
+ roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
143
+ }>>;
144
+ allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
145
+ autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
146
+ }>;
103
147
  export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
104
148
  enabled: import("@sinclair/typebox").TBoolean;
105
- methods: import("@sinclair/typebox").TObject<{
149
+ methods: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
106
150
  emailPassword: import("@sinclair/typebox").TBoolean;
107
151
  magicLink: import("@sinclair/typebox").TBoolean;
108
152
  google: import("@sinclair/typebox").TBoolean;
109
153
  github: import("@sinclair/typebox").TBoolean;
110
154
  apple: import("@sinclair/typebox").TBoolean;
111
155
  facebook: import("@sinclair/typebox").TBoolean;
112
- }>;
156
+ }>>;
113
157
  providers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
114
158
  google: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
115
159
  clientIdRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -133,10 +177,24 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
133
177
  clientSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
134
178
  audRefs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
135
179
  }>>;
180
+ oidc: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
181
+ issuer: import("@sinclair/typebox").TString;
182
+ clientId: import("@sinclair/typebox").TString;
183
+ clientSecretRef: import("@sinclair/typebox").TString;
184
+ scopes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
185
+ claims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
186
+ email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
187
+ name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
188
+ roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
189
+ }>>;
190
+ allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
191
+ autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
192
+ }>>;
136
193
  }>>;
137
194
  session: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
138
195
  ttlMinutes: import("@sinclair/typebox").TInteger;
139
196
  refreshTtlDays: import("@sinclair/typebox").TInteger;
197
+ maxConcurrent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
140
198
  }>>;
141
199
  password: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
142
200
  minLength: import("@sinclair/typebox").TInteger;
@@ -153,6 +211,7 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
153
211
  codeTtlMinutes: import("@sinclair/typebox").TInteger;
154
212
  maxAttempts: import("@sinclair/typebox").TInteger;
155
213
  resendCooldownSec: import("@sinclair/typebox").TInteger;
214
+ testRecipients: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
156
215
  }>>;
157
216
  anonymous: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
158
217
  enabled: import("@sinclair/typebox").TBoolean;
@@ -160,7 +219,21 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
160
219
  orgClaims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
161
220
  enabled: import("@sinclair/typebox").TBoolean;
162
221
  }>>;
222
+ security: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
223
+ lockout: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
224
+ maxFailures: import("@sinclair/typebox").TInteger;
225
+ windowMinutes: import("@sinclair/typebox").TInteger;
226
+ lockMinutes: import("@sinclair/typebox").TInteger;
227
+ }>>;
228
+ breachedPasswords: import("@sinclair/typebox").TBoolean;
229
+ captchaSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
230
+ allowedRedirectOrigins: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
231
+ }>>;
163
232
  }>;
233
+ /** The security bag as persisted (present ⇒ leaf defaults applied). */
234
+ export type AuthSecurityConfig = NonNullable<Static<typeof AuthConfigSchema>['security']>;
235
+ /** The RB-2 generic OIDC / SSO issuer block (`providers.oidc`). */
236
+ export type AuthOidcConfig = Static<typeof OidcProviderSchema>;
164
237
  export type AuthConfig = Static<typeof AuthConfigSchema>;
165
238
  export declare const RateLimitsConfigSchema: import("@sinclair/typebox").TObject<{
166
239
  enabled: import("@sinclair/typebox").TBoolean;
@@ -172,7 +245,6 @@ export declare const RateLimitsConfigSchema: import("@sinclair/typebox").TObject
172
245
  export type RateLimitsConfig = Static<typeof RateLimitsConfigSchema>;
173
246
  export declare const FilesConfigSchema: import("@sinclair/typebox").TObject<{
174
247
  enabled: import("@sinclair/typebox").TBoolean;
175
- bucketRef: import("@sinclair/typebox").TString;
176
248
  uploadUrlTtl: import("@sinclair/typebox").TInteger;
177
249
  downloadUrlTtl: import("@sinclair/typebox").TInteger;
178
250
  quotas: import("@sinclair/typebox").TObject<{
@@ -181,10 +253,6 @@ export declare const FilesConfigSchema: import("@sinclair/typebox").TObject<{
181
253
  maxObjectCount: import("@sinclair/typebox").TInteger;
182
254
  }>;
183
255
  allowedContentTypes: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
184
- contentScan: import("@sinclair/typebox").TObject<{
185
- enabled: import("@sinclair/typebox").TBoolean;
186
- quarantineOnFail: import("@sinclair/typebox").TBoolean;
187
- }>;
188
256
  sharedLinks: import("@sinclair/typebox").TObject<{
189
257
  enabled: import("@sinclair/typebox").TBoolean;
190
258
  maxTtl: import("@sinclair/typebox").TInteger;
@@ -268,9 +336,6 @@ export declare const McpConfigSchema: import("@sinclair/typebox").TObject<{
268
336
  enabled: import("@sinclair/typebox").TBoolean;
269
337
  exposureLevel: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"all">, import("@sinclair/typebox").TLiteral<"read-only">, import("@sinclair/typebox").TLiteral<"custom">]>;
270
338
  allowToolList: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
271
- scopedKey: import("@sinclair/typebox").TObject<{
272
- perAgentKeys: import("@sinclair/typebox").TBoolean;
273
- }>;
274
339
  rateLimits: import("@sinclair/typebox").TObject<{
275
340
  toolCallsPerMin: import("@sinclair/typebox").TInteger;
276
341
  }>;
@@ -328,9 +393,6 @@ export declare const ActivityFeedConfigSchema: import("@sinclair/typebox").TObje
328
393
  copyLimit: import("@sinclair/typebox").TInteger;
329
394
  maxFollowing: import("@sinclair/typebox").TInteger;
330
395
  }>;
331
- aggregation: import("@sinclair/typebox").TObject<{
332
- maxGroupActivities: import("@sinclair/typebox").TInteger;
333
- }>;
334
396
  realtime: import("@sinclair/typebox").TObject<{
335
397
  enabled: import("@sinclair/typebox").TBoolean;
336
398
  }>;
@@ -338,9 +400,6 @@ export declare const ActivityFeedConfigSchema: import("@sinclair/typebox").TObje
338
400
  enabled: import("@sinclair/typebox").TBoolean;
339
401
  digestCadence: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"off">, import("@sinclair/typebox").TLiteral<"hourly">, import("@sinclair/typebox").TLiteral<"daily">]>;
340
402
  }>;
341
- rateLimit: import("@sinclair/typebox").TObject<{
342
- addPerSec: import("@sinclair/typebox").TInteger;
343
- }>;
344
403
  }>;
345
404
  export type ActivityFeedConfig = Static<typeof ActivityFeedConfigSchema>;
346
405
  export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObject<{
@@ -458,6 +517,7 @@ export declare const PaymentsConfigSchema: import("@sinclair/typebox").TObject<{
458
517
  publicSdkKey: import("@sinclair/typebox").TString;
459
518
  secretApiKeyRef: import("@sinclair/typebox").TString;
460
519
  webhookSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
520
+ acceptSandbox: import("@sinclair/typebox").TBoolean;
461
521
  }>>;
462
522
  paypal: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
463
523
  clientIdRef: import("@sinclair/typebox").TString;
@@ -466,9 +526,6 @@ export declare const PaymentsConfigSchema: import("@sinclair/typebox").TObject<{
466
526
  sandbox: import("@sinclair/typebox").TBoolean;
467
527
  }>>;
468
528
  returnUrl: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
469
- prices: import("@sinclair/typebox").TObject<{
470
- catalogRef: import("@sinclair/typebox").TString;
471
- }>;
472
529
  defaults: import("@sinclair/typebox").TObject<{
473
530
  currency: import("@sinclair/typebox").TString;
474
531
  trialDays: import("@sinclair/typebox").TInteger;
@@ -491,19 +548,19 @@ export declare const PaymentsConfigSchema: import("@sinclair/typebox").TObject<{
491
548
  }>>;
492
549
  priceMap: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
493
550
  autoRefundOnJobFailure: import("@sinclair/typebox").TBoolean;
551
+ grace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
552
+ pastDueDays: import("@sinclair/typebox").TInteger;
553
+ }>>;
554
+ enforcePeriodEnd: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
555
+ slackHours: import("@sinclair/typebox").TInteger;
556
+ }>>;
494
557
  }>>;
495
- webhooks: import("@sinclair/typebox").TObject<{
496
- forwardToTenantUrl: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
497
- }>;
498
558
  }>;
499
559
  export type PaymentsConfig = Static<typeof PaymentsConfigSchema>;
500
560
  export declare const FunctionsConfigSchema: import("@sinclair/typebox").TObject<{
501
561
  enabled: import("@sinclair/typebox").TBoolean;
502
- runtime: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"isolate">, import("@sinclair/typebox").TLiteral<"container">]>;
503
562
  defaultLimits: import("@sinclair/typebox").TObject<{
504
563
  cpuMs: import("@sinclair/typebox").TInteger;
505
- timeoutMs: import("@sinclair/typebox").TInteger;
506
- memoryMb: import("@sinclair/typebox").TInteger;
507
564
  }>;
508
565
  metering: import("@sinclair/typebox").TObject<{
509
566
  enabled: import("@sinclair/typebox").TBoolean;
@@ -567,14 +624,8 @@ export declare const CopilotConfigSchema: import("@sinclair/typebox").TObject<{
567
624
  guestToolAllow: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
568
625
  }>>;
569
626
  }>>;
570
- escalation: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
571
- enabled: import("@sinclair/typebox").TBoolean;
572
- handler: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"comments">, import("@sinclair/typebox").TLiteral<"notifications">]>;
573
- notifyTemplate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
574
- }>>;
575
627
  limits: import("@sinclair/typebox").TObject<{
576
628
  consumeCredits: import("@sinclair/typebox").TBoolean;
577
- tokensPerUserPerDay: import("@sinclair/typebox").TInteger;
578
629
  }>;
579
630
  widget: import("@sinclair/typebox").TObject<{
580
631
  enabled: import("@sinclair/typebox").TBoolean;
@@ -590,6 +641,21 @@ export declare const CONFIG_FLAG_CAP = 15;
590
641
  * Per features/auth.md §4: an OPTIONAL object (e.g. a provider credential
591
642
  * block) counts as ONE flag — the tenant's decision is "configure it or
592
643
  * not", not each inner ref. */
644
+ /** The auth lifecycle events an `authHook` binding may name (F4-30) — the
645
+ * closed union `packages/config` types as AuthHookEvent; the control-plane
646
+ * reconciler maps each to its `auth.<event>` audit-event prefix. */
647
+ export declare const AUTH_HOOK_EVENTS: readonly ["user.created", "session.created", "session.revoked", "signin.failure"];
648
+ export type AuthHookEvent = (typeof AUTH_HOOK_EVENTS)[number];
649
+ /** F4-29 test-recipient entry grammar (shared by the validator and auth-v1's
650
+ * matcher): an exact email, a `*@domain` glob, or a +E.164 phone number. */
651
+ export declare const TEST_RECIPIENT_EMAIL_RE: RegExp;
652
+ export declare const TEST_RECIPIENT_GLOB_RE: RegExp;
653
+ export declare const TEST_RECIPIENT_E164_RE: RegExp;
654
+ export declare function isTestRecipientPattern(entry: string): boolean;
655
+ /** True iff `identifier` (an email today) matches one configured entry:
656
+ * exact (case-insensitive) or the `*@domain` glob. +E.164 entries never match
657
+ * an email. */
658
+ export declare function matchesTestRecipient(identifier: string, entries: readonly string[] | undefined): boolean;
593
659
  export declare function countLeaves(schema: TSchema): number;
594
660
  export interface ConfigValidation {
595
661
  ok: boolean;
package/dist/config.d.ts CHANGED
@@ -23,9 +23,34 @@ export interface FieldDef {
23
23
  * cannot reconcile surface as a visible plan warning, never a silent no-op. */
24
24
  unique?: boolean;
25
25
  /** Per-relation-field delete behavior (cms.md §11): bounded, atomic fan-out
26
- * when the referenced item is deleted. Same carriage + reconcile behavior
27
- * as `unique`. */
28
- onDelete?: 'cascade' | 'set_null';
26
+ * when the referenced item is deleted — or `restrict` (§11.1), which REFUSES
27
+ * the delete with 409 `referenced` while a live reference exists. Same
28
+ * carriage + reconcile behavior as `unique`. */
29
+ onDelete?: 'cascade' | 'set_null' | 'restrict';
30
+ /** FIELD-LEVEL READ SECURITY (cms.md §18): the end-user ORG ROLE slugs allowed
31
+ * to READ this field. Omitted/`[]` = ungated (every caller sees it — today's
32
+ * behavior). A non-empty list is FAIL-SAFE: in VERIFIED end-user mode the
33
+ * field is OMITTED from every read (get / list / query / `$expand` at any
34
+ * depth / the write-response echo / read-hook payloads) unless the session's
35
+ * verified `roles` claim intersects the list, it is UNQUERYABLE (a filter /
36
+ * sort / group-by naming it is a 422, so it cannot be a value oracle), and it
37
+ * is NEVER served on the anonymous public lane. A trusted SERVER caller (an
38
+ * API key with no end-user session) still sees every field — this gates END
39
+ * USERS, not the tenant. ≤16 entries, each `^[a-z0-9][a-z0-9_-]{0,31}$` (the
40
+ * `orgs` role alphabet). The gate
41
+ * never affects WRITES. Carried by BOTH push paths and alterable in place. */
42
+ readRoles?: string[];
43
+ }
44
+ /** One config-declared per-record ACTION button (cms.md §17): exactly ONE
45
+ * human-initiated step — pressing it invokes the deployed tenant function `fn`
46
+ * with `{ collection, item_id, action, actor, item }` and returns its result.
47
+ * No conditions, no chaining, no scheduling. `key` matches
48
+ * /^[a-z][a-z0-9_]{0,31}$/, `fn` is a function name (/^[a-z][a-z0-9-]{0,47}$/),
49
+ * ≤8 per collection. */
50
+ export interface CmsActionDef {
51
+ key: string;
52
+ label: string;
53
+ fn: string;
29
54
  }
30
55
  /** The CANONICAL field-attribute name set (config-side spelling), DERIVED from
31
56
  * `FieldDef` itself so it can never drift from the authoring type: it is every
@@ -66,10 +91,24 @@ export interface CollectionDef {
66
91
  * with a regression test on each — an attribute read by no enforcing path ships
67
92
  * inert (the validation.unique / owner_field precedent). */
68
93
  public?: boolean;
94
+ /** Per-record action buttons (cms.md §17): `[{ key, label, fn }]`, stored on
95
+ * the collection (model data, not a config leaf). The dashboard renders one
96
+ * button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
97
+ * :key` invokes `fn` once. Carried by BOTH push paths (create + reconcile on
98
+ * existing collections — config is the source of truth; `[]`/absent clears). */
99
+ actions?: CmsActionDef[];
69
100
  }
101
+ /** The auth lifecycle events an `authHook` binding may subscribe to (F4-30,
102
+ * auth wave 2026-09-10) — a CLOSED union; ONE event per binding, one audit
103
+ * event → one function invocation, no branching inside vxil:
104
+ * user.created — auth.user.created { user_id, method, is_anonymous }
105
+ * session.created — auth.session.created { user_id, session_id }
106
+ * session.revoked — auth.session.revoked { user_id, session_id, reason }
107
+ * signin.failure — auth.signin.failure { email_hash | user_id, reason } */
108
+ export type AuthHookEvent = 'user.created' | 'session.created' | 'session.revoked' | 'signin.failure';
70
109
  /** A function trigger (the §7.3 crossing). cmsHook fires on a CMS write;
71
- * authHook is the post-signup hook (fires on `auth.user.created`,
72
- * at-least-once, ~1min fanout latency). */
110
+ * authHook fires on ONE auth lifecycle event (default `user.created` — the
111
+ * post-signup hook; at-least-once, ~1min fanout latency). */
73
112
  export type FunctionTrigger = {
74
113
  kind: 'http';
75
114
  path?: string;
@@ -88,7 +127,7 @@ export type FunctionTrigger = {
88
127
  event: 'beforeCreate' | 'beforeUpdate' | 'beforeWrite';
89
128
  } | {
90
129
  kind: 'authHook';
91
- event?: 'user.created';
130
+ event?: AuthHookEvent;
92
131
  };
93
132
  /** A deployed tenant function — source in functions/, deployed on `vxil push`. */
94
133
  export interface FunctionDef {