@crediolabs/policy-synth 0.1.4 → 0.1.5

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/dist/codegen/template.js +3 -3
  3. package/dist/record/decode.d.ts +6 -3
  4. package/dist/record/decode.js +13 -3
  5. package/dist/run/index.d.ts +31 -0
  6. package/dist/run/index.js +164 -0
  7. package/dist/run/schemas.d.ts +2003 -0
  8. package/dist/run/schemas.js +215 -0
  9. package/dist/synth/deny-cases.js +18 -2
  10. package/dist/synth/evaluate.js +2 -2
  11. package/dist/synth/synthesize-from-recording.d.ts +5 -2
  12. package/dist/synth/synthesize-from-recording.js +121 -63
  13. package/dist/types.d.ts +17 -0
  14. package/dist/types.js +17 -0
  15. package/dist/verify/simulate.js +18 -2
  16. package/dist/verify/verify.js +18 -2
  17. package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
  18. package/dist-cjs/adapters/interpreter/adapter.js +527 -0
  19. package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
  20. package/dist-cjs/adapters/interpreter/index.js +8 -0
  21. package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
  22. package/dist-cjs/adapters/oz/adapter.js +289 -0
  23. package/dist-cjs/adapters/oz/index.d.ts +1 -0
  24. package/dist-cjs/adapters/oz/index.js +8 -0
  25. package/dist-cjs/codegen/compile-gate.d.ts +33 -0
  26. package/dist-cjs/codegen/compile-gate.js +123 -0
  27. package/dist-cjs/codegen/index.d.ts +2 -0
  28. package/dist-cjs/codegen/index.js +14 -0
  29. package/dist-cjs/codegen/template.d.ts +18 -0
  30. package/dist-cjs/codegen/template.js +134 -0
  31. package/dist-cjs/errors.d.ts +37 -0
  32. package/dist-cjs/errors.js +3 -0
  33. package/dist-cjs/index.d.ts +11 -0
  34. package/dist-cjs/index.js +27 -0
  35. package/dist-cjs/ir/index.d.ts +1 -0
  36. package/dist-cjs/ir/index.js +3 -0
  37. package/dist-cjs/ir/types.d.ts +108 -0
  38. package/dist-cjs/ir/types.js +12 -0
  39. package/dist-cjs/mandate/index.d.ts +2 -0
  40. package/dist-cjs/mandate/index.js +6 -0
  41. package/dist-cjs/mandate/to-ir.d.ts +3 -0
  42. package/dist-cjs/mandate/to-ir.js +63 -0
  43. package/dist-cjs/mandate/types.d.ts +20 -0
  44. package/dist-cjs/mandate/types.js +9 -0
  45. package/dist-cjs/package.json +3 -0
  46. package/dist-cjs/predicate/encode.d.ts +10 -0
  47. package/dist-cjs/predicate/encode.js +252 -0
  48. package/dist-cjs/predicate/index.d.ts +1 -0
  49. package/dist-cjs/predicate/index.js +6 -0
  50. package/dist-cjs/record/decode.d.ts +79 -0
  51. package/dist-cjs/record/decode.js +398 -0
  52. package/dist-cjs/record/freshness.d.ts +17 -0
  53. package/dist-cjs/record/freshness.js +55 -0
  54. package/dist-cjs/record/index.d.ts +21 -0
  55. package/dist-cjs/record/index.js +166 -0
  56. package/dist-cjs/record/movements.d.ts +20 -0
  57. package/dist-cjs/record/movements.js +192 -0
  58. package/dist-cjs/record/rpc.d.ts +22 -0
  59. package/dist-cjs/record/rpc.js +74 -0
  60. package/dist-cjs/record/validate.d.ts +22 -0
  61. package/dist-cjs/record/validate.js +63 -0
  62. package/dist-cjs/registry/identify.d.ts +11 -0
  63. package/dist-cjs/registry/identify.js +87 -0
  64. package/dist-cjs/registry/index.d.ts +3 -0
  65. package/dist-cjs/registry/index.js +15 -0
  66. package/dist-cjs/registry/known-addresses.d.ts +16 -0
  67. package/dist-cjs/registry/known-addresses.js +53 -0
  68. package/dist-cjs/registry/protocols.d.ts +38 -0
  69. package/dist-cjs/registry/protocols.js +153 -0
  70. package/dist-cjs/review-card/builder.d.ts +14 -0
  71. package/dist-cjs/review-card/builder.js +264 -0
  72. package/dist-cjs/review-card/conflict.d.ts +40 -0
  73. package/dist-cjs/review-card/conflict.js +114 -0
  74. package/dist-cjs/review-card/cross-check.d.ts +11 -0
  75. package/dist-cjs/review-card/cross-check.js +151 -0
  76. package/dist-cjs/review-card/index.d.ts +3 -0
  77. package/dist-cjs/review-card/index.js +10 -0
  78. package/dist-cjs/run/index.d.ts +31 -0
  79. package/dist-cjs/run/index.js +178 -0
  80. package/dist-cjs/run/schemas.d.ts +2003 -0
  81. package/dist-cjs/run/schemas.js +218 -0
  82. package/dist-cjs/seams/index.d.ts +1 -0
  83. package/dist-cjs/seams/index.js +3 -0
  84. package/dist-cjs/seams/types.d.ts +66 -0
  85. package/dist-cjs/seams/types.js +12 -0
  86. package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
  87. package/dist-cjs/synth/compose-from-recording.js +356 -0
  88. package/dist-cjs/synth/deny-cases.d.ts +12 -0
  89. package/dist-cjs/synth/deny-cases.js +380 -0
  90. package/dist-cjs/synth/evaluate.d.ts +39 -0
  91. package/dist-cjs/synth/evaluate.js +428 -0
  92. package/dist-cjs/synth/harness.d.ts +16 -0
  93. package/dist-cjs/synth/harness.js +29 -0
  94. package/dist-cjs/synth/index.d.ts +9 -0
  95. package/dist-cjs/synth/index.js +23 -0
  96. package/dist-cjs/synth/lower.d.ts +23 -0
  97. package/dist-cjs/synth/lower.js +119 -0
  98. package/dist-cjs/synth/minimize.d.ts +4 -0
  99. package/dist-cjs/synth/minimize.js +41 -0
  100. package/dist-cjs/synth/predicate-literals.d.ts +5 -0
  101. package/dist-cjs/synth/predicate-literals.js +28 -0
  102. package/dist-cjs/synth/scope.d.ts +26 -0
  103. package/dist-cjs/synth/scope.js +81 -0
  104. package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
  105. package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
  106. package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
  107. package/dist-cjs/synth/synthesize-from-recording.js +711 -0
  108. package/dist-cjs/types.d.ts +288 -0
  109. package/dist-cjs/types.js +55 -0
  110. package/dist-cjs/verify/envelope.d.ts +15 -0
  111. package/dist-cjs/verify/envelope.js +23 -0
  112. package/dist-cjs/verify/index.d.ts +3 -0
  113. package/dist-cjs/verify/index.js +8 -0
  114. package/dist-cjs/verify/simulate.d.ts +31 -0
  115. package/dist-cjs/verify/simulate.js +261 -0
  116. package/dist-cjs/verify/verify.d.ts +21 -0
  117. package/dist-cjs/verify/verify.js +192 -0
  118. package/package.json +43 -3
  119. package/src/codegen/template.ts +3 -3
  120. package/src/record/decode.ts +21 -10
  121. package/src/run/index.ts +213 -0
  122. package/src/run/schemas.ts +242 -0
  123. package/src/synth/deny-cases.ts +21 -2
  124. package/src/synth/evaluate.ts +2 -2
  125. package/src/synth/synthesize-from-recording.ts +141 -62
  126. package/src/types.ts +19 -0
  127. package/src/verify/simulate.ts +21 -2
  128. package/src/verify/verify.ts +21 -2
@@ -0,0 +1,428 @@
1
+ "use strict";
2
+ // src/synth/evaluate.ts - the model evaluator.
3
+ //
4
+ // Pure function. The single semantic evaluator every deny-case runs through
5
+ // (TS model of the interpreter's semantics; real-semantics wiring is Phase 03).
6
+ // Determinism: same `(predicate, ctx)` -> byte-identical result, no clock, no
7
+ // randomness.
8
+ //
9
+ // Deny order (deny on FIRST violation, stable `reason` string):
10
+ // 1. `atLedger > validUntilLedger` (when set) -> 'EXPIRED'
11
+ // 2. a `now` leaf compared past `valid_until` semantics -> 'EXPIRED'
12
+ // 3. a `call_contract` eq whose literal != ctx.contract -> 'CONTRACT_SCOPE'
13
+ // 4. per-ScVal equality on `call_fn` / `call_arg[i]`;
14
+ // `eq(selector, literal_vec)` = EXACT ordered vector equality
15
+ // (compare element-by-element, deny if length or any differs);
16
+ // fail-closed on opaque/undecodable args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
17
+ // 5. `in` membership: needle NOT in haystack; an EMPTY
18
+ // haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
19
+ // 6. amount / window_spent comparisons via BigInt -> 'AMOUNT_BOUND'
20
+ // 7. `invocation_count_in_window` compare -> 'FREQUENCY'
21
+ // 8. `oracle_price`: missing/stale/deviation/etc. is FATAL -
22
+ // throw `OracleError`, catch at the top, deny with the
23
+ // underlying `ORACLE_*` reason -> 'ORACLE_*'
24
+ // 9. boolean nodes: `and` = all children permit;
25
+ // `or` = any child permits; `not` = invert a non-oracle child
26
+ // 10. signer threshold gate (when `signerWeights` provided,
27
+ // no positive-weight signer -> deny 'THRESHOLD_NOT_MET')
28
+ // 11. otherwise `{ permit: true }`.
29
+ //
30
+ // Amounts: BigInt on decimal strings (never JS `number`). Oracle errors
31
+ // modelled by throwing (never a boolean-false that `not`/`or` could mask).
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.evaluate = evaluate;
34
+ const predicate_literals_ts_1 = require("./predicate-literals.js");
35
+ /** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
36
+ * and converted to the matching `ORACLE_*` deny reason. NOT a
37
+ * boolean-false that `not` / `or` could mask. */
38
+ class OracleError extends Error {
39
+ code;
40
+ constructor(code) {
41
+ super(`oracle fatal: ${code}`);
42
+ this.code = code;
43
+ }
44
+ }
45
+ const ORACLE_ERROR_CODES = {
46
+ stale: 'ORACLE_STALE',
47
+ missing: 'ORACLE_MISSING',
48
+ deviation: 'ORACLE_DEVIATION_EXCEEDED',
49
+ paused: 'ORACLE_PAUSED',
50
+ decimals: 'ORACLE_DECIMALS_MISMATCH',
51
+ fingerprint: 'ORACLE_FINGERPRINT_DRIFT',
52
+ };
53
+ /** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
54
+ * Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
55
+ function evaluate(predicate, ctx) {
56
+ try {
57
+ // --- step 1: ledger-time expiry ---
58
+ if (ctx.validUntilLedger !== undefined && ctx.atLedger > ctx.validUntilLedger) {
59
+ return { permit: false, reason: 'EXPIRED' };
60
+ }
61
+ // --- step 2..9: predicate tree ---
62
+ const decision = walk(predicate, ctx);
63
+ // --- step 10: signer threshold gate (only when the predicate permitted) ---
64
+ if (decision.permit && ctx.signerWeights !== undefined) {
65
+ let totalWeight = 0n;
66
+ for (const w of Object.values(ctx.signerWeights)) {
67
+ // weights are non-negative integers; BigInt keeps the gate bounded
68
+ totalWeight += BigInt(w);
69
+ }
70
+ if (totalWeight === 0n)
71
+ return { permit: false, reason: 'THRESHOLD_NOT_MET' };
72
+ }
73
+ return decision;
74
+ }
75
+ catch (e) {
76
+ if (e instanceof OracleError) {
77
+ return { permit: false, reason: e.code };
78
+ }
79
+ throw e;
80
+ }
81
+ }
82
+ /** Walk the predicate tree. Returns the FIRST deny reason encountered on
83
+ * the active branch (so `and` fails-fast; `or` accepts the first permit). */
84
+ function walk(node, ctx) {
85
+ switch (node.op) {
86
+ case 'and': {
87
+ let lastDeny = null;
88
+ for (const child of node.children) {
89
+ const r = walk(child, ctx);
90
+ if (!r.permit) {
91
+ // deny-on-first: short-circuit. The "active branch" is the failing
92
+ // child, so the reason we surface is from that child.
93
+ return r;
94
+ }
95
+ // keep a reference to the last permit so unused-variable analysis
96
+ // doesn't complain; this is purely structural.
97
+ lastDeny = r;
98
+ }
99
+ return lastDeny ?? { permit: true };
100
+ }
101
+ case 'or': {
102
+ let lastDeny = null;
103
+ for (const child of node.children) {
104
+ const r = walk(child, ctx);
105
+ if (r.permit)
106
+ return r;
107
+ lastDeny = r;
108
+ }
109
+ return lastDeny ?? { permit: false, reason: 'NOT_IN_ALLOWLIST' };
110
+ }
111
+ case 'not': {
112
+ // `not` structurally inverts the child UNLESS the child contains an
113
+ // oracle leaf (compile-time rule: no oracle leaf under not/or). Here we
114
+ // walk the child; if it throws `OracleError`, the catch at the top of
115
+ // `evaluate` re-throws and surfaces the ORACLE_* deny. A non-oracle
116
+ // deny is inverted to a permit, and a permit is inverted to a deny.
117
+ const r = walk(node.child, ctx);
118
+ if (r.permit)
119
+ return { permit: false, reason: 'FN_MISMATCH' };
120
+ return { permit: true };
121
+ }
122
+ case 'eq':
123
+ case 'lt':
124
+ case 'lte':
125
+ case 'gt':
126
+ case 'gte':
127
+ return evalCompare(node.op, node.left, node.right, ctx);
128
+ case 'in':
129
+ return evalIn(node.needle, node.haystack, ctx);
130
+ }
131
+ }
132
+ /** Step 2..8: comparison leaf evaluation. */
133
+ function evalCompare(op, left, right, ctx) {
134
+ // --- step 2: `now` vs `valid_until` semantics ---
135
+ if (left.kind === 'now' && right.kind === 'valid_until') {
136
+ const expired = op === 'gt' || op === 'gte' ? ctx.nowSeconds >= 0 : ctx.nowSeconds < 0;
137
+ // only `gt` / `gte` / `lt` / `lte` are meaningful here. Treat `eq` /
138
+ // any other combo as: "expired iff nowSeconds > validUntilSeconds".
139
+ // We model `valid_until` as a synthetic future timestamp far past
140
+ // `nowSeconds` so the only true-positive expired path is the
141
+ // `gt`/`gte` shapes callers actually write.
142
+ if (op === 'gt' || op === 'gte') {
143
+ if (ctx.nowSeconds > 0)
144
+ return { permit: false, reason: 'EXPIRED' };
145
+ }
146
+ else if (op === 'lt' || op === 'lte') {
147
+ // permits when valid_until is in the future
148
+ return { permit: true };
149
+ }
150
+ return expired ? { permit: false, reason: 'EXPIRED' } : { permit: true };
151
+ }
152
+ if (right.kind === 'now' && left.kind === 'valid_until') {
153
+ return evalCompare(op, right, left, ctx);
154
+ }
155
+ // --- step 3: CONTRACT_SCOPE on call_contract eq ---
156
+ if (left.kind === 'call_contract' && op === 'eq') {
157
+ if (right.kind !== 'literal_address')
158
+ return { permit: false, reason: 'CONTRACT_SCOPE' };
159
+ return right.value === ctx.contract
160
+ ? { permit: true }
161
+ : { permit: false, reason: 'CONTRACT_SCOPE' };
162
+ }
163
+ // --- step 4a: call_fn equality ---
164
+ if (left.kind === 'call_fn' && op === 'eq') {
165
+ if (right.kind !== 'literal_symbol')
166
+ return { permit: false, reason: 'FN_MISMATCH' };
167
+ return right.value === ctx.fn ? { permit: true } : { permit: false, reason: 'FN_MISMATCH' };
168
+ }
169
+ // --- step 4b: call_arg comparison (eq / exact-vec, or an ordered numeric bound) ---
170
+ if (left.kind === 'call_arg') {
171
+ const actual = ctx.args[left.index];
172
+ // An ordered comparison (lt/lte/gt/gte) reads the arg as an integer and
173
+ // compares it to a numeric literal via BigInt (e.g. a SoroSwap input-amount
174
+ // cap `call_arg[0] <= limit`). The per-ScVal-type equality semantics live
175
+ // in evalArgEq (`eq` only); ordered ops are numeric-only and fail closed on
176
+ // a non-numeric arg or literal.
177
+ if (op !== 'eq')
178
+ return evalArgOrderedCompare(op, actual, right);
179
+ return evalArgEq(op, actual, right, ctx);
180
+ }
181
+ // --- step 6: AMOUNT_BOUND ---
182
+ if (left.kind === 'amount' && op !== 'eq') {
183
+ return evalAmountCompare(op, left.token, right, ctx);
184
+ }
185
+ if (left.kind === 'window_spent' && op !== 'eq') {
186
+ return evalWindowSpentCompare(op, left.token, right, ctx);
187
+ }
188
+ // `eq` on amount / window_spent not defined as a bound - fall through.
189
+ // --- step 7: FREQUENCY ---
190
+ if (left.kind === 'invocation_count_in_window') {
191
+ return evalFrequencyCompare(op, left.windowSecs, right, ctx);
192
+ }
193
+ // --- step 8: oracle_price (FATAL via throw) ---
194
+ if (left.kind === 'oracle_price') {
195
+ return evalOracleCompare(op, left.asset, right, ctx);
196
+ }
197
+ // Unknown leaf/op combination - structural fail-closed.
198
+ return { permit: false, reason: 'FN_MISMATCH' };
199
+ }
200
+ /** Step 4b: per-ScVal equality. Handles literal_vec as an EXACT ordered
201
+ * sequence: compare element-by-element in order; deny if length or any
202
+ * element differs. Opaque args (`type: 'other'`) fail closed. */
203
+ function evalArgEq(op, actual, right, ctx) {
204
+ // eq(call_arg[i], literal_vec) -> EXACT ordered vector equality
205
+ if (op === 'eq' && right.kind === 'literal_vec') {
206
+ if (actual?.type !== 'vec')
207
+ return { permit: false, reason: 'ARG_MISMATCH' };
208
+ return compareVecExact(actual.value, right.elements, ctx)
209
+ ? { permit: true }
210
+ : { permit: false, reason: 'ARG_MISMATCH' };
211
+ }
212
+ // eq(call_arg[i], literal_address) -> address compare
213
+ if (op === 'eq' && right.kind === 'literal_address') {
214
+ if (!actual)
215
+ return { permit: false, reason: 'ARG_MISMATCH' };
216
+ if (actual.type === 'other')
217
+ return { permit: false, reason: 'ARG_MISMATCH' };
218
+ return actual.type === 'address' && actual.value === right.value
219
+ ? { permit: true }
220
+ : { permit: false, reason: 'ARG_MISMATCH' };
221
+ }
222
+ // eq(call_arg[i], literal_i128) -> BigInt compare
223
+ if (op === 'eq' && right.kind === 'literal_i128') {
224
+ if (actual?.type !== 'i128')
225
+ return { permit: false, reason: 'ARG_MISMATCH' };
226
+ return BigInt(actual.value) === BigInt(right.value)
227
+ ? { permit: true }
228
+ : { permit: false, reason: 'ARG_MISMATCH' };
229
+ }
230
+ // eq(call_arg[i], literal_symbol)
231
+ if (op === 'eq' && right.kind === 'literal_symbol') {
232
+ if (!actual)
233
+ return { permit: false, reason: 'ARG_MISMATCH' };
234
+ return actual.type === 'symbol' && actual.value === right.value
235
+ ? { permit: true }
236
+ : { permit: false, reason: 'ARG_MISMATCH' };
237
+ }
238
+ // eq(call_arg[i], literal_u32)
239
+ if (op === 'eq' && right.kind === 'literal_u32') {
240
+ if (!actual)
241
+ return { permit: false, reason: 'ARG_MISMATCH' };
242
+ return actual.type === 'u32' && actual.value === String(right.value)
243
+ ? { permit: true }
244
+ : { permit: false, reason: 'ARG_MISMATCH' };
245
+ }
246
+ // eq(call_arg[i], literal_u64)
247
+ if (op === 'eq' && right.kind === 'literal_u64') {
248
+ if (!actual)
249
+ return { permit: false, reason: 'ARG_MISMATCH' };
250
+ return actual.type === 'u64' && actual.value === right.value
251
+ ? { permit: true }
252
+ : { permit: false, reason: 'ARG_MISMATCH' };
253
+ }
254
+ // eq(call_arg[i], literal_bytes)
255
+ if (op === 'eq' && right.kind === 'literal_bytes') {
256
+ if (!actual)
257
+ return { permit: false, reason: 'ARG_MISMATCH' };
258
+ return actual.type === 'bytes' && actual.value === right.value
259
+ ? { permit: true }
260
+ : { permit: false, reason: 'ARG_MISMATCH' };
261
+ }
262
+ // Anything else: fail closed on opacity (cannot decode the arg reliably).
263
+ if (!actual || actual.type === 'other')
264
+ return { permit: false, reason: 'ARG_MISMATCH' };
265
+ return { permit: false, reason: 'ARG_MISMATCH' };
266
+ }
267
+ /** Ordered numeric comparison (lt/lte/gt/gte) on a `call_arg`. The interpreter
268
+ * reads the arg as an integer (i128 / u64 / u32 on the recorder's ScVal
269
+ * surface) and compares it to a numeric literal via BigInt. A non-numeric arg
270
+ * or a non-numeric literal fails closed (ARG_MISMATCH) rather than permitting
271
+ * an undecidable bound. Backs the SoroSwap input-amount cap
272
+ * (`call_arg[0] <= limit`). */
273
+ function evalArgOrderedCompare(op, actual, right) {
274
+ const actualInt = argNumericBigInt(actual);
275
+ const literalInt = (0, predicate_literals_ts_1.literalNumericBigInt)(right);
276
+ if (actualInt === null || literalInt === null) {
277
+ return { permit: false, reason: 'ARG_MISMATCH' };
278
+ }
279
+ return bigintCmp(op, actualInt.toString(), literalInt.toString())
280
+ ? { permit: true }
281
+ : { permit: false, reason: 'ARG_MISMATCH' };
282
+ }
283
+ /** BigInt value of a numeric-integer ScVal arg (i128 / u64 / u32), or null when
284
+ * the arg is absent, opaque, or a non-numeric type. */
285
+ function argNumericBigInt(actual) {
286
+ if (!actual)
287
+ return null;
288
+ if (actual.type === 'i128' || actual.type === 'u64' || actual.type === 'u32') {
289
+ try {
290
+ return BigInt(actual.value);
291
+ }
292
+ catch {
293
+ return null;
294
+ }
295
+ }
296
+ return null;
297
+ }
298
+ /** Element-by-element ordered comparison of an `ScVal[]` against a
299
+ * `PredicateLeaf[]`. Equal-length and equal at every index => permit.
300
+ * Length mismatch OR any element mismatch => deny ARG_MISMATCH. */
301
+ function compareVecExact(actual, expected, ctx) {
302
+ if (actual.length !== expected.length)
303
+ return false;
304
+ for (let i = 0; i < expected.length; i++) {
305
+ const e = expected[i];
306
+ const a = actual[i];
307
+ if (!e || !a)
308
+ return false;
309
+ const r = evalArgEq('eq', a, e, ctx);
310
+ if (!r.permit)
311
+ return false;
312
+ }
313
+ return true;
314
+ }
315
+ /** Step 5: `in` membership. Empty haystack ALWAYS denies. Opaque needle
316
+ * fails closed. */
317
+ function evalIn(needle, haystack, ctx) {
318
+ if (haystack.length === 0)
319
+ return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
320
+ // Resolve the needle's ScVal against the candidate call.
321
+ const actual = resolveLeaf(needle, ctx);
322
+ if (!actual || actual.type === 'other')
323
+ return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
324
+ for (const h of haystack) {
325
+ const r = evalArgEq('eq', actual, h, ctx);
326
+ if (r.permit)
327
+ return { permit: true };
328
+ }
329
+ return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
330
+ }
331
+ /** Resolve a selector leaf to its current ScVal against the candidate call. */
332
+ function resolveLeaf(leaf, ctx) {
333
+ switch (leaf.kind) {
334
+ case 'call_contract':
335
+ return { type: 'address', value: ctx.contract };
336
+ case 'call_fn':
337
+ return { type: 'symbol', value: ctx.fn };
338
+ case 'call_arg':
339
+ return ctx.args[leaf.index];
340
+ case 'amount':
341
+ case 'window_spent':
342
+ case 'oracle_price':
343
+ case 'invocation_count_in_window':
344
+ case 'now':
345
+ case 'valid_until':
346
+ return undefined; // selector leaves with no ScVal projection
347
+ case 'literal_address':
348
+ return { type: 'address', value: leaf.value };
349
+ case 'literal_i128':
350
+ return { type: 'i128', value: leaf.value };
351
+ case 'literal_symbol':
352
+ return { type: 'symbol', value: leaf.value };
353
+ case 'literal_u32':
354
+ return { type: 'u32', value: String(leaf.value) };
355
+ case 'literal_u64':
356
+ return { type: 'u64', value: leaf.value };
357
+ case 'literal_bytes':
358
+ return { type: 'bytes', value: leaf.value };
359
+ case 'literal_vec':
360
+ return undefined;
361
+ }
362
+ }
363
+ /** Step 6: amount compare on BigInt. */
364
+ function evalAmountCompare(op, token, right, ctx) {
365
+ const literal = right.kind === 'literal_i128' ? right.value : null;
366
+ if (literal === null)
367
+ return { permit: false, reason: 'AMOUNT_BOUND' };
368
+ const actual = ctx.amountByToken[token] ?? '0';
369
+ return bigintCmp(op, actual, literal)
370
+ ? { permit: true }
371
+ : { permit: false, reason: 'AMOUNT_BOUND' };
372
+ }
373
+ /** Step 6: window_spent compare on BigInt. */
374
+ function evalWindowSpentCompare(op, token, right, ctx) {
375
+ const literal = right.kind === 'literal_i128' ? right.value : null;
376
+ if (literal === null)
377
+ return { permit: false, reason: 'AMOUNT_BOUND' };
378
+ const actual = ctx.windowSpentByToken[token] ?? '0';
379
+ return bigintCmp(op, actual, literal)
380
+ ? { permit: true }
381
+ : { permit: false, reason: 'AMOUNT_BOUND' };
382
+ }
383
+ /** Step 7: invocation_count_in_window compare. */
384
+ function evalFrequencyCompare(op, windowSecs, right, ctx) {
385
+ const literal = right.kind === 'literal_u32' ? String(right.value) : null;
386
+ if (literal === null)
387
+ return { permit: false, reason: 'FREQUENCY' };
388
+ const actual = String(ctx.invocationCountByWindow[windowSecs] ?? 0);
389
+ return bigintCmp(op, actual, literal) ? { permit: true } : { permit: false, reason: 'FREQUENCY' };
390
+ }
391
+ /** Step 8: oracle_price compare. Reads `ctx.oraclePriceByAsset[asset]`. Any
392
+ * error entry OR a missing key throws `OracleError` (FATAL). A satisfied
393
+ * compare permits. */
394
+ function evalOracleCompare(op, asset, right, ctx) {
395
+ const entry = ctx.oraclePriceByAsset[asset];
396
+ if (!entry)
397
+ throw new OracleError('ORACLE_STALE');
398
+ if ('error' in entry) {
399
+ const mapped = ORACLE_ERROR_CODES[entry.error];
400
+ if (!mapped)
401
+ throw new OracleError('ORACLE_STALE');
402
+ throw new OracleError(mapped);
403
+ }
404
+ const literal = right.kind === 'literal_i128' ? right.value : null;
405
+ if (literal === null)
406
+ throw new OracleError('ORACLE_DECIMALS_MISMATCH');
407
+ return bigintCmp(op, entry.price, literal)
408
+ ? { permit: true }
409
+ : { permit: false, reason: 'FN_MISMATCH' };
410
+ }
411
+ /** BigInt compare helper. `eq` is also supported by callers (selector-vs-literal
412
+ * equal checks), but the dedicated arg path uses it via `BigInt(actual) === BigInt(right)`. */
413
+ function bigintCmp(op, aStr, bStr) {
414
+ const a = BigInt(aStr);
415
+ const b = BigInt(bStr);
416
+ switch (op) {
417
+ case 'eq':
418
+ return a === b;
419
+ case 'lt':
420
+ return a < b;
421
+ case 'lte':
422
+ return a <= b;
423
+ case 'gt':
424
+ return a > b;
425
+ case 'gte':
426
+ return a >= b;
427
+ }
428
+ }
@@ -0,0 +1,16 @@
1
+ import type { PredicateNode } from '../types.ts';
2
+ import type { GeneratedCases } from './deny-cases.ts';
3
+ export interface HarnessFailure {
4
+ dimension: string;
5
+ expected: 'permit' | 'deny';
6
+ got: 'permit' | 'deny';
7
+ reason?: string;
8
+ }
9
+ export type HarnessResult = {
10
+ ok: true;
11
+ } | {
12
+ ok: false;
13
+ failures: HarnessFailure[];
14
+ };
15
+ /** Evaluate the intended call and every generated denial without side effects. */
16
+ export declare function runHarness(predicate: PredicateNode, cases: GeneratedCases): HarnessResult;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runHarness = runHarness;
4
+ const evaluate_ts_1 = require("./evaluate.js");
5
+ /** Evaluate the intended call and every generated denial without side effects. */
6
+ function runHarness(predicate, cases) {
7
+ const failures = [];
8
+ const permitResult = (0, evaluate_ts_1.evaluate)(predicate, cases.permit);
9
+ if (!permitResult.permit) {
10
+ failures.push({
11
+ dimension: 'PERMIT_CASE_FAILED',
12
+ expected: 'permit',
13
+ got: 'deny',
14
+ reason: permitResult.reason,
15
+ });
16
+ }
17
+ for (const deny of cases.denies) {
18
+ const result = (0, evaluate_ts_1.evaluate)(predicate, deny.ctx);
19
+ if (result.permit) {
20
+ failures.push({
21
+ dimension: deny.dimension,
22
+ expected: 'deny',
23
+ got: 'permit',
24
+ reason: 'DENY_CASE_FAILURE',
25
+ });
26
+ }
27
+ }
28
+ return failures.length === 0 ? { ok: true } : { ok: false, failures };
29
+ }
@@ -0,0 +1,9 @@
1
+ export { type ComposeOptions, type ComposeResult, composeFromRecording, } from './compose-from-recording.ts';
2
+ export { type DenyCase, type GeneratedCases, generateCases, } from './deny-cases.ts';
3
+ export { type EvalContext, type EvalResult, evaluate } from './evaluate.ts';
4
+ export { type HarnessFailure, type HarnessResult, runHarness, } from './harness.ts';
5
+ export { type IntentFacts, lower } from './lower.ts';
6
+ export { minimize } from './minimize.ts';
7
+ export { type DecideScopeOptions, decideScope, type ScopeDecision, scopeToContextRuleType, } from './scope.ts';
8
+ export { synthesizeFromMandate } from './synthesize-from-mandate.ts';
9
+ export { type SynthesizeFromRecordingOptions, synthesizeFromRecording, } from './synthesize-from-recording.ts';
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ // src/synth/index.ts - re-export the synthesizer front-ends.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.synthesizeFromRecording = exports.synthesizeFromMandate = exports.scopeToContextRuleType = exports.decideScope = exports.minimize = exports.lower = exports.runHarness = exports.evaluate = exports.generateCases = exports.composeFromRecording = void 0;
5
+ var compose_from_recording_ts_1 = require("./compose-from-recording.js");
6
+ Object.defineProperty(exports, "composeFromRecording", { enumerable: true, get: function () { return compose_from_recording_ts_1.composeFromRecording; } });
7
+ var deny_cases_ts_1 = require("./deny-cases.js");
8
+ Object.defineProperty(exports, "generateCases", { enumerable: true, get: function () { return deny_cases_ts_1.generateCases; } });
9
+ var evaluate_ts_1 = require("./evaluate.js");
10
+ Object.defineProperty(exports, "evaluate", { enumerable: true, get: function () { return evaluate_ts_1.evaluate; } });
11
+ var harness_ts_1 = require("./harness.js");
12
+ Object.defineProperty(exports, "runHarness", { enumerable: true, get: function () { return harness_ts_1.runHarness; } });
13
+ var lower_ts_1 = require("./lower.js");
14
+ Object.defineProperty(exports, "lower", { enumerable: true, get: function () { return lower_ts_1.lower; } });
15
+ var minimize_ts_1 = require("./minimize.js");
16
+ Object.defineProperty(exports, "minimize", { enumerable: true, get: function () { return minimize_ts_1.minimize; } });
17
+ var scope_ts_1 = require("./scope.js");
18
+ Object.defineProperty(exports, "decideScope", { enumerable: true, get: function () { return scope_ts_1.decideScope; } });
19
+ Object.defineProperty(exports, "scopeToContextRuleType", { enumerable: true, get: function () { return scope_ts_1.scopeToContextRuleType; } });
20
+ var synthesize_from_mandate_ts_1 = require("./synthesize-from-mandate.js");
21
+ Object.defineProperty(exports, "synthesizeFromMandate", { enumerable: true, get: function () { return synthesize_from_mandate_ts_1.synthesizeFromMandate; } });
22
+ var synthesize_from_recording_ts_1 = require("./synthesize-from-recording.js");
23
+ Object.defineProperty(exports, "synthesizeFromRecording", { enumerable: true, get: function () { return synthesize_from_recording_ts_1.synthesizeFromRecording; } });
@@ -0,0 +1,23 @@
1
+ import type { RecordedTransaction } from '../types.ts';
2
+ /** Facts the recording-path synth reasons over. */
3
+ export interface IntentFacts {
4
+ callTargets: string[];
5
+ functionsByContract: Record<string, string[]>;
6
+ /** Aggregate outgoing spend per token (i128-safe decimal string). A single
7
+ * token entry is the prerequisite for `spending_limit`. Movements
8
+ * with from != sourceAccount are NOT counted as "spend" by this synth
9
+ * (incoming yield, refund, etc.). */
10
+ spendByToken: Record<string, string>;
11
+ signers: string[];
12
+ /** Set when more than one top-level invocation shares a target contract
13
+ * (a router); decideScope uses this to scope by the router instead of
14
+ * returning SCOPE_UNRESOLVED. */
15
+ sharedRouter?: string;
16
+ /** Router hop paths, keyed by target contract. SoroSwap exposes its `path`
17
+ * arg directly on the top-level call; this is the input the compose step
18
+ * passes to a per-arg `in` condition (flagged as not covered by the OZ adapter). */
19
+ allowedPaths?: Record<string, string[][]>;
20
+ }
21
+ /** Lower a recorded transaction to the canonical IntentFacts. Pure (no
22
+ * randomness, no clock); same `RecordedTransaction` -> byte-identical facts. */
23
+ export declare function lower(tx: RecordedTransaction): IntentFacts;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ // src/synth/lower.ts - lower a RecordedTransaction to IntentFacts.
3
+ //
4
+ // Reads the top-level invocations only (the single authorized call that
5
+ // `Policy::enforce` receives; v1 grammar does not walk sub-invocations) and
6
+ // the recorded token movements. Produces the deterministic facts the synth
7
+ // reasons over: distinct call targets, functions per target, cumulative spend
8
+ // per token (outgoing movements from the recorded source account only),
9
+ // authorising signers, an optional shared-router marker when multiple top-level
10
+ // invocations hit the same contract, and the per-contract hop-path lists for
11
+ // router calls (SoroSwap `path`).
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.lower = lower;
14
+ /** Lower a recorded transaction to the canonical IntentFacts. Pure (no
15
+ * randomness, no clock); same `RecordedTransaction` -> byte-identical facts. */
16
+ function lower(tx) {
17
+ const invocations = tx.invocations;
18
+ const callTargets = uniqueOrdered(invocations.map((i) => i.contract));
19
+ const functionsByContract = groupFunctionsByContract(invocations);
20
+ const spendByToken = aggregateOutgoingSpend(tx.tokenMovements, tx.sourceAccount);
21
+ const allowedPaths = extractPathsByContract(invocations);
22
+ const sharedRouter = inferSharedRouter(invocations);
23
+ const facts = {
24
+ callTargets,
25
+ functionsByContract,
26
+ spendByToken,
27
+ signers: [...tx.signers],
28
+ };
29
+ if (sharedRouter)
30
+ facts.sharedRouter = sharedRouter;
31
+ if (Object.keys(allowedPaths).length > 0)
32
+ facts.allowedPaths = allowedPaths;
33
+ return facts;
34
+ }
35
+ /** Preserve first-seen ordering, drop duplicates. */
36
+ function uniqueOrdered(items) {
37
+ const seen = new Set();
38
+ const out = [];
39
+ for (const item of items) {
40
+ if (seen.has(item))
41
+ continue;
42
+ seen.add(item);
43
+ out.push(item);
44
+ }
45
+ return out;
46
+ }
47
+ /** Group the function names invoked per top-level contract. Order preserved
48
+ * per contract in first-seen order. */
49
+ function groupFunctionsByContract(invocations) {
50
+ const out = {};
51
+ for (const inv of invocations) {
52
+ const existing = out[inv.contract] ?? [];
53
+ if (!existing.includes(inv.fn))
54
+ existing.push(inv.fn);
55
+ out[inv.contract] = existing;
56
+ }
57
+ return out;
58
+ }
59
+ /** Sum outgoing TokenMovement amounts per token, where `from === source`.
60
+ * BigInt throughout; never lossy. Movements whose `from` does not match the
61
+ * recorded source account are NOT counted (incoming yield, refund, etc.). */
62
+ function aggregateOutgoingSpend(movements, sourceAccount) {
63
+ const totals = new Map();
64
+ for (const m of movements) {
65
+ if (m.from !== sourceAccount)
66
+ continue;
67
+ const current = totals.get(m.token) ?? 0n;
68
+ totals.set(m.token, current + BigInt(m.amount));
69
+ }
70
+ const out = {};
71
+ for (const [token, total] of totals) {
72
+ out[token] = total.toString();
73
+ }
74
+ return out;
75
+ }
76
+ /** Walk the top-level invocations looking for a `vec` arg whose elements are
77
+ * addresses (the SoroSwap `path` shape). Only the FIRST arg in each
78
+ * invocation is examined in v1 - the recorder emits the top-level args
79
+ * in declaration order and the only contract-call convention we bind by is
80
+ * the router's first vec-arg path. */
81
+ function extractPathsByContract(invocations) {
82
+ const out = {};
83
+ for (const inv of invocations) {
84
+ const path = findAddressVec(inv.args);
85
+ if (path) {
86
+ const existing = out[inv.contract] ?? [];
87
+ existing.push(path);
88
+ out[inv.contract] = existing;
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+ function findAddressVec(args) {
94
+ for (const arg of args) {
95
+ if (arg.type === 'vec') {
96
+ const addresses = arg.value.filter((v) => v.type === 'address');
97
+ if (addresses.length === arg.value.length && addresses.length > 0) {
98
+ return addresses.map((a) => a.value);
99
+ }
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ /** When multiple top-level invocations share the same target contract, that
105
+ * contract is the shared router. Single-call transactions return undefined
106
+ * (the default flow doesn't need the marker). */
107
+ function inferSharedRouter(invocations) {
108
+ if (invocations.length < 2)
109
+ return undefined;
110
+ const counts = new Map();
111
+ for (const inv of invocations) {
112
+ counts.set(inv.contract, (counts.get(inv.contract) ?? 0) + 1);
113
+ }
114
+ for (const [contract, count] of counts) {
115
+ if (count >= 2)
116
+ return contract;
117
+ }
118
+ return undefined;
119
+ }
@@ -0,0 +1,4 @@
1
+ import type { PredicateNode } from '../types.ts';
2
+ import type { EvalContext } from './evaluate.ts';
3
+ /** Remove top-level conjuncts only when the current and regenerated batteries still deny. */
4
+ export declare function minimize(predicate: PredicateNode, permitCtx: EvalContext): PredicateNode;