@objectstack/lint 13.0.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 +234 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -26
- package/dist/index.d.ts +59 -26
- package/dist/index.js +226 -11
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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,7 +34,11 @@ __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,
|
|
38
|
+
SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
|
|
34
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,
|
|
35
42
|
SECURITY_OWD_ALIAS: () => SECURITY_OWD_ALIAS,
|
|
36
43
|
SECURITY_OWD_UNSET: () => SECURITY_OWD_UNSET,
|
|
37
44
|
SECURITY_PRIVATE_NO_READSCOPE: () => SECURITY_PRIVATE_NO_READSCOPE,
|
|
@@ -51,6 +58,7 @@ __export(index_exports, {
|
|
|
51
58
|
WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
|
|
52
59
|
buildAccessMatrix: () => buildAccessMatrix,
|
|
53
60
|
diffAccessMatrix: () => diffAccessMatrix,
|
|
61
|
+
validateApprovalApprovers: () => validateApprovalApprovers,
|
|
54
62
|
validateCapabilityReferences: () => validateCapabilityReferences,
|
|
55
63
|
validateFormLayout: () => validateFormLayout,
|
|
56
64
|
validateJsxPages: () => validateJsxPages,
|
|
@@ -1369,6 +1377,88 @@ function validateCapabilityReferences(stack) {
|
|
|
1369
1377
|
return findings;
|
|
1370
1378
|
}
|
|
1371
1379
|
|
|
1380
|
+
// src/validate-approval-approvers.ts
|
|
1381
|
+
var import_automation = require("@objectstack/spec/automation");
|
|
1382
|
+
var APPROVAL_ROLE_NOT_MEMBERSHIP_TIER = "approval-role-not-membership-tier";
|
|
1383
|
+
var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
|
|
1384
|
+
var APPROVAL_ESCALATION_REASSIGN_NO_TARGET = "approval-escalation-reassign-no-target";
|
|
1385
|
+
var MEMBERSHIP_TIERS = /* @__PURE__ */ new Set(["owner", "admin", "member", "guest"]);
|
|
1386
|
+
var TYPE_FIX = {
|
|
1387
|
+
business_unit: "department",
|
|
1388
|
+
bu: "department"
|
|
1389
|
+
};
|
|
1390
|
+
function asArray13(v) {
|
|
1391
|
+
if (Array.isArray(v)) return v;
|
|
1392
|
+
if (v && typeof v === "object") {
|
|
1393
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
1394
|
+
}
|
|
1395
|
+
return [];
|
|
1396
|
+
}
|
|
1397
|
+
function validateApprovalApprovers(stack) {
|
|
1398
|
+
const findings = [];
|
|
1399
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
1400
|
+
const flows = asArray13(stack.flows);
|
|
1401
|
+
const validTypes = new Set(import_automation.ApproverType.options);
|
|
1402
|
+
for (let fi = 0; fi < flows.length; fi++) {
|
|
1403
|
+
const flow = flows[fi];
|
|
1404
|
+
if (!flow || typeof flow !== "object") continue;
|
|
1405
|
+
const flowName = typeof flow.name === "string" ? flow.name : `(flow ${fi})`;
|
|
1406
|
+
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
1407
|
+
for (let ni = 0; ni < nodes.length; ni++) {
|
|
1408
|
+
const node = nodes[ni];
|
|
1409
|
+
if (!node || node.type !== import_automation.APPROVAL_NODE_TYPE) continue;
|
|
1410
|
+
const nodeId = typeof node.id === "string" ? node.id : `(node ${ni})`;
|
|
1411
|
+
const cfg = node.config ?? {};
|
|
1412
|
+
const approvers = Array.isArray(cfg.approvers) ? cfg.approvers : [];
|
|
1413
|
+
const where = `flow "${flowName}" \xB7 node "${nodeId}"`;
|
|
1414
|
+
for (let ai = 0; ai < approvers.length; ai++) {
|
|
1415
|
+
const a = approvers[ai];
|
|
1416
|
+
if (!a || typeof a !== "object") continue;
|
|
1417
|
+
const type = typeof a.type === "string" ? a.type : "";
|
|
1418
|
+
const value = typeof a.value === "string" ? a.value : "";
|
|
1419
|
+
const path = `flows[${fi}].nodes[${ni}].config.approvers[${ai}]`;
|
|
1420
|
+
if (type && !validTypes.has(type)) {
|
|
1421
|
+
const fix = TYPE_FIX[type];
|
|
1422
|
+
findings.push({
|
|
1423
|
+
severity: "warning",
|
|
1424
|
+
rule: APPROVAL_APPROVER_TYPE_UNKNOWN,
|
|
1425
|
+
where,
|
|
1426
|
+
path: `${path}.type`,
|
|
1427
|
+
message: `approver type '${type}' is not an ApproverType (${import_automation.ApproverType.options.join(" | ")}).`,
|
|
1428
|
+
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.`
|
|
1429
|
+
});
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (type === "role" && value && !MEMBERSHIP_TIERS.has(value.toLowerCase())) {
|
|
1433
|
+
findings.push({
|
|
1434
|
+
severity: "warning",
|
|
1435
|
+
rule: APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
|
|
1436
|
+
where,
|
|
1437
|
+
path: `${path}.value`,
|
|
1438
|
+
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.`,
|
|
1439
|
+
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).`
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
const escalation = cfg.escalation ?? null;
|
|
1444
|
+
if (escalation && typeof escalation === "object" && escalation.action === "reassign") {
|
|
1445
|
+
const target = typeof escalation.escalateTo === "string" ? escalation.escalateTo.trim() : "";
|
|
1446
|
+
if (!target) {
|
|
1447
|
+
findings.push({
|
|
1448
|
+
severity: "warning",
|
|
1449
|
+
rule: APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
1450
|
+
where,
|
|
1451
|
+
path: `flows[${fi}].nodes[${ni}].config.escalation.escalateTo`,
|
|
1452
|
+
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.`,
|
|
1453
|
+
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'.`
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return findings;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1372
1462
|
// src/validate-security-posture.ts
|
|
1373
1463
|
var import_security2 = require("@objectstack/spec/security");
|
|
1374
1464
|
var SECURITY_OWD_UNSET = "security-owd-unset";
|
|
@@ -1377,7 +1467,12 @@ var SECURITY_EXTERNAL_WIDER = "security-external-wider-than-internal";
|
|
|
1377
1467
|
var SECURITY_WILDCARD_VAMA = "security-wildcard-vama";
|
|
1378
1468
|
var SECURITY_ANCHOR_HIGH_PRIVILEGE = "security-anchor-high-privilege";
|
|
1379
1469
|
var SECURITY_ROLE_WORD = "security-role-word";
|
|
1470
|
+
var SECURITY_BOOK_AUDIENCE_UNKNOWN_SET = "security-book-audience-unknown-set";
|
|
1380
1471
|
var SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
|
|
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";
|
|
1381
1476
|
var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
|
|
1382
1477
|
var OWD_ALIAS_FIX = {
|
|
1383
1478
|
read: "public_read",
|
|
@@ -1390,7 +1485,7 @@ var OWD_WIDTH = {
|
|
|
1390
1485
|
public_read: 1,
|
|
1391
1486
|
public_read_write: 2
|
|
1392
1487
|
};
|
|
1393
|
-
function
|
|
1488
|
+
function asArray14(v) {
|
|
1394
1489
|
if (Array.isArray(v)) return v;
|
|
1395
1490
|
if (v && typeof v === "object") {
|
|
1396
1491
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1411,11 +1506,26 @@ function labelHasRoleWord(label) {
|
|
|
1411
1506
|
if (typeof label !== "string") return false;
|
|
1412
1507
|
return /\brole(s)?\b/i.test(label);
|
|
1413
1508
|
}
|
|
1414
|
-
function
|
|
1509
|
+
function refOf(def) {
|
|
1510
|
+
const r = def.reference ?? def.reference_to;
|
|
1511
|
+
return typeof r === "string" && r ? r : void 0;
|
|
1512
|
+
}
|
|
1513
|
+
function firstMasterDetailField(obj) {
|
|
1514
|
+
for (const f of asArray14(obj.fields)) {
|
|
1515
|
+
if (f.type === "master_detail") {
|
|
1516
|
+
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return void 0;
|
|
1520
|
+
}
|
|
1521
|
+
function grantsObjectAccess(p) {
|
|
1522
|
+
return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
|
|
1523
|
+
}
|
|
1524
|
+
function validateSecurityPosture(stack, opts) {
|
|
1415
1525
|
const findings = [];
|
|
1416
1526
|
if (!stack || typeof stack !== "object") return findings;
|
|
1417
|
-
const objects =
|
|
1418
|
-
const permissionSets =
|
|
1527
|
+
const objects = asArray14(stack.objects);
|
|
1528
|
+
const permissionSets = asArray14(stack.permissions);
|
|
1419
1529
|
for (let i = 0; i < objects.length; i++) {
|
|
1420
1530
|
const obj = objects[i];
|
|
1421
1531
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -1481,6 +1591,18 @@ function validateSecurityPosture(stack) {
|
|
|
1481
1591
|
const psName = typeof ps.name === "string" ? ps.name : `(permission set ${i})`;
|
|
1482
1592
|
const psPath = `permissions[${i}]`;
|
|
1483
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
|
+
}
|
|
1484
1606
|
const wildcard = objectsMap["*"];
|
|
1485
1607
|
if (wildcard && (wildcard.viewAllRecords === true || wildcard.modifyAllRecords === true)) {
|
|
1486
1608
|
findings.push({
|
|
@@ -1532,10 +1654,10 @@ function validateSecurityPosture(stack) {
|
|
|
1532
1654
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
1533
1655
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
1534
1656
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
1535
|
-
for (const f of
|
|
1657
|
+
for (const f of asArray14(obj.fields)) {
|
|
1536
1658
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
1537
1659
|
}
|
|
1538
|
-
for (const [ai, action] of
|
|
1660
|
+
for (const [ai, action] of asArray14(obj.actions).entries()) {
|
|
1539
1661
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
1540
1662
|
}
|
|
1541
1663
|
}
|
|
@@ -1544,12 +1666,34 @@ function validateSecurityPosture(stack) {
|
|
|
1544
1666
|
if (!ps || typeof ps !== "object") continue;
|
|
1545
1667
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
1546
1668
|
}
|
|
1547
|
-
for (const [i, pos] of
|
|
1669
|
+
for (const [i, pos] of asArray14(stack.positions).entries()) {
|
|
1548
1670
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
1549
1671
|
}
|
|
1550
|
-
for (const [i, app] of
|
|
1672
|
+
for (const [i, app] of asArray14(stack.apps).entries()) {
|
|
1551
1673
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
1552
1674
|
}
|
|
1675
|
+
for (const [i, book] of asArray14(stack.books).entries()) {
|
|
1676
|
+
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
1677
|
+
}
|
|
1678
|
+
const stackSetNames = new Set(
|
|
1679
|
+
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
1680
|
+
);
|
|
1681
|
+
for (const [i, book] of asArray14(stack.books).entries()) {
|
|
1682
|
+
const audience = book.audience;
|
|
1683
|
+
if (!audience || typeof audience !== "object") continue;
|
|
1684
|
+
const setName = audience.permissionSet;
|
|
1685
|
+
if (typeof setName !== "string" || setName.length === 0) continue;
|
|
1686
|
+
if (!stackSetNames.has(setName)) {
|
|
1687
|
+
findings.push({
|
|
1688
|
+
severity: "warning",
|
|
1689
|
+
rule: SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
1690
|
+
where: `book "${String(book.name ?? i)}"`,
|
|
1691
|
+
path: `books[${i}].audience.permissionSet`,
|
|
1692
|
+
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.`,
|
|
1693
|
+
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.`
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1553
1697
|
const privateObjects = new Set(
|
|
1554
1698
|
objects.filter((o) => o && typeof o === "object" && !isSystemObject(o)).filter((o) => {
|
|
1555
1699
|
const owd = owdOf(o);
|
|
@@ -1578,11 +1722,82 @@ function validateSecurityPosture(stack) {
|
|
|
1578
1722
|
}
|
|
1579
1723
|
}
|
|
1580
1724
|
}
|
|
1725
|
+
if (permissionSets.length > 0) {
|
|
1726
|
+
const wildcardGrantsAll = permissionSets.some(
|
|
1727
|
+
(ps) => grantsObjectAccess(ps.objects?.["*"] ?? {})
|
|
1728
|
+
);
|
|
1729
|
+
if (!wildcardGrantsAll) {
|
|
1730
|
+
const grantedObjects = /* @__PURE__ */ new Set();
|
|
1731
|
+
for (const ps of permissionSets) {
|
|
1732
|
+
const objectsMap = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
1733
|
+
for (const [objName, rawPerm] of Object.entries(objectsMap)) {
|
|
1734
|
+
if (objName === "*") continue;
|
|
1735
|
+
if (grantsObjectAccess(rawPerm ?? {})) grantedObjects.add(objName);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
for (let i = 0; i < objects.length; i++) {
|
|
1739
|
+
const obj = objects[i];
|
|
1740
|
+
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
1741
|
+
const objName = typeof obj.name === "string" ? obj.name : "";
|
|
1742
|
+
if (!objName || grantedObjects.has(objName)) continue;
|
|
1743
|
+
const md = firstMasterDetailField(obj);
|
|
1744
|
+
if (!md) continue;
|
|
1745
|
+
const parentText = md.parent ? ` \u2192 "${md.parent}"` : "";
|
|
1746
|
+
findings.push({
|
|
1747
|
+
severity: "warning",
|
|
1748
|
+
rule: SECURITY_MASTER_DETAIL_UNGRANTED,
|
|
1749
|
+
where: `object "${objName}"`,
|
|
1750
|
+
path: `objects[${i}].fields.${md.name}`,
|
|
1751
|
+
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).`,
|
|
1752
|
+
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.`
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
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
|
+
}
|
|
1581
1796
|
return findings;
|
|
1582
1797
|
}
|
|
1583
1798
|
|
|
1584
1799
|
// src/build-access-matrix.ts
|
|
1585
|
-
function
|
|
1800
|
+
function asArray15(v) {
|
|
1586
1801
|
if (Array.isArray(v)) return v;
|
|
1587
1802
|
if (v && typeof v === "object") {
|
|
1588
1803
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1593,13 +1808,13 @@ function buildAccessMatrix(stack) {
|
|
|
1593
1808
|
const entries = [];
|
|
1594
1809
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
1595
1810
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
1596
|
-
for (const obj of
|
|
1811
|
+
for (const obj of asArray15(stack.objects)) {
|
|
1597
1812
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
1598
1813
|
if (!name) continue;
|
|
1599
1814
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
1600
1815
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
1601
1816
|
}
|
|
1602
|
-
for (const ps of
|
|
1817
|
+
for (const ps of asArray15(stack.permissions)) {
|
|
1603
1818
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
1604
1819
|
if (!psName) continue;
|
|
1605
1820
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -1671,6 +1886,9 @@ function diffAccessMatrix(before, after) {
|
|
|
1671
1886
|
}
|
|
1672
1887
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1673
1888
|
0 && (module.exports = {
|
|
1889
|
+
APPROVAL_APPROVER_TYPE_UNKNOWN,
|
|
1890
|
+
APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
1891
|
+
APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
|
|
1674
1892
|
CAPABILITY_REFERENCE_UNKNOWN,
|
|
1675
1893
|
CHART_CONFIG_MISSING,
|
|
1676
1894
|
CHART_FIELD_UNKNOWN,
|
|
@@ -1682,7 +1900,11 @@ function diffAccessMatrix(before, after) {
|
|
|
1682
1900
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
1683
1901
|
PAGE_SOURCE_CLASSNAME,
|
|
1684
1902
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
1903
|
+
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
1904
|
+
SECURITY_DELEGATION_MISSING_REASON,
|
|
1685
1905
|
SECURITY_EXTERNAL_WIDER,
|
|
1906
|
+
SECURITY_GRANT_EXPIRED_AT_AUTHORING,
|
|
1907
|
+
SECURITY_MASTER_DETAIL_UNGRANTED,
|
|
1686
1908
|
SECURITY_OWD_ALIAS,
|
|
1687
1909
|
SECURITY_OWD_UNSET,
|
|
1688
1910
|
SECURITY_PRIVATE_NO_READSCOPE,
|
|
@@ -1702,6 +1924,7 @@ function diffAccessMatrix(before, after) {
|
|
|
1702
1924
|
WIDGET_MEASURE_UNKNOWN,
|
|
1703
1925
|
buildAccessMatrix,
|
|
1704
1926
|
diffAccessMatrix,
|
|
1927
|
+
validateApprovalApprovers,
|
|
1705
1928
|
validateCapabilityReferences,
|
|
1706
1929
|
validateFormLayout,
|
|
1707
1930
|
validateJsxPages,
|