@crediolabs/policy-synth 0.3.0 → 0.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.
Files changed (152) hide show
  1. package/dist/install/authority-overlap.d.ts +101 -0
  2. package/dist/install/authority-overlap.js +227 -0
  3. package/dist/install/index.d.ts +1 -0
  4. package/dist/install/index.js +4 -0
  5. package/dist/record/freshness.d.ts +17 -11
  6. package/dist/record/freshness.js +28 -18
  7. package/dist/record/index.d.ts +10 -0
  8. package/dist/record/index.js +32 -1
  9. package/dist/record/rpc.d.ts +4 -0
  10. package/dist/record/rpc.js +4 -1
  11. package/dist/registry/identify.d.ts +10 -1
  12. package/dist/registry/identify.js +4 -1
  13. package/dist/registry/on-chain-spec.d.ts +37 -0
  14. package/dist/registry/on-chain-spec.js +152 -0
  15. package/dist/run/index.d.ts +25 -4
  16. package/dist/run/index.js +65 -4
  17. package/dist/run/schemas.d.ts +313 -0
  18. package/dist/run/schemas.js +52 -0
  19. package/dist/synth/declare.d.ts +30 -0
  20. package/dist/synth/declare.js +98 -0
  21. package/dist/synth/index.d.ts +1 -0
  22. package/dist/synth/index.js +1 -0
  23. package/dist-cjs/install/authority-overlap.d.ts +101 -0
  24. package/dist-cjs/install/authority-overlap.js +236 -0
  25. package/dist-cjs/install/index.d.ts +1 -0
  26. package/dist-cjs/install/index.js +13 -2
  27. package/dist-cjs/record/freshness.d.ts +17 -11
  28. package/dist-cjs/record/freshness.js +28 -18
  29. package/dist-cjs/record/index.d.ts +10 -0
  30. package/dist-cjs/record/index.js +31 -0
  31. package/dist-cjs/record/rpc.d.ts +4 -0
  32. package/dist-cjs/record/rpc.js +7 -3
  33. package/dist-cjs/registry/identify.d.ts +10 -1
  34. package/dist-cjs/registry/identify.js +4 -0
  35. package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
  36. package/dist-cjs/registry/on-chain-spec.js +159 -0
  37. package/dist-cjs/run/index.d.ts +25 -4
  38. package/dist-cjs/run/index.js +65 -2
  39. package/dist-cjs/run/schemas.d.ts +313 -0
  40. package/dist-cjs/run/schemas.js +53 -1
  41. package/dist-cjs/synth/declare.d.ts +30 -0
  42. package/dist-cjs/synth/declare.js +101 -0
  43. package/dist-cjs/synth/index.d.ts +1 -0
  44. package/dist-cjs/synth/index.js +3 -1
  45. package/package.json +2 -2
  46. package/src/install/authority-overlap.ts +312 -0
  47. package/src/install/index.ts +20 -0
  48. package/src/record/freshness.ts +29 -18
  49. package/src/record/index.ts +59 -2
  50. package/src/record/rpc.ts +4 -1
  51. package/src/registry/identify.ts +4 -1
  52. package/src/registry/on-chain-spec.ts +168 -0
  53. package/src/run/index.ts +79 -2
  54. package/src/run/schemas.ts +57 -0
  55. package/src/synth/declare.ts +157 -0
  56. package/src/synth/index.ts +5 -0
  57. package/dist/adapters/interpreter/index.d.ts +0 -1
  58. package/dist/adapters/interpreter/index.js +0 -2
  59. package/dist/adapters/oz/adapter.d.ts +0 -20
  60. package/dist/adapters/oz/adapter.js +0 -281
  61. package/dist/adapters/oz/index.d.ts +0 -1
  62. package/dist/adapters/oz/index.js +0 -2
  63. package/dist/codegen/compile-gate.d.ts +0 -33
  64. package/dist/codegen/compile-gate.js +0 -124
  65. package/dist/codegen/index.d.ts +0 -2
  66. package/dist/codegen/index.js +0 -8
  67. package/dist/codegen/template.d.ts +0 -18
  68. package/dist/codegen/template.js +0 -148
  69. package/dist/ir/index.d.ts +0 -1
  70. package/dist/ir/index.js +0 -2
  71. package/dist/ir/types.d.ts +0 -71
  72. package/dist/ir/types.js +0 -9
  73. package/dist/mandate/index.d.ts +0 -2
  74. package/dist/mandate/index.js +0 -2
  75. package/dist/mandate/to-ir.d.ts +0 -3
  76. package/dist/mandate/to-ir.js +0 -60
  77. package/dist/mandate/types.d.ts +0 -20
  78. package/dist/mandate/types.js +0 -8
  79. package/dist/seams/index.d.ts +0 -1
  80. package/dist/seams/index.js +0 -2
  81. package/dist/seams/types.d.ts +0 -64
  82. package/dist/seams/types.js +0 -11
  83. package/dist/synth/deny-cases.d.ts +0 -28
  84. package/dist/synth/deny-cases.js +0 -212
  85. package/dist/synth/evaluate.d.ts +0 -18
  86. package/dist/synth/evaluate.js +0 -271
  87. package/dist/synth/harness.d.ts +0 -28
  88. package/dist/synth/harness.js +0 -47
  89. package/dist/synth/minimize.d.ts +0 -4
  90. package/dist/synth/minimize.js +0 -38
  91. package/dist/synth/permit-context.d.ts +0 -8
  92. package/dist/synth/permit-context.js +0 -35
  93. package/dist/synth/predicate-literals.d.ts +0 -5
  94. package/dist/synth/predicate-literals.js +0 -25
  95. package/dist/synth/synthesize-from-mandate.d.ts +0 -20
  96. package/dist/synth/synthesize-from-mandate.js +0 -59
  97. package/dist/verify/envelope.d.ts +0 -15
  98. package/dist/verify/envelope.js +0 -22
  99. package/dist/verify/index.d.ts +0 -3
  100. package/dist/verify/index.js +0 -3
  101. package/dist/verify/simulate.d.ts +0 -18
  102. package/dist/verify/simulate.js +0 -172
  103. package/dist/verify/verify.d.ts +0 -15
  104. package/dist/verify/verify.js +0 -181
  105. package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
  106. package/dist-cjs/adapters/interpreter/index.js +0 -7
  107. package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
  108. package/dist-cjs/adapters/oz/adapter.js +0 -286
  109. package/dist-cjs/adapters/oz/index.d.ts +0 -1
  110. package/dist-cjs/adapters/oz/index.js +0 -8
  111. package/dist-cjs/codegen/compile-gate.d.ts +0 -33
  112. package/dist-cjs/codegen/compile-gate.js +0 -128
  113. package/dist-cjs/codegen/index.d.ts +0 -2
  114. package/dist-cjs/codegen/index.js +0 -14
  115. package/dist-cjs/codegen/template.d.ts +0 -18
  116. package/dist-cjs/codegen/template.js +0 -151
  117. package/dist-cjs/ir/index.d.ts +0 -1
  118. package/dist-cjs/ir/index.js +0 -3
  119. package/dist-cjs/ir/types.d.ts +0 -71
  120. package/dist-cjs/ir/types.js +0 -10
  121. package/dist-cjs/mandate/index.d.ts +0 -2
  122. package/dist-cjs/mandate/index.js +0 -6
  123. package/dist-cjs/mandate/to-ir.d.ts +0 -3
  124. package/dist-cjs/mandate/to-ir.js +0 -63
  125. package/dist-cjs/mandate/types.d.ts +0 -20
  126. package/dist-cjs/mandate/types.js +0 -9
  127. package/dist-cjs/seams/index.d.ts +0 -1
  128. package/dist-cjs/seams/index.js +0 -3
  129. package/dist-cjs/seams/types.d.ts +0 -64
  130. package/dist-cjs/seams/types.js +0 -12
  131. package/dist-cjs/synth/deny-cases.d.ts +0 -28
  132. package/dist-cjs/synth/deny-cases.js +0 -217
  133. package/dist-cjs/synth/evaluate.d.ts +0 -18
  134. package/dist-cjs/synth/evaluate.js +0 -274
  135. package/dist-cjs/synth/harness.d.ts +0 -28
  136. package/dist-cjs/synth/harness.js +0 -50
  137. package/dist-cjs/synth/minimize.d.ts +0 -4
  138. package/dist-cjs/synth/minimize.js +0 -41
  139. package/dist-cjs/synth/permit-context.d.ts +0 -8
  140. package/dist-cjs/synth/permit-context.js +0 -38
  141. package/dist-cjs/synth/predicate-literals.d.ts +0 -5
  142. package/dist-cjs/synth/predicate-literals.js +0 -28
  143. package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
  144. package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
  145. package/dist-cjs/verify/envelope.d.ts +0 -15
  146. package/dist-cjs/verify/envelope.js +0 -23
  147. package/dist-cjs/verify/index.d.ts +0 -3
  148. package/dist-cjs/verify/index.js +0 -8
  149. package/dist-cjs/verify/simulate.d.ts +0 -18
  150. package/dist-cjs/verify/simulate.js +0 -175
  151. package/dist-cjs/verify/verify.d.ts +0 -15
  152. package/dist-cjs/verify/verify.js +0 -184
@@ -1,71 +0,0 @@
1
- /** Comparison operator. Mirrors NEAR-V2 `CompOp`. */
2
- export type IRCompOp = 'eq' | 'lte';
3
- /** Boolean combinator. Mirrors NEAR-V2 `LogicOp`. */
4
- export type IRLogic = 'and';
5
- /** Scalar value type. The subset the predicate grammar has a literal for. */
6
- export type IRScalarType = 'address' | 'i128' | 'u32' | 'symbol';
7
- /** WHERE in the authorized call a value is read. NEAR-V2 selectors first, OZ
8
- * extensions after (an adapter that cannot express an extension flags it). */
9
- export type IRSelector = {
10
- kind: 'arg';
11
- argIndex: number;
12
- scalarType: IRScalarType;
13
- } | {
14
- kind: 'arg_len';
15
- argIndex: number;
16
- } | {
17
- kind: 'arg_field';
18
- argIndex: number;
19
- element: number;
20
- field: string;
21
- scalarType: IRScalarType;
22
- };
23
- /** A single comparison leaf. `value` is a decimal/hex string (i128-safe;
24
- * never a JS number). */
25
- export interface IRCompare {
26
- selector: IRSelector;
27
- operator: IRCompOp;
28
- value: string;
29
- }
30
- /** Condition tree. NEAR-V2 guard/constraint are flat And/Or; the IR allows
31
- * nesting + `not` + `in` + `eq_seq` so the same IR can later lower to the OZ
32
- * predicate DSL. Week-1 adapters only lower the flat supported subset and flag
33
- * the rest. */
34
- export type IRCondition = {
35
- op: 'and';
36
- children: IRCondition[];
37
- } | {
38
- op: 'compare';
39
- compare: IRCompare;
40
- } | {
41
- op: 'in';
42
- selector: IRSelector;
43
- values: string[];
44
- }
45
- /** Exact ordered sequence equality. The IR-level selector value MUST EQUAL
46
- * `values` as an ORDERED sequence (e.g. a swap hop `path`). `in` is pure
47
- * set membership and cannot express order; `eq_seq` is the construct that
48
- * does. Adapters that cannot express an exact ordered vector (OZ built-ins)
49
- * flag this as `uncovered` rather than silently dropping it. */
50
- | {
51
- op: 'eq_seq';
52
- selector: IRSelector;
53
- values: string[];
54
- };
55
- export interface IRPolicyRule {
56
- /** NEAR-V2 scope filter; each field optional (absent = wildcard). */
57
- scope: {
58
- contract?: string;
59
- method?: string;
60
- };
61
- /** NEAR-V2 constraint (AND; reject the transaction when any fails). */
62
- constraints: IRCondition[];
63
- /** OZ context-rule expiry, by ledger sequence - the only clock the OZ
64
- * account model exposes to a rule. */
65
- expiry?: {
66
- validUntilLedger?: number;
67
- };
68
- }
69
- export interface PolicyIR {
70
- rules: IRPolicyRule[];
71
- }
package/dist/ir/types.js DELETED
@@ -1,9 +0,0 @@
1
- // src/ir/types.ts - the PolicyIR ("Policy Tree").
2
- //
3
- // `PolicyIR` is the custody-agnostic "Policy Tree" hub (the diagram's Policy
4
- // Tree) that every CustodyAdapter compiles FROM. It generalizes the NEAR-V2
5
- // policy schema (roles / scope filter / guard / constraint / comparison leaves /
6
- // default behaviour) into one chain-neutral shape and extends it with the
7
- // single IR can serve every backend. NEAR-V2 has no stateful spend window,
8
- // below and are only lowered by adapters that declare support for them.
9
- export {};
@@ -1,2 +0,0 @@
1
- export { mandateToPolicyIR } from './to-ir.ts';
2
- export type { MandateSpec } from './types.ts';
@@ -1,2 +0,0 @@
1
- // src/mandate/index.ts - re-export the Mandate source.
2
- export { mandateToPolicyIR } from "./to-ir.js";
@@ -1,3 +0,0 @@
1
- import type { PolicyIR } from '../ir/types.ts';
2
- import type { MandateSpec } from './types.ts';
3
- export declare function mandateToPolicyIR(spec: MandateSpec): PolicyIR;
@@ -1,60 +0,0 @@
1
- // src/mandate/to-ir.ts - deterministic lowering of a MandateSpec to a PolicyIR.
2
- //
3
- // `mandateToPolicyIR` is pure and total: the same spec always lowers to a
4
- // byte-identical PolicyIR. No decoding, no inference, no clock. Each mandate
5
- // field maps to exactly one IR construct:
6
- // contract/method -> rule.scope
7
- // spendingLimit -> a `window_spent(token,window) <= limit` compare
8
- // approvalThreshold-> rule.approval.threshold
9
- // recipients -> an `in` condition on the recipient arg (flagged as not
10
- // covered by the OZ adapter; that is expected)
11
- // expiry -> rule.expiry
12
- // The top-level default is `deny_all` (OZ context rules are deny-by-default).
13
- /** Arg index the recipient allowlist constrains. Pinned to the SEP-41
14
- * `transfer(from, to, amount)` convention where `to` is arg 1. This condition
15
- * is flagged as not covered by the OZ adapter (no built-in primitive expresses
16
- * an arg allowlist), so the index only needs to be deterministic in week-1. */
17
- const RECIPIENT_ARG_INDEX = 1;
18
- export function mandateToPolicyIR(spec) {
19
- const scope = { contract: spec.contract };
20
- if (spec.method !== undefined)
21
- scope.method = spec.method;
22
- const constraints = [];
23
- if (spec.spendingLimit) {
24
- constraints.push({
25
- op: 'compare',
26
- compare: {
27
- selector: {
28
- kind: 'window_spent',
29
- token: spec.spendingLimit.token,
30
- windowSeconds: spec.spendingLimit.windowSeconds,
31
- },
32
- operator: 'lte',
33
- value: spec.spendingLimit.limit,
34
- },
35
- });
36
- }
37
- if (spec.recipients && spec.recipients.length > 0) {
38
- const selector = {
39
- kind: 'arg',
40
- argIndex: RECIPIENT_ARG_INDEX,
41
- scalarType: 'address',
42
- };
43
- constraints.push({ op: 'in', selector, values: [...spec.recipients] });
44
- }
45
- const rule = { roles: [], scope, constraints };
46
- if (spec.approvalThreshold !== undefined) {
47
- rule.approval = { kind: 'threshold', threshold: spec.approvalThreshold };
48
- }
49
- if (spec.expiry) {
50
- const expiry = {};
51
- if (spec.expiry.validUntilLedger !== undefined) {
52
- expiry.validUntilLedger = spec.expiry.validUntilLedger;
53
- }
54
- if (spec.expiry.validUntilUnixSeconds !== undefined) {
55
- expiry.validUntilUnixSeconds = spec.expiry.validUntilUnixSeconds;
56
- }
57
- rule.expiry = expiry;
58
- }
59
- return { chain: spec.chain, defaultBehavior: 'deny_all', rules: [rule] };
60
- }
@@ -1,20 +0,0 @@
1
- export interface MandateSpec {
2
- chain: 'stellar';
3
- contract: string;
4
- method?: string;
5
- /** -> OZ `spending_limit` primitive. `limit` is an i128 decimal string. */
6
- spendingLimit?: {
7
- token: string;
8
- limit: string;
9
- windowSeconds: number;
10
- };
11
- /** -> OZ `simple_threshold` / `weighted_threshold` primitive. */
12
- approvalThreshold?: number;
13
- /** Recipient allowlist -> an `in` arg condition (not covered by an OZ built-in). */
14
- recipients?: string[];
15
- /** -> OZ context-rule expiry. */
16
- expiry?: {
17
- validUntilLedger?: number;
18
- validUntilUnixSeconds?: number;
19
- };
20
- }
@@ -1,8 +0,0 @@
1
- // src/mandate/types.ts - the deterministic Mandate source.
2
- //
3
- // A MandateSpec is a declarative policy statement: "this account may call this
4
- // contract, spending at most L per window, requiring M approvals, to these
5
- // recipients, until this expiry". It carries NO transaction to decode, NO
6
- // parseConfidence, and NO inference - it lowers deterministically to a
7
- // PolicyIR. It is the clean end-to-end demo path, co-equal with the recorder.
8
- export {};
@@ -1 +0,0 @@
1
- export type { ChainDecoder, CompileResult, CustodyAdapter, CustodyCapabilities, CustodyMode, PolicySource, SimulationResult, } from './types.ts';
@@ -1,2 +0,0 @@
1
- // src/seams/index.ts - re-export the three custody-seam contracts.
2
- export {};
@@ -1,64 +0,0 @@
1
- import type { ToolResponse } from '../errors.ts';
2
- import type { PolicyIR } from '../ir/types.ts';
3
- import type { Network, ProposedPolicy, RecordedTransaction, ScVal } from '../types.ts';
4
- /** INPUT: something that produces a normalized RecordedTransaction. */
5
- export interface PolicySource {
6
- readonly name: string;
7
- capture(input: {
8
- hash?: string;
9
- xdr?: string;
10
- network: Network;
11
- }): Promise<ToolResponse<RecordedTransaction>>;
12
- }
13
- /** DECODE: chain-specific call data -> the normalized facts the synth reasons
14
- * over. */
15
- export interface ChainDecoder {
16
- readonly chain: 'stellar' | 'evm';
17
- decodeInvocation(tx: RecordedTransaction): {
18
- contract: string;
19
- method: string;
20
- args: ScVal[];
21
- };
22
- }
23
- /** OZ = enforce; `log_only` is reserved for shadow mode (out of scope). */
24
- export type CustodyMode = 'enforce' | 'log_only';
25
- /** What a backend can express. A construct needing a false flag is flagged
26
- * `uncovered` by the adapter, never silently dropped. */
27
- export interface CustodyCapabilities {
28
- supportsSpendWindow: boolean;
29
- supportsTimeExpiry: boolean;
30
- supportsThreshold: boolean;
31
- supportsGeneralPredicate: boolean;
32
- }
33
- /** The result of compiling a PolicyIR for one backend. */
34
- export interface CompileResult {
35
- /** false => some IR construct this backend cannot express (see `uncovered`). */
36
- covered: boolean;
37
- /** Human-readable list of unsupported constructs. */
38
- uncovered: string[];
39
- /** The backend-native installable policy, assembled when a rule lowered. */
40
- proposed?: ProposedPolicy;
41
- }
42
- /** Result of a simulate() dry-run. `ts-model` is the off-chain TS evaluator.
43
- * Real permit/deny semantics wiring is a later phase; week-1 returns a
44
- * clearly-marked stub (empty `evaluations`, `permitted: null`). */
45
- export interface SimulationResult {
46
- backend: 'ts-model';
47
- /** Whether the permit tx would be allowed; null until real semantics land. */
48
- permitted: boolean | null;
49
- /** Per-construct evaluation trace; empty placeholder in this slice. */
50
- evaluations: unknown[];
51
- /** Diagnostics; marks the stub explicitly. */
52
- notes: string[];
53
- }
54
- /** OUTPUT: compile a PolicyIR to a backend-specific installable policy. */
55
- export interface CustodyAdapter {
56
- readonly name: string;
57
- readonly mode: CustodyMode;
58
- capabilities(): CustodyCapabilities;
59
- compile(ir: PolicyIR): CompileResult;
60
- /** Phase-03 wiring; a clearly-marked stub in week-1. */
61
- simulate(ir: PolicyIR, permitTx: RecordedTransaction): SimulationResult;
62
- /** Canonical JSON of the IR (portability / audit). */
63
- export(ir: PolicyIR): string;
64
- }
@@ -1,11 +0,0 @@
1
- // src/seams/types.ts - the three custody seams (contracts, not implementations).
2
- //
3
- // The synthesizer is decoupled from its edges by three seams:
4
- // - PolicySource (INPUT): produces a normalized RecordedTransaction. The
5
- // recorder is the reference source; a MandateSpec
6
- // lowering is a second, deterministic source.
7
- // - ChainDecoder (DECODE): chain-specific call data -> normalized facts.
8
- // - CustodyAdapter (OUTPUT): compile a PolicyIR to a backend-native policy +
9
- // verify/export it (OZ is the first adapter).
10
- // These are INTERFACES only. Implementations live in their own modules.
11
- export {};
@@ -1,28 +0,0 @@
1
- import type { PredicateNode, ScVal } from '../types.ts';
2
- import type { EvalContext } from './evaluate.ts';
3
- export interface DenyCase {
4
- dimension: string;
5
- ctx: EvalContext;
6
- /** Canonical reason the Rust interpreter returns for this dimension.
7
- * Optional: when set, `runHarness` asserts the TS evaluator emits this
8
- * exact string, so a future TS/Rust reason divergence fails CI. When
9
- * absent the case is only checked on the boolean decision. */
10
- expectedReason?: string;
11
- }
12
- export interface GeneratedCases {
13
- permit: EvalContext;
14
- denies: DenyCase[];
15
- }
16
- declare const OVERPERMISSIVE_DIMENSIONS: readonly ["argument_reorder"];
17
- export { OVERPERMISSIVE_DIMENSIONS };
18
- /** Build deterministic model-evaluated alternatives without mutating the intended call.
19
- *
20
- * @param predicate - the synthesized predicate
21
- * @param permitCtx - EvalContext for the intended (permitted) call
22
- * @param dimensions - optional whitelist of dimension names to emit; when omitted
23
- * all known dimensions (including over-permissiveness mutations)
24
- * are emitted. */
25
- export declare function generateCases(predicate: PredicateNode, permitCtx: EvalContext, dimensions?: string[]): GeneratedCases;
26
- /** Deep-copy an ScVal so a mutation cannot alias the recorded call.
27
- * Exported so the permit-context builder shares this one implementation. */
28
- export declare function cloneScVal(value: ScVal, depth?: number): ScVal;
@@ -1,212 +0,0 @@
1
- import { MAX_SCVAL_CLONE_DEPTH } from "../types.js";
2
- // Deterministic XLM/USDC adjacency fixture; the shared registry can replace this boundary later.
3
- const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
4
- export { OVERPERMISSIVE_DIMENSIONS };
5
- /** Build deterministic model-evaluated alternatives without mutating the intended call.
6
- *
7
- * @param predicate - the synthesized predicate
8
- * @param permitCtx - EvalContext for the intended (permitted) call
9
- * @param dimensions - optional whitelist of dimension names to emit; when omitted
10
- * all known dimensions (including over-permissiveness mutations)
11
- * are emitted. */
12
- export function generateCases(predicate, permitCtx, dimensions) {
13
- const facts = inspectPredicate(predicate);
14
- const denies = [];
15
- // arg_bound: set a constrained arg to an opaque ScVal.
16
- // The reason depends on the predicate shape:
17
- // - an `eq(call_arg[i], literal)` denies with ARG_MISMATCH
18
- // - an `in(call_arg[i], [literals])` denies with NOT_IN_ALLOWLIST
19
- // Both are correct in the TS evaluator; the reason is predicate-dependent
20
- // so we cannot pin a single canonical reason here.
21
- const argumentConstraints = [];
22
- for (const comparison of facts.comparisons) {
23
- if (comparison.op === 'eq' &&
24
- comparison.left.kind === 'call_arg' &&
25
- comparison.right.kind !== 'literal_vec') {
26
- argumentConstraints.push({ index: comparison.left.index });
27
- }
28
- }
29
- for (const membership of facts.memberships) {
30
- if (membership.needle.kind === 'call_arg') {
31
- argumentConstraints.push({ index: membership.needle.index });
32
- }
33
- }
34
- for (const constraint of argumentConstraints) {
35
- const ctx = cloneContext(permitCtx);
36
- ctx.args[constraint.index] = { type: 'other', value: 'deny-case-opaque-argument' };
37
- denies.push({ dimension: 'arg_bound', ctx });
38
- }
39
- // soroswap_allowed_path: replace a bound vec arg with a different vector.
40
- // Still valid for eq comparisons on literal_vec leaves.
41
- for (const comparison of facts.comparisons) {
42
- if (comparison.op !== 'eq' ||
43
- comparison.left.kind !== 'call_arg' ||
44
- comparison.right.kind !== 'literal_vec') {
45
- continue;
46
- }
47
- const ctx = cloneContext(permitCtx);
48
- ctx.args[comparison.left.index] = differentVector(ctx.args[comparison.left.index]);
49
- denies.push({ dimension: 'soroswap_allowed_path', ctx });
50
- }
51
- // argument_reorder: swap first two address args.
52
- // Skipped when dimensions filter is active so the synth pipeline can emit a policy;
53
- // the over-permissiveness harness then tests this mutation as a FINDING.
54
- const constrainedArgIndices = new Set();
55
- for (const comparison of facts.comparisons) {
56
- if (comparison.left.kind === 'call_arg')
57
- constrainedArgIndices.add(comparison.left.index);
58
- }
59
- for (const membership of facts.memberships) {
60
- if (membership.needle.kind === 'call_arg')
61
- constrainedArgIndices.add(membership.needle.index);
62
- }
63
- const hasConstrainedArg = constrainedArgIndices.size > 0;
64
- if ((!dimensions || dimensions.includes('argument_reorder')) &&
65
- hasConstrainedArg &&
66
- permitCtx.args.length >= 2 &&
67
- permitCtx.args[0]?.type === 'address' &&
68
- permitCtx.args[1]?.type === 'address' &&
69
- permitCtx.args[0].value !==
70
- permitCtx.args[1].value) {
71
- const ctx = cloneContext(permitCtx);
72
- // Guard above guarantees args[0] and args[1] exist and are address-typed.
73
- const a0 = ctx.args[0];
74
- const a1 = ctx.args[1];
75
- ctx.args[0] = a1;
76
- ctx.args[1] = a0;
77
- denies.push({ dimension: 'argument_reorder', ctx });
78
- }
79
- // map_field_flip: flip a bound map field to a different valid value of
80
- // the same type. OPT-IN only, never ORIGINAL.
81
- // Targets each `call_arg_field` leaf and produces a ctx whose vec element
82
- // has a different value of the recorded field. ---
83
- if (!dimensions || dimensions.includes('map_field_flip')) {
84
- for (const comparison of facts.comparisons) {
85
- const sel = comparison.left;
86
- if (sel.kind !== 'call_arg_field')
87
- continue;
88
- const arg = permitCtx.args[sel.index];
89
- if (arg?.type !== 'vec')
90
- continue;
91
- const element = arg.value[sel.element];
92
- if (element?.type !== 'map' || !Array.isArray(element.value))
93
- continue;
94
- const entry = element.value.find((e) => e.key === sel.field);
95
- if (!entry)
96
- continue;
97
- const flipped = flipFieldValue(entry.val);
98
- if (flipped === null)
99
- continue;
100
- const ctx = cloneContext(permitCtx);
101
- const clonedVec = arg.value.map(cloneScVal);
102
- const clonedElement = clonedVec[sel.element];
103
- if (clonedElement?.type !== 'map' || !Array.isArray(clonedElement.value))
104
- continue;
105
- clonedElement.value = clonedElement.value.map((e) => e.key === sel.field ? { key: e.key, val: flipped } : e);
106
- clonedVec[sel.element] = clonedElement;
107
- ctx.args[sel.index] = { type: 'vec', value: clonedVec };
108
- denies.push({ dimension: 'map_field_flip', ctx });
109
- }
110
- }
111
- // vec_append: append a new element to a bound vec. OPT-IN only, never ORIGINAL.
112
- // Targets every `call_arg_len` leaf; without the length pin a caller can
113
- // append an extra element to defeat per-element binds. ---
114
- if (!dimensions || dimensions.includes('vec_append')) {
115
- for (const comparison of facts.comparisons) {
116
- const sel = comparison.left;
117
- if (sel.kind !== 'call_arg_len')
118
- continue;
119
- const arg = permitCtx.args[sel.index];
120
- if (arg?.type !== 'vec')
121
- continue;
122
- const ctx = cloneContext(permitCtx);
123
- ctx.args[sel.index] = {
124
- type: 'vec',
125
- value: [...arg.value, { type: 'other', value: 'deny-case-vec-append' }],
126
- };
127
- denies.push({ dimension: 'vec_append', ctx });
128
- }
129
- }
130
- return { permit: cloneContext(permitCtx), denies };
131
- }
132
- function inspectPredicate(predicate) {
133
- const facts = { comparisons: [], memberships: [] };
134
- visit(predicate, facts);
135
- return facts;
136
- }
137
- function visit(node, facts) {
138
- switch (node.op) {
139
- case 'and':
140
- for (const child of node.children)
141
- visit(child, facts);
142
- return;
143
- case 'in':
144
- facts.memberships.push(node);
145
- return;
146
- case 'eq':
147
- case 'lte':
148
- facts.comparisons.push(node);
149
- }
150
- }
151
- function differentVector(actual) {
152
- if (actual?.type !== 'vec')
153
- return { type: 'vec', value: [] };
154
- if (actual.value.length === 0) {
155
- return { type: 'vec', value: [{ type: 'other', value: 'deny-case-extra-hop' }] };
156
- }
157
- if (actual.value.length > 1) {
158
- const reversed = actual.value.map(cloneScVal).reverse();
159
- if (JSON.stringify(reversed) !== JSON.stringify(actual.value)) {
160
- return { type: 'vec', value: reversed };
161
- }
162
- }
163
- const value = actual.value.map(cloneScVal);
164
- value[0] = { type: 'other', value: 'deny-case-different-hop' };
165
- return { type: 'vec', value };
166
- }
167
- function cloneContext(ctx) {
168
- return {
169
- contract: ctx.contract,
170
- fn: ctx.fn,
171
- args: ctx.args.map(cloneScVal),
172
- };
173
- }
174
- /** Deep-copy an ScVal so a mutation cannot alias the recorded call.
175
- * Exported so the permit-context builder shares this one implementation. */
176
- export function cloneScVal(value, depth = 0) {
177
- if (value.type === 'vec') {
178
- if (depth >= MAX_SCVAL_CLONE_DEPTH) {
179
- throw cloneDepthError(value);
180
- }
181
- return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
182
- }
183
- return { ...value };
184
- }
185
- function cloneDepthError(value) {
186
- const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
187
- err.code = 'SYNTHESIS_ERROR';
188
- err.severity = 'error';
189
- err.retryable = false;
190
- err.depthContext = value.type;
191
- throw err;
192
- }
193
- /** Build a value of the SAME ScVal type that differs from the recorded one,
194
- * used by the `map_field_flip` mutation to defeat a per-element pin without
195
- * changing the wire type. Returns null when the ScVal type has no obvious
196
- * different value (e.g. opaque/other). */
197
- function flipFieldValue(val) {
198
- switch (val.type) {
199
- case 'address': {
200
- const a = 'GBFKRGJYZXLTDEI36ZCQEIM225NMOCR2VDBOIHJTXJ54FEFFVL2FKALE';
201
- const b = 'GD6XSMQJ47EHHJOWXQOND5YDVZC37JWZJHYHBKE6QJFSLLJ5KQXM5QS5';
202
- return { type: 'address', value: val.value === a ? b : a };
203
- }
204
- case 'i128':
205
- case 'u32':
206
- return { type: val.type, value: String(BigInt(val.value) + 1n) };
207
- case 'symbol':
208
- return { type: 'symbol', value: `${val.value}x` };
209
- default:
210
- return null;
211
- }
212
- }
@@ -1,18 +0,0 @@
1
- import type { PredicateNode, ScVal } from '../types.ts';
2
- export interface EvalContext {
3
- /** Contract the interpreter is asked to enforce against. */
4
- contract: string;
5
- /** Function name on that contract. */
6
- fn: string;
7
- /** Decoded `ScVal[]` of the top-level authorized call. */
8
- args: ScVal[];
9
- }
10
- export type EvalResult = {
11
- permit: true;
12
- } | {
13
- permit: false;
14
- reason: string;
15
- };
16
- /** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
17
- * Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
18
- export declare function evaluate(predicate: PredicateNode, ctx: EvalContext): EvalResult;