@agirails/sdk 2.2.2 → 2.3.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 (97) hide show
  1. package/README.md +20 -23
  2. package/dist/ACTPClient.d.ts +7 -0
  3. package/dist/ACTPClient.d.ts.map +1 -1
  4. package/dist/ACTPClient.js +56 -1
  5. package/dist/ACTPClient.js.map +1 -1
  6. package/dist/abi/AgentRegistry.json +133 -0
  7. package/dist/adapters/X402Adapter.d.ts +34 -7
  8. package/dist/adapters/X402Adapter.d.ts.map +1 -1
  9. package/dist/adapters/X402Adapter.js +36 -8
  10. package/dist/adapters/X402Adapter.js.map +1 -1
  11. package/dist/adapters/index.d.ts +1 -1
  12. package/dist/adapters/index.d.ts.map +1 -1
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/cli/commands/diff.d.ts +11 -0
  15. package/dist/cli/commands/diff.d.ts.map +1 -0
  16. package/dist/cli/commands/diff.js +115 -0
  17. package/dist/cli/commands/diff.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts.map +1 -1
  19. package/dist/cli/commands/init.js +51 -2
  20. package/dist/cli/commands/init.js.map +1 -1
  21. package/dist/cli/commands/publish.d.ts +11 -0
  22. package/dist/cli/commands/publish.d.ts.map +1 -0
  23. package/dist/cli/commands/publish.js +170 -0
  24. package/dist/cli/commands/publish.js.map +1 -0
  25. package/dist/cli/commands/pull.d.ts +12 -0
  26. package/dist/cli/commands/pull.d.ts.map +1 -0
  27. package/dist/cli/commands/pull.js +99 -0
  28. package/dist/cli/commands/pull.js.map +1 -0
  29. package/dist/cli/index.js +7 -0
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/config/agirailsmd.d.ts +94 -0
  32. package/dist/config/agirailsmd.d.ts.map +1 -0
  33. package/dist/config/agirailsmd.js +209 -0
  34. package/dist/config/agirailsmd.js.map +1 -0
  35. package/dist/config/networks.d.ts +2 -0
  36. package/dist/config/networks.d.ts.map +1 -1
  37. package/dist/config/networks.js +10 -4
  38. package/dist/config/networks.js.map +1 -1
  39. package/dist/config/publishPipeline.d.ts +61 -0
  40. package/dist/config/publishPipeline.d.ts.map +1 -0
  41. package/dist/config/publishPipeline.js +192 -0
  42. package/dist/config/publishPipeline.js.map +1 -0
  43. package/dist/config/syncOperations.d.ts +67 -0
  44. package/dist/config/syncOperations.d.ts.map +1 -0
  45. package/dist/config/syncOperations.js +208 -0
  46. package/dist/config/syncOperations.js.map +1 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +7 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/level0/request.d.ts.map +1 -1
  52. package/dist/level0/request.js +23 -86
  53. package/dist/level0/request.js.map +1 -1
  54. package/dist/level1/Agent.d.ts +0 -11
  55. package/dist/level1/Agent.d.ts.map +1 -1
  56. package/dist/level1/Agent.js +15 -32
  57. package/dist/level1/Agent.js.map +1 -1
  58. package/dist/registry/AgentRegistryClient.d.ts +75 -0
  59. package/dist/registry/AgentRegistryClient.d.ts.map +1 -0
  60. package/dist/registry/AgentRegistryClient.js +160 -0
  61. package/dist/registry/AgentRegistryClient.js.map +1 -0
  62. package/dist/runtime/MockRuntime.d.ts.map +1 -1
  63. package/dist/runtime/MockRuntime.js +3 -1
  64. package/dist/runtime/MockRuntime.js.map +1 -1
  65. package/dist/types/adapter.d.ts +39 -0
  66. package/dist/types/adapter.d.ts.map +1 -1
  67. package/dist/types/adapter.js +7 -0
  68. package/dist/types/adapter.js.map +1 -1
  69. package/dist/types/x402.d.ts +23 -0
  70. package/dist/types/x402.d.ts.map +1 -1
  71. package/dist/types/x402.js.map +1 -1
  72. package/dist/wallet/keystore.d.ts +16 -0
  73. package/dist/wallet/keystore.d.ts.map +1 -0
  74. package/dist/wallet/keystore.js +132 -0
  75. package/dist/wallet/keystore.js.map +1 -0
  76. package/package.json +2 -1
  77. package/src/ACTPClient.ts +63 -1
  78. package/src/abi/AgentRegistry.json +133 -0
  79. package/src/adapters/X402Adapter.ts +94 -16
  80. package/src/adapters/index.ts +9 -1
  81. package/src/cli/commands/diff.ts +141 -0
  82. package/src/cli/commands/init.ts +65 -4
  83. package/src/cli/commands/publish.ts +209 -0
  84. package/src/cli/commands/pull.ts +124 -0
  85. package/src/cli/index.ts +8 -0
  86. package/src/config/agirailsmd.ts +262 -0
  87. package/src/config/networks.ts +12 -4
  88. package/src/config/publishPipeline.ts +276 -0
  89. package/src/config/syncOperations.ts +279 -0
  90. package/src/index.ts +3 -0
  91. package/src/level0/request.ts +27 -88
  92. package/src/level1/Agent.ts +16 -32
  93. package/src/registry/AgentRegistryClient.ts +202 -0
  94. package/src/runtime/MockRuntime.ts +3 -1
  95. package/src/types/adapter.ts +14 -0
  96. package/src/types/x402.ts +32 -0
  97. package/src/wallet/keystore.ts +119 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Sync Operations - Pull + Diff for AGIRAILS.md
3
+ *
4
+ * Terraform-style sync: compare local AGIRAILS.md with on-chain state.
5
+ * Never auto-overwrites — shows diff and requires explicit confirmation.
6
+ *
7
+ * @module config/syncOperations
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
11
+ import { Provider } from 'ethers';
12
+ import { computeConfigHash, parseAgirailsMd, serializeAgirailsMd } from './agirailsmd';
13
+ import { validateCID } from '../utils/validation';
14
+ import { AgentRegistryClient } from '../registry/AgentRegistryClient';
15
+
16
+ // ============================================================================
17
+ // Constants
18
+ // ============================================================================
19
+
20
+ /** Public IPFS gateways for read-only access (no credentials needed) */
21
+ const IPFS_GATEWAYS = [
22
+ 'https://ipfs.io/ipfs/',
23
+ 'https://dweb.link/ipfs/',
24
+ 'https://cloudflare-ipfs.com/ipfs/',
25
+ ];
26
+
27
+ // ============================================================================
28
+ // Types
29
+ // ============================================================================
30
+
31
+ export interface DiffResult {
32
+ /** Whether local and on-chain are in sync */
33
+ inSync: boolean;
34
+ /** Local config hash (or null if no local file) */
35
+ localHash: string | null;
36
+ /** On-chain config hash (or zero hash if not published) */
37
+ onChainHash: string;
38
+ /** On-chain IPFS CID (or empty if not published) */
39
+ onChainCID: string;
40
+ /** Whether on-chain has a published config */
41
+ hasOnChainConfig: boolean;
42
+ /** Whether local file exists */
43
+ hasLocalFile: boolean;
44
+ /** Human-readable status message */
45
+ status: 'in-sync' | 'local-ahead' | 'remote-ahead' | 'diverged' | 'no-local' | 'no-remote';
46
+ }
47
+
48
+ export interface PullResult {
49
+ /** Whether a file was written */
50
+ written: boolean;
51
+ /** The pulled content (if any) */
52
+ content?: string;
53
+ /** IPFS CID that was fetched */
54
+ cid?: string;
55
+ /** Status message */
56
+ status: string;
57
+ }
58
+
59
+ export interface DiffOptions {
60
+ /** Path to local AGIRAILS.md */
61
+ path: string;
62
+ /** Agent address to check on-chain */
63
+ agentAddress: string;
64
+ /** AgentRegistry contract address */
65
+ registryAddress: string;
66
+ /** Provider for reading on-chain state */
67
+ provider: Provider;
68
+ }
69
+
70
+ export interface PullOptions extends DiffOptions {
71
+ /** Force overwrite without confirmation */
72
+ force?: boolean;
73
+ }
74
+
75
+ // ============================================================================
76
+ // Diff
77
+ // ============================================================================
78
+
79
+ const ZERO_HASH = '0x' + '0'.repeat(64);
80
+
81
+ /**
82
+ * Compare local AGIRAILS.md with on-chain config state.
83
+ *
84
+ * @param options - Diff configuration
85
+ * @returns Diff result showing sync status
86
+ */
87
+ export async function diff(options: DiffOptions): Promise<DiffResult> {
88
+ const { path, agentAddress, registryAddress, provider } = options;
89
+
90
+ // Read on-chain state
91
+ const registryClient = AgentRegistryClient.readOnly(registryAddress, provider);
92
+ const onChainState = await registryClient.getConfig(agentAddress);
93
+
94
+ const hasOnChainConfig = onChainState.configHash !== ZERO_HASH && onChainState.configCID !== '';
95
+ const onChainHash = onChainState.configHash;
96
+ const onChainCID = onChainState.configCID;
97
+
98
+ // Read local state
99
+ const hasLocalFile = existsSync(path);
100
+ let localHash: string | null = null;
101
+
102
+ if (hasLocalFile) {
103
+ const content = readFileSync(path, 'utf-8');
104
+ const { configHash } = computeConfigHash(content);
105
+ localHash = configHash;
106
+ }
107
+
108
+ // Determine status
109
+ let status: DiffResult['status'];
110
+ let inSync: boolean;
111
+
112
+ if (!hasLocalFile && !hasOnChainConfig) {
113
+ status = 'no-local';
114
+ inSync = true; // both empty = in sync
115
+ } else if (!hasLocalFile && hasOnChainConfig) {
116
+ status = 'remote-ahead';
117
+ inSync = false;
118
+ } else if (hasLocalFile && !hasOnChainConfig) {
119
+ status = 'no-remote';
120
+ inSync = false;
121
+ } else if (localHash === onChainHash) {
122
+ status = 'in-sync';
123
+ inSync = true;
124
+ } else {
125
+ // Both exist but hashes differ. Use the stored config_hash in frontmatter
126
+ // to determine directionality:
127
+ // - config_hash matches on-chain → user edited locally after last publish → local-ahead
128
+ // - config_hash doesn't match on-chain → remote was updated too → diverged
129
+ // - no config_hash → never published from this file → local-ahead
130
+ status = 'diverged';
131
+ inSync = false;
132
+
133
+ if (hasLocalFile) {
134
+ try {
135
+ const content = readFileSync(path, 'utf-8');
136
+ const { frontmatter } = parseAgirailsMd(content);
137
+ if (!frontmatter.config_hash) {
138
+ // Never published — local is the only source
139
+ status = 'local-ahead';
140
+ } else if (frontmatter.config_hash === onChainHash) {
141
+ // Last publish matches on-chain, so local edits are newer
142
+ status = 'local-ahead';
143
+ }
144
+ // else: frontmatter.config_hash !== onChainHash → remote updated → diverged
145
+ } catch {
146
+ // Parse error — keep as diverged
147
+ }
148
+ }
149
+ }
150
+
151
+ return {
152
+ inSync,
153
+ localHash,
154
+ onChainHash,
155
+ onChainCID,
156
+ hasOnChainConfig,
157
+ hasLocalFile,
158
+ status,
159
+ };
160
+ }
161
+
162
+ // ============================================================================
163
+ // IPFS Fetch (public gateway, no credentials needed)
164
+ // ============================================================================
165
+
166
+ /**
167
+ * Fetch content from IPFS using public gateways (no Filebase credentials needed).
168
+ * Tries multiple gateways with fallback.
169
+ *
170
+ * @param cid - IPFS CID to fetch (validated before use)
171
+ * @returns Raw content as string
172
+ * @throws InvalidCIDError if CID format is invalid
173
+ * @throws Error if all gateways fail
174
+ */
175
+ async function fetchFromIPFS(cid: string): Promise<string> {
176
+ // Validate CID format before hitting any gateway
177
+ validateCID(cid, 'onChainCID');
178
+
179
+ const errors: string[] = [];
180
+
181
+ for (const gateway of IPFS_GATEWAYS) {
182
+ try {
183
+ const response = await fetch(`${gateway}${cid}`, {
184
+ signal: AbortSignal.timeout(15_000),
185
+ });
186
+ if (!response.ok) {
187
+ errors.push(`${gateway}: HTTP ${response.status}`);
188
+ continue;
189
+ }
190
+ return await response.text();
191
+ } catch (err) {
192
+ errors.push(`${gateway}: ${err instanceof Error ? err.message : String(err)}`);
193
+ }
194
+ }
195
+
196
+ throw new Error(
197
+ `Failed to fetch CID ${cid} from all IPFS gateways:\n${errors.map(e => ` - ${e}`).join('\n')}`
198
+ );
199
+ }
200
+
201
+ // ============================================================================
202
+ // Pull
203
+ // ============================================================================
204
+
205
+ /**
206
+ * Pull on-chain config to local AGIRAILS.md.
207
+ *
208
+ * Downloads from IPFS via public gateways (no Filebase credentials needed),
209
+ * verifies integrity against on-chain configHash, then writes locally.
210
+ *
211
+ * @param options - Pull configuration
212
+ * @returns Pull result
213
+ */
214
+ export async function pull(options: PullOptions): Promise<PullResult> {
215
+ const { path, agentAddress, registryAddress, provider, force = false } = options;
216
+
217
+ // First, run diff
218
+ const diffResult = await diff({ path, agentAddress, registryAddress, provider });
219
+
220
+ if (!diffResult.hasOnChainConfig) {
221
+ return {
222
+ written: false,
223
+ status: 'No config published on-chain for this agent.',
224
+ };
225
+ }
226
+
227
+ if (diffResult.inSync) {
228
+ return {
229
+ written: false,
230
+ status: 'Already in sync. No changes needed.',
231
+ };
232
+ }
233
+
234
+ // Fetch raw AGIRAILS.md from IPFS (public gateway, no credentials)
235
+ const content = await fetchFromIPFS(diffResult.onChainCID);
236
+
237
+ // Integrity verification: hash downloaded content and compare with on-chain hash
238
+ const { configHash: downloadedHash } = computeConfigHash(content);
239
+ if (downloadedHash !== diffResult.onChainHash) {
240
+ return {
241
+ written: false,
242
+ cid: diffResult.onChainCID,
243
+ status: `Integrity check failed! Downloaded content hash (${downloadedHash}) does not match on-chain hash (${diffResult.onChainHash}). The IPFS content may have been tampered with.`,
244
+ };
245
+ }
246
+
247
+ // Check if local file exists and we're not forcing
248
+ if (diffResult.hasLocalFile && !force) {
249
+ return {
250
+ written: false,
251
+ content,
252
+ cid: diffResult.onChainCID,
253
+ status: `Remote config differs from local. Use --force to overwrite. CID: ${diffResult.onChainCID}`,
254
+ };
255
+ }
256
+
257
+ // Stamp on-chain metadata into frontmatter so diff heuristic can detect
258
+ // future remote changes (without this, pulled files have no config_hash
259
+ // and diff would always report "local-ahead" instead of "diverged")
260
+ const { frontmatter, body } = parseAgirailsMd(content);
261
+ const stamped = serializeAgirailsMd(
262
+ {
263
+ ...frontmatter,
264
+ config_hash: diffResult.onChainHash,
265
+ config_cid: diffResult.onChainCID,
266
+ },
267
+ body
268
+ );
269
+
270
+ // Write the stamped file
271
+ writeFileSync(path, stamped, 'utf-8');
272
+
273
+ return {
274
+ written: true,
275
+ content: stamped,
276
+ cid: diffResult.onChainCID,
277
+ status: `Pulled and verified config from IPFS (${diffResult.onChainCID}) → ${path}`,
278
+ };
279
+ }
package/src/index.ts CHANGED
@@ -176,6 +176,9 @@ export {
176
176
  createUsedAttestationTracker,
177
177
  } from './utils/UsedAttestationTracker';
178
178
 
179
+ // Wallet
180
+ export { resolvePrivateKey, getCachedAddress } from './wallet/keystore';
181
+
179
182
  // Helper utilities
180
183
  export {
181
184
  USDC,
@@ -14,6 +14,7 @@ import { NoProviderFoundError, TimeoutError, ValidationError } from '../errors';
14
14
  import { safeJSONParse, validateServiceName, isValidAddress } from '../utils/security';
15
15
  import { Logger } from '../utils/Logger';
16
16
  import { ethers } from 'ethers';
17
+ import { resolvePrivateKey } from '../wallet/keystore';
17
18
 
18
19
  /**
19
20
  * Request a service
@@ -61,7 +62,6 @@ export async function request(
61
62
  service: string,
62
63
  options: RequestOptions
63
64
  ): Promise<RequestResult> {
64
- // SECURITY FIX (H-2): Validate service name to prevent injection
65
65
  const validatedService = validateServiceName(service);
66
66
 
67
67
  const logger = new Logger({ source: 'request' });
@@ -75,12 +75,8 @@ export async function request(
75
75
  });
76
76
  }
77
77
 
78
- // SECURITY FIX (RPCURL): Use rpcUrl from options or fallback to network default
79
- // This allows Level0 request() to work with testnet/mainnet without requiring
80
- // explicit rpcUrl if user is okay with public RPC endpoints.
81
78
  let rpcUrl = options.rpcUrl;
82
79
  if (!rpcUrl && (options.network === 'testnet' || options.network === 'mainnet')) {
83
- // Import getNetwork to get default rpcUrl from network config
84
80
  const { getNetwork } = await import('../config/networks');
85
81
  const networkName = options.network === 'testnet' ? 'base-sepolia' : 'base-mainnet';
86
82
  const networkConfig = getNetwork(networkName);
@@ -88,27 +84,27 @@ export async function request(
88
84
  logger.info(`Using default RPC URL for ${networkName}: ${rpcUrl}`);
89
85
  }
90
86
 
91
- // Create ACTP client
87
+ const resolvedKey = await resolveKeyIfNeeded(options.wallet, options.network, options.stateDirectory);
88
+ const resolvedAddress = resolvedKey
89
+ ? new ethers.Wallet(resolvedKey).address.toLowerCase()
90
+ : undefined;
91
+
92
92
  const client = await ACTPClient.create({
93
93
  mode: options.network === 'testnet' ? 'testnet' : options.network === 'mainnet' ? 'mainnet' : 'mock',
94
- requesterAddress: getRequesterAddress(options.wallet),
94
+ requesterAddress: resolvedAddress || getRequesterAddress(options.wallet),
95
95
  stateDirectory: options.stateDirectory,
96
- privateKey: getPrivateKey(options.wallet),
96
+ privateKey: resolvedKey || getPrivateKey(options.wallet),
97
97
  rpcUrl,
98
98
  });
99
99
 
100
- // Calculate deadline
101
100
  const deadline = calculateDeadline(options.deadline, options.timeout);
102
-
103
- // Create transaction with service metadata
104
101
  const startTime = Date.now();
105
102
 
106
103
  try {
107
- const requesterAddress = getRequesterAddress(options.wallet);
104
+ const requesterAddress = resolvedAddress || getRequesterAddress(options.wallet);
108
105
  const amountWei = (options.budget * 1_000_000).toString(); // Convert to USDC wei (6 decimals)
109
106
 
110
107
  // In mock mode, ensure requester has enough funds
111
- // This is a convenience feature for testing - won't exist in production
112
108
  if (client.runtime && 'mintTokens' in client.runtime) {
113
109
  const mockRuntime = client.runtime as any;
114
110
  const balance = await mockRuntime.getBalance(requesterAddress);
@@ -122,30 +118,19 @@ export async function request(
122
118
  }
123
119
  }
124
120
 
125
- // ARCHITECTURE FIX: Service metadata handling
126
- // - MockRuntime: Store plaintext for provider matching and input extraction
127
- // - BlockchainRuntime: Hashes plaintext internally before sending on-chain
128
- //
129
- // Provider needs plaintext to:
130
- // 1. Match service name to handler (findServiceHandler)
131
- // 2. Extract input data for job execution (extractJobInput)
132
- //
133
- // On-chain storage uses bytes32 hash (BlockchainRuntime.validateServiceHash handles this)
134
121
  const serviceMetadata = JSON.stringify({
135
122
  service: validatedService,
136
123
  input: options.input,
137
124
  timestamp: Date.now(),
138
125
  });
139
126
 
140
- // Create transaction with structured metadata
141
- // MockRuntime stores as-is, BlockchainRuntime hashes for on-chain
142
127
  const txId = await client.runtime.createTransaction({
143
128
  provider,
144
129
  requester: requesterAddress,
145
130
  amount: amountWei,
146
131
  deadline,
147
- disputeWindow: options.disputeWindow ?? 172800, // Default 2 days
148
- serviceDescription: serviceMetadata, // Structured JSON for provider parsing
132
+ disputeWindow: options.disputeWindow ?? 172800,
133
+ serviceDescription: serviceMetadata,
149
134
  });
150
135
 
151
136
  // Call onProgress if provided
@@ -187,12 +172,8 @@ export async function request(
187
172
  attempts++;
188
173
  }
189
174
 
190
- // Check if we got a result
191
175
  if (!tx || (tx.state !== 'DELIVERED' && tx.state !== 'SETTLED')) {
192
- const _timedOut = true; // Flag for potential future use
193
-
194
- // SECURITY FIX (H-3): Auto-cancel transaction on timeout if still in early state
195
- // This prevents funds from being locked indefinitely if provider never responds
176
+ // Auto-cancel on timeout if still in early state
196
177
  if (tx && (tx.state === 'INITIATED' || tx.state === 'COMMITTED')) {
197
178
  try {
198
179
  logger.warn('Transaction timed out, cancelling to release funds', {
@@ -200,8 +181,6 @@ export async function request(
200
181
  state: tx.state,
201
182
  });
202
183
 
203
- // ACTUALLY CANCEL THE TRANSACTION
204
- // Check if runtime has cancelTransaction method
205
184
  if ('cancelTransaction' in client.runtime) {
206
185
  await (client.runtime as any).cancelTransaction(txId);
207
186
  logger.info('Transaction cancelled successfully', { txId });
@@ -211,7 +190,6 @@ export async function request(
211
190
  (error as any).wasCancelled = true;
212
191
  throw error;
213
192
  } else {
214
- // Fallback: Transition to CANCELLED state
215
193
  await client.runtime.transitionState(txId, 'CANCELLED');
216
194
  logger.info('Transaction cancelled successfully (via transitionState)', { txId });
217
195
 
@@ -222,7 +200,6 @@ export async function request(
222
200
  }
223
201
  } catch (cancelError) {
224
202
  logger.error('Failed to cancel timed-out transaction', { txId }, cancelError as Error);
225
- // Continue with original timeout error
226
203
  }
227
204
  }
228
205
 
@@ -232,13 +209,8 @@ export async function request(
232
209
  throw error;
233
210
  }
234
211
 
235
- // SECURITY FIX (C-3): Safe JSON parsing with schema validation
236
- // Extract result from delivery proof
237
212
  let deliveredResult: any = {};
238
213
  if (tx.deliveryProof) {
239
- // Define expected schema for delivery proof
240
- // NOTE: 'type' field with value 'delivery.proof' is the unique wrapper marker
241
- // (set by ProofGenerator.generateDeliveryProof) that handlers won't naturally return
242
214
  const DELIVERY_PROOF_SCHEMA: Record<string, string> = {
243
215
  result: 'any',
244
216
  data: 'any',
@@ -247,28 +219,18 @@ export async function request(
247
219
  timestamp: 'number',
248
220
  contentHash: 'string',
249
221
  txId: 'string',
250
- type: 'string', // Unique marker: 'delivery.proof'
222
+ type: 'string',
251
223
  };
252
224
 
253
- // Use safeJSONParse with schema validation which:
254
- // 1. Validates JSON structure
255
- // 2. Removes __proto__, constructor, prototype properties
256
- // 3. Prevents prototype pollution attacks
257
- // 4. Validates against expected schema
258
- // 5. Checks size limits to prevent DoS
259
- // 6. Returns null if parsing fails
260
225
  const parsed = safeJSONParse(tx.deliveryProof, DELIVERY_PROOF_SCHEMA);
261
226
 
262
227
  if (parsed !== null) {
263
228
  deliveredResult = parsed;
264
229
  } else {
265
- // If parsing failed, treat as plain text (but don't execute or eval)
266
230
  deliveredResult = { data: tx.deliveryProof };
267
231
  logger.warn('Failed to parse delivery proof as JSON', { txId });
268
232
  }
269
233
  } else if (options.network === 'testnet' || options.network === 'mainnet') {
270
- // KNOWN LIMITATION: BlockchainRuntime doesn't fetch deliveryProof from IPFS yet
271
- // Result will be empty until this is implemented
272
234
  logger.warn(
273
235
  'Delivery proof retrieval not yet implemented for testnet/mainnet. ' +
274
236
  'Result may be empty. Use ACTPClient with manual proof handling for production.',
@@ -276,17 +238,11 @@ export async function request(
276
238
  );
277
239
  }
278
240
 
279
- // SECURITY FIX (CRITICAL-2): Release escrow only after proper validation
280
- // For mock mode, auto-release is safe. For testnet/mainnet, require attestation.
281
241
  if (tx.state === 'DELIVERED' && tx.escrowId) {
282
- // Wait for dispute window to expire
283
242
  const disputeWindowEnd = (tx.completedAt ?? 0) + tx.disputeWindow;
284
243
  const currentTime = client.runtime.time.now();
285
244
 
286
245
  if (currentTime >= disputeWindowEnd) {
287
- // SECURITY FIX (CRITICAL-2): Only auto-release in mock mode
288
- // For real networks, the requester should manually verify and release
289
- // or use attestation-based verification
290
246
  const isMockMode = options.network !== 'testnet' && options.network !== 'mainnet';
291
247
 
292
248
  if (isMockMode) {
@@ -353,6 +309,20 @@ export async function request(
353
309
  }
354
310
  }
355
311
 
312
+ /**
313
+ * Resolve private key from keystore if wallet is auto/undefined and network is testnet/mainnet.
314
+ * Returns undefined if wallet is explicitly set (caller should use getPrivateKey instead).
315
+ */
316
+ async function resolveKeyIfNeeded(
317
+ wallet?: 'auto' | 'connect' | string | { privateKey: string },
318
+ network?: string,
319
+ stateDirectory?: string
320
+ ): Promise<string | undefined> {
321
+ if (wallet && wallet !== 'auto') return undefined; // explicit wallet, skip auto-detect
322
+ if (network !== 'testnet' && network !== 'mainnet') return undefined;
323
+ return resolvePrivateKey(stateDirectory);
324
+ }
325
+
356
326
  /**
357
327
  * Find provider for service
358
328
  *
@@ -381,23 +351,10 @@ function findProvider(
381
351
  return providers[0];
382
352
  }
383
353
 
384
- /**
385
- * Get requester address from wallet option
386
- *
387
- * SECURITY FIX (HIGH): Properly derive addresses from private keys using ethers
388
- * Never fabricate addresses or use partial key slices as addresses.
389
- *
390
- * @param wallet - Wallet configuration
391
- * @returns Ethereum address
392
- * @throws {ValidationError} If address format is invalid
393
- */
394
354
  function getRequesterAddress(
395
355
  wallet?: 'auto' | 'connect' | string | { privateKey: string }
396
356
  ): string {
397
- // For mock mode only: generate deterministic address
398
- // This is only safe because mock mode doesn't involve real funds
399
357
  if (!wallet || wallet === 'auto') {
400
- // Create a valid Ethereum address (40 hex chars) - ONLY for mock mode
401
358
  const hex = Buffer.from('requester').toString('hex');
402
359
  return '0x' + hex.padEnd(40, '0');
403
360
  }
@@ -407,15 +364,12 @@ function getRequesterAddress(
407
364
  }
408
365
 
409
366
  if (typeof wallet === 'string') {
410
- // SECURITY FIX (HIGH): Validate address format
411
367
  if (!isValidAddress(wallet)) {
412
368
  throw new ValidationError('wallet', `Invalid Ethereum address format: ${wallet}`);
413
369
  }
414
370
  return wallet.toLowerCase();
415
371
  }
416
372
 
417
- // SECURITY FIX (HIGH): Derive address from private key using ethers
418
- // This is the correct way to get address from a private key
419
373
  try {
420
374
  const walletInstance = new ethers.Wallet(wallet.privateKey);
421
375
  return walletInstance.address.toLowerCase();
@@ -424,15 +378,6 @@ function getRequesterAddress(
424
378
  }
425
379
  }
426
380
 
427
- /**
428
- * Get private key from wallet option
429
- *
430
- * SECURITY FIX (HIGH): Validate private key format before use
431
- *
432
- * @param wallet - Wallet configuration
433
- * @returns Private key or undefined
434
- * @throws {ValidationError} If private key format is invalid
435
- */
436
381
  function getPrivateKey(
437
382
  wallet?: 'auto' | 'connect' | string | { privateKey: string }
438
383
  ): string | undefined {
@@ -440,12 +385,8 @@ function getPrivateKey(
440
385
  return undefined;
441
386
  }
442
387
 
443
- // If wallet is a string that looks like a private key (0x + 64 hex chars), use it
444
- // Otherwise treat it as an address and return undefined
445
388
  if (typeof wallet === 'string') {
446
- // Check if it looks like a private key (0x + 64 hex chars)
447
389
  if (/^0x[0-9a-fA-F]{64}$/.test(wallet)) {
448
- // Validate by trying to create a wallet
449
390
  try {
450
391
  new ethers.Wallet(wallet);
451
392
  return wallet;
@@ -453,11 +394,9 @@ function getPrivateKey(
453
394
  throw new ValidationError('wallet', 'Invalid private key format');
454
395
  }
455
396
  }
456
- // It's an address, not a private key
457
397
  return undefined;
458
398
  }
459
399
 
460
- // Validate private key format
461
400
  if (wallet.privateKey) {
462
401
  try {
463
402
  new ethers.Wallet(wallet.privateKey);