@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.d.cts CHANGED
@@ -397,6 +397,9 @@ declare const SECURITY_ANCHOR_HIGH_PRIVILEGE = "security-anchor-high-privilege";
397
397
  declare const SECURITY_ROLE_WORD = "security-role-word";
398
398
  declare const SECURITY_BOOK_AUDIENCE_UNKNOWN_SET = "security-book-audience-unknown-set";
399
399
  declare const SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
400
+ declare const SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
401
+ declare const SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
402
+ declare const SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
400
403
  type SecuritySeverity = 'error' | 'warning' | 'info';
401
404
  interface SecurityFinding {
402
405
  severity: SecuritySeverity;
@@ -415,8 +418,13 @@ type AnyRec$1 = Record<string, unknown>;
415
418
  /**
416
419
  * Validate the security posture of a stack. Returns findings (empty = clean).
417
420
  * `error` findings gate the build in `os compile`; `info` is advisory.
421
+ *
422
+ * `opts.nowMs` injects the clock for the ADR-0091 authoring-time expiry rule
423
+ * (tests); production callers omit it.
418
424
  */
419
- declare function validateSecurityPosture(stack: AnyRec$1): SecurityFinding[];
425
+ declare function validateSecurityPosture(stack: AnyRec$1, opts?: {
426
+ nowMs?: number;
427
+ }): SecurityFinding[];
420
428
 
421
429
  /**
422
430
  * [ADR-0090 D6] Access-matrix snapshot — authoring-time companion to the
@@ -442,4 +450,4 @@ declare function buildAccessMatrix(stack: AnyRec): AccessMatrix;
442
450
  */
443
451
  declare function diffAccessMatrix(before: AccessMatrix, after: AccessMatrix): string[];
444
452
 
445
- export { APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_ROLE_NOT_MEMBERSHIP_TIER, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_EXTERNAL_WIDER, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateWidgetBindings };
453
+ export { APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_ROLE_NOT_MEMBERSHIP_TIER, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateWidgetBindings };
package/dist/index.d.ts CHANGED
@@ -397,6 +397,9 @@ declare const SECURITY_ANCHOR_HIGH_PRIVILEGE = "security-anchor-high-privilege";
397
397
  declare const SECURITY_ROLE_WORD = "security-role-word";
398
398
  declare const SECURITY_BOOK_AUDIENCE_UNKNOWN_SET = "security-book-audience-unknown-set";
399
399
  declare const SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
400
+ declare const SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
401
+ declare const SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
402
+ declare const SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
400
403
  type SecuritySeverity = 'error' | 'warning' | 'info';
401
404
  interface SecurityFinding {
402
405
  severity: SecuritySeverity;
@@ -415,8 +418,13 @@ type AnyRec$1 = Record<string, unknown>;
415
418
  /**
416
419
  * Validate the security posture of a stack. Returns findings (empty = clean).
417
420
  * `error` findings gate the build in `os compile`; `info` is advisory.
421
+ *
422
+ * `opts.nowMs` injects the clock for the ADR-0091 authoring-time expiry rule
423
+ * (tests); production callers omit it.
418
424
  */
419
- declare function validateSecurityPosture(stack: AnyRec$1): SecurityFinding[];
425
+ declare function validateSecurityPosture(stack: AnyRec$1, opts?: {
426
+ nowMs?: number;
427
+ }): SecurityFinding[];
420
428
 
421
429
  /**
422
430
  * [ADR-0090 D6] Access-matrix snapshot — authoring-time companion to the
@@ -442,4 +450,4 @@ declare function buildAccessMatrix(stack: AnyRec): AccessMatrix;
442
450
  */
443
451
  declare function diffAccessMatrix(before: AccessMatrix, after: AccessMatrix): string[];
444
452
 
445
- export { APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_ROLE_NOT_MEMBERSHIP_TIER, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_EXTERNAL_WIDER, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateWidgetBindings };
453
+ export { APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_ROLE_NOT_MEMBERSHIP_TIER, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateWidgetBindings };
package/dist/index.js CHANGED
@@ -1391,6 +1391,9 @@ var SECURITY_ROLE_WORD = "security-role-word";
1391
1391
  var SECURITY_BOOK_AUDIENCE_UNKNOWN_SET = "security-book-audience-unknown-set";
1392
1392
  var SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
1393
1393
  var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
1394
+ var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
1395
+ var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
1396
+ var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
1394
1397
  var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
1395
1398
  var OWD_ALIAS_FIX = {
1396
1399
  read: "public_read",
@@ -1439,7 +1442,7 @@ function firstMasterDetailField(obj) {
1439
1442
  function grantsObjectAccess(p) {
1440
1443
  return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
1441
1444
  }
1442
- function validateSecurityPosture(stack) {
1445
+ function validateSecurityPosture(stack, opts) {
1443
1446
  const findings = [];
1444
1447
  if (!stack || typeof stack !== "object") return findings;
1445
1448
  const objects = asArray14(stack.objects);
@@ -1509,6 +1512,18 @@ function validateSecurityPosture(stack) {
1509
1512
  const psName = typeof ps.name === "string" ? ps.name : `(permission set ${i})`;
1510
1513
  const psPath = `permissions[${i}]`;
1511
1514
  const objectsMap = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
1515
+ const flsMap = ps.fields && typeof ps.fields === "object" ? ps.fields : {};
1516
+ for (const flsKey of Object.keys(flsMap)) {
1517
+ if (flsKey.includes(".")) continue;
1518
+ findings.push({
1519
+ severity: "error",
1520
+ rule: SECURITY_FLS_UNQUALIFIED_KEY,
1521
+ where: `permission set "${psName}"`,
1522
+ path: `${psPath}.fields["${flsKey}"]`,
1523
+ 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.`,
1524
+ hint: `Qualify the key with its object, e.g. 'crm_opportunity.${flsKey}': { readable: true, editable: false }.`
1525
+ });
1526
+ }
1512
1527
  const wildcard = objectsMap["*"];
1513
1528
  if (wildcard && (wildcard.viewAllRecords === true || wildcard.modifyAllRecords === true)) {
1514
1529
  findings.push({
@@ -1660,6 +1675,45 @@ function validateSecurityPosture(stack) {
1660
1675
  }
1661
1676
  }
1662
1677
  }
1678
+ const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
1679
+ const nowMs = opts?.nowMs ?? Date.now();
1680
+ for (const [i, seed] of asArray14(stack.data).entries()) {
1681
+ const seedObject = typeof seed.object === "string" ? seed.object : "";
1682
+ if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
1683
+ const records = Array.isArray(seed.records) ? seed.records : [];
1684
+ for (let j = 0; j < records.length; j++) {
1685
+ const rec = records[j] ?? {};
1686
+ const where = `seed "${seedObject}" record #${j}`;
1687
+ const until = rec.valid_until;
1688
+ if (until != null && until !== "") {
1689
+ const ms = typeof until === "number" ? until < 1e12 ? until * 1e3 : until : until instanceof Date ? until.getTime() : typeof until === "string" ? Date.parse(until) : Number.NaN;
1690
+ if (Number.isNaN(ms) || ms <= nowMs) {
1691
+ findings.push({
1692
+ severity: "error",
1693
+ rule: SECURITY_GRANT_EXPIRED_AT_AUTHORING,
1694
+ where,
1695
+ path: `data[${i}].records[${j}].valid_until`,
1696
+ 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).`,
1697
+ 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.`
1698
+ });
1699
+ }
1700
+ }
1701
+ const delegatedFrom = rec.delegated_from;
1702
+ if (delegatedFrom != null && delegatedFrom !== "") {
1703
+ const reason = rec.reason;
1704
+ if (typeof reason !== "string" || reason.trim().length === 0) {
1705
+ findings.push({
1706
+ severity: "error",
1707
+ rule: SECURITY_DELEGATION_MISSING_REASON,
1708
+ where,
1709
+ path: `data[${i}].records[${j}].reason`,
1710
+ 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).`,
1711
+ hint: `Add reason: 'vacation stand-in for \u5F20\u4E09, 2026-08-01..15' (free text, required).`
1712
+ });
1713
+ }
1714
+ }
1715
+ }
1716
+ }
1663
1717
  return findings;
1664
1718
  }
1665
1719
 
@@ -1767,7 +1821,10 @@ export {
1767
1821
  PAGE_SOURCE_CLASSNAME,
1768
1822
  SECURITY_ANCHOR_HIGH_PRIVILEGE,
1769
1823
  SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
1824
+ SECURITY_DELEGATION_MISSING_REASON,
1770
1825
  SECURITY_EXTERNAL_WIDER,
1826
+ SECURITY_GRANT_EXPIRED_AT_AUTHORING,
1827
+ SECURITY_MASTER_DETAIL_UNGRANTED,
1771
1828
  SECURITY_OWD_ALIAS,
1772
1829
  SECURITY_OWD_UNSET,
1773
1830
  SECURITY_PRIVATE_NO_READSCOPE,