@crediolabs/policy-synth 1.1.0 → 1.1.1
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-install-policy.d.ts +20 -0
- package/dist/install/build-install-policy.js +10 -2
- package/dist/run/schemas.d.ts +4 -4
- package/dist/run/schemas.js +19 -12
- package/dist-cjs/install/build-install-policy.d.ts +20 -0
- package/dist-cjs/install/build-install-policy.js +10 -2
- package/dist-cjs/run/schemas.d.ts +4 -4
- package/dist-cjs/run/schemas.js +19 -12
- package/package.json +1 -1
- package/src/install/build-install-policy.ts +35 -2
- package/src/run/schemas.ts +28 -12
|
@@ -112,6 +112,21 @@ export interface InstallCallDescribes {
|
|
|
112
112
|
export interface BuildInstallPolicyResult {
|
|
113
113
|
/** Unsigned Soroban transaction envelope, base64 XDR. */
|
|
114
114
|
unsignedXdr: string;
|
|
115
|
+
/** Length and SHA-256 of `unsignedXdr`, so a caller that has to move it can
|
|
116
|
+
* prove it arrived whole.
|
|
117
|
+
*
|
|
118
|
+
* This envelope runs to several thousand characters, and the only route
|
|
119
|
+
* from a tool result onto disk is the caller re-emitting it. A truncated
|
|
120
|
+
* copy is not obviously wrong - it fails later as
|
|
121
|
+
* "failed to decode XDR: xdr value invalid", which reads like a malformed
|
|
122
|
+
* transaction rather than a transport problem. Observed in practice: one of
|
|
123
|
+
* two envelopes written in the same session lost its tail and its base64
|
|
124
|
+
* length went from a multiple of four to `len % 4 == 3`.
|
|
125
|
+
*
|
|
126
|
+
* Check both before signing. They are cheap, and they turn a silent,
|
|
127
|
+
* fatal truncation into a retry. */
|
|
128
|
+
unsignedXdrLength: number;
|
|
129
|
+
unsignedXdrSha256: string;
|
|
115
130
|
/** Smart account contract address (echo). */
|
|
116
131
|
smartAccount: string;
|
|
117
132
|
/** Source account (echo) - the address that must sign. */
|
|
@@ -168,6 +183,11 @@ export declare function buildRevokePolicyXdr(args: {
|
|
|
168
183
|
}): Promise<BuildRevokePolicyResult>;
|
|
169
184
|
export interface BuildRevokePolicyResult {
|
|
170
185
|
unsignedXdr: string;
|
|
186
|
+
/** Same integrity pair as the install result, for the same reason: a revoke
|
|
187
|
+
* envelope also has to reach a signer intact, and a truncated copy fails as
|
|
188
|
+
* a malformed transaction rather than as a transport error. */
|
|
189
|
+
unsignedXdrLength: number;
|
|
190
|
+
unsignedXdrSha256: string;
|
|
171
191
|
smartAccount: string;
|
|
172
192
|
sourceAccount: string;
|
|
173
193
|
call: {
|
|
@@ -62,6 +62,14 @@ export function rpcClientFromServer(server, networkPassphrase) {
|
|
|
62
62
|
},
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
/** `unsignedXdr` plus the length and digest that prove it arrived whole. */
|
|
66
|
+
function xdrIntegrity(unsignedXdr) {
|
|
67
|
+
return {
|
|
68
|
+
unsignedXdr,
|
|
69
|
+
unsignedXdrLength: unsignedXdr.length,
|
|
70
|
+
unsignedXdrSha256: createHash('sha256').update(unsignedXdr, 'utf8').digest('hex'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
65
73
|
/** Build the unsigned transaction envelope for `account.add_context_rule(...)`.
|
|
66
74
|
* The output XDR is signed by the wallet, not by us. */
|
|
67
75
|
export async function buildInstallPolicyXdr(args) {
|
|
@@ -90,7 +98,7 @@ export async function buildInstallPolicyXdr(args) {
|
|
|
90
98
|
// The human approval binds to the exact bytes the wallet will sign.
|
|
91
99
|
const describes = decodeInstallCallDescribes(finalTx, args.installNonce);
|
|
92
100
|
return {
|
|
93
|
-
|
|
101
|
+
...xdrIntegrity(finalTx.toEnvelope().toXDR().toString('base64')),
|
|
94
102
|
smartAccount: args.smartAccount,
|
|
95
103
|
sourceAccount: args.sourceAccount,
|
|
96
104
|
call: { contract: args.smartAccount, fn: 'add_context_rule' },
|
|
@@ -119,7 +127,7 @@ export async function buildRevokePolicyXdr(args) {
|
|
|
119
127
|
// consumer supplies only the ordinary envelope signature.
|
|
120
128
|
const { finalTx, original, validUntilLedger } = await buildAuthorisedSmartAccountTx(args, 'remove_context_rule', [xdr.ScVal.scvU32(args.ruleId)], 'revoke_policy');
|
|
121
129
|
return {
|
|
122
|
-
|
|
130
|
+
...xdrIntegrity(finalTx.toEnvelope().toXDR().toString('base64')),
|
|
123
131
|
smartAccount: args.smartAccount,
|
|
124
132
|
sourceAccount: args.sourceAccount,
|
|
125
133
|
call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
|
package/dist/run/schemas.d.ts
CHANGED
|
@@ -2849,10 +2849,10 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodEffects<z.ZodOb
|
|
|
2849
2849
|
predicate?: unknown;
|
|
2850
2850
|
}>, "many">>;
|
|
2851
2851
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
2852
|
-
smartAccount: z.ZodString
|
|
2852
|
+
smartAccount: z.ZodEffects<z.ZodString, string, string>;
|
|
2853
2853
|
/** The signer that authorises the install (G... wallet). Used only for
|
|
2854
2854
|
* sequence number + auth nonce simulation; never persisted, never signed. */
|
|
2855
|
-
sourceAccount: z.ZodString
|
|
2855
|
+
sourceAccount: z.ZodEffects<z.ZodString, string, string>;
|
|
2856
2856
|
/** Target network for the install. Selects which interpreter pin and
|
|
2857
2857
|
* which RPC URL are valid by default. Defaults to `testnet` so the
|
|
2858
2858
|
* pre-mainnet callers keep working: they were always pointing at
|
|
@@ -4233,12 +4233,12 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodEffects<z.ZodOb
|
|
|
4233
4233
|
export type InstallPolicyInput = z.infer<typeof InstallPolicyInputSchema>;
|
|
4234
4234
|
export declare const RevokePolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
4235
4235
|
/** The smart account contract address (C...). */
|
|
4236
|
-
smartAccount: z.ZodString
|
|
4236
|
+
smartAccount: z.ZodEffects<z.ZodString, string, string>;
|
|
4237
4237
|
/** The wallet that will sign the removal. The ACCOUNT decides whether it
|
|
4238
4238
|
* accepts that signer; this schema does not assert a rule it cannot
|
|
4239
4239
|
* verify, since the account's source is not in this repo. Proven on
|
|
4240
4240
|
* testnet: the account's deployer can revoke. */
|
|
4241
|
-
sourceAccount: z.ZodString
|
|
4241
|
+
sourceAccount: z.ZodEffects<z.ZodString, string, string>;
|
|
4242
4242
|
/** Target network for the revoke. Same `testnet`-default as install,
|
|
4243
4243
|
* so pre-mainnet callers keep working without an explicit flag. */
|
|
4244
4244
|
network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
|
package/dist/run/schemas.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// This module is the SINGLE source of truth for these shapes. The MCP package
|
|
15
15
|
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
16
16
|
// them here so it can build the same args envelope the MCP transport builds.
|
|
17
|
+
import { StrKey } from '@stellar/stellar-sdk';
|
|
17
18
|
import { z } from 'zod';
|
|
18
19
|
import { isStellarAddress } from "../synth/address.js";
|
|
19
20
|
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
@@ -471,6 +472,20 @@ export const NETWORK_PASSPHRASES = {
|
|
|
471
472
|
// `sourceAccount` is the signing wallet (G...).
|
|
472
473
|
const STELLAR_CONTRACT_ADDRESS = /^C[2-7A-Z]{55}$/;
|
|
473
474
|
const STELLAR_ACCOUNT_ADDRESS = /^G[2-7A-Z]{55}$/;
|
|
475
|
+
// The regexes above check SHAPE only. A wrong-but-well-formed address - the
|
|
476
|
+
// classic case being one an agent reproduced from memory - passes them and then
|
|
477
|
+
// fails the SDK's StrKey decoder deep inside the build, where the throw is
|
|
478
|
+
// caught by the tool envelope and reported as a bare "invalid checksum" naming
|
|
479
|
+
// no field. A caller holding several addresses then cannot tell which one is
|
|
480
|
+
// wrong. Validating the checksum HERE keeps the field name attached.
|
|
481
|
+
const contractAddress = (field) => z
|
|
482
|
+
.string()
|
|
483
|
+
.regex(STELLAR_CONTRACT_ADDRESS, `${field} must be a Stellar contract address (C...)`)
|
|
484
|
+
.refine(StrKey.isValidContract, `${field} is not a valid contract address: the checksum does not match, so this address does not exist`);
|
|
485
|
+
const accountAddress = (field) => z
|
|
486
|
+
.string()
|
|
487
|
+
.regex(STELLAR_ACCOUNT_ADDRESS, `${field} must be a Stellar account address (G...)`)
|
|
488
|
+
.refine(StrKey.isValidEd25519PublicKey, `${field} is not a valid account address: the checksum does not match, so this address does not exist`);
|
|
474
489
|
// ===== declare_policy =====
|
|
475
490
|
//
|
|
476
491
|
// The declarative front-end: the constraint stated outright, with no
|
|
@@ -532,14 +547,10 @@ export const InstallPolicyInputSchema = z
|
|
|
532
547
|
* result says so rather than reporting "no overlaps found". */
|
|
533
548
|
existingRules: z.array(ObservedRuleSchema).optional(),
|
|
534
549
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
535
|
-
smartAccount:
|
|
536
|
-
.string()
|
|
537
|
-
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
550
|
+
smartAccount: contractAddress('smartAccount'),
|
|
538
551
|
/** The signer that authorises the install (G... wallet). Used only for
|
|
539
552
|
* sequence number + auth nonce simulation; never persisted, never signed. */
|
|
540
|
-
sourceAccount:
|
|
541
|
-
.string()
|
|
542
|
-
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
553
|
+
sourceAccount: accountAddress('sourceAccount'),
|
|
543
554
|
/** Target network for the install. Selects which interpreter pin and
|
|
544
555
|
* which RPC URL are valid by default. Defaults to `testnet` so the
|
|
545
556
|
* pre-mainnet callers keep working: they were always pointing at
|
|
@@ -673,16 +684,12 @@ export const InstallPolicyInputSchema = z
|
|
|
673
684
|
export const RevokePolicyInputSchema = z
|
|
674
685
|
.object({
|
|
675
686
|
/** The smart account contract address (C...). */
|
|
676
|
-
smartAccount:
|
|
677
|
-
.string()
|
|
678
|
-
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
687
|
+
smartAccount: contractAddress('smartAccount'),
|
|
679
688
|
/** The wallet that will sign the removal. The ACCOUNT decides whether it
|
|
680
689
|
* accepts that signer; this schema does not assert a rule it cannot
|
|
681
690
|
* verify, since the account's source is not in this repo. Proven on
|
|
682
691
|
* testnet: the account's deployer can revoke. */
|
|
683
|
-
sourceAccount:
|
|
684
|
-
.string()
|
|
685
|
-
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
692
|
+
sourceAccount: accountAddress('sourceAccount'),
|
|
686
693
|
/** Target network for the revoke. Same `testnet`-default as install,
|
|
687
694
|
* so pre-mainnet callers keep working without an explicit flag. */
|
|
688
695
|
network: NetworkSchema.optional(),
|
|
@@ -112,6 +112,21 @@ export interface InstallCallDescribes {
|
|
|
112
112
|
export interface BuildInstallPolicyResult {
|
|
113
113
|
/** Unsigned Soroban transaction envelope, base64 XDR. */
|
|
114
114
|
unsignedXdr: string;
|
|
115
|
+
/** Length and SHA-256 of `unsignedXdr`, so a caller that has to move it can
|
|
116
|
+
* prove it arrived whole.
|
|
117
|
+
*
|
|
118
|
+
* This envelope runs to several thousand characters, and the only route
|
|
119
|
+
* from a tool result onto disk is the caller re-emitting it. A truncated
|
|
120
|
+
* copy is not obviously wrong - it fails later as
|
|
121
|
+
* "failed to decode XDR: xdr value invalid", which reads like a malformed
|
|
122
|
+
* transaction rather than a transport problem. Observed in practice: one of
|
|
123
|
+
* two envelopes written in the same session lost its tail and its base64
|
|
124
|
+
* length went from a multiple of four to `len % 4 == 3`.
|
|
125
|
+
*
|
|
126
|
+
* Check both before signing. They are cheap, and they turn a silent,
|
|
127
|
+
* fatal truncation into a retry. */
|
|
128
|
+
unsignedXdrLength: number;
|
|
129
|
+
unsignedXdrSha256: string;
|
|
115
130
|
/** Smart account contract address (echo). */
|
|
116
131
|
smartAccount: string;
|
|
117
132
|
/** Source account (echo) - the address that must sign. */
|
|
@@ -168,6 +183,11 @@ export declare function buildRevokePolicyXdr(args: {
|
|
|
168
183
|
}): Promise<BuildRevokePolicyResult>;
|
|
169
184
|
export interface BuildRevokePolicyResult {
|
|
170
185
|
unsignedXdr: string;
|
|
186
|
+
/** Same integrity pair as the install result, for the same reason: a revoke
|
|
187
|
+
* envelope also has to reach a signer intact, and a truncated copy fails as
|
|
188
|
+
* a malformed transaction rather than as a transport error. */
|
|
189
|
+
unsignedXdrLength: number;
|
|
190
|
+
unsignedXdrSha256: string;
|
|
171
191
|
smartAccount: string;
|
|
172
192
|
sourceAccount: string;
|
|
173
193
|
call: {
|
|
@@ -68,6 +68,14 @@ function rpcClientFromServer(server, networkPassphrase) {
|
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
+
/** `unsignedXdr` plus the length and digest that prove it arrived whole. */
|
|
72
|
+
function xdrIntegrity(unsignedXdr) {
|
|
73
|
+
return {
|
|
74
|
+
unsignedXdr,
|
|
75
|
+
unsignedXdrLength: unsignedXdr.length,
|
|
76
|
+
unsignedXdrSha256: (0, node_crypto_1.createHash)('sha256').update(unsignedXdr, 'utf8').digest('hex'),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
71
79
|
/** Build the unsigned transaction envelope for `account.add_context_rule(...)`.
|
|
72
80
|
* The output XDR is signed by the wallet, not by us. */
|
|
73
81
|
async function buildInstallPolicyXdr(args) {
|
|
@@ -96,7 +104,7 @@ async function buildInstallPolicyXdr(args) {
|
|
|
96
104
|
// The human approval binds to the exact bytes the wallet will sign.
|
|
97
105
|
const describes = decodeInstallCallDescribes(finalTx, args.installNonce);
|
|
98
106
|
return {
|
|
99
|
-
|
|
107
|
+
...xdrIntegrity(finalTx.toEnvelope().toXDR().toString('base64')),
|
|
100
108
|
smartAccount: args.smartAccount,
|
|
101
109
|
sourceAccount: args.sourceAccount,
|
|
102
110
|
call: { contract: args.smartAccount, fn: 'add_context_rule' },
|
|
@@ -125,7 +133,7 @@ async function buildRevokePolicyXdr(args) {
|
|
|
125
133
|
// consumer supplies only the ordinary envelope signature.
|
|
126
134
|
const { finalTx, original, validUntilLedger } = await buildAuthorisedSmartAccountTx(args, 'remove_context_rule', [stellar_sdk_1.xdr.ScVal.scvU32(args.ruleId)], 'revoke_policy');
|
|
127
135
|
return {
|
|
128
|
-
|
|
136
|
+
...xdrIntegrity(finalTx.toEnvelope().toXDR().toString('base64')),
|
|
129
137
|
smartAccount: args.smartAccount,
|
|
130
138
|
sourceAccount: args.sourceAccount,
|
|
131
139
|
call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
|
|
@@ -2849,10 +2849,10 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodEffects<z.ZodOb
|
|
|
2849
2849
|
predicate?: unknown;
|
|
2850
2850
|
}>, "many">>;
|
|
2851
2851
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
2852
|
-
smartAccount: z.ZodString
|
|
2852
|
+
smartAccount: z.ZodEffects<z.ZodString, string, string>;
|
|
2853
2853
|
/** The signer that authorises the install (G... wallet). Used only for
|
|
2854
2854
|
* sequence number + auth nonce simulation; never persisted, never signed. */
|
|
2855
|
-
sourceAccount: z.ZodString
|
|
2855
|
+
sourceAccount: z.ZodEffects<z.ZodString, string, string>;
|
|
2856
2856
|
/** Target network for the install. Selects which interpreter pin and
|
|
2857
2857
|
* which RPC URL are valid by default. Defaults to `testnet` so the
|
|
2858
2858
|
* pre-mainnet callers keep working: they were always pointing at
|
|
@@ -4233,12 +4233,12 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodEffects<z.ZodOb
|
|
|
4233
4233
|
export type InstallPolicyInput = z.infer<typeof InstallPolicyInputSchema>;
|
|
4234
4234
|
export declare const RevokePolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
4235
4235
|
/** The smart account contract address (C...). */
|
|
4236
|
-
smartAccount: z.ZodString
|
|
4236
|
+
smartAccount: z.ZodEffects<z.ZodString, string, string>;
|
|
4237
4237
|
/** The wallet that will sign the removal. The ACCOUNT decides whether it
|
|
4238
4238
|
* accepts that signer; this schema does not assert a rule it cannot
|
|
4239
4239
|
* verify, since the account's source is not in this repo. Proven on
|
|
4240
4240
|
* testnet: the account's deployer can revoke. */
|
|
4241
|
-
sourceAccount: z.ZodString
|
|
4241
|
+
sourceAccount: z.ZodEffects<z.ZodString, string, string>;
|
|
4242
4242
|
/** Target network for the revoke. Same `testnet`-default as install,
|
|
4243
4243
|
* so pre-mainnet callers keep working without an explicit flag. */
|
|
4244
4244
|
network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
|
package/dist-cjs/run/schemas.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// them here so it can build the same args envelope the MCP transport builds.
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.ToolErrorSchema = exports.GetInterpreterInfoInputSchema = exports.RevokePolicyInputSchema = exports.InstallPolicyInputSchema = exports.DeclarePolicyInputSchema = exports.NETWORK_PASSPHRASES = exports.PINNED_OZ_POLICY_WASM_SHA256 = exports.PINNED_OZ_POLICY_ADDRESS_BY_NETWORK = exports.PINNED_OZ_STELLAR_CONTRACTS_TAG = exports.RPC_URL_BY_NETWORK = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.MAINNET_RPC_URL = exports.TESTNET_RPC_URL = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.ObservedRuleSchema = exports.VerifyPolicyInputSchema = exports.SimulatePolicyInputSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.SynthesizePolicyInputSchema = exports.InterpreterOptionsSchema = exports.RecordTransactionInputSchema = exports.ComposeUserResponsesSchema = exports.RecordedTransactionSchema = exports.ParseConfidenceSchema = exports.OnChainEventSchema = exports.TokenMovementSchema = exports.ContractInvocationSchema = exports.ScValSchema = exports.NetworkSchema = void 0;
|
|
20
|
+
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
20
21
|
const zod_1 = require("zod");
|
|
21
22
|
const address_ts_1 = require("../synth/address.js");
|
|
22
23
|
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
@@ -474,6 +475,20 @@ exports.NETWORK_PASSPHRASES = {
|
|
|
474
475
|
// `sourceAccount` is the signing wallet (G...).
|
|
475
476
|
const STELLAR_CONTRACT_ADDRESS = /^C[2-7A-Z]{55}$/;
|
|
476
477
|
const STELLAR_ACCOUNT_ADDRESS = /^G[2-7A-Z]{55}$/;
|
|
478
|
+
// The regexes above check SHAPE only. A wrong-but-well-formed address - the
|
|
479
|
+
// classic case being one an agent reproduced from memory - passes them and then
|
|
480
|
+
// fails the SDK's StrKey decoder deep inside the build, where the throw is
|
|
481
|
+
// caught by the tool envelope and reported as a bare "invalid checksum" naming
|
|
482
|
+
// no field. A caller holding several addresses then cannot tell which one is
|
|
483
|
+
// wrong. Validating the checksum HERE keeps the field name attached.
|
|
484
|
+
const contractAddress = (field) => zod_1.z
|
|
485
|
+
.string()
|
|
486
|
+
.regex(STELLAR_CONTRACT_ADDRESS, `${field} must be a Stellar contract address (C...)`)
|
|
487
|
+
.refine(stellar_sdk_1.StrKey.isValidContract, `${field} is not a valid contract address: the checksum does not match, so this address does not exist`);
|
|
488
|
+
const accountAddress = (field) => zod_1.z
|
|
489
|
+
.string()
|
|
490
|
+
.regex(STELLAR_ACCOUNT_ADDRESS, `${field} must be a Stellar account address (G...)`)
|
|
491
|
+
.refine(stellar_sdk_1.StrKey.isValidEd25519PublicKey, `${field} is not a valid account address: the checksum does not match, so this address does not exist`);
|
|
477
492
|
// ===== declare_policy =====
|
|
478
493
|
//
|
|
479
494
|
// The declarative front-end: the constraint stated outright, with no
|
|
@@ -535,14 +550,10 @@ exports.InstallPolicyInputSchema = zod_1.z
|
|
|
535
550
|
* result says so rather than reporting "no overlaps found". */
|
|
536
551
|
existingRules: zod_1.z.array(exports.ObservedRuleSchema).optional(),
|
|
537
552
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
538
|
-
smartAccount:
|
|
539
|
-
.string()
|
|
540
|
-
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
553
|
+
smartAccount: contractAddress('smartAccount'),
|
|
541
554
|
/** The signer that authorises the install (G... wallet). Used only for
|
|
542
555
|
* sequence number + auth nonce simulation; never persisted, never signed. */
|
|
543
|
-
sourceAccount:
|
|
544
|
-
.string()
|
|
545
|
-
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
556
|
+
sourceAccount: accountAddress('sourceAccount'),
|
|
546
557
|
/** Target network for the install. Selects which interpreter pin and
|
|
547
558
|
* which RPC URL are valid by default. Defaults to `testnet` so the
|
|
548
559
|
* pre-mainnet callers keep working: they were always pointing at
|
|
@@ -676,16 +687,12 @@ exports.InstallPolicyInputSchema = zod_1.z
|
|
|
676
687
|
exports.RevokePolicyInputSchema = zod_1.z
|
|
677
688
|
.object({
|
|
678
689
|
/** The smart account contract address (C...). */
|
|
679
|
-
smartAccount:
|
|
680
|
-
.string()
|
|
681
|
-
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
690
|
+
smartAccount: contractAddress('smartAccount'),
|
|
682
691
|
/** The wallet that will sign the removal. The ACCOUNT decides whether it
|
|
683
692
|
* accepts that signer; this schema does not assert a rule it cannot
|
|
684
693
|
* verify, since the account's source is not in this repo. Proven on
|
|
685
694
|
* testnet: the account's deployer can revoke. */
|
|
686
|
-
sourceAccount:
|
|
687
|
-
.string()
|
|
688
|
-
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
695
|
+
sourceAccount: accountAddress('sourceAccount'),
|
|
689
696
|
/** Target network for the revoke. Same `testnet`-default as install,
|
|
690
697
|
* so pre-mainnet callers keep working without an explicit flag. */
|
|
691
698
|
network: exports.NetworkSchema.optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-synth",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Off-chain TypeScript synthesis core for the OZ Accounts Policy Builder. Records Soroban transactions, synthesises the minimal policy that permits exactly that flow, verifies it, and returns an unsigned install transaction.",
|
|
6
6
|
"type": "module",
|
|
@@ -182,12 +182,40 @@ export interface InstallCallDescribes {
|
|
|
182
182
|
installNonce: number
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/** `unsignedXdr` plus the length and digest that prove it arrived whole. */
|
|
186
|
+
function xdrIntegrity(unsignedXdr: string): {
|
|
187
|
+
unsignedXdr: string
|
|
188
|
+
unsignedXdrLength: number
|
|
189
|
+
unsignedXdrSha256: string
|
|
190
|
+
} {
|
|
191
|
+
return {
|
|
192
|
+
unsignedXdr,
|
|
193
|
+
unsignedXdrLength: unsignedXdr.length,
|
|
194
|
+
unsignedXdrSha256: createHash('sha256').update(unsignedXdr, 'utf8').digest('hex'),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
185
198
|
/** Output of the install-policy build. The unsigned XDR is the wallet's
|
|
186
199
|
* input; the captured auth nonce + invocation root make the response
|
|
187
200
|
* self-describing for callers that want to inspect what they signed. */
|
|
188
201
|
export interface BuildInstallPolicyResult {
|
|
189
202
|
/** Unsigned Soroban transaction envelope, base64 XDR. */
|
|
190
203
|
unsignedXdr: string
|
|
204
|
+
/** Length and SHA-256 of `unsignedXdr`, so a caller that has to move it can
|
|
205
|
+
* prove it arrived whole.
|
|
206
|
+
*
|
|
207
|
+
* This envelope runs to several thousand characters, and the only route
|
|
208
|
+
* from a tool result onto disk is the caller re-emitting it. A truncated
|
|
209
|
+
* copy is not obviously wrong - it fails later as
|
|
210
|
+
* "failed to decode XDR: xdr value invalid", which reads like a malformed
|
|
211
|
+
* transaction rather than a transport problem. Observed in practice: one of
|
|
212
|
+
* two envelopes written in the same session lost its tail and its base64
|
|
213
|
+
* length went from a multiple of four to `len % 4 == 3`.
|
|
214
|
+
*
|
|
215
|
+
* Check both before signing. They are cheap, and they turn a silent,
|
|
216
|
+
* fatal truncation into a retry. */
|
|
217
|
+
unsignedXdrLength: number
|
|
218
|
+
unsignedXdrSha256: string
|
|
191
219
|
/** Smart account contract address (echo). */
|
|
192
220
|
smartAccount: string
|
|
193
221
|
/** Source account (echo) - the address that must sign. */
|
|
@@ -258,7 +286,7 @@ export async function buildInstallPolicyXdr(
|
|
|
258
286
|
const describes = decodeInstallCallDescribes(finalTx, args.installNonce)
|
|
259
287
|
|
|
260
288
|
return {
|
|
261
|
-
|
|
289
|
+
...xdrIntegrity(finalTx.toEnvelope().toXDR().toString('base64')),
|
|
262
290
|
smartAccount: args.smartAccount,
|
|
263
291
|
sourceAccount: args.sourceAccount,
|
|
264
292
|
call: { contract: args.smartAccount, fn: 'add_context_rule' },
|
|
@@ -302,7 +330,7 @@ export async function buildRevokePolicyXdr(args: {
|
|
|
302
330
|
)
|
|
303
331
|
|
|
304
332
|
return {
|
|
305
|
-
|
|
333
|
+
...xdrIntegrity(finalTx.toEnvelope().toXDR().toString('base64')),
|
|
306
334
|
smartAccount: args.smartAccount,
|
|
307
335
|
sourceAccount: args.sourceAccount,
|
|
308
336
|
call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
|
|
@@ -314,6 +342,11 @@ export async function buildRevokePolicyXdr(args: {
|
|
|
314
342
|
|
|
315
343
|
export interface BuildRevokePolicyResult {
|
|
316
344
|
unsignedXdr: string
|
|
345
|
+
/** Same integrity pair as the install result, for the same reason: a revoke
|
|
346
|
+
* envelope also has to reach a signer intact, and a truncated copy fails as
|
|
347
|
+
* a malformed transaction rather than as a transport error. */
|
|
348
|
+
unsignedXdrLength: number
|
|
349
|
+
unsignedXdrSha256: string
|
|
317
350
|
smartAccount: string
|
|
318
351
|
sourceAccount: string
|
|
319
352
|
call: { contract: string; fn: 'remove_context_rule'; ruleId: number }
|
package/src/run/schemas.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
16
16
|
// them here so it can build the same args envelope the MCP transport builds.
|
|
17
17
|
|
|
18
|
+
import { StrKey } from '@stellar/stellar-sdk'
|
|
18
19
|
import { z } from 'zod'
|
|
19
20
|
import { isStellarAddress } from '../synth/address.ts'
|
|
20
21
|
|
|
@@ -550,6 +551,29 @@ export const NETWORK_PASSPHRASES: Record<Network, string> = {
|
|
|
550
551
|
const STELLAR_CONTRACT_ADDRESS = /^C[2-7A-Z]{55}$/
|
|
551
552
|
const STELLAR_ACCOUNT_ADDRESS = /^G[2-7A-Z]{55}$/
|
|
552
553
|
|
|
554
|
+
// The regexes above check SHAPE only. A wrong-but-well-formed address - the
|
|
555
|
+
// classic case being one an agent reproduced from memory - passes them and then
|
|
556
|
+
// fails the SDK's StrKey decoder deep inside the build, where the throw is
|
|
557
|
+
// caught by the tool envelope and reported as a bare "invalid checksum" naming
|
|
558
|
+
// no field. A caller holding several addresses then cannot tell which one is
|
|
559
|
+
// wrong. Validating the checksum HERE keeps the field name attached.
|
|
560
|
+
const contractAddress = (field: string) =>
|
|
561
|
+
z
|
|
562
|
+
.string()
|
|
563
|
+
.regex(STELLAR_CONTRACT_ADDRESS, `${field} must be a Stellar contract address (C...)`)
|
|
564
|
+
.refine(
|
|
565
|
+
StrKey.isValidContract,
|
|
566
|
+
`${field} is not a valid contract address: the checksum does not match, so this address does not exist`
|
|
567
|
+
)
|
|
568
|
+
const accountAddress = (field: string) =>
|
|
569
|
+
z
|
|
570
|
+
.string()
|
|
571
|
+
.regex(STELLAR_ACCOUNT_ADDRESS, `${field} must be a Stellar account address (G...)`)
|
|
572
|
+
.refine(
|
|
573
|
+
StrKey.isValidEd25519PublicKey,
|
|
574
|
+
`${field} is not a valid account address: the checksum does not match, so this address does not exist`
|
|
575
|
+
)
|
|
576
|
+
|
|
553
577
|
// ===== declare_policy =====
|
|
554
578
|
//
|
|
555
579
|
// The declarative front-end: the constraint stated outright, with no
|
|
@@ -613,14 +637,10 @@ export const InstallPolicyInputSchema = z
|
|
|
613
637
|
* result says so rather than reporting "no overlaps found". */
|
|
614
638
|
existingRules: z.array(ObservedRuleSchema).optional(),
|
|
615
639
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
616
|
-
smartAccount:
|
|
617
|
-
.string()
|
|
618
|
-
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
640
|
+
smartAccount: contractAddress('smartAccount'),
|
|
619
641
|
/** The signer that authorises the install (G... wallet). Used only for
|
|
620
642
|
* sequence number + auth nonce simulation; never persisted, never signed. */
|
|
621
|
-
sourceAccount:
|
|
622
|
-
.string()
|
|
623
|
-
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
643
|
+
sourceAccount: accountAddress('sourceAccount'),
|
|
624
644
|
/** Target network for the install. Selects which interpreter pin and
|
|
625
645
|
* which RPC URL are valid by default. Defaults to `testnet` so the
|
|
626
646
|
* pre-mainnet callers keep working: they were always pointing at
|
|
@@ -760,16 +780,12 @@ export type InstallPolicyInput = z.infer<typeof InstallPolicyInputSchema>
|
|
|
760
780
|
export const RevokePolicyInputSchema = z
|
|
761
781
|
.object({
|
|
762
782
|
/** The smart account contract address (C...). */
|
|
763
|
-
smartAccount:
|
|
764
|
-
.string()
|
|
765
|
-
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
783
|
+
smartAccount: contractAddress('smartAccount'),
|
|
766
784
|
/** The wallet that will sign the removal. The ACCOUNT decides whether it
|
|
767
785
|
* accepts that signer; this schema does not assert a rule it cannot
|
|
768
786
|
* verify, since the account's source is not in this repo. Proven on
|
|
769
787
|
* testnet: the account's deployer can revoke. */
|
|
770
|
-
sourceAccount:
|
|
771
|
-
.string()
|
|
772
|
-
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
788
|
+
sourceAccount: accountAddress('sourceAccount'),
|
|
773
789
|
/** Target network for the revoke. Same `testnet`-default as install,
|
|
774
790
|
* so pre-mainnet callers keep working without an explicit flag. */
|
|
775
791
|
network: NetworkSchema.optional(),
|