@vxil/config 0.2.0 → 0.3.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/index.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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/config",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "defineConfig() — one typed source of truth for a whole vxil backend (features + CMS schema + functions + secret refs + seed). INTERNAL workspace package: bundled into the published `vxil` package's `vxil/config` subpath, not published separately.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://vxil.com",
@@ -22,7 +22,7 @@
22
22
  "src"
23
23
  ],
24
24
  "dependencies": {
25
- "@vxil/feature-configs": "0.2.0"
25
+ "@vxil/feature-configs": "0.3.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"
package/src/index.ts CHANGED
@@ -42,9 +42,35 @@ export interface FieldDef {
42
42
  * cannot reconcile surface as a visible plan warning, never a silent no-op. */
43
43
  unique?: boolean;
44
44
  /** Per-relation-field delete behavior (cms.md §11): bounded, atomic fan-out
45
- * when the referenced item is deleted. Same carriage + reconcile behavior
46
- * as `unique`. */
47
- onDelete?: 'cascade' | 'set_null';
45
+ * when the referenced item is deleted — or `restrict` (§11.1), which REFUSES
46
+ * the delete with 409 `referenced` while a live reference exists. Same
47
+ * carriage + reconcile behavior as `unique`. */
48
+ onDelete?: 'cascade' | 'set_null' | 'restrict';
49
+ /** FIELD-LEVEL READ SECURITY (cms.md §18): the end-user ORG ROLE slugs allowed
50
+ * to READ this field. Omitted/`[]` = ungated (every caller sees it — today's
51
+ * behavior). A non-empty list is FAIL-SAFE: in VERIFIED end-user mode the
52
+ * field is OMITTED from every read (get / list / query / `$expand` at any
53
+ * depth / the write-response echo / read-hook payloads) unless the session's
54
+ * verified `roles` claim intersects the list, it is UNQUERYABLE (a filter /
55
+ * sort / group-by naming it is a 422, so it cannot be a value oracle), and it
56
+ * is NEVER served on the anonymous public lane. A trusted SERVER caller (an
57
+ * API key with no end-user session) still sees every field — this gates END
58
+ * USERS, not the tenant. ≤16 entries, each `^[a-z0-9][a-z0-9_-]{0,31}$` (the
59
+ * `orgs` role alphabet). The gate
60
+ * never affects WRITES. Carried by BOTH push paths and alterable in place. */
61
+ readRoles?: string[];
62
+ }
63
+
64
+ /** One config-declared per-record ACTION button (cms.md §17): exactly ONE
65
+ * human-initiated step — pressing it invokes the deployed tenant function `fn`
66
+ * with `{ collection, item_id, action, actor, item }` and returns its result.
67
+ * No conditions, no chaining, no scheduling. `key` matches
68
+ * /^[a-z][a-z0-9_]{0,31}$/, `fn` is a function name (/^[a-z][a-z0-9-]{0,47}$/),
69
+ * ≤8 per collection. */
70
+ export interface CmsActionDef {
71
+ key: string;
72
+ label: string;
73
+ fn: string;
48
74
  }
49
75
 
50
76
  /** The CANONICAL field-attribute name set (config-side spelling), DERIVED from
@@ -87,18 +113,33 @@ export interface CollectionDef {
87
113
  * with a regression test on each — an attribute read by no enforcing path ships
88
114
  * inert (the validation.unique / owner_field precedent). */
89
115
  public?: boolean;
116
+ /** Per-record action buttons (cms.md §17): `[{ key, label, fn }]`, stored on
117
+ * the collection (model data, not a config leaf). The dashboard renders one
118
+ * button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
119
+ * :key` invokes `fn` once. Carried by BOTH push paths (create + reconcile on
120
+ * existing collections — config is the source of truth; `[]`/absent clears). */
121
+ actions?: CmsActionDef[];
90
122
  }
91
123
 
124
+ /** The auth lifecycle events an `authHook` binding may subscribe to (F4-30,
125
+ * auth wave 2026-09-10) — a CLOSED union; ONE event per binding, one audit
126
+ * event → one function invocation, no branching inside vxil:
127
+ * user.created — auth.user.created { user_id, method, is_anonymous }
128
+ * session.created — auth.session.created { user_id, session_id }
129
+ * session.revoked — auth.session.revoked { user_id, session_id, reason }
130
+ * signin.failure — auth.signin.failure { email_hash | user_id, reason } */
131
+ export type AuthHookEvent = 'user.created' | 'session.created' | 'session.revoked' | 'signin.failure';
132
+
92
133
  /** A function trigger (the §7.3 crossing). cmsHook fires on a CMS write;
93
- * authHook is the post-signup hook (fires on `auth.user.created`,
94
- * at-least-once, ~1min fanout latency). */
134
+ * authHook fires on ONE auth lifecycle event (default `user.created` — the
135
+ * post-signup hook; at-least-once, ~1min fanout latency). */
95
136
  export type FunctionTrigger =
96
137
  | { kind: 'http'; path?: string }
97
138
  | { kind: 'cron'; schedule: string }
98
139
  | { kind: 'queue'; source: string }
99
140
  | { kind: 'webhook'; source: string }
100
141
  | { kind: 'cmsHook'; collection: string; event: 'beforeCreate' | 'beforeUpdate' | 'beforeWrite' }
101
- | { kind: 'authHook'; event?: 'user.created' };
142
+ | { kind: 'authHook'; event?: AuthHookEvent };
102
143
 
103
144
  /** A deployed tenant function — source in functions/, deployed on `vxil push`. */
104
145
  export interface FunctionDef {