@objectstack/lint 14.3.0 → 14.5.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
@@ -35,7 +35,10 @@ __export(index_exports, {
35
35
  PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
36
36
  SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
37
37
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
38
+ SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
38
39
  SECURITY_EXTERNAL_WIDER: () => SECURITY_EXTERNAL_WIDER,
40
+ SECURITY_GRANT_EXPIRED_AT_AUTHORING: () => SECURITY_GRANT_EXPIRED_AT_AUTHORING,
41
+ SECURITY_MASTER_DETAIL_UNGRANTED: () => SECURITY_MASTER_DETAIL_UNGRANTED,
39
42
  SECURITY_OWD_ALIAS: () => SECURITY_OWD_ALIAS,
40
43
  SECURITY_OWD_UNSET: () => SECURITY_OWD_UNSET,
41
44
  SECURITY_PRIVATE_NO_READSCOPE: () => SECURITY_PRIVATE_NO_READSCOPE,
@@ -1467,6 +1470,9 @@ var SECURITY_ROLE_WORD = "security-role-word";
1467
1470
  var SECURITY_BOOK_AUDIENCE_UNKNOWN_SET = "security-book-audience-unknown-set";
1468
1471
  var SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
1469
1472
  var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
1473
+ var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
1474
+ var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
1475
+ var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
1470
1476
  var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
1471
1477
  var OWD_ALIAS_FIX = {
1472
1478
  read: "public_read",
@@ -1515,7 +1521,7 @@ function firstMasterDetailField(obj) {
1515
1521
  function grantsObjectAccess(p) {
1516
1522
  return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
1517
1523
  }
1518
- function validateSecurityPosture(stack) {
1524
+ function validateSecurityPosture(stack, opts) {
1519
1525
  const findings = [];
1520
1526
  if (!stack || typeof stack !== "object") return findings;
1521
1527
  const objects = asArray14(stack.objects);
@@ -1585,6 +1591,18 @@ function validateSecurityPosture(stack) {
1585
1591
  const psName = typeof ps.name === "string" ? ps.name : `(permission set ${i})`;
1586
1592
  const psPath = `permissions[${i}]`;
1587
1593
  const objectsMap = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
1594
+ const flsMap = ps.fields && typeof ps.fields === "object" ? ps.fields : {};
1595
+ for (const flsKey of Object.keys(flsMap)) {
1596
+ if (flsKey.includes(".")) continue;
1597
+ findings.push({
1598
+ severity: "error",
1599
+ rule: SECURITY_FLS_UNQUALIFIED_KEY,
1600
+ where: `permission set "${psName}"`,
1601
+ path: `${psPath}.fields["${flsKey}"]`,
1602
+ message: `field-permission key '${flsKey}' is not object-qualified \u2014 the runtime matches FLS keys by '<object>.<field>' prefix, so a bare key is silently IGNORED and the declared masking never enforces.`,
1603
+ hint: `Qualify the key with its object, e.g. 'crm_opportunity.${flsKey}': { readable: true, editable: false }.`
1604
+ });
1605
+ }
1588
1606
  const wildcard = objectsMap["*"];
1589
1607
  if (wildcard && (wildcard.viewAllRecords === true || wildcard.modifyAllRecords === true)) {
1590
1608
  findings.push({
@@ -1736,6 +1754,45 @@ function validateSecurityPosture(stack) {
1736
1754
  }
1737
1755
  }
1738
1756
  }
1757
+ const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
1758
+ const nowMs = opts?.nowMs ?? Date.now();
1759
+ for (const [i, seed] of asArray14(stack.data).entries()) {
1760
+ const seedObject = typeof seed.object === "string" ? seed.object : "";
1761
+ if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
1762
+ const records = Array.isArray(seed.records) ? seed.records : [];
1763
+ for (let j = 0; j < records.length; j++) {
1764
+ const rec = records[j] ?? {};
1765
+ const where = `seed "${seedObject}" record #${j}`;
1766
+ const until = rec.valid_until;
1767
+ if (until != null && until !== "") {
1768
+ const ms = typeof until === "number" ? until < 1e12 ? until * 1e3 : until : until instanceof Date ? until.getTime() : typeof until === "string" ? Date.parse(until) : Number.NaN;
1769
+ if (Number.isNaN(ms) || ms <= nowMs) {
1770
+ findings.push({
1771
+ severity: "error",
1772
+ rule: SECURITY_GRANT_EXPIRED_AT_AUTHORING,
1773
+ where,
1774
+ path: `data[${i}].records[${j}].valid_until`,
1775
+ message: Number.isNaN(ms) ? `valid_until ${JSON.stringify(until)} is not a parseable timestamp \u2014 the resolver fails closed (ADR-0091 D2), so this grant will NEVER be active.` : `valid_until ${JSON.stringify(until)} is already in the past \u2014 this grant is expired at authoring time and will never resolve (ADR-0091 D2 filters it fail-closed).`,
1776
+ hint: `Set valid_until to a future instant (ISO-8601 UTC), or drop the column for an unbounded grant. If the row is a historical record, it belongs in audit history, not seed data.`
1777
+ });
1778
+ }
1779
+ }
1780
+ const delegatedFrom = rec.delegated_from;
1781
+ if (delegatedFrom != null && delegatedFrom !== "") {
1782
+ const reason = rec.reason;
1783
+ if (typeof reason !== "string" || reason.trim().length === 0) {
1784
+ findings.push({
1785
+ severity: "error",
1786
+ rule: SECURITY_DELEGATION_MISSING_REASON,
1787
+ where,
1788
+ path: `data[${i}].records[${j}].reason`,
1789
+ message: `delegation row (delegated_from = ${JSON.stringify(delegatedFrom)}) has no reason. ADR-0091 D3 requires a mandatory reason on every delegation for the dual audit trail (granted_by = writer, delegated_from = authority source, reason = why).`,
1790
+ hint: `Add reason: 'vacation stand-in for \u5F20\u4E09, 2026-08-01..15' (free text, required).`
1791
+ });
1792
+ }
1793
+ }
1794
+ }
1795
+ }
1739
1796
  return findings;
1740
1797
  }
1741
1798
 
@@ -1844,7 +1901,10 @@ function diffAccessMatrix(before, after) {
1844
1901
  PAGE_SOURCE_CLASSNAME,
1845
1902
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
1846
1903
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
1904
+ SECURITY_DELEGATION_MISSING_REASON,
1847
1905
  SECURITY_EXTERNAL_WIDER,
1906
+ SECURITY_GRANT_EXPIRED_AT_AUTHORING,
1907
+ SECURITY_MASTER_DETAIL_UNGRANTED,
1848
1908
  SECURITY_OWD_ALIAS,
1849
1909
  SECURITY_OWD_UNSET,
1850
1910
  SECURITY_PRIVATE_NO_READSCOPE,