@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 +15 -0
- package/dist/vxil.js +45 -5
- package/package.json +3 -3
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.
|
|
4547
|
-
var VXIL_SDK_PKG_VERSION = "0.
|
|
4548
|
-
var VXIL_CLI_PKG_VERSION = "0.
|
|
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.
|
|
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.
|
|
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": {
|