@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.
- package/dist/cli.cjs +132 -61
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +132 -61
- package/dist/cli.js.map +1 -1
- package/dist/compiler.cjs +139 -33
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +6 -4
- package/dist/compiler.d.ts +6 -4
- package/dist/compiler.js +139 -33
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +9 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +9 -1
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +89 -3
- package/dist/ir.d.ts +89 -3
- package/dist/ir.js +8 -2
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +127 -14
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +49 -4
- package/dist/predicate-v2.d.ts +49 -4
- package/dist/predicate-v2.js +127 -14
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +2 -1
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.js +2 -1
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
package/dist/predicate-v2.d.cts
CHANGED
|
@@ -11,7 +11,7 @@ type PolicyOperand =
|
|
|
11
11
|
kind: 'path';
|
|
12
12
|
segments: readonly string[];
|
|
13
13
|
}
|
|
14
|
-
/** `claim('sub')` / `claim('org')`
|
|
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 };
|
package/dist/predicate-v2.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ type PolicyOperand =
|
|
|
11
11
|
kind: 'path';
|
|
12
12
|
segments: readonly string[];
|
|
13
13
|
}
|
|
14
|
-
/** `claim('sub')` / `claim('org')`
|
|
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 };
|
package/dist/predicate-v2.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
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 =
|
|
1953
|
-
const grant = variant.allow === true ? { kind: "const", value: true } :
|
|
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] };
|