@crediolabs/policy-synth 0.5.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/install/build-add-context-rule.js +48 -16
- package/dist/install/build-install-policy.d.ts +21 -0
- package/dist/install/build-install-policy.js +41 -3
- package/dist/predicate/encode.d.ts +12 -0
- package/dist/predicate/encode.js +5 -1
- package/dist/run/index.d.ts +14 -5
- package/dist/run/index.js +263 -14
- package/dist/run/schemas.d.ts +2277 -474
- package/dist/run/schemas.js +173 -14
- package/dist/synth/lower.d.ts +6 -2
- package/dist/synth/lower.js +21 -8
- package/dist/synth/synthesize-from-recording.js +1 -1
- package/dist/types.d.ts +18 -1
- package/dist-cjs/install/build-add-context-rule.js +48 -16
- package/dist-cjs/install/build-install-policy.d.ts +21 -0
- package/dist-cjs/install/build-install-policy.js +42 -3
- package/dist-cjs/predicate/encode.d.ts +12 -0
- package/dist-cjs/predicate/encode.js +5 -0
- package/dist-cjs/run/index.d.ts +14 -5
- package/dist-cjs/run/index.js +263 -13
- package/dist-cjs/run/schemas.d.ts +2277 -474
- package/dist-cjs/run/schemas.js +174 -15
- package/dist-cjs/synth/lower.d.ts +6 -2
- package/dist-cjs/synth/lower.js +21 -8
- package/dist-cjs/synth/synthesize-from-recording.js +1 -1
- package/dist-cjs/types.d.ts +18 -1
- package/package.json +1 -1
- package/src/install/build-add-context-rule.ts +65 -21
- package/src/install/build-install-policy.ts +65 -10
- package/src/predicate/encode.ts +5 -1
- package/src/run/index.ts +280 -23
- package/src/run/schemas.ts +201 -31
- package/src/synth/lower.ts +22 -8
- package/src/synth/synthesize-from-recording.ts +1 -1
- package/src/types.ts +25 -6
package/dist-cjs/run/index.js
CHANGED
|
@@ -23,6 +23,7 @@ exports.runRecordTransaction = runRecordTransaction;
|
|
|
23
23
|
exports.runSynthesizePolicy = runSynthesizePolicy;
|
|
24
24
|
exports.runInstallPolicy = runInstallPolicy;
|
|
25
25
|
exports.runRevokePolicy = runRevokePolicy;
|
|
26
|
+
exports.contextTypeForPredicate = contextTypeForPredicate;
|
|
26
27
|
exports.runSimulatePolicy = runSimulatePolicy;
|
|
27
28
|
exports.runDeclarePolicy = runDeclarePolicy;
|
|
28
29
|
exports.runVerifyPolicy = runVerifyPolicy;
|
|
@@ -30,6 +31,7 @@ exports.runGetInterpreterInfo = runGetInterpreterInfo;
|
|
|
30
31
|
exports.caughtError = caughtError;
|
|
31
32
|
const node_crypto_1 = require("node:crypto");
|
|
32
33
|
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
34
|
+
const adapter_ts_1 = require("../adapters/interpreter/adapter.js");
|
|
33
35
|
const index_ts_1 = require("../index.js");
|
|
34
36
|
const authority_overlap_ts_1 = require("../install/authority-overlap.js");
|
|
35
37
|
const build_install_policy_ts_1 = require("../install/build-install-policy.js");
|
|
@@ -130,7 +132,24 @@ async function runSynthesizePolicy(raw) {
|
|
|
130
132
|
}
|
|
131
133
|
const input = parsed.data;
|
|
132
134
|
try {
|
|
133
|
-
|
|
135
|
+
// `hash` is the agent-friendly alternative to `recordedTx`: re-record here
|
|
136
|
+
// rather than make the caller retype a recording it cannot copy faithfully.
|
|
137
|
+
// A recording failure is returned as-is, so the caller sees why the hash was
|
|
138
|
+
// refused instead of a synthesis error about a payload it never sent.
|
|
139
|
+
let recorded;
|
|
140
|
+
if (input.recordedTx === undefined) {
|
|
141
|
+
const rerecorded = await runRecordTransaction({
|
|
142
|
+
hash: input.transactionHash,
|
|
143
|
+
network: input.network,
|
|
144
|
+
});
|
|
145
|
+
if (!rerecorded.ok) {
|
|
146
|
+
return { ok: false, error: rerecorded.error };
|
|
147
|
+
}
|
|
148
|
+
recorded = rerecorded.data;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
recorded = input.recordedTx;
|
|
152
|
+
}
|
|
134
153
|
return await (0, index_ts_1.synthesizeFromRecording)(recorded, {
|
|
135
154
|
network: input.network,
|
|
136
155
|
...(input.userResponses !== undefined ? { userResponses: input.userResponses } : {}),
|
|
@@ -152,10 +171,143 @@ async function runInstallPolicy(raw) {
|
|
|
152
171
|
}
|
|
153
172
|
const input = parsed.data;
|
|
154
173
|
const network = input.network ?? 'testnet';
|
|
174
|
+
// `fromHash` builds the rule here rather than accepting a transcribed copy.
|
|
175
|
+
// The pinning gates below then run against the rule we just synthesized, so
|
|
176
|
+
// this path is gated identically to a caller-supplied one - it is a shortcut
|
|
177
|
+
// for the caller, never for the checks.
|
|
178
|
+
let rule = input.rule;
|
|
179
|
+
if (rule === undefined && input.fromPredicate !== undefined) {
|
|
180
|
+
const fp = input.fromPredicate;
|
|
181
|
+
let scope;
|
|
182
|
+
try {
|
|
183
|
+
scope = contextTypeForPredicate((0, decode_ts_1.decodePredicate)(fp.encodedPredicate));
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
return toolFailure('install_policy', e);
|
|
187
|
+
}
|
|
188
|
+
rule = {
|
|
189
|
+
contextRuleType: scope,
|
|
190
|
+
name: fp.name ?? 'policy',
|
|
191
|
+
validUntilLedger: fp.validUntilLedger ?? null,
|
|
192
|
+
signers: fp.signers.map((address) => ({ kind: 'delegated', address })),
|
|
193
|
+
policies: [
|
|
194
|
+
{
|
|
195
|
+
kind: 'interpreter',
|
|
196
|
+
interpreterAddress: schemas_ts_1.PINNED_INTERPRETER_ADDRESS_BY_NETWORK[network],
|
|
197
|
+
predicateBlobBase64: fp.encodedPredicate,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (rule === undefined) {
|
|
203
|
+
// Typed rather than inline: every tool body takes `unknown`, so a
|
|
204
|
+
// misspelled key here would compile and fail only at runtime, as a
|
|
205
|
+
// validation error blamed on the caller. Naming the type restores the
|
|
206
|
+
// check on this hop.
|
|
207
|
+
const synthArgs = {
|
|
208
|
+
source: 'recording',
|
|
209
|
+
network,
|
|
210
|
+
transactionHash: input.fromHash?.transactionHash,
|
|
211
|
+
interpreter: { smartAccountAddress: input.smartAccount },
|
|
212
|
+
...(input.fromHash?.userResponses !== undefined
|
|
213
|
+
? { userResponses: input.fromHash.userResponses }
|
|
214
|
+
: {}),
|
|
215
|
+
};
|
|
216
|
+
const synthesized = await runSynthesizePolicy(synthArgs);
|
|
217
|
+
if (!synthesized.ok) {
|
|
218
|
+
return { ok: false, error: synthesized.error };
|
|
219
|
+
}
|
|
220
|
+
// The synthesizer saw a spend it could not bound. Installing anyway yields
|
|
221
|
+
// a rule that reads as a cap and enforces nothing, and nothing downstream
|
|
222
|
+
// catches it: it installs cleanly and verifies cleanly, because a missing
|
|
223
|
+
// constraint generates no deny case that could fail. That combination
|
|
224
|
+
// reached the chain once. Refuse rather than emit a warning to skim past.
|
|
225
|
+
const unbounded = synthesized.data.ambiguities.some((a) => a.code === 'AMOUNT_BOUND_MISSING');
|
|
226
|
+
if (unbounded && input.allowUnboundedAmount !== true) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
error: {
|
|
230
|
+
code: 'INSTALL_BUILD_FAILED',
|
|
231
|
+
message: 'install_policy: the recorded call spends an amount this policy does not bound, so the rule would constrain everything about the call except how much it moves; set `fromHash.userResponses.limitAmount` to the per-call cap, or `allowUnboundedAmount: true` to install an unbounded rule deliberately',
|
|
232
|
+
severity: 'error',
|
|
233
|
+
retryable: false,
|
|
234
|
+
remediation: { toolCall: { name: 'install_policy', args: {} } },
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// Synthesis leaves the signer set empty - it reads a transaction, and which
|
|
239
|
+
// keys a rule binds is a security decision no single recording answers.
|
|
240
|
+
// The caller names them here.
|
|
241
|
+
rule = {
|
|
242
|
+
...synthesized.data.contextRule,
|
|
243
|
+
signers: (input.fromHash?.signers ?? []).map((address) => ({
|
|
244
|
+
kind: 'delegated',
|
|
245
|
+
address,
|
|
246
|
+
})),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// A rule that governs no key is refused on chain, and the refusal arrives as
|
|
250
|
+
// a bare contract error code with nothing to act on. Say what is missing
|
|
251
|
+
// instead, while the caller still has the recording in hand.
|
|
252
|
+
if (rule.signers.length === 0) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
error: {
|
|
256
|
+
code: 'INSTALL_BUILD_FAILED',
|
|
257
|
+
message: 'install_policy: the rule names no signer, so it would govern no key; name the keys it applies to',
|
|
258
|
+
severity: 'error',
|
|
259
|
+
retryable: false,
|
|
260
|
+
remediation: { toolCall: { name: 'install_policy', args: {} } },
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
155
264
|
// ---- Pinning gates (default-deny) ----
|
|
156
265
|
const expectedInterpreter = schemas_ts_1.PINNED_INTERPRETER_ADDRESS_BY_NETWORK[network];
|
|
157
266
|
const expectedRpc = schemas_ts_1.RPC_URL_BY_NETWORK[network];
|
|
158
|
-
|
|
267
|
+
// Synthesis stamps every interpreter policy with the placeholder marker: it
|
|
268
|
+
// is handed a recording, not a network, so it emits a marker rather than
|
|
269
|
+
// inventing a deploy address. Install DOES know the network, and resolves
|
|
270
|
+
// the pin just above, so it fills the marker in here - otherwise the
|
|
271
|
+
// synthesize -> install path is unreachable, because the marker is not a
|
|
272
|
+
// strkey and fails the pin on every call. Only the exact marker is replaced;
|
|
273
|
+
// a caller-supplied address is still checked against the pin unchanged, so
|
|
274
|
+
// this widens nothing.
|
|
275
|
+
rule = {
|
|
276
|
+
...rule,
|
|
277
|
+
policies: rule.policies.map((p) => p.kind === 'interpreter' && p.interpreterAddress === adapter_ts_1.PLACEHOLDER_INTERPRETER_ADDRESS
|
|
278
|
+
? { ...p, interpreterAddress: expectedInterpreter }
|
|
279
|
+
: p),
|
|
280
|
+
};
|
|
281
|
+
// A rolling total, when asked for. The predicate bounds each call; this
|
|
282
|
+
// bounds the sum across calls, which is state the interpreter does not keep.
|
|
283
|
+
// Both sit on the one rule and compose as all-of.
|
|
284
|
+
if (input.spendingLimit !== undefined) {
|
|
285
|
+
if (rule.contextRuleType.kind !== 'call_contract') {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: {
|
|
289
|
+
code: 'INSTALL_BUILD_FAILED',
|
|
290
|
+
message: `install_policy: a spending limit meters transfers of one token, so the rule must be scoped to that token's contract; this rule's scope is "${rule.contextRuleType.kind}"`,
|
|
291
|
+
severity: 'error',
|
|
292
|
+
retryable: false,
|
|
293
|
+
remediation: { toolCall: { name: 'install_policy', args: {} } },
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
rule = {
|
|
298
|
+
...rule,
|
|
299
|
+
policies: [
|
|
300
|
+
...rule.policies,
|
|
301
|
+
{
|
|
302
|
+
kind: 'spending_limit',
|
|
303
|
+
policyAddress: schemas_ts_1.PINNED_OZ_POLICY_ADDRESS_BY_NETWORK[network].spending_limit,
|
|
304
|
+
periodLedgers: input.spendingLimit.periodLedgers,
|
|
305
|
+
spendingLimit: input.spendingLimit.amount,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const pinningError = enforceInterpreterPin(rule.policies, input.allowUnpinnedInterpreter, expectedInterpreter);
|
|
159
311
|
if (pinningError) {
|
|
160
312
|
return { ok: false, error: pinningError };
|
|
161
313
|
}
|
|
@@ -171,7 +323,7 @@ async function runInstallPolicy(raw) {
|
|
|
171
323
|
return toolFailure('install_policy', e);
|
|
172
324
|
}
|
|
173
325
|
try {
|
|
174
|
-
const interpreterPolicy =
|
|
326
|
+
const interpreterPolicy = rule.policies.find((p) => p.kind === 'interpreter');
|
|
175
327
|
const encodedPredicate = interpreterPolicy?.predicateBlobBase64 ?? '';
|
|
176
328
|
const predicateHash = (0, node_crypto_1.createHash)('sha256')
|
|
177
329
|
.update(Buffer.from(encodedPredicate, 'base64'))
|
|
@@ -180,8 +332,10 @@ async function runInstallPolicy(raw) {
|
|
|
180
332
|
smartAccount: input.smartAccount,
|
|
181
333
|
sourceAccount: input.sourceAccount,
|
|
182
334
|
networkPassphrase: schemas_ts_1.NETWORK_PASSPHRASES[network],
|
|
183
|
-
rule
|
|
184
|
-
|
|
335
|
+
rule,
|
|
336
|
+
// A fresh rule has no stored nonce, so 1 is the value the interpreter
|
|
337
|
+
// expects unless the caller is deliberately re-installing.
|
|
338
|
+
installNonce: input.installNonce ?? 1,
|
|
185
339
|
encodedPredicate,
|
|
186
340
|
predicateHash,
|
|
187
341
|
rpc: rpcClient,
|
|
@@ -206,8 +360,8 @@ async function runInstallPolicy(raw) {
|
|
|
206
360
|
// no existing rule this install replaces. A sentinel no real id
|
|
207
361
|
// can equal keeps every observed rule in scope.
|
|
208
362
|
ruleId: -1,
|
|
209
|
-
contextType:
|
|
210
|
-
signers:
|
|
363
|
+
contextType: rule.contextRuleType,
|
|
364
|
+
signers: rule.signers,
|
|
211
365
|
predicate: (0, decode_ts_1.decodePredicate)(encodedPredicate),
|
|
212
366
|
},
|
|
213
367
|
existing: observed,
|
|
@@ -301,23 +455,116 @@ function noInvocationError(toolName) {
|
|
|
301
455
|
retryable: false,
|
|
302
456
|
};
|
|
303
457
|
}
|
|
458
|
+
/** Scope a rule to whatever contract its predicate pins.
|
|
459
|
+
*
|
|
460
|
+
* Taking this from the predicate rather than from a separate argument means
|
|
461
|
+
* the rule's scope cannot drift from what the predicate actually checks. A
|
|
462
|
+
* predicate that pins no contract yields the default (account-wide) type,
|
|
463
|
+
* which is what an unpinned predicate means. Only the top level is walked:
|
|
464
|
+
* a contract pin nested under an `or` does not scope the rule, because the
|
|
465
|
+
* other branch would not be covered by it. */
|
|
466
|
+
function contextTypeForPredicate(predicate) {
|
|
467
|
+
const conjuncts = predicate.op === 'and' ? predicate.children : [predicate];
|
|
468
|
+
for (const node of conjuncts) {
|
|
469
|
+
if (node.op !== 'eq')
|
|
470
|
+
continue;
|
|
471
|
+
if (node.left?.kind !== 'call_contract')
|
|
472
|
+
continue;
|
|
473
|
+
if (node.right?.kind !== 'literal_address')
|
|
474
|
+
continue;
|
|
475
|
+
return { kind: 'call_contract', contract: node.right.value };
|
|
476
|
+
}
|
|
477
|
+
return { kind: 'default' };
|
|
478
|
+
}
|
|
479
|
+
/** Resolve what `simulate_policy` and `verify_policy` evaluate.
|
|
480
|
+
*
|
|
481
|
+
* Both want a predicate TREE plus the recording it came from, and neither is
|
|
482
|
+
* something a caller holds by default: the tree is only returned by
|
|
483
|
+
* `synthesize_policy` under `explain`, so a caller who did not ask for it has
|
|
484
|
+
* nothing to pass and skips the check. Skipping is the worst outcome here -
|
|
485
|
+
* these two ARE the check - so a transaction hash is accepted instead and the
|
|
486
|
+
* server rebuilds both from it. Recording is deterministic for a settled
|
|
487
|
+
* transaction, so this evaluates the same predicate the synthesiser produced. */
|
|
488
|
+
async function resolveCheckInputs(input, tool) {
|
|
489
|
+
const network = input.network ?? 'testnet';
|
|
490
|
+
// The call to check against: whichever the caller supplied, recording only
|
|
491
|
+
// when they gave a hash instead.
|
|
492
|
+
let permitTx;
|
|
493
|
+
if (input.permitTx !== undefined) {
|
|
494
|
+
permitTx = input.permitTx;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
const recordArgs = { hash: input.transactionHash, network };
|
|
498
|
+
const recorded = await runRecordTransaction(recordArgs);
|
|
499
|
+
if (!recorded.ok)
|
|
500
|
+
return { ok: false, error: recorded.error };
|
|
501
|
+
permitTx = recorded.data;
|
|
502
|
+
}
|
|
503
|
+
// The thing to check. A caller-supplied predicate wins over re-synthesis,
|
|
504
|
+
// in either form: a DECLARED policy has no recording behind it, so
|
|
505
|
+
// re-deriving one from the transaction would check a different predicate
|
|
506
|
+
// than the one the caller is asking about.
|
|
507
|
+
if (input.predicate !== undefined) {
|
|
508
|
+
return { ok: true, data: { predicate: input.predicate, permitTx } };
|
|
509
|
+
}
|
|
510
|
+
if (input.encodedPredicate !== undefined) {
|
|
511
|
+
try {
|
|
512
|
+
return { ok: true, data: { predicate: (0, decode_ts_1.decodePredicate)(input.encodedPredicate), permitTx } };
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
return toolFailure(tool, e);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const synthArgs = {
|
|
519
|
+
source: 'recording',
|
|
520
|
+
network,
|
|
521
|
+
// The schema's inferred type is `passthrough`, so it carries an index
|
|
522
|
+
// signature the core type does not; the shapes agree field for field.
|
|
523
|
+
recordedTx: permitTx,
|
|
524
|
+
explain: true,
|
|
525
|
+
...(input.smartAccount !== undefined
|
|
526
|
+
? { interpreter: { smartAccountAddress: input.smartAccount } }
|
|
527
|
+
: {}),
|
|
528
|
+
...(input.userResponses !== undefined ? { userResponses: input.userResponses } : {}),
|
|
529
|
+
};
|
|
530
|
+
const synthesized = await runSynthesizePolicy(synthArgs);
|
|
531
|
+
if (!synthesized.ok)
|
|
532
|
+
return { ok: false, error: synthesized.error };
|
|
533
|
+
const tree = synthesized.explain?.predicateTree;
|
|
534
|
+
if (!tree) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
error: {
|
|
538
|
+
code: TOOL_ERROR_CODE[tool],
|
|
539
|
+
message: `${tool}: synthesis produced no predicate to check for that transaction`,
|
|
540
|
+
severity: 'error',
|
|
541
|
+
retryable: false,
|
|
542
|
+
remediation: { toolCall: { name: tool, args: {} } },
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return { ok: true, data: { predicate: tree, permitTx } };
|
|
547
|
+
}
|
|
304
548
|
/** `simulate_policy` body - evaluate a predicate against one recorded call.
|
|
305
549
|
*
|
|
306
550
|
* The evaluator is a second implementation of the on-chain semantics, and the
|
|
307
551
|
* conformance harness asserts it agrees with the Rust interpreter case for
|
|
308
552
|
* case. A verdict here is therefore a claim about what the contract would do,
|
|
309
553
|
* not a guess. */
|
|
310
|
-
function runSimulatePolicy(raw) {
|
|
554
|
+
async function runSimulatePolicy(raw) {
|
|
311
555
|
const parsed = schemas_ts_1.SimulatePolicyInputSchema.safeParse(raw);
|
|
312
556
|
if (!parsed.success) {
|
|
313
557
|
return { ok: false, error: validationError('simulate_policy', parsed.error.issues) };
|
|
314
558
|
}
|
|
315
559
|
const input = parsed.data;
|
|
316
|
-
const
|
|
560
|
+
const resolved = await resolveCheckInputs(input, 'simulate_policy');
|
|
561
|
+
if (!resolved.ok)
|
|
562
|
+
return { ok: false, error: resolved.error };
|
|
563
|
+
const ctx = evalContextFromRecording(resolved.data.permitTx);
|
|
317
564
|
if (!ctx)
|
|
318
565
|
return { ok: false, error: noInvocationError('simulate_policy') };
|
|
319
566
|
try {
|
|
320
|
-
const res = (0, index_ts_2.evaluate)(
|
|
567
|
+
const res = (0, index_ts_2.evaluate)(resolved.data.predicate, ctx);
|
|
321
568
|
return {
|
|
322
569
|
ok: true,
|
|
323
570
|
data: {
|
|
@@ -391,17 +638,20 @@ function runDeclarePolicy(raw) {
|
|
|
391
638
|
* very transaction it was synthesised from. A deny case that permits means it
|
|
392
639
|
* is too LOOSE: some mutation of that transaction still gets through. `ok` is
|
|
393
640
|
* true only when neither holds. */
|
|
394
|
-
function runVerifyPolicy(raw) {
|
|
641
|
+
async function runVerifyPolicy(raw) {
|
|
395
642
|
const parsed = schemas_ts_1.VerifyPolicyInputSchema.safeParse(raw);
|
|
396
643
|
if (!parsed.success) {
|
|
397
644
|
return { ok: false, error: validationError('verify_policy', parsed.error.issues) };
|
|
398
645
|
}
|
|
399
646
|
const input = parsed.data;
|
|
400
|
-
const
|
|
647
|
+
const resolved = await resolveCheckInputs(input, 'verify_policy');
|
|
648
|
+
if (!resolved.ok)
|
|
649
|
+
return { ok: false, error: resolved.error };
|
|
650
|
+
const ctx = evalContextFromRecording(resolved.data.permitTx);
|
|
401
651
|
if (!ctx)
|
|
402
652
|
return { ok: false, error: noInvocationError('verify_policy') };
|
|
403
653
|
try {
|
|
404
|
-
const predicate =
|
|
654
|
+
const predicate = resolved.data.predicate;
|
|
405
655
|
const cases = (0, index_ts_2.generateCases)(predicate, ctx);
|
|
406
656
|
const permitRes = (0, index_ts_2.evaluate)(predicate, cases.permit);
|
|
407
657
|
const denies = cases.denies.map((d) => {
|