@sequenceholdings/orm 2.3.0 → 2.4.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.
@@ -50,6 +50,16 @@ type PolicyNode = {
50
50
  kind: 'exists';
51
51
  path: readonly string[];
52
52
  where: PolicyNode;
53
+ }
54
+ /**
55
+ * `visible(<link-path>)` — a related row exists AND the caller may read it:
56
+ * the final target's own read policy is compiled in as the inner predicate.
57
+ * Lets a write policy say "only on a parent you can see" without restating
58
+ * the parent's rule (and without drifting from it).
59
+ */
60
+ | {
61
+ kind: 'visible';
62
+ path: readonly string[];
53
63
  };
54
64
  interface ParsedPolicy {
55
65
  readonly source: string;
@@ -63,7 +73,7 @@ declare class PolicyParseError extends Error {
63
73
  readonly name = "PolicyParseError";
64
74
  constructor(message: string, position: number);
65
75
  }
66
- type PolicyCompileErrorCode = 'unknown-table' | 'unknown-column' | 'unknown-link' | 'not-to-one' | 'link-depth-exceeded' | 'unsupported-comparison' | 'invalid-null-comparison' | 'when-not-claims-only' | 'claim-type-mismatch';
76
+ type PolicyCompileErrorCode = 'unknown-table' | 'unknown-column' | 'unknown-link' | 'not-to-one' | 'link-depth-exceeded' | 'unsupported-comparison' | 'invalid-null-comparison' | 'when-not-claims-only' | 'claim-type-mismatch' | 'visible-unresolvable' | 'visible-cycle';
67
77
  /**
68
78
  * Compile-time failure — the parsed policy references something the table
69
79
  * doesn't declare, or uses a shape the v2.0 IR cannot represent. Carries
@@ -96,6 +106,11 @@ declare function parsePolicy(source: string): ParsedPolicy;
96
106
  * - variant `when` guards are claims-only: capability()/role() under
97
107
  * and/or/not. (claim() comparisons in `when` are not representable in the
98
108
  * v2.0 IR — a targeted error says so.)
109
+ * - `visible(<link-path>)` lowers to `exists(<link-path>, <target read policy>)`,
110
+ * compiled from the target's own read policy expression at the combined
111
+ * depth. A read policy that reaches itself again through visible() is a
112
+ * cycle and refused; a target with no read policy is readable to everyone
113
+ * who may use the namespace, so visibility reduces to existence.
99
114
  */
100
115
 
101
116
  declare const POLICY_MAX_LINK_DEPTH = 3;
@@ -120,6 +135,19 @@ interface PolicyTableInfo {
120
135
  /** `{ tables }` keyed by table name — TableMetaIr's record is assignable. */
121
136
  interface PolicyResolver {
122
137
  readonly tables: Record<string, PolicyTableInfo>;
138
+ /**
139
+ * A table's read policy expression, for `visible()`. Return `undefined` when
140
+ * the table has none (readable to every namespace user). A resolver without
141
+ * this callback cannot compile `visible()`: it is refused, never guessed.
142
+ */
143
+ readonly readPolicy?: (table: string) => PolicyExpression | undefined;
144
+ /**
145
+ * The namespace that owns a table's policies, in a composed namespace. A
146
+ * visible() expansion of an imported table's read policy evaluates its
147
+ * capability() and role() checks in that namespace, not the importer's.
148
+ * Omitted (or undefined for a table) leaves them unscoped.
149
+ */
150
+ readonly policyNamespace?: (table: string) => string | undefined;
123
151
  }
124
152
  interface CompilePredicateInput {
125
153
  /** DSL source text, or an already-parsed policy. */
@@ -131,11 +159,20 @@ interface CompilePredicateInput {
131
159
  declare function compilePredicateV2({ expression, table, resolver, maxLinkDepth }: CompilePredicateInput): PredIr;
132
160
  /** Variant `when` guards may only test claims: capability()/role() under and/or/not. */
133
161
  declare function assertClaimsOnlyWhen(parsed: ParsedPolicy): void;
162
+ /** Which of a table's policies is being compiled. */
163
+ type PolicyAccess = 'read' | 'write' | 'insert' | 'update';
134
164
  interface CompilePolicyInput {
135
165
  expression: PolicyExpression;
136
166
  table: string;
137
167
  resolver: PolicyResolver;
138
168
  maxLinkDepth?: number;
169
+ /**
170
+ * Default 'read'. A read policy that reaches its own table through visible()
171
+ * is a cycle; a write policy that follows a self-link (a subtask written
172
+ * under a parent the caller can read) only expands the read policy, so it
173
+ * does not start the cycle check at its own table.
174
+ */
175
+ access?: PolicyAccess;
139
176
  }
140
177
  /**
141
178
  * Policy expression (string or `{default, variants}`) → one PredIr under §4c
@@ -146,6 +183,6 @@ interface CompilePolicyInput {
146
183
  * Granting a role/capability only ever widens access; `allow: true`
147
184
  * short-circuits its branch to the guard alone.
148
185
  */
149
- declare function compilePolicy({ expression, table, resolver, maxLinkDepth }: CompilePolicyInput): PredIr;
186
+ declare function compilePolicy({ expression, table, resolver, maxLinkDepth, access }: CompilePolicyInput): PredIr;
150
187
 
151
- export { type CompilePolicyInput, type CompilePredicateInput, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, type ParsedPolicy, type PolicyColumnInfo, type PolicyCompareOp, PolicyCompileError, type PolicyCompileErrorCode, type PolicyLinkInfo, type PolicyNode, type PolicyOperand, PolicyParseError, type PolicyResolver, type PolicyTableInfo, assertClaimsOnlyWhen, compilePolicy, compilePredicateV2, parsePolicy, walkPolicy };
188
+ export { type CompilePolicyInput, type CompilePredicateInput, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, type ParsedPolicy, type PolicyAccess, type PolicyColumnInfo, type PolicyCompareOp, PolicyCompileError, type PolicyCompileErrorCode, type PolicyLinkInfo, type PolicyNode, type PolicyOperand, PolicyParseError, type PolicyResolver, type PolicyTableInfo, assertClaimsOnlyWhen, compilePolicy, compilePredicateV2, parsePolicy, walkPolicy };
@@ -50,6 +50,16 @@ type PolicyNode = {
50
50
  kind: 'exists';
51
51
  path: readonly string[];
52
52
  where: PolicyNode;
53
+ }
54
+ /**
55
+ * `visible(<link-path>)` — a related row exists AND the caller may read it:
56
+ * the final target's own read policy is compiled in as the inner predicate.
57
+ * Lets a write policy say "only on a parent you can see" without restating
58
+ * the parent's rule (and without drifting from it).
59
+ */
60
+ | {
61
+ kind: 'visible';
62
+ path: readonly string[];
53
63
  };
54
64
  interface ParsedPolicy {
55
65
  readonly source: string;
@@ -63,7 +73,7 @@ declare class PolicyParseError extends Error {
63
73
  readonly name = "PolicyParseError";
64
74
  constructor(message: string, position: number);
65
75
  }
66
- type PolicyCompileErrorCode = 'unknown-table' | 'unknown-column' | 'unknown-link' | 'not-to-one' | 'link-depth-exceeded' | 'unsupported-comparison' | 'invalid-null-comparison' | 'when-not-claims-only' | 'claim-type-mismatch';
76
+ type PolicyCompileErrorCode = 'unknown-table' | 'unknown-column' | 'unknown-link' | 'not-to-one' | 'link-depth-exceeded' | 'unsupported-comparison' | 'invalid-null-comparison' | 'when-not-claims-only' | 'claim-type-mismatch' | 'visible-unresolvable' | 'visible-cycle';
67
77
  /**
68
78
  * Compile-time failure — the parsed policy references something the table
69
79
  * doesn't declare, or uses a shape the v2.0 IR cannot represent. Carries
@@ -96,6 +106,11 @@ declare function parsePolicy(source: string): ParsedPolicy;
96
106
  * - variant `when` guards are claims-only: capability()/role() under
97
107
  * and/or/not. (claim() comparisons in `when` are not representable in the
98
108
  * v2.0 IR — a targeted error says so.)
109
+ * - `visible(<link-path>)` lowers to `exists(<link-path>, <target read policy>)`,
110
+ * compiled from the target's own read policy expression at the combined
111
+ * depth. A read policy that reaches itself again through visible() is a
112
+ * cycle and refused; a target with no read policy is readable to everyone
113
+ * who may use the namespace, so visibility reduces to existence.
99
114
  */
100
115
 
101
116
  declare const POLICY_MAX_LINK_DEPTH = 3;
@@ -120,6 +135,19 @@ interface PolicyTableInfo {
120
135
  /** `{ tables }` keyed by table name — TableMetaIr's record is assignable. */
121
136
  interface PolicyResolver {
122
137
  readonly tables: Record<string, PolicyTableInfo>;
138
+ /**
139
+ * A table's read policy expression, for `visible()`. Return `undefined` when
140
+ * the table has none (readable to every namespace user). A resolver without
141
+ * this callback cannot compile `visible()`: it is refused, never guessed.
142
+ */
143
+ readonly readPolicy?: (table: string) => PolicyExpression | undefined;
144
+ /**
145
+ * The namespace that owns a table's policies, in a composed namespace. A
146
+ * visible() expansion of an imported table's read policy evaluates its
147
+ * capability() and role() checks in that namespace, not the importer's.
148
+ * Omitted (or undefined for a table) leaves them unscoped.
149
+ */
150
+ readonly policyNamespace?: (table: string) => string | undefined;
123
151
  }
124
152
  interface CompilePredicateInput {
125
153
  /** DSL source text, or an already-parsed policy. */
@@ -131,11 +159,20 @@ interface CompilePredicateInput {
131
159
  declare function compilePredicateV2({ expression, table, resolver, maxLinkDepth }: CompilePredicateInput): PredIr;
132
160
  /** Variant `when` guards may only test claims: capability()/role() under and/or/not. */
133
161
  declare function assertClaimsOnlyWhen(parsed: ParsedPolicy): void;
162
+ /** Which of a table's policies is being compiled. */
163
+ type PolicyAccess = 'read' | 'write' | 'insert' | 'update';
134
164
  interface CompilePolicyInput {
135
165
  expression: PolicyExpression;
136
166
  table: string;
137
167
  resolver: PolicyResolver;
138
168
  maxLinkDepth?: number;
169
+ /**
170
+ * Default 'read'. A read policy that reaches its own table through visible()
171
+ * is a cycle; a write policy that follows a self-link (a subtask written
172
+ * under a parent the caller can read) only expands the read policy, so it
173
+ * does not start the cycle check at its own table.
174
+ */
175
+ access?: PolicyAccess;
139
176
  }
140
177
  /**
141
178
  * Policy expression (string or `{default, variants}`) → one PredIr under §4c
@@ -146,6 +183,6 @@ interface CompilePolicyInput {
146
183
  * Granting a role/capability only ever widens access; `allow: true`
147
184
  * short-circuits its branch to the guard alone.
148
185
  */
149
- declare function compilePolicy({ expression, table, resolver, maxLinkDepth }: CompilePolicyInput): PredIr;
186
+ declare function compilePolicy({ expression, table, resolver, maxLinkDepth, access }: CompilePolicyInput): PredIr;
150
187
 
151
- export { type CompilePolicyInput, type CompilePredicateInput, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, type ParsedPolicy, type PolicyColumnInfo, type PolicyCompareOp, PolicyCompileError, type PolicyCompileErrorCode, type PolicyLinkInfo, type PolicyNode, type PolicyOperand, PolicyParseError, type PolicyResolver, type PolicyTableInfo, assertClaimsOnlyWhen, compilePolicy, compilePredicateV2, parsePolicy, walkPolicy };
188
+ export { type CompilePolicyInput, type CompilePredicateInput, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, type ParsedPolicy, type PolicyAccess, type PolicyColumnInfo, type PolicyCompareOp, PolicyCompileError, type PolicyCompileErrorCode, type PolicyLinkInfo, type PolicyNode, type PolicyOperand, PolicyParseError, type PolicyResolver, type PolicyTableInfo, assertClaimsOnlyWhen, compilePolicy, compilePredicateV2, parsePolicy, walkPolicy };
@@ -1640,6 +1640,7 @@ var Parser2 = class {
1640
1640
  return node;
1641
1641
  }
1642
1642
  if (this.peekKeyword("exists")) return this.parseExists();
1643
+ if (this.peekKeyword("visible") && this.tokens[this.index + 1]?.kind === "lparen") return this.parseVisible();
1643
1644
  return this.parseComparison();
1644
1645
  }
1645
1646
  parseBooleanCall(fn) {
@@ -1659,6 +1660,13 @@ var Parser2 = class {
1659
1660
  this.expect("rparen", "')' closing exists(...)");
1660
1661
  return { kind: "exists", path, where };
1661
1662
  }
1663
+ parseVisible() {
1664
+ this.advance();
1665
+ this.expect("lparen", "'(' after visible");
1666
+ const path = this.parsePath("a link path in visible(...)");
1667
+ this.expect("rparen", "')' closing visible(...)");
1668
+ return { kind: "visible", path };
1669
+ }
1662
1670
  parseComparison() {
1663
1671
  const left = this.parseOperand();
1664
1672
  const operator = this.expect("operator", "a comparison operator");
@@ -1774,6 +1782,24 @@ function parsePolicy(source) {
1774
1782
 
1775
1783
  // src/predicate-v2/compile.ts
1776
1784
  var POLICY_MAX_LINK_DEPTH = 3;
1785
+ function scopeToNamespace(pred, namespace) {
1786
+ switch (pred.kind) {
1787
+ case "capability":
1788
+ case "role":
1789
+ return pred.namespace ? pred : { ...pred, namespace };
1790
+ case "and":
1791
+ case "or":
1792
+ return { ...pred, operands: pred.operands.map((operand) => scopeToNamespace(operand, namespace)) };
1793
+ case "not":
1794
+ return { ...pred, operand: scopeToNamespace(pred.operand, namespace) };
1795
+ case "exists":
1796
+ return { ...pred, where: scopeToNamespace(pred.where, namespace) };
1797
+ case "aggPredicate":
1798
+ return pred.where ? { ...pred, where: scopeToNamespace(pred.where, namespace) } : pred;
1799
+ default:
1800
+ return pred;
1801
+ }
1802
+ }
1777
1803
  var CMP_BY_OP = {
1778
1804
  "=": "eq",
1779
1805
  "!=": "neq",
@@ -1807,7 +1833,7 @@ function resolveLink(info, ref) {
1807
1833
  function columnCandidates(info) {
1808
1834
  return [...info.columns.map((c) => c.name), ...info.links.map((l) => l.name)];
1809
1835
  }
1810
- var PolicyCompiler = class {
1836
+ var PolicyCompiler = class _PolicyCompiler {
1811
1837
  constructor(ctx) {
1812
1838
  this.ctx = ctx;
1813
1839
  }
@@ -1820,13 +1846,15 @@ var PolicyCompiler = class {
1820
1846
  return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
1821
1847
  case "not":
1822
1848
  assertNoNegatedExternalClaim(node.operand);
1823
- return { kind: "not", operand: this.compile(node.operand, table, depth) };
1849
+ return { kind: "not", operand: new _PolicyCompiler({ ...this.ctx, negated: true }).compile(node.operand, table, depth) };
1824
1850
  case "capability":
1825
1851
  return { kind: "capability", key: node.key };
1826
1852
  case "role":
1827
1853
  return { kind: "role", key: node.key };
1828
1854
  case "exists":
1829
1855
  return this.compileExists(node.path, node.where, table, depth);
1856
+ case "visible":
1857
+ return this.compileVisible(node.path, table, depth);
1830
1858
  case "compare":
1831
1859
  return this.compileCompare(node, table, depth);
1832
1860
  }
@@ -1840,6 +1868,36 @@ var PolicyCompiler = class {
1840
1868
  }
1841
1869
  return pred;
1842
1870
  }
1871
+ /** `visible(path)` → `exists(path, <final target's read policy>)`. */
1872
+ compileVisible(path, table, depth) {
1873
+ const { links, finalTable } = this.resolveLinkPath(path, table, depth, { toOneOnly: false });
1874
+ const readPolicy = this.ctx.resolver.readPolicy;
1875
+ if (!readPolicy) {
1876
+ throw new PolicyCompileError(
1877
+ `visible(${path.join(".")}) needs the target's read policy, which this compile context does not provide`,
1878
+ "visible-unresolvable"
1879
+ );
1880
+ }
1881
+ if (this.ctx.visiting.includes(finalTable)) {
1882
+ throw new PolicyCompileError(
1883
+ `visible(${path.join(".")}) on '${table}' reaches '${finalTable}' whose read policy is already being expanded (${[...this.ctx.visiting, finalTable].join(" \u2192 ")})`,
1884
+ "visible-cycle"
1885
+ );
1886
+ }
1887
+ const expression = readPolicy(finalTable);
1888
+ let pred = expression === void 0 ? { kind: "const", value: true } : compilePolicyAt({
1889
+ expression,
1890
+ table: finalTable,
1891
+ ctx: { ...this.ctx, visiting: [...this.ctx.visiting, finalTable] },
1892
+ depth: depth + links.length
1893
+ });
1894
+ const owner = this.ctx.resolver.policyNamespace?.(finalTable);
1895
+ if (owner) pred = scopeToNamespace(pred, owner);
1896
+ for (let i = links.length - 1; i >= 0; i -= 1) {
1897
+ pred = { kind: "exists", link: links[i].name, where: pred };
1898
+ }
1899
+ return pred;
1900
+ }
1843
1901
  resolveLinkPath(path, table, depth, { toOneOnly }) {
1844
1902
  const links = [];
1845
1903
  let current = table;
@@ -1900,6 +1958,7 @@ var PolicyCompiler = class {
1900
1958
  );
1901
1959
  }
1902
1960
  if (valueSide.kind === "claim" && isExternalPolicyClaimKey(valueSide.key)) {
1961
+ if (this.ctx.negated) throw negatedExternalClaim(valueSide.key);
1903
1962
  assertExternalClaimComparison({ op, key: valueSide.key, column: info.columns.find((c) => c.name === column) });
1904
1963
  }
1905
1964
  let cmp = this.buildCmp(op, column, valueSide);
@@ -1937,22 +1996,30 @@ function assertExternalClaimComparison({ op, key, column }) {
1937
1996
  );
1938
1997
  }
1939
1998
  }
1999
+ function negatedExternalClaim(key) {
2000
+ return new PolicyCompileError(
2001
+ `claim('${key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
2002
+ "unsupported-comparison"
2003
+ );
2004
+ }
1940
2005
  function assertNoNegatedExternalClaim(operand) {
1941
2006
  walkPolicy(operand, (node) => {
1942
2007
  if (node.kind !== "compare") return;
1943
2008
  const claim = [node.left, node.right].find((side) => side.kind === "claim" && isExternalPolicyClaimKey(side.key));
1944
- if (claim?.kind === "claim") {
1945
- throw new PolicyCompileError(
1946
- `claim('${claim.key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
1947
- "unsupported-comparison"
1948
- );
1949
- }
2009
+ if (claim?.kind === "claim") throw negatedExternalClaim(claim.key);
1950
2010
  });
1951
2011
  }
1952
2012
  function compilePredicateV2({ expression, table, resolver, maxLinkDepth }) {
2013
+ return compilePredicateAt({
2014
+ expression,
2015
+ table,
2016
+ depth: 0,
2017
+ ctx: { resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH, visiting: [table] }
2018
+ });
2019
+ }
2020
+ function compilePredicateAt({ expression, table, ctx, depth }) {
1953
2021
  const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
1954
- const compiler = new PolicyCompiler({ resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH });
1955
- return compiler.compile(parsed.ast, table, 0);
2022
+ return new PolicyCompiler(ctx).compile(parsed.ast, table, depth);
1956
2023
  }
1957
2024
  function assertClaimsOnlyWhen(parsed) {
1958
2025
  walkPolicy(parsed.ast, (node) => {
@@ -1969,25 +2036,34 @@ function assertClaimsOnlyWhen(parsed) {
1969
2036
  "when-not-claims-only"
1970
2037
  );
1971
2038
  }
1972
- if (node.kind === "exists") {
2039
+ if (node.kind === "exists" || node.kind === "visible") {
1973
2040
  throw new PolicyCompileError(
1974
- "variant 'when' guards are claims-only \u2014 exists() is not allowed",
2041
+ `variant 'when' guards are claims-only \u2014 ${node.kind}() is not allowed`,
1975
2042
  "when-not-claims-only"
1976
2043
  );
1977
2044
  }
1978
2045
  });
1979
2046
  }
1980
- function compilePolicy({ expression, table, resolver, maxLinkDepth }) {
1981
- if (typeof expression === "string") {
1982
- return compilePredicateV2({ expression, table, resolver, maxLinkDepth });
1983
- }
1984
- const defaultPred = compilePredicateV2({ expression: expression.default, table, resolver, maxLinkDepth });
2047
+ function initialVisiting({ table, access }) {
2048
+ return (access ?? "read") === "read" ? [table] : [];
2049
+ }
2050
+ function compilePolicy({ expression, table, resolver, maxLinkDepth, access }) {
2051
+ return compilePolicyAt({
2052
+ expression,
2053
+ table,
2054
+ depth: 0,
2055
+ ctx: { resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH, visiting: initialVisiting({ table, access }) }
2056
+ });
2057
+ }
2058
+ function compilePolicyAt({ expression, table, ctx, depth }) {
2059
+ if (typeof expression === "string") return compilePredicateAt({ expression, table, ctx, depth });
2060
+ const defaultPred = compilePredicateAt({ expression: expression.default, table, ctx, depth });
1985
2061
  if (expression.variants.length === 0) return defaultPred;
1986
2062
  const branches = expression.variants.map((variant) => {
1987
2063
  const parsedWhen = parsePolicy(variant.when);
1988
2064
  assertClaimsOnlyWhen(parsedWhen);
1989
- const when = compilePredicateV2({ expression: parsedWhen, table, resolver, maxLinkDepth });
1990
- const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateV2({ expression: variant.policy, table, resolver, maxLinkDepth });
2065
+ const when = compilePredicateAt({ expression: parsedWhen, table, ctx, depth });
2066
+ const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateAt({ expression: variant.policy, table, ctx, depth });
1991
2067
  return { kind: "and", operands: [when, grant] };
1992
2068
  });
1993
2069
  return { kind: "or", operands: [defaultPred, ...branches] };