@vxil/cli 0.16.1 → 0.17.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/config.d.ts CHANGED
@@ -106,6 +106,21 @@ export interface CollectionDef {
106
106
  * with a regression test on each — an attribute read by no enforcing path ships
107
107
  * inert (the validation.unique / owner_field precedent). */
108
108
  public?: boolean;
109
+ /** What a VERIFIED end user (a thin-client key + a session) may do on this
110
+ * collection (https://vxil.com/docs/guide/09-security-and-multitenancy):
111
+ * 'readwrite' (default) — read and write (owner-scoped when ownerField is set);
112
+ * 'read' — read only (owner-scoped as above); every end-user write
113
+ * (create, update, $inc, delete, publish, transaction step,
114
+ * filtered delete, batch write — and a delete elsewhere
115
+ * whose on_delete cascade / set_null would change a row
116
+ * here) is `403 server_only`;
117
+ * 'none' — no end-user reads or writes at all, even with ownerField.
118
+ * Server keys (and platform writes such as a jobs status mirror) are never
119
+ * affected. Stored on the collection (model data, not a config leaf) and
120
+ * carried by BOTH push paths; widening ('read'/'none' → 'readwrite') needs
121
+ * `vxil push --allow-destructive`. Any other value fails `vxil plan` /
122
+ * `vxil push` (it never falls back to 'readwrite'). */
123
+ endUserAccess?: 'readwrite' | 'read' | 'none';
109
124
  /** Per-record action buttons (guide ch. 4): `[{ key, label, fn }]`, stored on
110
125
  * the collection (model data, not a config leaf). The dashboard renders one
111
126
  * button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
package/dist/vxil.js CHANGED
@@ -4543,9 +4543,9 @@ function lowerAllTriggerBindings(def, name) {
4543
4543
  return bindings;
4544
4544
  }
4545
4545
  var CONFIG_FILENAMES = ["vxil.config.ts", "vxil.config.mjs", "vxil.config.js"];
4546
- var VXIL_CONFIG_PKG_VERSION = "0.10.1";
4547
- var VXIL_SDK_PKG_VERSION = "0.16.1";
4548
- var VXIL_CLI_PKG_VERSION = "0.16.1";
4546
+ var VXIL_CONFIG_PKG_VERSION = "0.11.0";
4547
+ var VXIL_SDK_PKG_VERSION = "0.17.0";
4548
+ var VXIL_CLI_PKG_VERSION = "0.17.0";
4549
4549
  function ensureScaffoldPackageJson(cwd, opts = {}) {
4550
4550
  const file = resolve(cwd, "package.json");
4551
4551
  const wanted = {
@@ -8240,6 +8240,12 @@ function indexArmNote(rows) {
8240
8240
  if (rows === null) return "arms inline or through the re-index push runs (live row count unavailable)";
8241
8241
  return rows <= EQ_INDEX_INLINE_MAX_ROWS ? `arms inline (${rows} rows)` : `needs reindex (${rows} rows; push runs it)`;
8242
8242
  }
8243
+ function endUserAccessOf(v) {
8244
+ return v === "read" || v === "none" ? v : "readwrite";
8245
+ }
8246
+ function endUserAccessRank(m) {
8247
+ return m === "none" ? 0 : m === "read" ? 1 : 2;
8248
+ }
8243
8249
  function normalizeActions(raw) {
8244
8250
  if (!Array.isArray(raw)) return [];
8245
8251
  return raw.filter((a) => !!a && typeof a === "object").map((a) => ({ key: String(a.key), label: String(a.label), fn: String(a.fn) }));
@@ -8334,6 +8340,7 @@ async function readRemote2(api) {
8334
8340
  collection: c.collection,
8335
8341
  ownerField: c.owner_field ?? null,
8336
8342
  public: c.public === true,
8343
+ endUserAccess: endUserAccessOf(c.end_user_access),
8337
8344
  actions: normalizeActions(c.actions),
8338
8345
  fields: Array.isArray(c.fields) ? c.fields : []
8339
8346
  });
@@ -8382,6 +8389,12 @@ async function reindexCollection({ api, collection, field, onProgress }) {
8382
8389
  return { scanned, updated, pages, skipped, ...indexCertify ? { indexCertify } : {} };
8383
8390
  }
8384
8391
  async function planCmsSchema({ api, collections, apply, allowDestructive = false, onProgress }) {
8392
+ for (const [name, def] of Object.entries(collections)) {
8393
+ const v = def.endUserAccess;
8394
+ if (v !== void 0 && v !== "readwrite" && v !== "read" && v !== "none") {
8395
+ throw new Error(`cms.collections.${name}.endUserAccess must be 'readwrite' | 'read' | 'none' (got ${JSON.stringify(v)})`);
8396
+ }
8397
+ }
8385
8398
  const read = await readRemote2(api);
8386
8399
  if (read.unavailable) {
8387
8400
  if (apply) {
@@ -8526,6 +8539,23 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
8526
8539
  detail: `${name} public \u2192 ${declaredPublic} (keyless public delivery ${declaredPublic ? "ON" : "OFF"})`
8527
8540
  });
8528
8541
  }
8542
+ const declaredAccess = endUserAccessOf(def.endUserAccess);
8543
+ if (declaredAccess !== rc.endUserAccess) {
8544
+ const widens = endUserAccessRank(declaredAccess) > endUserAccessRank(rc.endUserAccess);
8545
+ if (!widens || allowDestructive) {
8546
+ collChanges.push({
8547
+ collection: name,
8548
+ kind: "set-end-user-access",
8549
+ detail: `${name} endUserAccess \u2192 '${declaredAccess}' (was '${rc.endUserAccess}')` + (widens ? " \u2014 WIDENS end-user access (full-config reconciliation)" : " (narrows end-user access)")
8550
+ });
8551
+ } else {
8552
+ collChanges.push({
8553
+ collection: name,
8554
+ kind: "warn",
8555
+ detail: `${name}: endUserAccess is '${rc.endUserAccess}' remotely but '${declaredAccess}' in config \u2014 that WIDENS what end users may do, so it is NOT applied. Declare endUserAccess: '${rc.endUserAccess}' to keep it, or re-run with --allow-destructive to widen.`
8556
+ });
8557
+ }
8558
+ }
8529
8559
  const declaredActions = normalizeActions(def.actions);
8530
8560
  if (stableStringify(declaredActions) !== stableStringify(rc.actions)) {
8531
8561
  collChanges.push({
@@ -8597,6 +8627,8 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
8597
8627
  if (def.singular) body.singular = def.singular;
8598
8628
  if (def.ownerField) body.owner_field = def.ownerField;
8599
8629
  if (def.public === true) body.public = true;
8630
+ const access = endUserAccessOf(def.endUserAccess);
8631
+ if (access !== "readwrite") body.end_user_access = access;
8600
8632
  if (def.actions && def.actions.length > 0) body.actions = normalizeActions(def.actions);
8601
8633
  const res = await api("POST", "/v1/cms/collections", body);
8602
8634
  if (res.status !== 201 && res.status !== 200 && res.body.error?.code !== "collection_exists") {
@@ -8636,6 +8668,14 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
8636
8668
  throw new Error(`cms: set public on ${c.collection} failed: ${e.code ?? res.status} ${e.message ?? ""}`);
8637
8669
  }
8638
8670
  applied++;
8671
+ } else if (c.kind === "set-end-user-access") {
8672
+ const target = endUserAccessOf(collections[c.collection].endUserAccess);
8673
+ const res = await api("POST", `/v1/cms/collections/${encodeURIComponent(c.collection)}/fields`, { end_user_access: target });
8674
+ if (res.status !== 200 && res.status !== 201) {
8675
+ const e = res.body.error ?? {};
8676
+ throw new Error(`cms: set endUserAccess on ${c.collection} failed: ${e.code ?? res.status} ${e.message ?? ""}`);
8677
+ }
8678
+ applied++;
8639
8679
  } else if (c.kind === "set-actions") {
8640
8680
  const target = normalizeActions(collections[c.collection].actions);
8641
8681
  const res = await api("POST", `/v1/cms/collections/${encodeURIComponent(c.collection)}/fields`, { actions: target });
@@ -8701,7 +8741,7 @@ function formatCmsChanges(changes) {
8701
8741
  if (c.kind === "destructive") return ` ! ${c.detail}`;
8702
8742
  if (c.kind === "warn") return ` \u26A0 ${c.detail}`;
8703
8743
  if (c.kind === "reindex") return ` \u21BB ${c.detail}`;
8704
- if (c.kind === "alter-flags" || c.kind === "set-owner-field") return ` ~ ${c.detail}`;
8744
+ if (c.kind === "alter-flags" || c.kind === "set-owner-field" || c.kind === "set-end-user-access") return ` ~ ${c.detail}`;
8705
8745
  return ` + ${c.detail}`;
8706
8746
  }).join("\n");
8707
8747
  }
@@ -12093,7 +12133,7 @@ var TOOLS = [
12093
12133
  {
12094
12134
  name: "cms_list_collections",
12095
12135
  feature: "cms",
12096
- description: "List the tenant's content model: collections with their typed field definitions (the schema items are validated against). Each field row carries its attributes \u2014 required, validation, index_slot, relation_to, computed/compute, is_unique, on_delete, read_roles (the end-user org roles allowed to READ that field; null = ungated), and on an equality-indexed field indexed: true + index_ready (equality filters on it are index-served once ready; until then, and on unindexed unslotted fields, they are a bounded scan \u2014 same results). A field with read_roles is omitted from end-user reads without a matching role and is never served on the public lane, so a missing key in an item's data may be a read gate rather than an unset value.",
12136
+ description: "List the tenant's content model: collections with their typed field definitions (the schema items are validated against). Each field row carries its attributes \u2014 required, validation, index_slot, relation_to, computed/compute, is_unique, on_delete, read_roles (the end-user org roles allowed to READ that field; null = ungated), and on an equality-indexed field indexed: true + index_ready (equality filters on it are index-served once ready; until then, and on unindexed unslotted fields, they are a bounded scan \u2014 same results). A field with read_roles is omitted from end-user reads without a matching role and is never served on the public lane, so a missing key in an item's data may be a read gate rather than an unset value. Each collection row also carries end_user_access: 'readwrite' (default), 'read' (a verified end user \u2014 a thin-client key with a session \u2014 may read but every write answers 403 server_only) or 'none' (end-user reads and writes are 403 server_only); server keys are never affected.",
12097
12137
  inputSchema: { type: "object", properties: {} },
12098
12138
  method: "GET",
12099
12139
  path: "/v1/cms/collections"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/cli",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "private": false,
5
5
  "description": "The vxil CLI — init, quickstart, push, gen, secrets, keys, migrate, doctor, diff, functions, payments; installs the `vxil` command (npm i -g @vxil/cli).",
6
6
  "license": "MIT",
@@ -46,9 +46,9 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "miniflare": "^4.20260611.0",
49
- "@vxil/config": "0.10.1",
50
- "@vxil/feature-configs": "0.9.1",
49
+ "@vxil/config": "0.11.0",
51
50
  "@vxil/runtime": "0.0.1",
51
+ "@vxil/feature-configs": "0.9.1",
52
52
  "@vxil/types": "0.0.1"
53
53
  },
54
54
  "scripts": {