@vxil/cli 0.17.0 → 0.19.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.
@@ -10,12 +10,16 @@ export declare const HOOK_LIMITS: {
10
10
  readonly maxReadHooksPerCollection: 10;
11
11
  };
12
12
  /** The ONLY root variables an expression may reference. `caller` is the VERIFIED
13
- * end-user principal (https://vxil.com/docs/guide/09-security-and-multitenancy) — read-only and
14
- * populated ONLY on the READ path (runReadHooks); on the write path and in
15
- * server-caller mode it is null, exactly like `before` on a create. It carries
16
- * `caller.endUserId` (the verified session sub, or null) and `caller.principal`
17
- * ('end_user' | 'tenant'). It is the missing "caller/session context" a read
18
- * hook keys on to redact/derive per-viewer. */
13
+ * end-user principal (https://vxil.com/docs/guide/09-security-and-multitenancy) — read-only.
14
+ * It carries `caller.endUserId` (the verified session sub, or null),
15
+ * `caller.principal` ('end_user' | 'tenant') and `caller.roles` (the VERIFIED
16
+ * session role claims of the signed-in session — never a body or header
17
+ * value; null in server-caller mode). READ hooks always get it
18
+ * (server mode: `{ endUserId: null, principal: 'tenant', roles: null }`).
19
+ * WRITE hooks get it in verified end-user mode only; on a
20
+ * server write it is null, exactly like `before` on a create — so a role rule
21
+ * reads `isNull(caller) || contains(caller.roles, 'manager')`. `contains` on
22
+ * an array is exact element membership. */
19
23
  export declare const HOOK_ROOT_VARS: readonly ["item", "before", "now", "caller"];
20
24
  /** The ONLY callable functions. Each is pure + deterministic + bounded. */
21
25
  export declare const HOOK_FUNCTIONS: readonly ["min", "max", "abs", "round", "floor", "ceil", "sqrt", "pow", "sign", "len", "lower", "upper", "trim", "substr", "contains", "startsWith", "endsWith", "concat", "coalesce", "ifNull", "not", "isNull", "number", "string", "bool", "daysBetween", "yearsBetween"];
@@ -64,25 +68,41 @@ export declare class HookEvalError extends Error {
64
68
  /** Thrown by a `validate` hook whose expression is falsy — maps to a clean 422. */
65
69
  export declare class HookRejection extends Error {
66
70
  }
71
+ /** A WRITE `validate` hook whose expression could not be evaluated (null
72
+ * arithmetic, a missing operand, a bad date …). Still a HookEvalError — the
73
+ * write is refused, never let through — but it carries the hook's id and its
74
+ * declared `message`, so the API can show the end user the tenant's own copy
75
+ * while keeping the engine detail for the developer. `message` stays the
76
+ * engine detail. */
77
+ export declare class HookValidateFault extends HookEvalError {
78
+ readonly hookId: string;
79
+ readonly tenantMessage: string | undefined;
80
+ constructor(hookId: string, tenantMessage: string | undefined, detail: string);
81
+ }
67
82
  /** Parse a hook expression into an AST. Throws HookParseError on any malformed input. */
68
83
  export declare function parseExpr(src: string): Node;
69
84
  /** Walk the AST and reject anything outside the allow-list, plus depth bounds.
70
85
  * Returns an array of human-readable errors (empty = safe). Pure, no eval. */
71
86
  export declare function validateAst(root: Node): string[];
72
- /** The VERIFIED caller/session context for read hooks (guide ch. 7). Read-only;
73
- * present only on the read path in end-user mode. In server mode / write path
74
- * the whole object is null. */
87
+ /** The VERIFIED caller/session context for Lane-A hooks (guide ch. 7).
88
+ * Read-only. Read hooks always get it; write hooks get it in verified end-user
89
+ * mode only (a server write passes null — see HOOK_ROOT_VARS). */
75
90
  export interface HookCaller {
76
91
  /** the verified end-user session sub, or null in server-caller mode. */
77
92
  endUserId: string | null;
78
93
  /** 'end_user' when a session was verified at the edge, else 'tenant'. */
79
94
  principal: 'end_user' | 'tenant';
95
+ /** the VERIFIED session role claims of the signed-in session; `[]` for a
96
+ * session without roles, null in server-caller mode. Optional so an older
97
+ * caller object still reads
98
+ * as "no roles". */
99
+ roles?: readonly string[] | null;
80
100
  }
81
101
  export interface HookContext {
82
102
  item: Record<string, unknown>;
83
103
  before: Record<string, unknown> | null;
84
104
  now: string;
85
- /** verified caller/session context (read path only); null otherwise. */
105
+ /** verified caller/session context; null on a server write. */
86
106
  caller?: HookCaller | null;
87
107
  }
88
108
  /** A mutable step counter SHARED across many evalExpr calls (the read path's
@@ -116,7 +136,10 @@ export interface HookRunResult {
116
136
  * Mutates a CLONE: derive hooks set fields; a validate hook that returns falsy
117
137
  * throws HookRejection (→ the caller maps to a 422 and the tx rolls back).
118
138
  * Throws HookEvalError on a runtime fault (also a clean 422, never a 500). */
119
- export declare function runWriteHooks(hooks: Record<string, HookDef> | undefined, collection: string, phase: 'create' | 'update', data: Record<string, unknown>, before: Record<string, unknown> | null, now: string): HookRunResult;
139
+ export declare function runWriteHooks(hooks: Record<string, HookDef> | undefined, collection: string, phase: 'create' | 'update', data: Record<string, unknown>, before: Record<string, unknown> | null, now: string,
140
+ /** the VERIFIED end-user caller (endUserId + principal + roles) in
141
+ * end-user mode; omitted / null on a server write (expressions see null). */
142
+ caller?: HookCaller | null): HookRunResult;
120
143
  export interface ReadHookResult {
121
144
  /** Surviving rows in input order. Each is a SHALLOW COPY with a copied `data`
122
145
  * bag — the caller's rows are never mutated and nothing here is persisted. */
@@ -143,8 +166,8 @@ export interface ReadHookResult {
143
166
  * fire here, and read events never fire in runWriteHooks. */
144
167
  export declare function runReadHooks(hooks: Record<string, HookDef> | undefined, collection: string, rows: ReadonlyArray<Record<string, unknown>>, now: string,
145
168
  /** verified caller/session context (guide ch. 7) — read-only, exposed to
146
- * expressions as `caller.endUserId` / `caller.principal`. Omitted ⇒ null
147
- * (server-caller mode). */
169
+ * expressions as `caller.endUserId` / `caller.principal` / `caller.roles`.
170
+ * Omitted ⇒ null (server-caller mode). */
148
171
  caller?: HookCaller | null): ReadHookResult;
149
172
  export interface ExprRef {
150
173
  root: 'item' | 'before' | 'now' | 'caller';
@@ -157,6 +180,36 @@ export type InferredType = 'number' | 'string' | 'boolean' | 'null' | 'unknown';
157
180
  /** Best-effort static type of an expression's result (its output). 'unknown' for
158
181
  * member access / coalesce / mixed ternaries — never a false-positive mismatch. */
159
182
  export declare function inferType(nd: Node): InferredType;
183
+ /** One declared field, as the typing pass needs it (cms field type + required). */
184
+ export interface HookFieldInfo {
185
+ type: string;
186
+ required?: boolean;
187
+ }
188
+ /** collection → field → declared info. */
189
+ export type HookFieldTypes = Record<string, Record<string, HookFieldInfo>>;
190
+ /** Type one parsed expression against one collection's declared fields.
191
+ * `errors` are shapes that can never work (refuse); `warnings` are shapes
192
+ * that work only for some stored values (json vs a scalar, `+` on a text
193
+ * field) and arithmetic on an optional field with no coalesce() — it faults
194
+ * the hook whenever the field is missing. `present` lists fields a derive sets first. */
195
+ export declare function checkHookExprTypes(root: Node, fields: Record<string, HookFieldInfo> | undefined, opts: {
196
+ kind: HookDef['kind'];
197
+ event: HookDef['event'];
198
+ present?: ReadonlySet<string> | undefined;
199
+ }): {
200
+ errors: string[];
201
+ warnings: string[];
202
+ };
203
+ /** Project declared collections (`{ <name>: { fields: { <f>: { type, required? } } } }`
204
+ * — the vxil.config / apply-bundle shape) onto HookFieldTypes. Malformed
205
+ * entries are skipped: no declared field means no verdict. */
206
+ export declare function hookFieldTypesOf(collections: unknown): HookFieldTypes;
207
+ /** Type every hook whose collection the caller declares (see checkHookExprTypes).
208
+ * Unparseable expressions are skipped — validateHookDef already reports them. */
209
+ export declare function checkHookTypes(hooks: Record<string, HookDef> | undefined, fieldTypes: HookFieldTypes): {
210
+ errors: string[];
211
+ warnings: string[];
212
+ };
160
213
  export type HlKind = 'num' | 'str' | 'fn' | 'var' | 'kw' | 'op' | 'ident' | 'ws' | 'err';
161
214
  /** Token-level highlighter over the FULL source (whitespace + bad chars kept),
162
215
  * so a dashboard editor can render a colored overlay behind a textarea. */
@@ -35,4 +35,22 @@ interface CdcRuleLike {
35
35
  }
36
36
  /** Cross-field rule for CmsConfig.cdc (called from validateFeatureConfig). */
37
37
  export declare function validateCdcConfig(cdc: Record<string, CdcRuleLike> | undefined): string[];
38
+ /** What a payload:'full' change-data rule must know about its target
39
+ * collection's read gates. `readRoleFields`: fields with a non-empty
40
+ * `readRoles`; `endUserAccess`: the collection's end-user access mode. */
41
+ export interface CdcCollectionGates {
42
+ readRoleFields?: readonly string[];
43
+ endUserAccess?: 'readwrite' | 'read' | 'none';
44
+ }
45
+ /**
46
+ * A `payload: 'full'` change-data rule inlines the stored row into a realtime
47
+ * frame that every subscriber of the channel receives — it does not go through
48
+ * the read projection. So it must never target a collection whose reads are
49
+ * gated: a field with `readRoles` (the frame would carry the gated value), or
50
+ * an `endUserAccess` of 'read' / 'none' (the frame would bypass the
51
+ * collection's end-user gate). Pure; one problem line per offending ENABLED
52
+ * rule (an unknown collection is skipped). The safe shape is `payload: 'ids'`
53
+ * and a re-read through the cms, where every gate applies.
54
+ */
55
+ export declare function cdcFullFramesOverGatedCollections(cdc: unknown, collections: Record<string, CdcCollectionGates>): string[];
38
56
  export {};
@@ -78,9 +78,10 @@ export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObj
78
78
  maxAttempts: import("@sinclair/typebox").TInteger;
79
79
  backoff: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"exponential">, import("@sinclair/typebox").TLiteral<"linear">]>;
80
80
  }>>;
81
- suppression: import("@sinclair/typebox").TObject<{
81
+ suppression: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
82
82
  softBounceThreshold: import("@sinclair/typebox").TInteger;
83
- }>;
83
+ testRecipients: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
84
+ }>>;
84
85
  rateLimit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
85
86
  perDay: import("@sinclair/typebox").TInteger;
86
87
  perTenantSec: import("@sinclair/typebox").TInteger;
@@ -217,6 +218,7 @@ declare const OidcProviderSchema: import("@sinclair/typebox").TObject<{
217
218
  email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
218
219
  name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
219
220
  roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
221
+ rolePrefix: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
220
222
  }>>;
221
223
  allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
222
224
  autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
@@ -274,6 +276,7 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
274
276
  email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
275
277
  name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
276
278
  roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
279
+ rolePrefix: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
277
280
  }>>;
278
281
  allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
279
282
  autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
@@ -318,6 +321,8 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
318
321
  breachedPasswords: import("@sinclair/typebox").TBoolean;
319
322
  captchaSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
320
323
  allowedRedirectOrigins: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
324
+ inviteOnly: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
325
+ allowedEmailDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
321
326
  }>>;
322
327
  }>;
323
328
  /** The security bag as persisted (present ⇒ leaf defaults applied). */
@@ -632,6 +637,7 @@ export declare const RagConfigSchema: import("@sinclair/typebox").TObject<{
632
637
  topK: import("@sinclair/typebox").TInteger;
633
638
  mode: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"hybrid">, import("@sinclair/typebox").TLiteral<"vector">, import("@sinclair/typebox").TLiteral<"keyword">]>;
634
639
  minScore: import("@sinclair/typebox").TNumber;
640
+ minSimilarity: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
635
641
  rerank: import("@sinclair/typebox").TBoolean;
636
642
  }>;
637
643
  boosts: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
@@ -832,6 +838,9 @@ export declare const TEST_RECIPIENT_EMAIL_RE: RegExp;
832
838
  export declare const TEST_RECIPIENT_GLOB_RE: RegExp;
833
839
  export declare const TEST_RECIPIENT_E164_RE: RegExp;
834
840
  export declare function isTestRecipientPattern(entry: string): boolean;
841
+ /** The MAIL subset of the grammar (notifications suppression.testRecipients):
842
+ * an exact email or an `*@domain` glob. */
843
+ export declare function isMailTestRecipientPattern(entry: string): boolean;
835
844
  /** True iff `identifier` (an email today) matches one configured entry:
836
845
  * exact (case-insensitive) or the `*@domain` glob. +E.164 entries never match
837
846
  * an email. */
package/dist/config.d.ts CHANGED
@@ -121,6 +121,17 @@ export interface CollectionDef {
121
121
  * `vxil push --allow-destructive`. Any other value fails `vxil plan` /
122
122
  * `vxil push` (it never falls back to 'readwrite'). */
123
123
  endUserAccess?: 'readwrite' | 'read' | 'none';
124
+ /** Role slugs one of which a VERIFIED end user must hold to WRITE this
125
+ * collection (create, update, $inc, delete, publish, transaction step,
126
+ * filtered delete, batch write, and an end-user delete's cascade into it) —
127
+ * otherwise `403 role_required`. Reads are unaffected. Roles are the
128
+ * session's verified role claims (its orgs roles — the
129
+ * same ones `readRoles` and `caller.roles` in hooks see). Server keys are
130
+ * never affected. Absent / `[]` = ungated. Up to 16 slugs
131
+ * (`^[a-z0-9][a-z0-9_-]{0,31}$`). Stored on the collection (model data, not
132
+ * a config leaf) and carried by BOTH push paths; widening (clearing, or
133
+ * adding a role) needs `vxil push --allow-destructive`. */
134
+ writeRoles?: string[];
124
135
  /** Per-record action buttons (guide ch. 4): `[{ key, label, fn }]`, stored on
125
136
  * the collection (model data, not a config leaf). The dashboard renders one
126
137
  * button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/