@objectstack/lint 14.7.0 → 15.0.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.cjs +145 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -23
- package/dist/index.d.ts +100 -23
- package/dist/index.js +142 -25
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1193,10 +1193,121 @@ function validateFormLayout(stack) {
|
|
|
1193
1193
|
return findings;
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
1196
|
+
// src/validate-visibility-predicates.ts
|
|
1197
|
+
var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
|
|
1198
|
+
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
1199
|
+
var CANONICAL = "visibleWhen";
|
|
1200
|
+
var ALIASES = ["visibleOn", "visibility"];
|
|
1201
|
+
function asArray12(v) {
|
|
1202
|
+
if (Array.isArray(v)) return v;
|
|
1203
|
+
if (v && typeof v === "object") {
|
|
1204
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
1205
|
+
}
|
|
1206
|
+
return [];
|
|
1207
|
+
}
|
|
1208
|
+
function predicateSource(v) {
|
|
1209
|
+
if (typeof v === "string") return v;
|
|
1210
|
+
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
1211
|
+
return v.source;
|
|
1212
|
+
}
|
|
1213
|
+
return void 0;
|
|
1214
|
+
}
|
|
1215
|
+
function usesRoot(source, root) {
|
|
1216
|
+
return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
|
|
1217
|
+
}
|
|
1218
|
+
var MISLAYER_BY_LAYER = {
|
|
1219
|
+
runtime: {
|
|
1220
|
+
forbiddenRoot: "data",
|
|
1221
|
+
message: "visibility predicate is rooted at `data.` \u2014 that is the metadata-editing-form root (a `*.form.ts` row under edit), not a runtime surface. A runtime view/page predicate that binds `data.` never matches and the element renders unconditionally (ADR-0089).",
|
|
1222
|
+
hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
|
|
1223
|
+
},
|
|
1224
|
+
metadata: {
|
|
1225
|
+
forbiddenRoot: "record",
|
|
1226
|
+
message: "visibility predicate is rooted at `record.` \u2014 that is the runtime record-surface root (a `*.view.ts` / `*.page.ts` live record), not a metadata-editing form. A `*.form.ts` predicate that binds `record.` never matches and the element renders unconditionally (ADR-0089).",
|
|
1227
|
+
hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
function checkElement(el, where, path, layer, findings) {
|
|
1231
|
+
for (const alias of ALIASES) {
|
|
1232
|
+
if (el[alias] !== void 0) {
|
|
1233
|
+
findings.push({
|
|
1234
|
+
severity: "warning",
|
|
1235
|
+
rule: VISIBILITY_ALIAS_DEPRECATED,
|
|
1236
|
+
where,
|
|
1237
|
+
path: `${path}.${alias}`,
|
|
1238
|
+
message: `\`${alias}\` is the deprecated spelling of the conditional-visibility predicate (ADR-0089). It still works \u2014 it is normalized to \`visibleWhen\` at parse \u2014 but the canonical key is \`visibleWhen\`.`,
|
|
1239
|
+
hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
1244
|
+
const source = predicateSource(raw);
|
|
1245
|
+
const rule = MISLAYER_BY_LAYER[layer];
|
|
1246
|
+
if (source && usesRoot(source, rule.forbiddenRoot)) {
|
|
1247
|
+
findings.push({
|
|
1248
|
+
severity: "warning",
|
|
1249
|
+
rule: VISIBILITY_ROOT_MISLAYERED,
|
|
1250
|
+
where,
|
|
1251
|
+
path,
|
|
1252
|
+
message: rule.message,
|
|
1253
|
+
hint: rule.hint
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function isFieldObject(entry) {
|
|
1258
|
+
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
1259
|
+
}
|
|
1260
|
+
function validateVisibilityPredicates(stack, opts = {}) {
|
|
1261
|
+
const layer = opts.layer ?? "runtime";
|
|
1262
|
+
const findings = [];
|
|
1263
|
+
const views = asArray12(stack.views);
|
|
1264
|
+
for (let i = 0; i < views.length; i++) {
|
|
1265
|
+
const view = views[i];
|
|
1266
|
+
if (!view || typeof view !== "object") continue;
|
|
1267
|
+
const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
|
|
1268
|
+
const where = `view "${viewName}"`;
|
|
1269
|
+
for (const bucket of ["sections", "groups"]) {
|
|
1270
|
+
const sections = Array.isArray(view[bucket]) ? view[bucket] : [];
|
|
1271
|
+
for (let s = 0; s < sections.length; s++) {
|
|
1272
|
+
const sec = sections[s];
|
|
1273
|
+
if (!sec || typeof sec !== "object") continue;
|
|
1274
|
+
const secPath = `views[${i}].${bucket}[${s}]`;
|
|
1275
|
+
checkElement(sec, where, secPath, layer, findings);
|
|
1276
|
+
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
1277
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
1278
|
+
const entry = secFields[f];
|
|
1279
|
+
if (isFieldObject(entry)) {
|
|
1280
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const pages = asArray12(stack.pages);
|
|
1287
|
+
for (let i = 0; i < pages.length; i++) {
|
|
1288
|
+
const page = pages[i];
|
|
1289
|
+
if (!page || typeof page !== "object") continue;
|
|
1290
|
+
const pageName = typeof page.name === "string" ? page.name : `(page ${i})`;
|
|
1291
|
+
const where = `page "${pageName}"`;
|
|
1292
|
+
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1293
|
+
for (let r = 0; r < regions.length; r++) {
|
|
1294
|
+
const region = regions[r];
|
|
1295
|
+
const components = region && typeof region === "object" && Array.isArray(region.components) ? region.components : [];
|
|
1296
|
+
for (let c = 0; c < components.length; c++) {
|
|
1297
|
+
const comp = components[c];
|
|
1298
|
+
if (comp && typeof comp === "object") {
|
|
1299
|
+
checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, layer, findings);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return findings;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1196
1307
|
// src/validate-capability-references.ts
|
|
1197
1308
|
import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
|
|
1198
1309
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
1199
|
-
function
|
|
1310
|
+
function asArray13(v) {
|
|
1200
1311
|
if (Array.isArray(v)) return v;
|
|
1201
1312
|
if (v && typeof v === "object") {
|
|
1202
1313
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1221,17 +1332,20 @@ function validateCapabilityReferences(stack) {
|
|
|
1221
1332
|
const findings = [];
|
|
1222
1333
|
if (!stack || typeof stack !== "object") return findings;
|
|
1223
1334
|
const known = new Set(PLATFORM_CAPABILITY_NAMES);
|
|
1224
|
-
for (const
|
|
1335
|
+
for (const cap of asArray13(stack.capabilities)) {
|
|
1336
|
+
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
1337
|
+
}
|
|
1338
|
+
for (const ps of asArray13(stack.permissions)) {
|
|
1225
1339
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
1226
1340
|
}
|
|
1227
|
-
for (const seed of
|
|
1341
|
+
for (const seed of asArray13(stack.data)) {
|
|
1228
1342
|
if (seed.object !== "sys_capability") continue;
|
|
1229
1343
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
1230
1344
|
const name = rec?.name;
|
|
1231
1345
|
if (typeof name === "string" && name.length > 0) known.add(name);
|
|
1232
1346
|
}
|
|
1233
1347
|
}
|
|
1234
|
-
const hint = "Fix the capability name, declare it on a permission set\u2019s systemPermissions, ship a sys_capability seed row, or ignore this if the capability is provided by another installed package (references fail closed at runtime).";
|
|
1348
|
+
const hint = "Fix the capability name, define it with defineCapability (stack.capabilities), declare it on a permission set\u2019s systemPermissions, ship a sys_capability seed row, or ignore this if the capability is provided by another installed package (references fail closed at runtime).";
|
|
1235
1349
|
const flag = (cap, where, path) => {
|
|
1236
1350
|
if (known.has(cap)) return;
|
|
1237
1351
|
findings.push({
|
|
@@ -1243,7 +1357,7 @@ function validateCapabilityReferences(stack) {
|
|
|
1243
1357
|
hint
|
|
1244
1358
|
});
|
|
1245
1359
|
};
|
|
1246
|
-
const objects =
|
|
1360
|
+
const objects = asArray13(stack.objects);
|
|
1247
1361
|
for (let i = 0; i < objects.length; i++) {
|
|
1248
1362
|
const obj = objects[i];
|
|
1249
1363
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -1252,27 +1366,27 @@ function validateCapabilityReferences(stack) {
|
|
|
1252
1366
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
1253
1367
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
1254
1368
|
}
|
|
1255
|
-
const fields =
|
|
1369
|
+
const fields = asArray13(obj.fields);
|
|
1256
1370
|
for (const f of fields) {
|
|
1257
1371
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
1258
1372
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
1259
1373
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
1260
1374
|
}
|
|
1261
1375
|
}
|
|
1262
|
-
for (const [ai, action] of
|
|
1376
|
+
for (const [ai, action] of asArray13(obj.actions).entries()) {
|
|
1263
1377
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
1264
1378
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
1265
1379
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
1266
1380
|
}
|
|
1267
1381
|
}
|
|
1268
1382
|
}
|
|
1269
|
-
for (const [i, action] of
|
|
1383
|
+
for (const [i, action] of asArray13(stack.actions).entries()) {
|
|
1270
1384
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
1271
1385
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
1272
1386
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
1273
1387
|
}
|
|
1274
1388
|
}
|
|
1275
|
-
const apps =
|
|
1389
|
+
const apps = asArray13(stack.apps);
|
|
1276
1390
|
for (let i = 0; i < apps.length; i++) {
|
|
1277
1391
|
const app = apps[i];
|
|
1278
1392
|
if (!app || typeof app !== "object") continue;
|
|
@@ -1308,7 +1422,7 @@ var TYPE_FIX = {
|
|
|
1308
1422
|
business_unit: "department",
|
|
1309
1423
|
bu: "department"
|
|
1310
1424
|
};
|
|
1311
|
-
function
|
|
1425
|
+
function asArray14(v) {
|
|
1312
1426
|
if (Array.isArray(v)) return v;
|
|
1313
1427
|
if (v && typeof v === "object") {
|
|
1314
1428
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1318,7 +1432,7 @@ function asArray13(v) {
|
|
|
1318
1432
|
function validateApprovalApprovers(stack) {
|
|
1319
1433
|
const findings = [];
|
|
1320
1434
|
if (!stack || typeof stack !== "object") return findings;
|
|
1321
|
-
const flows =
|
|
1435
|
+
const flows = asArray14(stack.flows);
|
|
1322
1436
|
const validTypes = new Set(ApproverType.options);
|
|
1323
1437
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
1324
1438
|
const flow = flows[fi];
|
|
@@ -1406,7 +1520,7 @@ var OWD_WIDTH = {
|
|
|
1406
1520
|
public_read: 1,
|
|
1407
1521
|
public_read_write: 2
|
|
1408
1522
|
};
|
|
1409
|
-
function
|
|
1523
|
+
function asArray15(v) {
|
|
1410
1524
|
if (Array.isArray(v)) return v;
|
|
1411
1525
|
if (v && typeof v === "object") {
|
|
1412
1526
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1432,7 +1546,7 @@ function refOf(def) {
|
|
|
1432
1546
|
return typeof r === "string" && r ? r : void 0;
|
|
1433
1547
|
}
|
|
1434
1548
|
function firstMasterDetailField(obj) {
|
|
1435
|
-
for (const f of
|
|
1549
|
+
for (const f of asArray15(obj.fields)) {
|
|
1436
1550
|
if (f.type === "master_detail") {
|
|
1437
1551
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
1438
1552
|
}
|
|
@@ -1445,8 +1559,8 @@ function grantsObjectAccess(p) {
|
|
|
1445
1559
|
function validateSecurityPosture(stack, opts) {
|
|
1446
1560
|
const findings = [];
|
|
1447
1561
|
if (!stack || typeof stack !== "object") return findings;
|
|
1448
|
-
const objects =
|
|
1449
|
-
const permissionSets =
|
|
1562
|
+
const objects = asArray15(stack.objects);
|
|
1563
|
+
const permissionSets = asArray15(stack.permissions);
|
|
1450
1564
|
for (let i = 0; i < objects.length; i++) {
|
|
1451
1565
|
const obj = objects[i];
|
|
1452
1566
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -1575,10 +1689,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
1575
1689
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
1576
1690
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
1577
1691
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
1578
|
-
for (const f of
|
|
1692
|
+
for (const f of asArray15(obj.fields)) {
|
|
1579
1693
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
1580
1694
|
}
|
|
1581
|
-
for (const [ai, action] of
|
|
1695
|
+
for (const [ai, action] of asArray15(obj.actions).entries()) {
|
|
1582
1696
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
1583
1697
|
}
|
|
1584
1698
|
}
|
|
@@ -1587,19 +1701,19 @@ function validateSecurityPosture(stack, opts) {
|
|
|
1587
1701
|
if (!ps || typeof ps !== "object") continue;
|
|
1588
1702
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
1589
1703
|
}
|
|
1590
|
-
for (const [i, pos] of
|
|
1704
|
+
for (const [i, pos] of asArray15(stack.positions).entries()) {
|
|
1591
1705
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
1592
1706
|
}
|
|
1593
|
-
for (const [i, app] of
|
|
1707
|
+
for (const [i, app] of asArray15(stack.apps).entries()) {
|
|
1594
1708
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
1595
1709
|
}
|
|
1596
|
-
for (const [i, book] of
|
|
1710
|
+
for (const [i, book] of asArray15(stack.books).entries()) {
|
|
1597
1711
|
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
1598
1712
|
}
|
|
1599
1713
|
const stackSetNames = new Set(
|
|
1600
1714
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
1601
1715
|
);
|
|
1602
|
-
for (const [i, book] of
|
|
1716
|
+
for (const [i, book] of asArray15(stack.books).entries()) {
|
|
1603
1717
|
const audience = book.audience;
|
|
1604
1718
|
if (!audience || typeof audience !== "object") continue;
|
|
1605
1719
|
const setName = audience.permissionSet;
|
|
@@ -1677,7 +1791,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
1677
1791
|
}
|
|
1678
1792
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
1679
1793
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
1680
|
-
for (const [i, seed] of
|
|
1794
|
+
for (const [i, seed] of asArray15(stack.data).entries()) {
|
|
1681
1795
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
1682
1796
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
1683
1797
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -1718,7 +1832,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
1718
1832
|
}
|
|
1719
1833
|
|
|
1720
1834
|
// src/build-access-matrix.ts
|
|
1721
|
-
function
|
|
1835
|
+
function asArray16(v) {
|
|
1722
1836
|
if (Array.isArray(v)) return v;
|
|
1723
1837
|
if (v && typeof v === "object") {
|
|
1724
1838
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1729,13 +1843,13 @@ function buildAccessMatrix(stack) {
|
|
|
1729
1843
|
const entries = [];
|
|
1730
1844
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
1731
1845
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
1732
|
-
for (const obj of
|
|
1846
|
+
for (const obj of asArray16(stack.objects)) {
|
|
1733
1847
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
1734
1848
|
if (!name) continue;
|
|
1735
1849
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
1736
1850
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
1737
1851
|
}
|
|
1738
|
-
for (const ps of
|
|
1852
|
+
for (const ps of asArray16(stack.permissions)) {
|
|
1739
1853
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
1740
1854
|
if (!psName) continue;
|
|
1741
1855
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -1839,6 +1953,8 @@ export {
|
|
|
1839
1953
|
TABLE_COUNT_ONLY,
|
|
1840
1954
|
TITLE_FORMAT_RETIRED,
|
|
1841
1955
|
TITLE_UNRESOLVABLE,
|
|
1956
|
+
VISIBILITY_ALIAS_DEPRECATED,
|
|
1957
|
+
VISIBILITY_ROOT_MISLAYERED,
|
|
1842
1958
|
WIDGET_DATASET_UNKNOWN,
|
|
1843
1959
|
WIDGET_DIMENSION_UNKNOWN,
|
|
1844
1960
|
WIDGET_MEASURE_UNKNOWN,
|
|
@@ -1857,6 +1973,7 @@ export {
|
|
|
1857
1973
|
validateSecurityPosture,
|
|
1858
1974
|
validateSemanticRoles,
|
|
1859
1975
|
validateStackExpressions,
|
|
1976
|
+
validateVisibilityPredicates,
|
|
1860
1977
|
validateWidgetBindings
|
|
1861
1978
|
};
|
|
1862
1979
|
//# sourceMappingURL=index.js.map
|