@objectstack/lint 12.5.0 → 13.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 CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ CAPABILITY_REFERENCE_UNKNOWN: () => CAPABILITY_REFERENCE_UNKNOWN,
23
24
  CHART_CONFIG_MISSING: () => CHART_CONFIG_MISSING,
24
25
  CHART_FIELD_UNKNOWN: () => CHART_FIELD_UNKNOWN,
25
26
  FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
@@ -29,6 +30,13 @@ __export(index_exports, {
29
30
  LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
30
31
  MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
31
32
  PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
33
+ SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
34
+ SECURITY_EXTERNAL_WIDER: () => SECURITY_EXTERNAL_WIDER,
35
+ SECURITY_OWD_ALIAS: () => SECURITY_OWD_ALIAS,
36
+ SECURITY_OWD_UNSET: () => SECURITY_OWD_UNSET,
37
+ SECURITY_PRIVATE_NO_READSCOPE: () => SECURITY_PRIVATE_NO_READSCOPE,
38
+ SECURITY_ROLE_WORD: () => SECURITY_ROLE_WORD,
39
+ SECURITY_WILDCARD_VAMA: () => SECURITY_WILDCARD_VAMA,
32
40
  SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
33
41
  STYLE_CLASSNAME_TAILWIND: () => STYLE_CLASSNAME_TAILWIND,
34
42
  STYLE_NODE_MISSING_ID: () => STYLE_NODE_MISSING_ID,
@@ -41,6 +49,9 @@ __export(index_exports, {
41
49
  WIDGET_DATASET_UNKNOWN: () => WIDGET_DATASET_UNKNOWN,
42
50
  WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
43
51
  WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
52
+ buildAccessMatrix: () => buildAccessMatrix,
53
+ diffAccessMatrix: () => diffAccessMatrix,
54
+ validateCapabilityReferences: () => validateCapabilityReferences,
44
55
  validateFormLayout: () => validateFormLayout,
45
56
  validateJsxPages: () => validateJsxPages,
46
57
  validateListViewMode: () => validateListViewMode,
@@ -49,6 +60,7 @@ __export(index_exports, {
49
60
  validateReactPages: () => validateReactPages,
50
61
  validateRecordTitle: () => validateRecordTitle,
51
62
  validateResponsiveStyles: () => validateResponsiveStyles,
63
+ validateSecurityPosture: () => validateSecurityPosture,
52
64
  validateSemanticRoles: () => validateSemanticRoles,
53
65
  validateStackExpressions: () => validateStackExpressions,
54
66
  validateWidgetBindings: () => validateWidgetBindings
@@ -430,7 +442,6 @@ function validateStackExpressions(stack) {
430
442
 
431
443
  // src/validate-list-view-mode.ts
432
444
  var LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
433
- var FORBIDDEN_FIELDS = ["userFilters", "quickFilters"];
434
445
  function asArray3(v) {
435
446
  if (Array.isArray(v)) return v;
436
447
  if (v && typeof v === "object") {
@@ -444,17 +455,30 @@ function asArray3(v) {
444
455
  function scanView(view, where, path, out) {
445
456
  if (!view || typeof view !== "object") return;
446
457
  const rec = view;
447
- for (const field of FORBIDDEN_FIELDS) {
448
- if (rec[field] == null) continue;
458
+ if (rec.quickFilters != null) {
449
459
  out.push({
450
460
  severity: "error",
451
461
  rule: LIST_VIEW_FILTERS_IN_VIEWS_MODE,
452
462
  where,
453
- path: `${path}.${field}`,
454
- message: `\`${field}\` is a page filters-mode control and is ignored on an object list view ("views" mode) \u2014 the ViewTabBar is the only nav control here.`,
455
- hint: `Move \`${field}\` to a page list (InterfaceListPage, "filters" mode), or remove it. See ADR-0053.`
463
+ path: `${path}.quickFilters`,
464
+ message: '`quickFilters` is a page filters-mode control and is ignored on an object list view ("views" mode) \u2014 the ViewTabBar owns nav here.',
465
+ hint: 'Move `quickFilters` to a page list (InterfaceListPage, "filters" mode), or remove it. See ADR-0047.'
456
466
  });
457
467
  }
468
+ const uf = rec.userFilters;
469
+ if (uf && typeof uf === "object") {
470
+ const ufRec = uf;
471
+ if (ufRec.element === "tabs" || ufRec.tabs != null) {
472
+ out.push({
473
+ severity: "error",
474
+ rule: LIST_VIEW_FILTERS_IN_VIEWS_MODE,
475
+ where,
476
+ path: `${path}.userFilters`,
477
+ message: '`userFilters` with `element: "tabs"` is page-only and is ignored on an object list view ("views" mode) \u2014 it would collide with the ViewTabBar.',
478
+ hint: 'Use `listViews` for named presets on an object (each becomes a segmented tab), switch to `element: "dropdown"` for value chips, or move the `tabs` filter to a page list (InterfaceListPage, "filters" mode). See ADR-0047.'
479
+ });
480
+ }
481
+ }
458
482
  }
459
483
  function scanListViews(listViews, wherePrefix, pathPrefix, out) {
460
484
  if (!listViews || typeof listViews !== "object") return;
@@ -1239,8 +1263,415 @@ function validateFormLayout(stack) {
1239
1263
  }
1240
1264
  return findings;
1241
1265
  }
1266
+
1267
+ // src/validate-capability-references.ts
1268
+ var import_security = require("@objectstack/spec/security");
1269
+ var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
1270
+ function asArray12(v) {
1271
+ if (Array.isArray(v)) return v;
1272
+ if (v && typeof v === "object") {
1273
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1274
+ }
1275
+ return [];
1276
+ }
1277
+ function asCapArray(v) {
1278
+ return Array.isArray(v) ? v.filter((s) => typeof s === "string" && s.length > 0) : [];
1279
+ }
1280
+ function flattenObjectRequired(v) {
1281
+ if (Array.isArray(v)) return asCapArray(v).map((cap) => ({ cap }));
1282
+ if (v && typeof v === "object") {
1283
+ const out = [];
1284
+ for (const [key, val] of Object.entries(v)) {
1285
+ for (const cap of asCapArray(val)) out.push({ cap, key });
1286
+ }
1287
+ return out;
1288
+ }
1289
+ return [];
1290
+ }
1291
+ function validateCapabilityReferences(stack) {
1292
+ const findings = [];
1293
+ if (!stack || typeof stack !== "object") return findings;
1294
+ const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
1295
+ for (const ps of asArray12(stack.permissions)) {
1296
+ for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
1297
+ }
1298
+ for (const seed of asArray12(stack.data)) {
1299
+ if (seed.object !== "sys_capability") continue;
1300
+ for (const rec of Array.isArray(seed.records) ? seed.records : []) {
1301
+ const name = rec?.name;
1302
+ if (typeof name === "string" && name.length > 0) known.add(name);
1303
+ }
1304
+ }
1305
+ 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).";
1306
+ const flag = (cap, where, path) => {
1307
+ if (known.has(cap)) return;
1308
+ findings.push({
1309
+ severity: "warning",
1310
+ rule: CAPABILITY_REFERENCE_UNKNOWN,
1311
+ where,
1312
+ path,
1313
+ message: `requiredPermissions references capability "${cap}" which is registered nowhere \u2014 no built-in capability, no permission set in this package grants it via systemPermissions, and no sys_capability seed declares it`,
1314
+ hint
1315
+ });
1316
+ };
1317
+ const objects = asArray12(stack.objects);
1318
+ for (let i = 0; i < objects.length; i++) {
1319
+ const obj = objects[i];
1320
+ if (!obj || typeof obj !== "object") continue;
1321
+ const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
1322
+ const objPath = `objects[${i}]`;
1323
+ for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
1324
+ flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
1325
+ }
1326
+ const fields = asArray12(obj.fields);
1327
+ for (const f of fields) {
1328
+ const fname = typeof f.name === "string" ? f.name : "(field)";
1329
+ for (const cap of asCapArray(f.requiredPermissions)) {
1330
+ flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
1331
+ }
1332
+ }
1333
+ for (const [ai, action] of asArray12(obj.actions).entries()) {
1334
+ const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
1335
+ for (const cap of asCapArray(action.requiredPermissions)) {
1336
+ flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
1337
+ }
1338
+ }
1339
+ }
1340
+ for (const [i, action] of asArray12(stack.actions).entries()) {
1341
+ const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
1342
+ for (const cap of asCapArray(action.requiredPermissions)) {
1343
+ flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
1344
+ }
1345
+ }
1346
+ const apps = asArray12(stack.apps);
1347
+ for (let i = 0; i < apps.length; i++) {
1348
+ const app = apps[i];
1349
+ if (!app || typeof app !== "object") continue;
1350
+ const appName = typeof app.name === "string" ? app.name : `(app ${i})`;
1351
+ const walk = (node, path) => {
1352
+ if (!node || typeof node !== "object") return;
1353
+ if (Array.isArray(node)) {
1354
+ node.forEach((child, ci) => walk(child, `${path}[${ci}]`));
1355
+ return;
1356
+ }
1357
+ const rec = node;
1358
+ for (const cap of asCapArray(rec.requiredPermissions)) {
1359
+ flag(cap, `app "${appName}"`, `${path}.requiredPermissions`);
1360
+ }
1361
+ if (rec.navigation) walk(rec.navigation, `${path}.navigation`);
1362
+ if (rec.areas) walk(rec.areas, `${path}.areas`);
1363
+ if (rec.tabs) walk(rec.tabs, `${path}.tabs`);
1364
+ if (rec.children) walk(rec.children, `${path}.children`);
1365
+ if (rec.items) walk(rec.items, `${path}.items`);
1366
+ };
1367
+ walk(app, `apps[${i}]`);
1368
+ }
1369
+ return findings;
1370
+ }
1371
+
1372
+ // src/validate-security-posture.ts
1373
+ var import_security2 = require("@objectstack/spec/security");
1374
+ var SECURITY_OWD_UNSET = "security-owd-unset";
1375
+ var SECURITY_OWD_ALIAS = "security-owd-alias";
1376
+ var SECURITY_EXTERNAL_WIDER = "security-external-wider-than-internal";
1377
+ var SECURITY_WILDCARD_VAMA = "security-wildcard-vama";
1378
+ var SECURITY_ANCHOR_HIGH_PRIVILEGE = "security-anchor-high-privilege";
1379
+ var SECURITY_ROLE_WORD = "security-role-word";
1380
+ var SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
1381
+ var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
1382
+ var OWD_ALIAS_FIX = {
1383
+ read: "public_read",
1384
+ read_write: "public_read_write",
1385
+ full: "public_read_write",
1386
+ public: "public_read_write"
1387
+ };
1388
+ var OWD_WIDTH = {
1389
+ private: 0,
1390
+ public_read: 1,
1391
+ public_read_write: 2
1392
+ };
1393
+ function asArray13(v) {
1394
+ if (Array.isArray(v)) return v;
1395
+ if (v && typeof v === "object") {
1396
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1397
+ }
1398
+ return [];
1399
+ }
1400
+ function owdOf(obj) {
1401
+ return obj.sharingModel ?? obj.security?.sharingModel;
1402
+ }
1403
+ function isSystemObject(obj) {
1404
+ return obj.isSystem === true || String(obj.name ?? "").startsWith("sys_");
1405
+ }
1406
+ function identifierHasRoleToken(name) {
1407
+ if (typeof name !== "string") return false;
1408
+ return name.toLowerCase().split(/[^a-z0-9]+/).some((tok) => tok === "role" || tok === "roles");
1409
+ }
1410
+ function labelHasRoleWord(label) {
1411
+ if (typeof label !== "string") return false;
1412
+ return /\brole(s)?\b/i.test(label);
1413
+ }
1414
+ function validateSecurityPosture(stack) {
1415
+ const findings = [];
1416
+ if (!stack || typeof stack !== "object") return findings;
1417
+ const objects = asArray13(stack.objects);
1418
+ const permissionSets = asArray13(stack.permissions);
1419
+ for (let i = 0; i < objects.length; i++) {
1420
+ const obj = objects[i];
1421
+ if (!obj || typeof obj !== "object") continue;
1422
+ const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
1423
+ const objPath = `objects[${i}]`;
1424
+ const owd = owdOf(obj);
1425
+ const external = obj.externalSharingModel;
1426
+ if (!isSystemObject(obj)) {
1427
+ if (owd == null) {
1428
+ findings.push({
1429
+ severity: "error",
1430
+ rule: SECURITY_OWD_UNSET,
1431
+ where: `object "${objName}"`,
1432
+ path: `${objPath}.sharingModel`,
1433
+ message: `custom object "${objName}" declares no sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline must be an authored decision, not an accident \u2014 this is the exact shape of the leave_request incident (objectui#2348).`,
1434
+ hint: `Declare sharingModel explicitly: 'private' (owner + shares; recommended default), 'public_read', 'public_read_write', or 'controlled_by_parent' (master-detail children).`
1435
+ });
1436
+ } else if (typeof owd === "string" && OWD_ALIAS_FIX[owd]) {
1437
+ findings.push({
1438
+ severity: "error",
1439
+ rule: SECURITY_OWD_ALIAS,
1440
+ where: `object "${objName}"`,
1441
+ path: `${objPath}.sharingModel`,
1442
+ message: `sharingModel '${owd}' is a retired alias (ADR-0090 D4). The runtime fails CLOSED to 'private' on unknown values, so this object is NOT ${owd === "read" ? "readable" : "writable"} org-wide.`,
1443
+ hint: `Replace with the canonical value: sharingModel: '${OWD_ALIAS_FIX[owd]}'.`
1444
+ });
1445
+ } else if (typeof owd === "string" && !CANONICAL_OWD.includes(owd)) {
1446
+ findings.push({
1447
+ severity: "error",
1448
+ rule: SECURITY_OWD_ALIAS,
1449
+ where: `object "${objName}"`,
1450
+ path: `${objPath}.sharingModel`,
1451
+ message: `sharingModel '${owd}' is not a canonical OWD value; the runtime fails CLOSED to 'private'.`,
1452
+ hint: `Use one of: ${CANONICAL_OWD.join(", ")}.`
1453
+ });
1454
+ }
1455
+ }
1456
+ if (typeof external === "string") {
1457
+ if (OWD_ALIAS_FIX[external]) {
1458
+ findings.push({
1459
+ severity: "error",
1460
+ rule: SECURITY_OWD_ALIAS,
1461
+ where: `object "${objName}"`,
1462
+ path: `${objPath}.externalSharingModel`,
1463
+ message: `externalSharingModel '${external}' is a retired alias (ADR-0090 D4).`,
1464
+ hint: `Replace with the canonical value: externalSharingModel: '${OWD_ALIAS_FIX[external]}'.`
1465
+ });
1466
+ } else if (typeof owd === "string" && external in OWD_WIDTH && owd in OWD_WIDTH && OWD_WIDTH[external] > OWD_WIDTH[owd]) {
1467
+ findings.push({
1468
+ severity: "error",
1469
+ rule: SECURITY_EXTERNAL_WIDER,
1470
+ where: `object "${objName}"`,
1471
+ path: `${objPath}.externalSharingModel`,
1472
+ message: `externalSharingModel '${external}' is WIDER than the internal sharingModel '${owd}' \u2014 the external baseline must never exceed the internal one (ADR-0090 D11).`,
1473
+ hint: `Narrow externalSharingModel to '${owd}' or below (ordering: private < public_read < public_read_write).`
1474
+ });
1475
+ }
1476
+ }
1477
+ }
1478
+ for (let i = 0; i < permissionSets.length; i++) {
1479
+ const ps = permissionSets[i];
1480
+ if (!ps || typeof ps !== "object") continue;
1481
+ const psName = typeof ps.name === "string" ? ps.name : `(permission set ${i})`;
1482
+ const psPath = `permissions[${i}]`;
1483
+ const objectsMap = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
1484
+ const wildcard = objectsMap["*"];
1485
+ if (wildcard && (wildcard.viewAllRecords === true || wildcard.modifyAllRecords === true)) {
1486
+ findings.push({
1487
+ severity: "error",
1488
+ rule: SECURITY_WILDCARD_VAMA,
1489
+ where: `permission set "${psName}"`,
1490
+ path: `${psPath}.objects.*`,
1491
+ message: `'*' wildcard carrying View All / Modify All Data \u2014 a package-authored superuser. Only the platform's own admin set may combine the wildcard with VAMA (ADR-0066).`,
1492
+ hint: `Enumerate the objects this set really needs, or drop viewAllRecords/modifyAllRecords from the wildcard entry. App-level admins belong in an ordinary set the customer binds to a position of their choosing (ADR-0090 D9).`
1493
+ });
1494
+ }
1495
+ if (ps.isDefault === true) {
1496
+ const offending = (0, import_security2.describeAnchorForbiddenBits)(ps, "everyone");
1497
+ if (offending) {
1498
+ findings.push({
1499
+ severity: "error",
1500
+ rule: SECURITY_ANCHOR_HIGH_PRIVILEGE,
1501
+ where: `permission set "${psName}"`,
1502
+ path: `${psPath}.isDefault`,
1503
+ message: `isDefault:true suggests binding this set to the 'everyone' audience anchor, but it carries ${offending} \u2014 the runtime will refuse the binding (ADR-0090 D5/D9).`,
1504
+ hint: `Split the powerful bits into a separate set granted through ordinary positions, and keep the everyone-suggested set low-privilege.`
1505
+ });
1506
+ }
1507
+ }
1508
+ }
1509
+ const flagRole = (kind, name, label, where, path) => {
1510
+ if (identifierHasRoleToken(name)) {
1511
+ findings.push({
1512
+ severity: "error",
1513
+ rule: SECURITY_ROLE_WORD,
1514
+ where,
1515
+ path,
1516
+ message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
1517
+ hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
1518
+ });
1519
+ } else if (labelHasRoleWord(label)) {
1520
+ findings.push({
1521
+ severity: "error",
1522
+ rule: SECURITY_ROLE_WORD,
1523
+ where,
1524
+ path: `${path.replace(/\.name$/, "")}.label`,
1525
+ message: `${kind} label "${String(label)}" uses the reserved word "role" (ADR-0090 D3).`,
1526
+ hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
1527
+ });
1528
+ }
1529
+ };
1530
+ for (let i = 0; i < objects.length; i++) {
1531
+ const obj = objects[i];
1532
+ if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
1533
+ const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
1534
+ flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
1535
+ for (const f of asArray13(obj.fields)) {
1536
+ flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
1537
+ }
1538
+ for (const [ai, action] of asArray13(obj.actions).entries()) {
1539
+ flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
1540
+ }
1541
+ }
1542
+ for (let i = 0; i < permissionSets.length; i++) {
1543
+ const ps = permissionSets[i];
1544
+ if (!ps || typeof ps !== "object") continue;
1545
+ flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
1546
+ }
1547
+ for (const [i, pos] of asArray13(stack.positions).entries()) {
1548
+ flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
1549
+ }
1550
+ for (const [i, app] of asArray13(stack.apps).entries()) {
1551
+ flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
1552
+ }
1553
+ const privateObjects = new Set(
1554
+ objects.filter((o) => o && typeof o === "object" && !isSystemObject(o)).filter((o) => {
1555
+ const owd = owdOf(o);
1556
+ return owd == null || owd === "private";
1557
+ }).map((o) => String(o.name ?? ""))
1558
+ );
1559
+ if (privateObjects.size > 0) {
1560
+ for (let i = 0; i < permissionSets.length; i++) {
1561
+ const ps = permissionSets[i];
1562
+ if (!ps || typeof ps !== "object") continue;
1563
+ const psName = typeof ps.name === "string" ? ps.name : `(permission set ${i})`;
1564
+ const objectsMap = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
1565
+ for (const [objName, rawPerm] of Object.entries(objectsMap)) {
1566
+ if (!privateObjects.has(objName)) continue;
1567
+ const p = rawPerm ?? {};
1568
+ if (p.allowRead === true && p.readScope == null && p.viewAllRecords !== true) {
1569
+ findings.push({
1570
+ severity: "info",
1571
+ rule: SECURITY_PRIVATE_NO_READSCOPE,
1572
+ where: `permission set "${psName}"`,
1573
+ path: `permissions[${i}].objects.${objName}.readScope`,
1574
+ message: `"${objName}" is private (OWD) and this set grants allowRead without a readScope \u2014 holders see ONLY records they own (plus explicit shares).`,
1575
+ hint: `If that is intended (personal data), ignore this. Otherwise add readScope: 'own_and_reports' | 'unit' | 'unit_and_below' | 'org', or widen the object's sharingModel.`
1576
+ });
1577
+ }
1578
+ }
1579
+ }
1580
+ }
1581
+ return findings;
1582
+ }
1583
+
1584
+ // src/build-access-matrix.ts
1585
+ function asArray14(v) {
1586
+ if (Array.isArray(v)) return v;
1587
+ if (v && typeof v === "object") {
1588
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1589
+ }
1590
+ return [];
1591
+ }
1592
+ function buildAccessMatrix(stack) {
1593
+ const entries = [];
1594
+ if (!stack || typeof stack !== "object") return { version: 1, entries };
1595
+ const owdByObject = /* @__PURE__ */ new Map();
1596
+ for (const obj of asArray14(stack.objects)) {
1597
+ const name = typeof obj.name === "string" ? obj.name : "";
1598
+ if (!name) continue;
1599
+ const owd = obj.sharingModel ?? obj.security?.sharingModel;
1600
+ if (typeof owd === "string") owdByObject.set(name, owd);
1601
+ }
1602
+ for (const ps of asArray14(stack.permissions)) {
1603
+ const psName = typeof ps.name === "string" ? ps.name : "";
1604
+ if (!psName) continue;
1605
+ const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
1606
+ for (const [objName, rawPerm] of Object.entries(objects)) {
1607
+ const p = rawPerm ?? {};
1608
+ const entry = {
1609
+ permissionSet: psName,
1610
+ object: objName,
1611
+ create: p.allowCreate === true,
1612
+ read: p.allowRead === true || p.viewAllRecords === true || p.modifyAllRecords === true,
1613
+ edit: p.allowEdit === true || p.modifyAllRecords === true,
1614
+ delete: p.allowDelete === true || p.modifyAllRecords === true,
1615
+ viewAllRecords: p.viewAllRecords === true,
1616
+ modifyAllRecords: p.modifyAllRecords === true
1617
+ };
1618
+ if (typeof p.readScope === "string") entry.readScope = p.readScope;
1619
+ if (typeof p.writeScope === "string") entry.writeScope = p.writeScope;
1620
+ const owd = owdByObject.get(objName);
1621
+ if (owd) entry.sharingModel = owd;
1622
+ entries.push(entry);
1623
+ }
1624
+ }
1625
+ entries.sort(
1626
+ (a, b) => a.permissionSet === b.permissionSet ? a.object.localeCompare(b.object) : a.permissionSet.localeCompare(b.permissionSet)
1627
+ );
1628
+ return { version: 1, entries };
1629
+ }
1630
+ var BIT_LABELS = [
1631
+ ["create", "create"],
1632
+ ["read", "read"],
1633
+ ["edit", "edit"],
1634
+ ["delete", "delete"],
1635
+ ["viewAllRecords", "View All Data"],
1636
+ ["modifyAllRecords", "Modify All Data"]
1637
+ ];
1638
+ function diffAccessMatrix(before, after) {
1639
+ const lines = [];
1640
+ const key = (e) => `${e.permissionSet}\0${e.object}`;
1641
+ const beforeMap = new Map((before?.entries ?? []).map((e) => [key(e), e]));
1642
+ const afterMap = new Map((after?.entries ?? []).map((e) => [key(e), e]));
1643
+ for (const [k, b] of beforeMap) {
1644
+ if (!afterMap.has(k)) {
1645
+ lines.push(`'${b.permissionSet}' loses ALL access to '${b.object}' (entry removed)`);
1646
+ }
1647
+ }
1648
+ for (const [k, a] of afterMap) {
1649
+ const b = beforeMap.get(k);
1650
+ if (!b) {
1651
+ const grants = BIT_LABELS.filter(([bit]) => a[bit] === true).map(([, label]) => label);
1652
+ lines.push(`'${a.permissionSet}' gains access to '${a.object}' (${grants.join(", ") || "no bits set"})`);
1653
+ continue;
1654
+ }
1655
+ for (const [bit, label] of BIT_LABELS) {
1656
+ if (b[bit] !== a[bit]) {
1657
+ lines.push(`'${a.permissionSet}' ${a[bit] ? "gains" : "loses"} ${label} on '${a.object}'`);
1658
+ }
1659
+ }
1660
+ if ((b.readScope ?? "own") !== (a.readScope ?? "own")) {
1661
+ lines.push(`'${a.permissionSet}' read depth on '${a.object}': ${b.readScope ?? "own"} \u2192 ${a.readScope ?? "own"}`);
1662
+ }
1663
+ if ((b.writeScope ?? "own") !== (a.writeScope ?? "own")) {
1664
+ lines.push(`'${a.permissionSet}' write depth on '${a.object}': ${b.writeScope ?? "own"} \u2192 ${a.writeScope ?? "own"}`);
1665
+ }
1666
+ if ((b.sharingModel ?? "") !== (a.sharingModel ?? "")) {
1667
+ lines.push(`'${a.object}' record baseline (OWD): ${b.sharingModel ?? "(unset)"} \u2192 ${a.sharingModel ?? "(unset)"} (affects every principal)`);
1668
+ }
1669
+ }
1670
+ return lines;
1671
+ }
1242
1672
  // Annotate the CommonJS export names for ESM import in node:
1243
1673
  0 && (module.exports = {
1674
+ CAPABILITY_REFERENCE_UNKNOWN,
1244
1675
  CHART_CONFIG_MISSING,
1245
1676
  CHART_FIELD_UNKNOWN,
1246
1677
  FIELD_GROUP_EMPTY,
@@ -1250,6 +1681,13 @@ function validateFormLayout(stack) {
1250
1681
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
1251
1682
  MEASURE_AGGREGATE_INCOHERENT,
1252
1683
  PAGE_SOURCE_CLASSNAME,
1684
+ SECURITY_ANCHOR_HIGH_PRIVILEGE,
1685
+ SECURITY_EXTERNAL_WIDER,
1686
+ SECURITY_OWD_ALIAS,
1687
+ SECURITY_OWD_UNSET,
1688
+ SECURITY_PRIVATE_NO_READSCOPE,
1689
+ SECURITY_ROLE_WORD,
1690
+ SECURITY_WILDCARD_VAMA,
1253
1691
  SEMANTIC_ROLE_FIELD_UNKNOWN,
1254
1692
  STYLE_CLASSNAME_TAILWIND,
1255
1693
  STYLE_NODE_MISSING_ID,
@@ -1262,6 +1700,9 @@ function validateFormLayout(stack) {
1262
1700
  WIDGET_DATASET_UNKNOWN,
1263
1701
  WIDGET_DIMENSION_UNKNOWN,
1264
1702
  WIDGET_MEASURE_UNKNOWN,
1703
+ buildAccessMatrix,
1704
+ diffAccessMatrix,
1705
+ validateCapabilityReferences,
1265
1706
  validateFormLayout,
1266
1707
  validateJsxPages,
1267
1708
  validateListViewMode,
@@ -1270,6 +1711,7 @@ function validateFormLayout(stack) {
1270
1711
  validateReactPages,
1271
1712
  validateRecordTitle,
1272
1713
  validateResponsiveStyles,
1714
+ validateSecurityPosture,
1273
1715
  validateSemanticRoles,
1274
1716
  validateStackExpressions,
1275
1717
  validateWidgetBindings