@crediolabs/policy-synth 0.3.1 → 0.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.
Files changed (100) hide show
  1. package/dist/adapters/interpreter/adapter.d.ts +2 -2
  2. package/dist/adapters/interpreter/adapter.js +11 -3
  3. package/dist/errors.d.ts +6 -1
  4. package/dist/install/authority-overlap.d.ts +101 -0
  5. package/dist/install/authority-overlap.js +239 -0
  6. package/dist/install/build-add-context-rule.d.ts +1 -1
  7. package/dist/install/index.d.ts +1 -0
  8. package/dist/install/index.js +4 -0
  9. package/dist/install/read-account-rules.d.ts +79 -0
  10. package/dist/install/read-account-rules.js +241 -0
  11. package/dist/predicate/decode.js +22 -1
  12. package/dist/predicate/encode.js +52 -5
  13. package/dist/predicate/from-json.js +14 -1
  14. package/dist/record/index.d.ts +10 -0
  15. package/dist/record/index.js +32 -1
  16. package/dist/record/rpc.d.ts +4 -0
  17. package/dist/record/rpc.js +4 -1
  18. package/dist/registry/identify.d.ts +10 -1
  19. package/dist/registry/identify.js +4 -1
  20. package/dist/registry/on-chain-spec.d.ts +37 -0
  21. package/dist/registry/on-chain-spec.js +152 -0
  22. package/dist/review-card/builder.js +40 -0
  23. package/dist/review-card/cross-check.js +34 -0
  24. package/dist/review-card/render-leaf.d.ts +1 -1
  25. package/dist/review-card/render-leaf.js +7 -0
  26. package/dist/run/index.d.ts +25 -4
  27. package/dist/run/index.js +108 -4
  28. package/dist/run/schemas.d.ts +350 -6
  29. package/dist/run/schemas.js +95 -6
  30. package/dist/simulate/deny-cases.js +11 -0
  31. package/dist/simulate/evaluate.js +86 -5
  32. package/dist/synth/declare.d.ts +43 -0
  33. package/dist/synth/declare.js +127 -0
  34. package/dist/synth/index.d.ts +1 -0
  35. package/dist/synth/index.js +1 -0
  36. package/dist/synth/synthesize-from-recording.js +1 -1
  37. package/dist/types.d.ts +21 -1
  38. package/dist/types.js +1 -1
  39. package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
  40. package/dist-cjs/adapters/interpreter/adapter.js +11 -3
  41. package/dist-cjs/errors.d.ts +6 -1
  42. package/dist-cjs/install/authority-overlap.d.ts +101 -0
  43. package/dist-cjs/install/authority-overlap.js +248 -0
  44. package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
  45. package/dist-cjs/install/index.d.ts +1 -0
  46. package/dist-cjs/install/index.js +13 -2
  47. package/dist-cjs/install/read-account-rules.d.ts +79 -0
  48. package/dist-cjs/install/read-account-rules.js +252 -0
  49. package/dist-cjs/predicate/decode.js +22 -1
  50. package/dist-cjs/predicate/encode.js +52 -5
  51. package/dist-cjs/predicate/from-json.js +14 -1
  52. package/dist-cjs/record/index.d.ts +10 -0
  53. package/dist-cjs/record/index.js +31 -0
  54. package/dist-cjs/record/rpc.d.ts +4 -0
  55. package/dist-cjs/record/rpc.js +7 -3
  56. package/dist-cjs/registry/identify.d.ts +10 -1
  57. package/dist-cjs/registry/identify.js +4 -0
  58. package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
  59. package/dist-cjs/registry/on-chain-spec.js +159 -0
  60. package/dist-cjs/review-card/builder.js +40 -0
  61. package/dist-cjs/review-card/cross-check.js +34 -0
  62. package/dist-cjs/review-card/render-leaf.d.ts +1 -1
  63. package/dist-cjs/review-card/render-leaf.js +7 -0
  64. package/dist-cjs/run/index.d.ts +25 -4
  65. package/dist-cjs/run/index.js +108 -2
  66. package/dist-cjs/run/schemas.d.ts +350 -6
  67. package/dist-cjs/run/schemas.js +96 -7
  68. package/dist-cjs/simulate/deny-cases.js +11 -0
  69. package/dist-cjs/simulate/evaluate.js +86 -5
  70. package/dist-cjs/synth/declare.d.ts +43 -0
  71. package/dist-cjs/synth/declare.js +130 -0
  72. package/dist-cjs/synth/index.d.ts +1 -0
  73. package/dist-cjs/synth/index.js +3 -1
  74. package/dist-cjs/synth/synthesize-from-recording.js +1 -1
  75. package/dist-cjs/types.d.ts +21 -1
  76. package/dist-cjs/types.js +1 -1
  77. package/package.json +1 -1
  78. package/src/adapters/interpreter/adapter.ts +13 -5
  79. package/src/errors.ts +5 -0
  80. package/src/install/authority-overlap.ts +323 -0
  81. package/src/install/index.ts +20 -0
  82. package/src/install/read-account-rules.ts +313 -0
  83. package/src/predicate/decode.ts +22 -1
  84. package/src/predicate/encode.ts +55 -5
  85. package/src/predicate/from-json.ts +14 -1
  86. package/src/record/index.ts +59 -2
  87. package/src/record/rpc.ts +4 -1
  88. package/src/registry/identify.ts +4 -1
  89. package/src/registry/on-chain-spec.ts +168 -0
  90. package/src/review-card/builder.ts +45 -2
  91. package/src/review-card/cross-check.ts +35 -1
  92. package/src/review-card/render-leaf.ts +8 -1
  93. package/src/run/index.ts +125 -2
  94. package/src/run/schemas.ts +100 -6
  95. package/src/simulate/deny-cases.ts +12 -1
  96. package/src/simulate/evaluate.ts +101 -9
  97. package/src/synth/declare.ts +206 -0
  98. package/src/synth/index.ts +5 -0
  99. package/src/synth/synthesize-from-recording.ts +1 -1
  100. package/src/types.ts +16 -1
@@ -12,9 +12,9 @@ export interface InterpreterAdapterConfig {
12
12
  * rejected as `SCOPE_SELF_CALL`. */
13
13
  smartAccountAddress: string;
14
14
  }
15
- /** The result of compiling a PolicyIR for this backend. */
15
+ /** The result of compiling a `ComposedRule` for this backend. */
16
16
  export interface CompileResult {
17
- /** false => some IR construct this backend cannot express (see `uncovered`). */
17
+ /** false => some constraint this backend cannot express (see `uncovered`). */
18
18
  covered: boolean;
19
19
  /** Human-readable list of unsupported constructs. */
20
20
  uncovered: string[];
@@ -1,6 +1,6 @@
1
- // src/adapters/interpreter/adapter.ts - the interpreter-policy CustodyAdapter.
1
+ // src/adapters/interpreter/adapter.ts - the interpreter-policy adapter.
2
2
  //
3
- // Compiles a PolicyIR to a single interpreter `PolicyDocument` + `PolicyRef`
3
+ // Compiles a `ComposedRule` to a single interpreter `PolicyDocument` + `PolicyRef`
4
4
  // carrying the canonical predicate encoding from `predicate/encode.ts`. This is
5
5
  // the only backend; the compose step lowers every constraint to it.
6
6
  //
@@ -133,7 +133,11 @@ function assertNoSelfCall(node, config) {
133
133
  }
134
134
  };
135
135
  switch (node.op) {
136
+ // Both boolean nodes recurse. `or` is listed explicitly rather than left
137
+ // to a default branch: this is a security check, and a new node kind must
138
+ // fail to compile here rather than silently skip it.
136
139
  case 'and':
140
+ case 'or':
137
141
  for (const c of node.children)
138
142
  assertNoSelfCall(c, config);
139
143
  return;
@@ -142,7 +146,11 @@ function assertNoSelfCall(node, config) {
142
146
  for (const h of node.haystack)
143
147
  checkLeaf(h);
144
148
  return;
145
- default:
149
+ case 'eq':
150
+ case 'lt':
151
+ case 'lte':
152
+ case 'gt':
153
+ case 'gte':
146
154
  checkLeaf(node.left);
147
155
  checkLeaf(node.right);
148
156
  }
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- export type ErrorCode = 'RECORDING_FAILED' | 'RECORDING_VALIDATION_FAILED' | 'SCOPE_UNRESOLVED' | 'SYNTHESIS_ERROR' | 'MALFORMED_PREDICATE' | 'SIMULATION_ERROR' | 'VERIFICATION_FAILED' | 'DENY_CASE_FAILURE' | 'PERMIT_CASE_FAILED' | 'SUMMARY_DRIFT' | 'INSTALL_BUILD_FAILED' | 'INSTALL_CONFIRM_MISSING' | 'INSTALL_CONFIRM_EXPIRED' | 'REVOKE_BUILD_FAILED' | 'REVOKE_CONFIRM_MISSING' | 'USER_REJECTED_SIGN' | 'WALLET_TIMEOUT' | 'WALLET_UNAVAILABLE' | 'PREDICATE_TOO_LARGE' | 'PREDICATE_TOO_DEEP' | 'TOO_MANY_LEAVES' | 'IN_OPERAND_LIMIT' | 'POLICY_CAP_EXCEEDED' | 'WASM_TOO_LARGE' | 'MASTER_AUTH_REQUIRED' | 'NONCE_REPLAY' | 'VERSION_MISMATCH' | 'ARITHMETIC_OVERFLOW' | 'AMOUNT_OVERFLOW' | 'RULE_SIGNERS_CHANGED' | 'SCOPE_SELF_CALL' | 'ARG_MISMATCH' | 'CONTRACT_SCOPE' | 'UNSUPPORTED_NODE' | 'STATEFUL_BOUND' | 'NOT_IN_ALLOWLIST' | 'COMPILE_OK' | 'COMPILE_GATE_FAILED';
1
+ export type ErrorCode = 'RECORDING_FAILED' | 'RECORDING_VALIDATION_FAILED' | 'SCOPE_UNRESOLVED' | 'SYNTHESIS_ERROR' | 'MALFORMED_PREDICATE' | 'SIMULATION_ERROR' | 'VERIFICATION_FAILED' | 'DENY_CASE_FAILURE' | 'PERMIT_CASE_FAILED' | 'SUMMARY_DRIFT' | 'INSTALL_BUILD_FAILED' | 'INSTALL_CONFIRM_MISSING' | 'INSTALL_CONFIRM_EXPIRED' | 'REVOKE_BUILD_FAILED' | 'REVOKE_CONFIRM_MISSING' | 'USER_REJECTED_SIGN' | 'WALLET_TIMEOUT' | 'WALLET_UNAVAILABLE' | 'PREDICATE_TOO_LARGE' | 'PREDICATE_TOO_DEEP' | 'TOO_MANY_LEAVES' | 'IN_OPERAND_LIMIT' | 'POLICY_CAP_EXCEEDED' | 'WASM_TOO_LARGE' | 'MASTER_AUTH_REQUIRED' | 'NONCE_REPLAY' | 'VERSION_MISMATCH' | 'ARITHMETIC_OVERFLOW' | 'AMOUNT_OVERFLOW' | 'RULE_SIGNERS_CHANGED' | 'SCOPE_SELF_CALL' | 'ARG_MISMATCH' | 'CONTRACT_SCOPE' | 'UNSUPPORTED_NODE' | 'STATEFUL_BOUND' | 'NOT_IN_ALLOWLIST'
2
+ /** A comparison against a `call_arg_scaled` operand failed (contract 107). */
3
+ | 'SLIPPAGE_FLOOR'
4
+ /** A `call_arg_scaled` ratio is zero or non-positive. Refused at install
5
+ * (contract 214) and mirrored at encode. */
6
+ | 'INVALID_SCALED_RATIO' | 'COMPILE_OK' | 'COMPILE_GATE_FAILED';
2
7
  export interface ToolError {
3
8
  code: ErrorCode;
4
9
  message: string;
@@ -0,0 +1,101 @@
1
+ import type { PredicateNode, SignerDraft } from '../types.ts';
2
+ /** Wildcard component of a `Selector`: the predicate does not pin this half. */
3
+ export declare const ANY = "*";
4
+ /** A (contract, function) pair a predicate may permit. `ANY` in either half
5
+ * means unconstrained, so `{contract: ANY, fn: ANY}` is "any call at all". */
6
+ export interface Selector {
7
+ contract: string;
8
+ fn: string;
9
+ }
10
+ export type ContextType = {
11
+ kind: 'default';
12
+ } | {
13
+ kind: 'call_contract';
14
+ contract: string;
15
+ } | {
16
+ kind: 'create_contract';
17
+ wasmHash: string;
18
+ };
19
+ /** How much can be said about a neighbouring rule.
20
+ * - `interpreter`: policed by our interpreter and the predicate was readable,
21
+ * so its authority is known exactly.
22
+ * - `foreign`: policed by some other contract. The address is visible, the
23
+ * semantics are not, so it needs review by hand.
24
+ * - `unpoliced`: no policy at all. Whatever its context type allows, its
25
+ * signers may do without constraint. */
26
+ export type RuleClass = 'interpreter' | 'foreign' | 'unpoliced';
27
+ export interface ObservedRule {
28
+ id: number;
29
+ contextType: ContextType;
30
+ signers: SignerDraft[];
31
+ /** Policy contract addresses attached to the rule, in OZ's order. */
32
+ policyAddresses: string[];
33
+ /** Decoded predicate. Present only when the rule is policed by OUR
34
+ * interpreter and the stored document was readable. */
35
+ predicate?: PredicateNode;
36
+ }
37
+ export interface IntendedInstall {
38
+ /** Rule the predicate is being installed onto. A re-install onto the same
39
+ * id REPLACES its predicate rather than adding a second source of
40
+ * authority, so that id is skipped. */
41
+ ruleId: number;
42
+ contextType: ContextType;
43
+ signers: SignerDraft[];
44
+ predicate: PredicateNode;
45
+ }
46
+ export type OverlapSeverity =
47
+ /** A neighbouring rule imposes no constraint at all on the shared calls. */
48
+ 'bypass'
49
+ /** A neighbouring policy exists but what it permits cannot be read. */
50
+ | 'unknown'
51
+ /** Both rules are ours. The new rule will not restrict the shared calls,
52
+ * because the signer names whichever is more permissive. */
53
+ | 'not-restricting';
54
+ export interface AuthorityOverlap {
55
+ ruleId: number;
56
+ ruleClass: RuleClass;
57
+ severity: OverlapSeverity;
58
+ /** Signers present in BOTH rules. An overlap is only reachable by a signer
59
+ * who can name both, so a rule sharing no signer is not a collision. */
60
+ sharedSigners: SignerDraft[];
61
+ /** The selectors both rules can serve. Non-empty by construction. */
62
+ sharedSelectors: Selector[];
63
+ advice: string;
64
+ }
65
+ /** Canonical key for signer equality. Mirrors OZ's `Signer` enum: a delegated
66
+ * signer is its address, an external signer is the verifier plus the key
67
+ * bytes, since one verifier may hold many keys. */
68
+ export declare function signerKey(s: SignerDraft): string;
69
+ /** Intersection of two selector SETS: every compatible pairing survives. */
70
+ export declare function intersectSelectors(a: Selector[], b: Selector[]): Selector[];
71
+ /**
72
+ * The set of `(contract, fn)` selectors a predicate may permit.
73
+ *
74
+ * A deliberate OVER-approximation: every call the predicate actually permits is
75
+ * covered by some returned selector, and unrecognised structure widens to the
76
+ * wildcard rather than narrowing. That direction is what makes the emptiness
77
+ * test sound. A call carries exactly one `(contract, fn)`, so if two
78
+ * predicates' over-approximations do not intersect, no single call can be
79
+ * routed to either and the rules provably cannot collide.
80
+ *
81
+ * Narrowing instead would be the fail-OPEN direction: it would let this report
82
+ * "no overlap" for rules that do collide.
83
+ */
84
+ export declare function permittedSelectors(node: PredicateNode): Selector[];
85
+ /** Selectors a context type admits, before the predicate narrows them. */
86
+ export declare function selectorsForContextType(ct: ContextType): Selector[];
87
+ /** What a rule can actually authorise: its context type narrowed by its
88
+ * predicate. An unpoliced or unreadable rule contributes no narrowing. */
89
+ export declare function effectiveSelectors(rule: ObservedRule): Selector[];
90
+ /**
91
+ * Every existing rule a signer of the intended install could name instead.
92
+ *
93
+ * A rule collides when it shares at least one signer AND at least one selector.
94
+ * Both are needed for the signer to have a choice: same signer but disjoint
95
+ * calls means no call can be rerouted, and same calls but no shared signer
96
+ * means nobody can reroute them.
97
+ */
98
+ export declare function findAuthorityOverlaps(args: {
99
+ intended: IntendedInstall;
100
+ existing: ObservedRule[];
101
+ }): AuthorityOverlap[];
@@ -0,0 +1,239 @@
1
+ // src/install/authority-overlap.ts - cross-rule authority analysis.
2
+ //
3
+ // An OZ smart account selects a context rule by CALLER DECLARATION and enforces
4
+ // only the policies of the rule that was named. A signer belonging to several
5
+ // rules therefore picks which one applies, so for any given call their
6
+ // authority is the MAXIMUM over the matching rules, never the intersection.
7
+ //
8
+ // The consequence is the one that catches people: installing a second, tighter
9
+ // rule restricts nothing. A key that also sits on an unpoliced rule is not
10
+ // constrained at all - it names that rule and the predicate never runs. This
11
+ // module detects that at install time, before the caller acts on a policy that
12
+ // looks binding and is not.
13
+ //
14
+ // Not theoretical. Proven on chain 2026-08-22: the same key, the same account
15
+ // and the same forbidden call was denied `#100` naming the policed rule and
16
+ // PERMITTED naming an unpoliced one. It happened in this project's own end-to-
17
+ // end harness, written by the author of the grammar, and was caught by review
18
+ // rather than by tooling - which is why the tooling now exists.
19
+ //
20
+ // Adapted to grammar 3 from the version published in `@crediolabs/policy-synth`
21
+ // 0.2.0, which came from the `octogate` repository and was lost when the npm
22
+ // lineage moved here. `or` and `not` are gone from the grammar, so the cases
23
+ // handling them are gone too; oracle bounds are gone from the stored document.
24
+ //
25
+ // Pure: no network. The caller supplies the account's rules.
26
+ /** Wildcard component of a `Selector`: the predicate does not pin this half. */
27
+ export const ANY = '*';
28
+ // ---- signer identity ----
29
+ /** Canonical key for signer equality. Mirrors OZ's `Signer` enum: a delegated
30
+ * signer is its address, an external signer is the verifier plus the key
31
+ * bytes, since one verifier may hold many keys. */
32
+ export function signerKey(s) {
33
+ return s.kind === 'delegated' ? `delegated:${s.address}` : `external:${s.verifier}:${s.keyBytes}`;
34
+ }
35
+ function sharedSigners(a, b) {
36
+ const bKeys = new Set(b.map(signerKey));
37
+ return a.filter((s) => bKeys.has(signerKey(s)));
38
+ }
39
+ // ---- selector extraction ----
40
+ const WILDCARD = { contract: ANY, fn: ANY };
41
+ function selectorKey(s) {
42
+ return `${s.contract} ${s.fn}`;
43
+ }
44
+ function dedupe(sels) {
45
+ const seen = new Map();
46
+ for (const s of sels)
47
+ seen.set(selectorKey(s), s);
48
+ return [...seen.values()];
49
+ }
50
+ /** Intersect one pair. `ANY` absorbs, equal literals survive, and two
51
+ * different literals cannot both hold for a single call. */
52
+ function intersectOne(a, b) {
53
+ const contract = a.contract === ANY
54
+ ? b.contract
55
+ : b.contract === ANY
56
+ ? a.contract
57
+ : a.contract === b.contract
58
+ ? a.contract
59
+ : null;
60
+ if (contract === null)
61
+ return null;
62
+ const fn = a.fn === ANY ? b.fn : b.fn === ANY ? a.fn : a.fn === b.fn ? a.fn : null;
63
+ if (fn === null)
64
+ return null;
65
+ return { contract, fn };
66
+ }
67
+ /** Intersection of two selector SETS: every compatible pairing survives. */
68
+ export function intersectSelectors(a, b) {
69
+ const out = [];
70
+ for (const x of a) {
71
+ for (const y of b) {
72
+ const hit = intersectOne(x, y);
73
+ if (hit)
74
+ out.push(hit);
75
+ }
76
+ }
77
+ return dedupe(out);
78
+ }
79
+ function literalAddress(leaf) {
80
+ return leaf.kind === 'literal_address' ? leaf.value : null;
81
+ }
82
+ function literalSymbol(leaf) {
83
+ return leaf.kind === 'literal_symbol' ? leaf.value : null;
84
+ }
85
+ /** Selector pinned by a single `eq`, whichever side the literal sits on. */
86
+ function selectorFromEq(left, right) {
87
+ if (left.kind === 'call_contract') {
88
+ const addr = literalAddress(right);
89
+ return addr === null ? null : { contract: addr, fn: ANY };
90
+ }
91
+ if (right.kind === 'call_contract') {
92
+ const addr = literalAddress(left);
93
+ return addr === null ? null : { contract: addr, fn: ANY };
94
+ }
95
+ if (left.kind === 'call_fn') {
96
+ const sym = literalSymbol(right);
97
+ return sym === null ? null : { contract: ANY, fn: sym };
98
+ }
99
+ if (right.kind === 'call_fn') {
100
+ const sym = literalSymbol(left);
101
+ return sym === null ? null : { contract: ANY, fn: sym };
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * The set of `(contract, fn)` selectors a predicate may permit.
107
+ *
108
+ * A deliberate OVER-approximation: every call the predicate actually permits is
109
+ * covered by some returned selector, and unrecognised structure widens to the
110
+ * wildcard rather than narrowing. That direction is what makes the emptiness
111
+ * test sound. A call carries exactly one `(contract, fn)`, so if two
112
+ * predicates' over-approximations do not intersect, no single call can be
113
+ * routed to either and the rules provably cannot collide.
114
+ *
115
+ * Narrowing instead would be the fail-OPEN direction: it would let this report
116
+ * "no overlap" for rules that do collide.
117
+ */
118
+ export function permittedSelectors(node) {
119
+ switch (node.op) {
120
+ case 'and': {
121
+ // Every conjunct must hold at once, so the permitted set is the
122
+ // intersection. Intersecting over-approximations stays one.
123
+ let acc = [WILDCARD];
124
+ for (const child of node.children)
125
+ acc = intersectSelectors(acc, permittedSelectors(child));
126
+ return acc;
127
+ }
128
+ case 'or': {
129
+ // Any branch may hold, so the permitted set is the UNION. A union of
130
+ // over-approximations is still an over-approximation, so this keeps the
131
+ // fail-safe direction while staying tighter than the wildcard the
132
+ // default branch would give. Precision matters here: `or` is how a
133
+ // policy says "pair A or pair B", and widening that to the wildcard
134
+ // would report an overlap against every rule on the account.
135
+ const acc = [];
136
+ for (const child of node.children)
137
+ acc.push(...permittedSelectors(child));
138
+ return dedupe(acc);
139
+ }
140
+ case 'eq': {
141
+ const sel = selectorFromEq(node.left, node.right);
142
+ return sel === null ? [WILDCARD] : [sel];
143
+ }
144
+ case 'in': {
145
+ // Set membership over the selector halves: `call_fn in {a, b}` permits
146
+ // both. A haystack element that is not the matching literal kind makes
147
+ // the node uninformative rather than narrower.
148
+ if (node.needle.kind === 'call_contract') {
149
+ const addrs = node.haystack.map(literalAddress);
150
+ if (addrs.some((a) => a === null))
151
+ return [WILDCARD];
152
+ return dedupe(addrs.map((a) => ({ contract: a, fn: ANY })));
153
+ }
154
+ if (node.needle.kind === 'call_fn') {
155
+ const syms = node.haystack.map(literalSymbol);
156
+ if (syms.some((s) => s === null))
157
+ return [WILDCARD];
158
+ return dedupe(syms.map((s) => ({ contract: ANY, fn: s })));
159
+ }
160
+ return [WILDCARD];
161
+ }
162
+ default:
163
+ // `lte` binds an amount, never the selector.
164
+ return [WILDCARD];
165
+ }
166
+ }
167
+ /** Selectors a context type admits, before the predicate narrows them. */
168
+ export function selectorsForContextType(ct) {
169
+ switch (ct.kind) {
170
+ case 'default':
171
+ return [WILDCARD];
172
+ case 'call_contract':
173
+ return [{ contract: ct.contract, fn: ANY }];
174
+ case 'create_contract':
175
+ // A contract-creation context is a different `Context` shape. The
176
+ // interpreter refuses anything that is not `Context::Contract`, and a
177
+ // creation rule can never serve a call, so it shares no selector.
178
+ return [];
179
+ }
180
+ }
181
+ /** What a rule can actually authorise: its context type narrowed by its
182
+ * predicate. An unpoliced or unreadable rule contributes no narrowing. */
183
+ export function effectiveSelectors(rule) {
184
+ const fromType = selectorsForContextType(rule.contextType);
185
+ if (!rule.predicate)
186
+ return fromType;
187
+ return intersectSelectors(fromType, permittedSelectors(rule.predicate));
188
+ }
189
+ function classifyRule(rule) {
190
+ if (rule.policyAddresses.length === 0)
191
+ return 'unpoliced';
192
+ return rule.predicate ? 'interpreter' : 'foreign';
193
+ }
194
+ function adviceFor(cls, ruleId) {
195
+ switch (cls) {
196
+ case 'unpoliced':
197
+ return `rule ${ruleId} has no policy attached, so a shared signer may make these calls with no constraint at all - the predicate you are installing will never run for them. Remove the shared signer from rule ${ruleId}, or attach a policy to it.`;
198
+ case 'foreign':
199
+ return `rule ${ruleId} is policed by a contract this tool cannot decode, so its authority over these calls is unknown. Review it by hand before relying on the new rule.`;
200
+ case 'interpreter':
201
+ return `a shared signer may name rule ${ruleId} instead, so the new rule will not restrict these calls. To TIGHTEN, edit rule ${ruleId} itself rather than adding a second rule. To ADD a separate capability, keep both and expect neither to constrain the other.`;
202
+ }
203
+ }
204
+ /**
205
+ * Every existing rule a signer of the intended install could name instead.
206
+ *
207
+ * A rule collides when it shares at least one signer AND at least one selector.
208
+ * Both are needed for the signer to have a choice: same signer but disjoint
209
+ * calls means no call can be rerouted, and same calls but no shared signer
210
+ * means nobody can reroute them.
211
+ */
212
+ export function findAuthorityOverlaps(args) {
213
+ const intendedSelectors = intersectSelectors(selectorsForContextType(args.intended.contextType), permittedSelectors(args.intended.predicate));
214
+ const out = [];
215
+ for (const rule of args.existing) {
216
+ if (rule.id === args.intended.ruleId)
217
+ continue;
218
+ const shared = sharedSigners(args.intended.signers, rule.signers);
219
+ if (shared.length === 0)
220
+ continue;
221
+ const sharedSelectors = intersectSelectors(intendedSelectors, effectiveSelectors(rule));
222
+ if (sharedSelectors.length === 0)
223
+ continue;
224
+ const ruleClass = classifyRule(rule);
225
+ out.push({
226
+ ruleId: rule.id,
227
+ ruleClass,
228
+ severity: ruleClass === 'unpoliced'
229
+ ? 'bypass'
230
+ : ruleClass === 'foreign'
231
+ ? 'unknown'
232
+ : 'not-restricting',
233
+ sharedSigners: shared,
234
+ sharedSelectors,
235
+ advice: adviceFor(ruleClass, rule.id),
236
+ });
237
+ }
238
+ return out;
239
+ }
@@ -23,7 +23,7 @@ export interface BuildAddContextRuleArgs {
23
23
  * before the chain does; the contract will refuse a mismatch again. */
24
24
  grammarVersion?: number;
25
25
  }
26
- export declare const DEFAULT_GRAMMAR_VERSION: 3;
26
+ export declare const DEFAULT_GRAMMAR_VERSION: 4;
27
27
  /** The verb `add_context_rule` takes on the wire. */
28
28
  export declare const ADD_CONTEXT_RULE_SYMBOL: "add_context_rule";
29
29
  /** Tuple of `ScVal` arguments to pass to `Operation.invokeHostFunction` for
@@ -1 +1,2 @@
1
+ export { ANY, type AuthorityOverlap, type ContextType, effectiveSelectors, findAuthorityOverlaps, type IntendedInstall, intersectSelectors, type ObservedRule, type OverlapSeverity, permittedSelectors, type RuleClass, type Selector, selectorsForContextType, signerKey, } from './authority-overlap.ts';
1
2
  export { ADD_CONTEXT_RULE_SYMBOL, type AddContextRuleArgs, type BuildAddContextRuleArgs, buildAddContextRuleArgs, DEFAULT_GRAMMAR_VERSION, } from './build-add-context-rule.ts';
@@ -12,4 +12,8 @@
12
12
  // Exported here rather than from the package root to keep the root surface
13
13
  // about synthesis, and because these are transaction-building primitives whose
14
14
  // callers should know they are reaching for them.
15
+ // Cross-rule authority analysis. Exported because the check has to happen
16
+ // wherever an install is BUILT, and a client that assembles its own
17
+ // `add_context_rule` call never reaches `runInstallPolicy`.
18
+ export { ANY, effectiveSelectors, findAuthorityOverlaps, intersectSelectors, permittedSelectors, selectorsForContextType, signerKey, } from "./authority-overlap.js";
15
19
  export { ADD_CONTEXT_RULE_SYMBOL, buildAddContextRuleArgs, DEFAULT_GRAMMAR_VERSION, } from "./build-add-context-rule.js";
@@ -0,0 +1,79 @@
1
+ import { rpc, xdr } from '@stellar/stellar-sdk';
2
+ import type { SignerDraft } from '../types.ts';
3
+ import type { ContextType, ObservedRule } from './authority-overlap.ts';
4
+ /** `storage.rs` - the third element of the persistent doc key tuple. */
5
+ export declare const K_DOC = 1;
6
+ /** Persistent-storage key for a rule's stored document:
7
+ * `(account, rule_id, K_DOC)`. */
8
+ export declare function docKeyScVal(smartAccount: string, ruleId: number): xdr.ScVal;
9
+ /** Ledger key for the interpreter's persistent entry holding that document. */
10
+ export declare function docLedgerKey(interpreter: string, smartAccount: string, ruleId: number): xdr.LedgerKey;
11
+ /** OZ `ContextRuleType`. An unrecognised tag is reported as `default`, which
12
+ * is the widest reading and therefore the safe one: it makes the rule look
13
+ * like it could serve any call, so overlap is over-reported, never missed. */
14
+ export declare function decodeContextType(v: xdr.ScVal | undefined): ContextType;
15
+ /** OZ `Signer::Delegated(Address) | Signer::External(Address, Bytes)`. */
16
+ export declare function decodeSigner(v: xdr.ScVal): SignerDraft | undefined;
17
+ /** A full OZ `ContextRule` as returned by `get_context_rule(id)`.
18
+ * `predicate` is filled in separately from the ledger entry. */
19
+ export declare function decodeContextRule(v: xdr.ScVal): ObservedRule | undefined;
20
+ /** The interpreter's `StoredDoc { predicate_bytes }`. */
21
+ export declare function decodeStoredPredicateBytes(v: xdr.ScVal): Buffer | undefined;
22
+ /** The three reads the scan needs. Kept as an interface so the collection
23
+ * below is testable without a network. */
24
+ export interface AccountRuleReader {
25
+ /** OZ `get_context_rules_count()`. */
26
+ getContextRuleCount(smartAccount: string): Promise<number>;
27
+ /** OZ `get_context_rule(id)`. Undefined when the id is absent. */
28
+ getContextRule(smartAccount: string, ruleId: number): Promise<xdr.ScVal | undefined>;
29
+ /** The interpreter's persistent `StoredDoc` entry, read as a ledger entry.
30
+ * Undefined when no document is stored for that rule. */
31
+ getStoredDoc(interpreter: string, smartAccount: string, ruleId: number): Promise<xdr.ScVal | undefined>;
32
+ }
33
+ /** How far the id scan will probe before giving up. OZ imposes no per-account
34
+ * rule cap, so there is no exact bound to derive; this one is far above any
35
+ * realistic account and keeps a malformed `Count` from spinning forever. */
36
+ export declare const MAX_RULE_ID_SCAN = 512;
37
+ export interface CollectedRules {
38
+ rules: ObservedRule[];
39
+ /** Rule ids whose stored predicate could not be read even though the
40
+ * interpreter is attached. Such a rule is reported without a predicate,
41
+ * which classifies it as opaque rather than as safely narrow. */
42
+ unreadablePredicateRuleIds: number[];
43
+ /** True when the scan stopped before accounting for every live rule. The
44
+ * result is then a SUBSET of the account's rules, so an empty overlap list
45
+ * proves nothing and the caller must not present it as safety. */
46
+ incomplete: boolean;
47
+ }
48
+ /**
49
+ * Every context rule on the account, with predicates filled in for the rules
50
+ * our interpreter polices.
51
+ *
52
+ * Rule ids are NOT contiguous. OZ assigns them from a monotonic `NextId` and
53
+ * decrements `Count` on removal without ever reusing an id, so after any
54
+ * removal `Count < NextId` and the live ids have gaps. Iterating `0..Count-1`
55
+ * would silently skip live rules at higher ids, and a skipped rule is a missed
56
+ * overlap - the one error that reports safety which does not exist. Instead the
57
+ * scan walks ids upward until it has accounted for `Count` live rules.
58
+ *
59
+ * A rule whose predicate cannot be read is deliberately left without one. That
60
+ * demotes it to the `foreign` class, so the scan reports it as opaque instead
61
+ * of assuming it is narrow.
62
+ */
63
+ export declare function collectObservedRules(args: {
64
+ reader: AccountRuleReader;
65
+ smartAccount: string;
66
+ interpreterAddress: string;
67
+ maxRuleIdScan?: number;
68
+ }): Promise<CollectedRules>;
69
+ /**
70
+ * An `AccountRuleReader` over a live RPC server.
71
+ *
72
+ * The two OZ getters are read-only simulations: the source account is
73
+ * constructed locally because a simulation never checks its sequence number,
74
+ * and asking the network for a random key would 404.
75
+ *
76
+ * The stored document is fetched as a ledger entry rather than a contract
77
+ * call, because the interpreter publishes no getter for it.
78
+ */
79
+ export declare function accountRuleReaderFromServer(server: rpc.Server, networkPassphrase: string): AccountRuleReader;