@objectstack/lint 13.0.0 → 14.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.cjs CHANGED
@@ -20,6 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ APPROVAL_APPROVER_TYPE_UNKNOWN: () => APPROVAL_APPROVER_TYPE_UNKNOWN,
24
+ APPROVAL_ESCALATION_REASSIGN_NO_TARGET: () => APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
25
+ APPROVAL_ROLE_NOT_MEMBERSHIP_TIER: () => APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
23
26
  CAPABILITY_REFERENCE_UNKNOWN: () => CAPABILITY_REFERENCE_UNKNOWN,
24
27
  CHART_CONFIG_MISSING: () => CHART_CONFIG_MISSING,
25
28
  CHART_FIELD_UNKNOWN: () => CHART_FIELD_UNKNOWN,
@@ -31,6 +34,7 @@ __export(index_exports, {
31
34
  MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
32
35
  PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
33
36
  SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
37
+ SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
34
38
  SECURITY_EXTERNAL_WIDER: () => SECURITY_EXTERNAL_WIDER,
35
39
  SECURITY_OWD_ALIAS: () => SECURITY_OWD_ALIAS,
36
40
  SECURITY_OWD_UNSET: () => SECURITY_OWD_UNSET,
@@ -51,6 +55,7 @@ __export(index_exports, {
51
55
  WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
52
56
  buildAccessMatrix: () => buildAccessMatrix,
53
57
  diffAccessMatrix: () => diffAccessMatrix,
58
+ validateApprovalApprovers: () => validateApprovalApprovers,
54
59
  validateCapabilityReferences: () => validateCapabilityReferences,
55
60
  validateFormLayout: () => validateFormLayout,
56
61
  validateJsxPages: () => validateJsxPages,
@@ -1369,6 +1374,88 @@ function validateCapabilityReferences(stack) {
1369
1374
  return findings;
1370
1375
  }
1371
1376
 
1377
+ // src/validate-approval-approvers.ts
1378
+ var import_automation = require("@objectstack/spec/automation");
1379
+ var APPROVAL_ROLE_NOT_MEMBERSHIP_TIER = "approval-role-not-membership-tier";
1380
+ var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
1381
+ var APPROVAL_ESCALATION_REASSIGN_NO_TARGET = "approval-escalation-reassign-no-target";
1382
+ var MEMBERSHIP_TIERS = /* @__PURE__ */ new Set(["owner", "admin", "member", "guest"]);
1383
+ var TYPE_FIX = {
1384
+ business_unit: "department",
1385
+ bu: "department"
1386
+ };
1387
+ function asArray13(v) {
1388
+ if (Array.isArray(v)) return v;
1389
+ if (v && typeof v === "object") {
1390
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1391
+ }
1392
+ return [];
1393
+ }
1394
+ function validateApprovalApprovers(stack) {
1395
+ const findings = [];
1396
+ if (!stack || typeof stack !== "object") return findings;
1397
+ const flows = asArray13(stack.flows);
1398
+ const validTypes = new Set(import_automation.ApproverType.options);
1399
+ for (let fi = 0; fi < flows.length; fi++) {
1400
+ const flow = flows[fi];
1401
+ if (!flow || typeof flow !== "object") continue;
1402
+ const flowName = typeof flow.name === "string" ? flow.name : `(flow ${fi})`;
1403
+ const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
1404
+ for (let ni = 0; ni < nodes.length; ni++) {
1405
+ const node = nodes[ni];
1406
+ if (!node || node.type !== import_automation.APPROVAL_NODE_TYPE) continue;
1407
+ const nodeId = typeof node.id === "string" ? node.id : `(node ${ni})`;
1408
+ const cfg = node.config ?? {};
1409
+ const approvers = Array.isArray(cfg.approvers) ? cfg.approvers : [];
1410
+ const where = `flow "${flowName}" \xB7 node "${nodeId}"`;
1411
+ for (let ai = 0; ai < approvers.length; ai++) {
1412
+ const a = approvers[ai];
1413
+ if (!a || typeof a !== "object") continue;
1414
+ const type = typeof a.type === "string" ? a.type : "";
1415
+ const value = typeof a.value === "string" ? a.value : "";
1416
+ const path = `flows[${fi}].nodes[${ni}].config.approvers[${ai}]`;
1417
+ if (type && !validTypes.has(type)) {
1418
+ const fix = TYPE_FIX[type];
1419
+ findings.push({
1420
+ severity: "warning",
1421
+ rule: APPROVAL_APPROVER_TYPE_UNKNOWN,
1422
+ where,
1423
+ path: `${path}.type`,
1424
+ message: `approver type '${type}' is not an ApproverType (${import_automation.ApproverType.options.join(" | ")}).`,
1425
+ hint: fix ? `Use the spec value: { type: '${fix}', value: '${value}' }.` : `Pick one of the spec values; unmapped types degrade to an inert '${type}:${value}' literal at runtime.`
1426
+ });
1427
+ continue;
1428
+ }
1429
+ if (type === "role" && value && !MEMBERSHIP_TIERS.has(value.toLowerCase())) {
1430
+ findings.push({
1431
+ severity: "warning",
1432
+ rule: APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
1433
+ where,
1434
+ path: `${path}.value`,
1435
+ message: `approver { type: 'role', value: '${value}' } resolves against the better-auth org-membership tier (sys_member.role: owner/admin/member) \u2014 '${value}' is not a membership tier, so this approver matches nobody and the request stalls.`,
1436
+ hint: `If '${value}' is an org position, author { type: 'position', value: '${value}' } (resolved via sys_user_position, ADR-0090 D3). Keep type 'role' only for membership tiers (owner/admin/member).`
1437
+ });
1438
+ }
1439
+ }
1440
+ const escalation = cfg.escalation ?? null;
1441
+ if (escalation && typeof escalation === "object" && escalation.action === "reassign") {
1442
+ const target = typeof escalation.escalateTo === "string" ? escalation.escalateTo.trim() : "";
1443
+ if (!target) {
1444
+ findings.push({
1445
+ severity: "warning",
1446
+ rule: APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
1447
+ where,
1448
+ path: `flows[${fi}].nodes[${ni}].config.escalation.escalateTo`,
1449
+ message: `escalation.action is 'reassign' but escalateTo is empty \u2014 at runtime the escalation degrades to a notify and the request stays with the original approvers.`,
1450
+ hint: `Set escalateTo to a position machine name (expanded via sys_user_position, ADR-0090 D3) or a specific user id, or change action to 'notify'.`
1451
+ });
1452
+ }
1453
+ }
1454
+ }
1455
+ }
1456
+ return findings;
1457
+ }
1458
+
1372
1459
  // src/validate-security-posture.ts
1373
1460
  var import_security2 = require("@objectstack/spec/security");
1374
1461
  var SECURITY_OWD_UNSET = "security-owd-unset";
@@ -1377,7 +1464,9 @@ var SECURITY_EXTERNAL_WIDER = "security-external-wider-than-internal";
1377
1464
  var SECURITY_WILDCARD_VAMA = "security-wildcard-vama";
1378
1465
  var SECURITY_ANCHOR_HIGH_PRIVILEGE = "security-anchor-high-privilege";
1379
1466
  var SECURITY_ROLE_WORD = "security-role-word";
1467
+ var SECURITY_BOOK_AUDIENCE_UNKNOWN_SET = "security-book-audience-unknown-set";
1380
1468
  var SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
1469
+ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
1381
1470
  var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
1382
1471
  var OWD_ALIAS_FIX = {
1383
1472
  read: "public_read",
@@ -1390,7 +1479,7 @@ var OWD_WIDTH = {
1390
1479
  public_read: 1,
1391
1480
  public_read_write: 2
1392
1481
  };
1393
- function asArray13(v) {
1482
+ function asArray14(v) {
1394
1483
  if (Array.isArray(v)) return v;
1395
1484
  if (v && typeof v === "object") {
1396
1485
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1411,11 +1500,26 @@ function labelHasRoleWord(label) {
1411
1500
  if (typeof label !== "string") return false;
1412
1501
  return /\brole(s)?\b/i.test(label);
1413
1502
  }
1503
+ function refOf(def) {
1504
+ const r = def.reference ?? def.reference_to;
1505
+ return typeof r === "string" && r ? r : void 0;
1506
+ }
1507
+ function firstMasterDetailField(obj) {
1508
+ for (const f of asArray14(obj.fields)) {
1509
+ if (f.type === "master_detail") {
1510
+ return { name: String(f.name ?? "?"), parent: refOf(f) };
1511
+ }
1512
+ }
1513
+ return void 0;
1514
+ }
1515
+ function grantsObjectAccess(p) {
1516
+ return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
1517
+ }
1414
1518
  function validateSecurityPosture(stack) {
1415
1519
  const findings = [];
1416
1520
  if (!stack || typeof stack !== "object") return findings;
1417
- const objects = asArray13(stack.objects);
1418
- const permissionSets = asArray13(stack.permissions);
1521
+ const objects = asArray14(stack.objects);
1522
+ const permissionSets = asArray14(stack.permissions);
1419
1523
  for (let i = 0; i < objects.length; i++) {
1420
1524
  const obj = objects[i];
1421
1525
  if (!obj || typeof obj !== "object") continue;
@@ -1532,10 +1636,10 @@ function validateSecurityPosture(stack) {
1532
1636
  if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
1533
1637
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
1534
1638
  flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
1535
- for (const f of asArray13(obj.fields)) {
1639
+ for (const f of asArray14(obj.fields)) {
1536
1640
  flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
1537
1641
  }
1538
- for (const [ai, action] of asArray13(obj.actions).entries()) {
1642
+ for (const [ai, action] of asArray14(obj.actions).entries()) {
1539
1643
  flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
1540
1644
  }
1541
1645
  }
@@ -1544,12 +1648,34 @@ function validateSecurityPosture(stack) {
1544
1648
  if (!ps || typeof ps !== "object") continue;
1545
1649
  flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
1546
1650
  }
1547
- for (const [i, pos] of asArray13(stack.positions).entries()) {
1651
+ for (const [i, pos] of asArray14(stack.positions).entries()) {
1548
1652
  flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
1549
1653
  }
1550
- for (const [i, app] of asArray13(stack.apps).entries()) {
1654
+ for (const [i, app] of asArray14(stack.apps).entries()) {
1551
1655
  flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
1552
1656
  }
1657
+ for (const [i, book] of asArray14(stack.books).entries()) {
1658
+ flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
1659
+ }
1660
+ const stackSetNames = new Set(
1661
+ permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
1662
+ );
1663
+ for (const [i, book] of asArray14(stack.books).entries()) {
1664
+ const audience = book.audience;
1665
+ if (!audience || typeof audience !== "object") continue;
1666
+ const setName = audience.permissionSet;
1667
+ if (typeof setName !== "string" || setName.length === 0) continue;
1668
+ if (!stackSetNames.has(setName)) {
1669
+ findings.push({
1670
+ severity: "warning",
1671
+ rule: SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
1672
+ where: `book "${String(book.name ?? i)}"`,
1673
+ path: `books[${i}].audience.permissionSet`,
1674
+ message: `book audience references permission set "${setName}", which this stack does not declare. The runtime fails closed \u2014 no holder means NO reader can open the book.`,
1675
+ hint: `Gate the book on one of this package's own permission sets (ADR-0090 D9, e.g. its admin set), or fix the typo. Ignore if the set is intentionally provided by another installed package.`
1676
+ });
1677
+ }
1678
+ }
1553
1679
  const privateObjects = new Set(
1554
1680
  objects.filter((o) => o && typeof o === "object" && !isSystemObject(o)).filter((o) => {
1555
1681
  const owd = owdOf(o);
@@ -1578,11 +1704,43 @@ function validateSecurityPosture(stack) {
1578
1704
  }
1579
1705
  }
1580
1706
  }
1707
+ if (permissionSets.length > 0) {
1708
+ const wildcardGrantsAll = permissionSets.some(
1709
+ (ps) => grantsObjectAccess(ps.objects?.["*"] ?? {})
1710
+ );
1711
+ if (!wildcardGrantsAll) {
1712
+ const grantedObjects = /* @__PURE__ */ new Set();
1713
+ for (const ps of permissionSets) {
1714
+ const objectsMap = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
1715
+ for (const [objName, rawPerm] of Object.entries(objectsMap)) {
1716
+ if (objName === "*") continue;
1717
+ if (grantsObjectAccess(rawPerm ?? {})) grantedObjects.add(objName);
1718
+ }
1719
+ }
1720
+ for (let i = 0; i < objects.length; i++) {
1721
+ const obj = objects[i];
1722
+ if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
1723
+ const objName = typeof obj.name === "string" ? obj.name : "";
1724
+ if (!objName || grantedObjects.has(objName)) continue;
1725
+ const md = firstMasterDetailField(obj);
1726
+ if (!md) continue;
1727
+ const parentText = md.parent ? ` \u2192 "${md.parent}"` : "";
1728
+ findings.push({
1729
+ severity: "warning",
1730
+ rule: SECURITY_MASTER_DETAIL_UNGRANTED,
1731
+ where: `object "${objName}"`,
1732
+ path: `objects[${i}].fields.${md.name}`,
1733
+ message: `detail object "${objName}" (master_detail "${md.name}"${parentText}) has no object-level CRUD grant in any permission set. A master-detail child derives its RECORD-level access from the master (ADR-0055 controlled_by_parent), but object-level CRUD is a SEPARATE gate that is never derived \u2014 role-bound non-admin users are denied (403) before the parent-derived access is ever consulted (the silent "can't submit the subtable" trap).`,
1734
+ hint: `Grant "${objName}" in at least one permission set that already grants its master${md.parent ? ` "${md.parent}"` : ""} \u2014 e.g. permissions[i].objects.${objName} = { allowRead: true, allowCreate: true, allowEdit: true }. If no role should ever touch it (a pure system/internal table), name it sys_* or set isSystem: true.`
1735
+ });
1736
+ }
1737
+ }
1738
+ }
1581
1739
  return findings;
1582
1740
  }
1583
1741
 
1584
1742
  // src/build-access-matrix.ts
1585
- function asArray14(v) {
1743
+ function asArray15(v) {
1586
1744
  if (Array.isArray(v)) return v;
1587
1745
  if (v && typeof v === "object") {
1588
1746
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1593,13 +1751,13 @@ function buildAccessMatrix(stack) {
1593
1751
  const entries = [];
1594
1752
  if (!stack || typeof stack !== "object") return { version: 1, entries };
1595
1753
  const owdByObject = /* @__PURE__ */ new Map();
1596
- for (const obj of asArray14(stack.objects)) {
1754
+ for (const obj of asArray15(stack.objects)) {
1597
1755
  const name = typeof obj.name === "string" ? obj.name : "";
1598
1756
  if (!name) continue;
1599
1757
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
1600
1758
  if (typeof owd === "string") owdByObject.set(name, owd);
1601
1759
  }
1602
- for (const ps of asArray14(stack.permissions)) {
1760
+ for (const ps of asArray15(stack.permissions)) {
1603
1761
  const psName = typeof ps.name === "string" ? ps.name : "";
1604
1762
  if (!psName) continue;
1605
1763
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -1671,6 +1829,9 @@ function diffAccessMatrix(before, after) {
1671
1829
  }
1672
1830
  // Annotate the CommonJS export names for ESM import in node:
1673
1831
  0 && (module.exports = {
1832
+ APPROVAL_APPROVER_TYPE_UNKNOWN,
1833
+ APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
1834
+ APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
1674
1835
  CAPABILITY_REFERENCE_UNKNOWN,
1675
1836
  CHART_CONFIG_MISSING,
1676
1837
  CHART_FIELD_UNKNOWN,
@@ -1682,6 +1843,7 @@ function diffAccessMatrix(before, after) {
1682
1843
  MEASURE_AGGREGATE_INCOHERENT,
1683
1844
  PAGE_SOURCE_CLASSNAME,
1684
1845
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
1846
+ SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
1685
1847
  SECURITY_EXTERNAL_WIDER,
1686
1848
  SECURITY_OWD_ALIAS,
1687
1849
  SECURITY_OWD_UNSET,
@@ -1702,6 +1864,7 @@ function diffAccessMatrix(before, after) {
1702
1864
  WIDGET_MEASURE_UNKNOWN,
1703
1865
  buildAccessMatrix,
1704
1866
  diffAccessMatrix,
1867
+ validateApprovalApprovers,
1705
1868
  validateCapabilityReferences,
1706
1869
  validateFormLayout,
1707
1870
  validateJsxPages,