@sequenceholdings/orm 2.2.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.
@@ -11,7 +11,7 @@ type PolicyOperand =
11
11
  kind: 'path';
12
12
  segments: readonly string[];
13
13
  }
14
- /** `claim('sub')` / `claim('org')` — the only claim() keys in CLAIMS_CONTRACT. */
14
+ /** `claim('sub')` / `claim('org')` / `claim('ext_subject')` / `claim('ext_email')` — the scalar keys in CLAIMS_CONTRACT. */
15
15
  | {
16
16
  kind: 'claim';
17
17
  key: ClaimKey;
@@ -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';
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
@@ -90,15 +100,28 @@ declare function parsePolicy(source: string): ParsedPolicy;
90
100
  * - a comparison must reference exactly ONE column side (column↔column and
91
101
  * claims↔literal comparisons have no PredIr shape in v2.0);
92
102
  * - `col = null` / `col != null` normalize to the isNull operator;
103
+ * - verified-borrower claims (`claim('ext_subject')`, `claim('ext_email')`)
104
+ * are text: they compare to a text column with `=` only, never under
105
+ * not(), so no negation, ordering or pattern can widen a borrower's rows;
93
106
  * - variant `when` guards are claims-only: capability()/role() under
94
107
  * and/or/not. (claim() comparisons in `when` are not representable in the
95
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.
96
114
  */
97
115
 
98
116
  declare const POLICY_MAX_LINK_DEPTH = 3;
99
117
  interface PolicyColumnInfo {
100
118
  readonly name: string;
101
119
  readonly columnName: string;
120
+ /**
121
+ * The column's IR type when the resolver knows it (TableMetaIr always does).
122
+ * Required for a verified-borrower claim comparison: unknown is refused.
123
+ */
124
+ readonly type?: unknown;
102
125
  }
103
126
  interface PolicyLinkInfo {
104
127
  readonly name: string;
@@ -112,6 +135,19 @@ interface PolicyTableInfo {
112
135
  /** `{ tables }` keyed by table name — TableMetaIr's record is assignable. */
113
136
  interface PolicyResolver {
114
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;
115
151
  }
116
152
  interface CompilePredicateInput {
117
153
  /** DSL source text, or an already-parsed policy. */
@@ -123,11 +159,20 @@ interface CompilePredicateInput {
123
159
  declare function compilePredicateV2({ expression, table, resolver, maxLinkDepth }: CompilePredicateInput): PredIr;
124
160
  /** Variant `when` guards may only test claims: capability()/role() under and/or/not. */
125
161
  declare function assertClaimsOnlyWhen(parsed: ParsedPolicy): void;
162
+ /** Which of a table's policies is being compiled. */
163
+ type PolicyAccess = 'read' | 'write' | 'insert' | 'update';
126
164
  interface CompilePolicyInput {
127
165
  expression: PolicyExpression;
128
166
  table: string;
129
167
  resolver: PolicyResolver;
130
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;
131
176
  }
132
177
  /**
133
178
  * Policy expression (string or `{default, variants}`) → one PredIr under §4c
@@ -138,6 +183,6 @@ interface CompilePolicyInput {
138
183
  * Granting a role/capability only ever widens access; `allow: true`
139
184
  * short-circuits its branch to the guard alone.
140
185
  */
141
- declare function compilePolicy({ expression, table, resolver, maxLinkDepth }: CompilePolicyInput): PredIr;
186
+ declare function compilePolicy({ expression, table, resolver, maxLinkDepth, access }: CompilePolicyInput): PredIr;
142
187
 
143
- 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 };
@@ -11,7 +11,7 @@ type PolicyOperand =
11
11
  kind: 'path';
12
12
  segments: readonly string[];
13
13
  }
14
- /** `claim('sub')` / `claim('org')` — the only claim() keys in CLAIMS_CONTRACT. */
14
+ /** `claim('sub')` / `claim('org')` / `claim('ext_subject')` / `claim('ext_email')` — the scalar keys in CLAIMS_CONTRACT. */
15
15
  | {
16
16
  kind: 'claim';
17
17
  key: ClaimKey;
@@ -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';
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
@@ -90,15 +100,28 @@ declare function parsePolicy(source: string): ParsedPolicy;
90
100
  * - a comparison must reference exactly ONE column side (column↔column and
91
101
  * claims↔literal comparisons have no PredIr shape in v2.0);
92
102
  * - `col = null` / `col != null` normalize to the isNull operator;
103
+ * - verified-borrower claims (`claim('ext_subject')`, `claim('ext_email')`)
104
+ * are text: they compare to a text column with `=` only, never under
105
+ * not(), so no negation, ordering or pattern can widen a borrower's rows;
93
106
  * - variant `when` guards are claims-only: capability()/role() under
94
107
  * and/or/not. (claim() comparisons in `when` are not representable in the
95
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.
96
114
  */
97
115
 
98
116
  declare const POLICY_MAX_LINK_DEPTH = 3;
99
117
  interface PolicyColumnInfo {
100
118
  readonly name: string;
101
119
  readonly columnName: string;
120
+ /**
121
+ * The column's IR type when the resolver knows it (TableMetaIr always does).
122
+ * Required for a verified-borrower claim comparison: unknown is refused.
123
+ */
124
+ readonly type?: unknown;
102
125
  }
103
126
  interface PolicyLinkInfo {
104
127
  readonly name: string;
@@ -112,6 +135,19 @@ interface PolicyTableInfo {
112
135
  /** `{ tables }` keyed by table name — TableMetaIr's record is assignable. */
113
136
  interface PolicyResolver {
114
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;
115
151
  }
116
152
  interface CompilePredicateInput {
117
153
  /** DSL source text, or an already-parsed policy. */
@@ -123,11 +159,20 @@ interface CompilePredicateInput {
123
159
  declare function compilePredicateV2({ expression, table, resolver, maxLinkDepth }: CompilePredicateInput): PredIr;
124
160
  /** Variant `when` guards may only test claims: capability()/role() under and/or/not. */
125
161
  declare function assertClaimsOnlyWhen(parsed: ParsedPolicy): void;
162
+ /** Which of a table's policies is being compiled. */
163
+ type PolicyAccess = 'read' | 'write' | 'insert' | 'update';
126
164
  interface CompilePolicyInput {
127
165
  expression: PolicyExpression;
128
166
  table: string;
129
167
  resolver: PolicyResolver;
130
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;
131
176
  }
132
177
  /**
133
178
  * Policy expression (string or `{default, variants}`) → one PredIr under §4c
@@ -138,6 +183,6 @@ interface CompilePolicyInput {
138
183
  * Granting a role/capability only ever widens access; `allow: true`
139
184
  * short-circuits its branch to the guard alone.
140
185
  */
141
- declare function compilePolicy({ expression, table, resolver, maxLinkDepth }: CompilePolicyInput): PredIr;
186
+ declare function compilePolicy({ expression, table, resolver, maxLinkDepth, access }: CompilePolicyInput): PredIr;
142
187
 
143
- 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 };
@@ -1056,14 +1056,20 @@ z.object({ irVersion: irVersionSchema });
1056
1056
  var CLAIMS_CONTRACT = {
1057
1057
  sub: "string",
1058
1058
  org: "string?",
1059
+ ext_subject: "string?",
1060
+ ext_email: "string?",
1059
1061
  roles: "string[]",
1060
1062
  capabilities: "string[]"
1061
1063
  };
1064
+ var EXTERNAL_POLICY_CLAIM_KEYS = ["ext_subject", "ext_email"];
1065
+ function isExternalPolicyClaimKey(key) {
1066
+ return EXTERNAL_POLICY_CLAIM_KEYS.includes(key);
1067
+ }
1062
1068
  var scalarLiteral = z.union([z.string(), z.number(), z.boolean(), z.null()]);
1063
1069
  var valueIrSchema = z.discriminatedUnion("kind", [
1064
1070
  z.object({ kind: z.literal("literal"), value: z.union([scalarLiteral, z.array(scalarLiteral)]) }),
1065
1071
  z.object({ kind: z.literal("variable"), name: z.string() }),
1066
- z.object({ kind: z.literal("claim"), key: z.enum(["sub", "org"]) })
1072
+ z.object({ kind: z.literal("claim"), key: z.enum(["sub", "org", ...EXTERNAL_POLICY_CLAIM_KEYS]) })
1067
1073
  ]);
1068
1074
  var CMP_OPS = [
1069
1075
  "eq",
@@ -1634,6 +1640,7 @@ var Parser2 = class {
1634
1640
  return node;
1635
1641
  }
1636
1642
  if (this.peekKeyword("exists")) return this.parseExists();
1643
+ if (this.peekKeyword("visible") && this.tokens[this.index + 1]?.kind === "lparen") return this.parseVisible();
1637
1644
  return this.parseComparison();
1638
1645
  }
1639
1646
  parseBooleanCall(fn) {
@@ -1653,6 +1660,13 @@ var Parser2 = class {
1653
1660
  this.expect("rparen", "')' closing exists(...)");
1654
1661
  return { kind: "exists", path, where };
1655
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
+ }
1656
1670
  parseComparison() {
1657
1671
  const left = this.parseOperand();
1658
1672
  const operator = this.expect("operator", "a comparison operator");
@@ -1768,6 +1782,24 @@ function parsePolicy(source) {
1768
1782
 
1769
1783
  // src/predicate-v2/compile.ts
1770
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
+ }
1771
1803
  var CMP_BY_OP = {
1772
1804
  "=": "eq",
1773
1805
  "!=": "neq",
@@ -1801,7 +1833,7 @@ function resolveLink(info, ref) {
1801
1833
  function columnCandidates(info) {
1802
1834
  return [...info.columns.map((c) => c.name), ...info.links.map((l) => l.name)];
1803
1835
  }
1804
- var PolicyCompiler = class {
1836
+ var PolicyCompiler = class _PolicyCompiler {
1805
1837
  constructor(ctx) {
1806
1838
  this.ctx = ctx;
1807
1839
  }
@@ -1813,13 +1845,16 @@ var PolicyCompiler = class {
1813
1845
  case "or":
1814
1846
  return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
1815
1847
  case "not":
1816
- return { kind: "not", operand: this.compile(node.operand, table, depth) };
1848
+ assertNoNegatedExternalClaim(node.operand);
1849
+ return { kind: "not", operand: new _PolicyCompiler({ ...this.ctx, negated: true }).compile(node.operand, table, depth) };
1817
1850
  case "capability":
1818
1851
  return { kind: "capability", key: node.key };
1819
1852
  case "role":
1820
1853
  return { kind: "role", key: node.key };
1821
1854
  case "exists":
1822
1855
  return this.compileExists(node.path, node.where, table, depth);
1856
+ case "visible":
1857
+ return this.compileVisible(node.path, table, depth);
1823
1858
  case "compare":
1824
1859
  return this.compileCompare(node, table, depth);
1825
1860
  }
@@ -1833,6 +1868,36 @@ var PolicyCompiler = class {
1833
1868
  }
1834
1869
  return pred;
1835
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
+ }
1836
1901
  resolveLinkPath(path, table, depth, { toOneOnly }) {
1837
1902
  const links = [];
1838
1903
  let current = table;
@@ -1892,6 +1957,10 @@ var PolicyCompiler = class {
1892
1957
  columnCandidates(info)
1893
1958
  );
1894
1959
  }
1960
+ if (valueSide.kind === "claim" && isExternalPolicyClaimKey(valueSide.key)) {
1961
+ if (this.ctx.negated) throw negatedExternalClaim(valueSide.key);
1962
+ assertExternalClaimComparison({ op, key: valueSide.key, column: info.columns.find((c) => c.name === column) });
1963
+ }
1895
1964
  let cmp = this.buildCmp(op, column, valueSide);
1896
1965
  for (let i = links.length - 1; i >= 0; i -= 1) {
1897
1966
  cmp = { kind: "exists", link: links[i].name, where: cmp };
@@ -1912,10 +1981,45 @@ var PolicyCompiler = class {
1912
1981
  return { kind: "cmp", op: CMP_BY_OP[op], column, value };
1913
1982
  }
1914
1983
  };
1984
+ function assertExternalClaimComparison({ op, key, column }) {
1985
+ if (op !== "=") {
1986
+ throw new PolicyCompileError(
1987
+ `claim('${key}') supports = comparisons only, got '${op}'`,
1988
+ "unsupported-comparison"
1989
+ );
1990
+ }
1991
+ if (column?.type !== "text") {
1992
+ const actual = column?.type === void 0 ? "of unknown type" : typeof column.type === "string" ? column.type : "an enum";
1993
+ throw new PolicyCompileError(
1994
+ `claim('${key}') is text and compares to a text column only \u2014 '${column?.name ?? "?"}' is ${actual}`,
1995
+ "claim-type-mismatch"
1996
+ );
1997
+ }
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
+ }
2005
+ function assertNoNegatedExternalClaim(operand) {
2006
+ walkPolicy(operand, (node) => {
2007
+ if (node.kind !== "compare") return;
2008
+ const claim = [node.left, node.right].find((side) => side.kind === "claim" && isExternalPolicyClaimKey(side.key));
2009
+ if (claim?.kind === "claim") throw negatedExternalClaim(claim.key);
2010
+ });
2011
+ }
1915
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 }) {
1916
2021
  const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
1917
- const compiler = new PolicyCompiler({ resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH });
1918
- return compiler.compile(parsed.ast, table, 0);
2022
+ return new PolicyCompiler(ctx).compile(parsed.ast, table, depth);
1919
2023
  }
1920
2024
  function assertClaimsOnlyWhen(parsed) {
1921
2025
  walkPolicy(parsed.ast, (node) => {
@@ -1932,25 +2036,34 @@ function assertClaimsOnlyWhen(parsed) {
1932
2036
  "when-not-claims-only"
1933
2037
  );
1934
2038
  }
1935
- if (node.kind === "exists") {
2039
+ if (node.kind === "exists" || node.kind === "visible") {
1936
2040
  throw new PolicyCompileError(
1937
- "variant 'when' guards are claims-only \u2014 exists() is not allowed",
2041
+ `variant 'when' guards are claims-only \u2014 ${node.kind}() is not allowed`,
1938
2042
  "when-not-claims-only"
1939
2043
  );
1940
2044
  }
1941
2045
  });
1942
2046
  }
1943
- function compilePolicy({ expression, table, resolver, maxLinkDepth }) {
1944
- if (typeof expression === "string") {
1945
- return compilePredicateV2({ expression, table, resolver, maxLinkDepth });
1946
- }
1947
- 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 });
1948
2061
  if (expression.variants.length === 0) return defaultPred;
1949
2062
  const branches = expression.variants.map((variant) => {
1950
2063
  const parsedWhen = parsePolicy(variant.when);
1951
2064
  assertClaimsOnlyWhen(parsedWhen);
1952
- const when = compilePredicateV2({ expression: parsedWhen, table, resolver, maxLinkDepth });
1953
- 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 });
1954
2067
  return { kind: "and", operands: [when, grant] };
1955
2068
  });
1956
2069
  return { kind: "or", operands: [defaultPred, ...branches] };