@vxil/config 0.1.2 → 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
@@ -14,7 +14,64 @@ export interface FieldDef {
14
14
  relationTo?: string;
15
15
  computed?: boolean;
16
16
  compute?: unknown;
17
+ /** Claims-table-enforced value uniqueness across live items (cms.md §9.3):
18
+ * a duplicate write is a clean 409 unique_violation. Scalar types only
19
+ * (string/int/float/datetime/relation/file). Carried on collection/field
20
+ * CREATE by `vxil push` and server apply; since 2026-07-17 `vxil push` also
21
+ * RECONCILES it on an existing same-type field (in-place alter via the REST
22
+ * field re-add — packages/cli/src/cms.ts FIELD_ATTRS); attributes push
23
+ * cannot reconcile surface as a visible plan warning, never a silent no-op. */
24
+ unique?: boolean;
25
+ /** Per-relation-field delete behavior (cms.md §11): bounded, atomic fan-out
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[];
17
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;
54
+ }
55
+ /** The CANONICAL field-attribute name set (config-side spelling), DERIVED from
56
+ * `FieldDef` itself so it can never drift from the authoring type: it is every
57
+ * key of `FieldDef` except `type` (which is required, not an optional carried
58
+ * attribute). This is the compile-time source of truth for the config-carriage
59
+ * completeness ratchet (audit 2026-07-15e #2, roadmap §4.5 P3): the CLI carriage
60
+ * table (`packages/cli/src/cms.ts` FIELD_ATTRS) and the server one
61
+ * (`workers/control-plane/src/handlers/apply.ts` FIELD_ATTR_DELTA) each pin
62
+ * `keyof` coverage against a set that equals this — so a 9th attribute added to
63
+ * `FieldDef` WITHOUT threading it through BOTH push paths is a tsc error naming
64
+ * the uncovered key, not a silent half-carriage. */
65
+ export type FieldAttrName = Exclude<keyof FieldDef, 'type'>;
66
+ /** Compile-time exact-key closure helper: resolves to `Set` iff `Covered`
67
+ * covers EVERY member of `Required` (i.e. `Required` is assignable to
68
+ * `Covered`), else to the specific uncovered key literal(s) — so a `satisfies`
69
+ * against the covered literal union produces a tsc error naming the field the
70
+ * developer forgot to thread through the carriage table. Used by both push
71
+ * paths' carriage tables to gate additions to `FieldAttrName`. */
72
+ export type CoversFieldAttrs<Covered extends FieldAttrName> = [
73
+ FieldAttrName
74
+ ] extends [Covered] ? Covered : Exclude<FieldAttrName, Covered>;
18
75
  /** A CMS collection-as-code: collection slug → fields. */
19
76
  export interface CollectionDef {
20
77
  singular?: string;
@@ -23,13 +80,35 @@ export interface CollectionDef {
23
80
  * existing `string` field on this collection that holds the owner (end-user)
24
81
  * id. When set, the cms worker auto-scopes owned reads/writes to the VERIFIED
25
82
  * end-user principal in end-user mode (default-deny) — and is a no-op in
26
- * server-caller mode. NOT an RLS predicate; a single declarative flag the
27
- * typed worker consults. Omit for shared/reference collections. */
83
+ * server-caller mode. Not a hard-isolation predicate; a single declarative
84
+ * flag the typed worker consults. Omit for shared/reference collections. */
28
85
  ownerField?: string;
86
+ /** Public delivery (roadmap §4.4): when true, this collection's PUBLISHED items
87
+ * are servable through the keyless, edge-cached GET /v1/cms/public/:tenantId/
88
+ * :collection lane (no API key). Owner-UNSCOPED (public content, not per-user);
89
+ * the owner_field is stripped from every served row. Default false. Carried by
90
+ * BOTH push paths (the `vxil push` cms reconciler AND control-plane /v1/apply)
91
+ * with a regression test on each — an attribute read by no enforcing path ships
92
+ * inert (the validation.unique / owner_field precedent). */
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[];
29
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';
30
109
  /** A function trigger (the §7.3 crossing). cmsHook fires on a CMS write;
31
- * authHook is the post-signup hook (fires on `auth.user.created`,
32
- * 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). */
33
112
  export type FunctionTrigger = {
34
113
  kind: 'http';
35
114
  path?: string;
@@ -48,7 +127,7 @@ export type FunctionTrigger = {
48
127
  event: 'beforeCreate' | 'beforeUpdate' | 'beforeWrite';
49
128
  } | {
50
129
  kind: 'authHook';
51
- event?: 'user.created';
130
+ event?: AuthHookEvent;
52
131
  };
53
132
  /** A deployed tenant function — source in functions/, deployed on `vxil push`. */
54
133
  export interface FunctionDef {
@@ -77,7 +156,7 @@ export interface FunctionDef {
77
156
  }
78
157
  /** A secret REFERENCE (never a value). `vxil secrets set <name>` writes the value. */
79
158
  export interface SecretRef {
80
- /** which feature's KEK the value is envelope-encrypted under. */
159
+ /** which feature the value is encrypted at rest under. */
81
160
  feature: string;
82
161
  description?: string;
83
162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/config",
3
- "version": "0.1.2",
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.1.1"
25
+ "@vxil/feature-configs": "0.3.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"
package/src/index.ts CHANGED
@@ -33,8 +33,67 @@ export interface FieldDef {
33
33
  relationTo?: string;
34
34
  computed?: boolean;
35
35
  compute?: unknown;
36
+ /** Claims-table-enforced value uniqueness across live items (cms.md §9.3):
37
+ * a duplicate write is a clean 409 unique_violation. Scalar types only
38
+ * (string/int/float/datetime/relation/file). Carried on collection/field
39
+ * CREATE by `vxil push` and server apply; since 2026-07-17 `vxil push` also
40
+ * RECONCILES it on an existing same-type field (in-place alter via the REST
41
+ * field re-add — packages/cli/src/cms.ts FIELD_ATTRS); attributes push
42
+ * cannot reconcile surface as a visible plan warning, never a silent no-op. */
43
+ unique?: boolean;
44
+ /** Per-relation-field delete behavior (cms.md §11): bounded, atomic fan-out
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[];
36
62
  }
37
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;
74
+ }
75
+
76
+ /** The CANONICAL field-attribute name set (config-side spelling), DERIVED from
77
+ * `FieldDef` itself so it can never drift from the authoring type: it is every
78
+ * key of `FieldDef` except `type` (which is required, not an optional carried
79
+ * attribute). This is the compile-time source of truth for the config-carriage
80
+ * completeness ratchet (audit 2026-07-15e #2, roadmap §4.5 P3): the CLI carriage
81
+ * table (`packages/cli/src/cms.ts` FIELD_ATTRS) and the server one
82
+ * (`workers/control-plane/src/handlers/apply.ts` FIELD_ATTR_DELTA) each pin
83
+ * `keyof` coverage against a set that equals this — so a 9th attribute added to
84
+ * `FieldDef` WITHOUT threading it through BOTH push paths is a tsc error naming
85
+ * the uncovered key, not a silent half-carriage. */
86
+ export type FieldAttrName = Exclude<keyof FieldDef, 'type'>;
87
+
88
+ /** Compile-time exact-key closure helper: resolves to `Set` iff `Covered`
89
+ * covers EVERY member of `Required` (i.e. `Required` is assignable to
90
+ * `Covered`), else to the specific uncovered key literal(s) — so a `satisfies`
91
+ * against the covered literal union produces a tsc error naming the field the
92
+ * developer forgot to thread through the carriage table. Used by both push
93
+ * paths' carriage tables to gate additions to `FieldAttrName`. */
94
+ export type CoversFieldAttrs<Covered extends FieldAttrName> =
95
+ [FieldAttrName] extends [Covered] ? Covered : Exclude<FieldAttrName, Covered>;
96
+
38
97
  /** A CMS collection-as-code: collection slug → fields. */
39
98
  export interface CollectionDef {
40
99
  singular?: string;
@@ -43,21 +102,44 @@ export interface CollectionDef {
43
102
  * existing `string` field on this collection that holds the owner (end-user)
44
103
  * id. When set, the cms worker auto-scopes owned reads/writes to the VERIFIED
45
104
  * end-user principal in end-user mode (default-deny) — and is a no-op in
46
- * server-caller mode. NOT an RLS predicate; a single declarative flag the
47
- * typed worker consults. Omit for shared/reference collections. */
105
+ * server-caller mode. Not a hard-isolation predicate; a single declarative
106
+ * flag the typed worker consults. Omit for shared/reference collections. */
48
107
  ownerField?: string;
108
+ /** Public delivery (roadmap §4.4): when true, this collection's PUBLISHED items
109
+ * are servable through the keyless, edge-cached GET /v1/cms/public/:tenantId/
110
+ * :collection lane (no API key). Owner-UNSCOPED (public content, not per-user);
111
+ * the owner_field is stripped from every served row. Default false. Carried by
112
+ * BOTH push paths (the `vxil push` cms reconciler AND control-plane /v1/apply)
113
+ * with a regression test on each — an attribute read by no enforcing path ships
114
+ * inert (the validation.unique / owner_field precedent). */
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[];
49
122
  }
50
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
+
51
133
  /** A function trigger (the §7.3 crossing). cmsHook fires on a CMS write;
52
- * authHook is the post-signup hook (fires on `auth.user.created`,
53
- * 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). */
54
136
  export type FunctionTrigger =
55
137
  | { kind: 'http'; path?: string }
56
138
  | { kind: 'cron'; schedule: string }
57
139
  | { kind: 'queue'; source: string }
58
140
  | { kind: 'webhook'; source: string }
59
141
  | { kind: 'cmsHook'; collection: string; event: 'beforeCreate' | 'beforeUpdate' | 'beforeWrite' }
60
- | { kind: 'authHook'; event?: 'user.created' };
142
+ | { kind: 'authHook'; event?: AuthHookEvent };
61
143
 
62
144
  /** A deployed tenant function — source in functions/, deployed on `vxil push`. */
63
145
  export interface FunctionDef {
@@ -80,7 +162,7 @@ export interface FunctionDef {
80
162
 
81
163
  /** A secret REFERENCE (never a value). `vxil secrets set <name>` writes the value. */
82
164
  export interface SecretRef {
83
- /** which feature's KEK the value is envelope-encrypted under. */
165
+ /** which feature the value is encrypted at rest under. */
84
166
  feature: string;
85
167
  description?: string;
86
168
  }