@agirails/sdk 2.4.2 → 2.5.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/ACTPClient.js +1 -1
- package/dist/ACTPClient.js.map +1 -1
- package/dist/cli/commands/deploy-check.d.ts +24 -0
- package/dist/cli/commands/deploy-check.d.ts.map +1 -0
- package/dist/cli/commands/deploy-check.js +316 -0
- package/dist/cli/commands/deploy-check.js.map +1 -0
- package/dist/cli/commands/deploy-env.d.ts +19 -0
- package/dist/cli/commands/deploy-env.d.ts.map +1 -0
- package/dist/cli/commands/deploy-env.js +123 -0
- package/dist/cli/commands/deploy-env.js.map +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +15 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/publish.js +1 -1
- package/dist/cli/commands/publish.js.map +1 -1
- package/dist/cli/commands/register.js +1 -1
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/config.d.ts +12 -0
- package/dist/cli/utils/config.d.ts.map +1 -1
- package/dist/cli/utils/config.js +61 -1
- package/dist/cli/utils/config.js.map +1 -1
- package/dist/level0/request.js +1 -1
- package/dist/level0/request.js.map +1 -1
- package/dist/level1/Agent.js +1 -1
- package/dist/level1/Agent.js.map +1 -1
- package/dist/wallet/keystore.d.ts +10 -3
- package/dist/wallet/keystore.d.ts.map +1 -1
- package/dist/wallet/keystore.js +91 -11
- package/dist/wallet/keystore.js.map +1 -1
- package/package.json +1 -1
- package/src/ACTPClient.ts +1 -1
- package/src/cli/commands/deploy-check.ts +364 -0
- package/src/cli/commands/deploy-env.ts +120 -0
- package/src/cli/commands/init.ts +15 -1
- package/src/cli/commands/publish.ts +1 -1
- package/src/cli/commands/register.ts +1 -1
- package/src/cli/index.ts +6 -0
- package/src/cli/utils/config.ts +68 -0
- package/src/level0/request.ts +1 -1
- package/src/level1/Agent.ts +1 -1
- package/src/wallet/keystore.ts +116 -11
package/src/cli/commands/init.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { Command } from 'commander';
|
|
|
14
14
|
import {
|
|
15
15
|
saveConfig,
|
|
16
16
|
addToGitignore,
|
|
17
|
+
addToDockerignore,
|
|
18
|
+
addToRailwayignore,
|
|
17
19
|
isInitialized,
|
|
18
20
|
getActpDir,
|
|
19
21
|
CLIConfig,
|
|
@@ -236,13 +238,25 @@ async function runInit(options: InitOptions, output: Output, cmd?: Command): Pro
|
|
|
236
238
|
output.info('Minted 10,000 USDC to your address');
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
// Add to gitignore
|
|
241
|
+
// Add to ignore files (AIP-13: gitignore + dockerignore + railwayignore)
|
|
240
242
|
try {
|
|
241
243
|
addToGitignore(projectRoot);
|
|
242
244
|
output.success('Added .actp/ to .gitignore');
|
|
243
245
|
} catch {
|
|
244
246
|
output.warning('Could not update .gitignore (may not exist)');
|
|
245
247
|
}
|
|
248
|
+
try {
|
|
249
|
+
addToDockerignore(projectRoot);
|
|
250
|
+
output.success('Added .actp/ to .dockerignore');
|
|
251
|
+
} catch {
|
|
252
|
+
output.warning('Could not update .dockerignore');
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
addToRailwayignore(projectRoot);
|
|
256
|
+
output.success('Added .actp/ to .railwayignore');
|
|
257
|
+
} catch {
|
|
258
|
+
output.warning('Could not update .railwayignore');
|
|
259
|
+
}
|
|
246
260
|
|
|
247
261
|
// Output result
|
|
248
262
|
output.blank();
|
|
@@ -394,7 +394,7 @@ async function activateOnTestnet(
|
|
|
394
394
|
const { buildActivationBatch, buildTestnetMintBatch } = await import('../../wallet/aa/TransactionBatcher');
|
|
395
395
|
const { getOnChainAgentState, detectLazyPublishScenario } = await import('../../ACTPClient');
|
|
396
396
|
|
|
397
|
-
const privateKey = await resolvePrivateKey(projectRoot);
|
|
397
|
+
const privateKey = await resolvePrivateKey(projectRoot, { network: 'testnet' });
|
|
398
398
|
if (!privateKey) {
|
|
399
399
|
throw new Error('No wallet found. Cannot activate on testnet.');
|
|
400
400
|
}
|
|
@@ -85,7 +85,7 @@ async function runRegister(
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Resolve private key
|
|
88
|
-
const privateKey = await resolvePrivateKey(projectRoot);
|
|
88
|
+
const privateKey = await resolvePrivateKey(projectRoot, { network: config.mode });
|
|
89
89
|
if (!privateKey) {
|
|
90
90
|
throw new Error(
|
|
91
91
|
'No wallet found. Run "actp init" first to generate a wallet.'
|
package/src/cli/index.ts
CHANGED
|
@@ -52,6 +52,8 @@ import { createPublishCommand } from './commands/publish';
|
|
|
52
52
|
import { createPullCommand } from './commands/pull';
|
|
53
53
|
import { createDiffCommand } from './commands/diff';
|
|
54
54
|
import { createRegisterCommand } from './commands/register';
|
|
55
|
+
import { createDeployEnvCommand } from './commands/deploy-env';
|
|
56
|
+
import { createDeployCheckCommand } from './commands/deploy-check';
|
|
55
57
|
|
|
56
58
|
// ============================================================================
|
|
57
59
|
// Program Setup
|
|
@@ -104,6 +106,10 @@ program.addCommand(createDiffCommand());
|
|
|
104
106
|
// AIP-12: Gas-free registration
|
|
105
107
|
program.addCommand(createRegisterCommand());
|
|
106
108
|
|
|
109
|
+
// AIP-13: Deployment security
|
|
110
|
+
program.addCommand(createDeployEnvCommand());
|
|
111
|
+
program.addCommand(createDeployCheckCommand());
|
|
112
|
+
|
|
107
113
|
// ============================================================================
|
|
108
114
|
// Error Handling
|
|
109
115
|
// ============================================================================
|
package/src/cli/utils/config.ts
CHANGED
|
@@ -319,3 +319,71 @@ export function addToGitignore(projectRoot: string = process.cwd()): void {
|
|
|
319
319
|
|
|
320
320
|
fs.writeFileSync(gitignorePath, newContent, 'utf-8');
|
|
321
321
|
}
|
|
322
|
+
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// Ignore File Management (AIP-13)
|
|
325
|
+
// ============================================================================
|
|
326
|
+
|
|
327
|
+
const IGNORE_ENTRIES = ['.actp/', '.env', '.env.*', 'node_modules/'];
|
|
328
|
+
const IGNORE_HEADER = '# ACTP deployment security (AIP-13)';
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Validate that a file path is not a symlink.
|
|
332
|
+
* Throws if the path exists and is a symlink (security: prevents symlink attacks).
|
|
333
|
+
*/
|
|
334
|
+
function assertNotSymlink(filePath: string): void {
|
|
335
|
+
try {
|
|
336
|
+
const stat = fs.lstatSync(filePath);
|
|
337
|
+
if (stat.isSymbolicLink()) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Refusing to write ${path.basename(filePath)}: path is a symlink. Remove the symlink and retry.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
// Re-throw symlink errors; ENOENT (file doesn't exist) is fine
|
|
344
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Add standard ignore entries to a file (idempotent).
|
|
350
|
+
* Creates the file if it doesn't exist.
|
|
351
|
+
*/
|
|
352
|
+
function addIgnoreEntries(filePath: string): void {
|
|
353
|
+
assertNotSymlink(filePath);
|
|
354
|
+
|
|
355
|
+
let content = '';
|
|
356
|
+
if (fs.existsSync(filePath)) {
|
|
357
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Only add entries that are missing (check each individually)
|
|
361
|
+
const missingEntries = IGNORE_ENTRIES.filter(entry => !content.includes(entry));
|
|
362
|
+
if (missingEntries.length === 0) return;
|
|
363
|
+
|
|
364
|
+
const newContent =
|
|
365
|
+
content +
|
|
366
|
+
(content.length > 0 && !content.endsWith('\n') ? '\n' : '') +
|
|
367
|
+
IGNORE_HEADER + '\n' +
|
|
368
|
+
missingEntries.join('\n') + '\n';
|
|
369
|
+
|
|
370
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Add .actp/ and related entries to .dockerignore (AIP-13).
|
|
375
|
+
* Creates the file if it doesn't exist. Idempotent.
|
|
376
|
+
* Throws if .dockerignore is a symlink.
|
|
377
|
+
*/
|
|
378
|
+
export function addToDockerignore(projectRoot: string = process.cwd()): void {
|
|
379
|
+
addIgnoreEntries(path.join(projectRoot, '.dockerignore'));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Add .actp/ and related entries to .railwayignore (AIP-13).
|
|
384
|
+
* Creates the file if it doesn't exist. Idempotent.
|
|
385
|
+
* Throws if .railwayignore is a symlink.
|
|
386
|
+
*/
|
|
387
|
+
export function addToRailwayignore(projectRoot: string = process.cwd()): void {
|
|
388
|
+
addIgnoreEntries(path.join(projectRoot, '.railwayignore'));
|
|
389
|
+
}
|
package/src/level0/request.ts
CHANGED
|
@@ -320,7 +320,7 @@ async function resolveKeyIfNeeded(
|
|
|
320
320
|
): Promise<string | undefined> {
|
|
321
321
|
if (wallet && wallet !== 'auto') return undefined; // explicit wallet, skip auto-detect
|
|
322
322
|
if (network !== 'testnet' && network !== 'mainnet') return undefined;
|
|
323
|
-
return resolvePrivateKey(stateDirectory);
|
|
323
|
+
return resolvePrivateKey(stateDirectory, { network });
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
/**
|
package/src/level1/Agent.ts
CHANGED
|
@@ -1391,7 +1391,7 @@ export class Agent extends EventEmitter {
|
|
|
1391
1391
|
private async getPrivateKey(): Promise<string | undefined> {
|
|
1392
1392
|
if (!this.config.wallet || this.config.wallet === 'auto') {
|
|
1393
1393
|
if (this.network === 'testnet' || this.network === 'mainnet') {
|
|
1394
|
-
return resolvePrivateKey(this.config.stateDirectory);
|
|
1394
|
+
return resolvePrivateKey(this.config.stateDirectory, { network: this.network });
|
|
1395
1395
|
}
|
|
1396
1396
|
return undefined;
|
|
1397
1397
|
}
|
package/src/wallet/keystore.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keystore auto-resolution for ACTP wallets.
|
|
3
3
|
*
|
|
4
|
-
* Resolution order:
|
|
5
|
-
* 1. ACTP_PRIVATE_KEY env var (
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
4
|
+
* Resolution order (AIP-13):
|
|
5
|
+
* 1. ACTP_PRIVATE_KEY env var (policy-gated: mainnet/unknown = hard fail)
|
|
6
|
+
* 2. ACTP_KEYSTORE_BASE64 + ACTP_KEY_PASSWORD (deployment-safe, preferred)
|
|
7
|
+
* 3. .actp/keystore.json decrypted with ACTP_KEY_PASSWORD
|
|
8
|
+
* 4. undefined (caller decides what to do)
|
|
8
9
|
*/
|
|
9
10
|
import * as fs from 'fs';
|
|
10
11
|
import * as path from 'path';
|
|
11
12
|
import { Wallet } from 'ethers';
|
|
13
|
+
import { sdkLogger } from '../utils/Logger';
|
|
12
14
|
|
|
13
15
|
/** 30-minute TTL for cached private keys */
|
|
14
16
|
const CACHE_TTL_MS = 30 * 60 * 1000;
|
|
@@ -19,12 +21,20 @@ interface CacheEntry {
|
|
|
19
21
|
expiresAt: number;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
export interface ResolvePrivateKeyOptions {
|
|
25
|
+
/** Network mode: 'mainnet', 'testnet', or 'mock'. Controls ACTP_PRIVATE_KEY policy. */
|
|
26
|
+
network?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
// Cache keyed by resolved keystorePath to support multiple stateDirectories
|
|
23
30
|
const _cache = new Map<string, CacheEntry>();
|
|
24
31
|
|
|
25
32
|
// Separate cache for env-var-resolved key (no path dependency)
|
|
26
33
|
let _envCache: CacheEntry | null = null;
|
|
27
34
|
|
|
35
|
+
// Separate cache for base64-resolved key (no path dependency)
|
|
36
|
+
let _base64Cache: CacheEntry | null = null;
|
|
37
|
+
|
|
28
38
|
function isExpired(entry: CacheEntry): boolean {
|
|
29
39
|
return Date.now() >= entry.expiresAt;
|
|
30
40
|
}
|
|
@@ -59,14 +69,58 @@ function validateRawKey(raw: string, source: string): string {
|
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
/**
|
|
62
|
-
*
|
|
72
|
+
* Determine the effective network for ACTP_PRIVATE_KEY policy.
|
|
73
|
+
* Falls back to ACTP_NETWORK env var. Null means unknown (fail-closed).
|
|
74
|
+
*/
|
|
75
|
+
function getEffectiveNetwork(options?: ResolvePrivateKeyOptions): string | null {
|
|
76
|
+
return options?.network ?? process.env.ACTP_NETWORK ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enforce ACTP_PRIVATE_KEY policy based on network (AIP-13).
|
|
81
|
+
* - mainnet/unknown: hard fail
|
|
82
|
+
* - testnet: warn once (on first resolution, not cache hits)
|
|
83
|
+
* - mock: silent
|
|
84
|
+
*/
|
|
85
|
+
function enforcePrivateKeyPolicy(network: string | null): void {
|
|
86
|
+
if (network === 'mock') return;
|
|
87
|
+
|
|
88
|
+
if (network === 'testnet') {
|
|
89
|
+
// Warn once (only on first resolution — _envCache is null)
|
|
90
|
+
if (_envCache === null) {
|
|
91
|
+
sdkLogger.warn(
|
|
92
|
+
'ACTP_PRIVATE_KEY is deprecated. Use ACTP_KEYSTORE_BASE64 instead.\n' +
|
|
93
|
+
'Run: actp deploy:env'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// mainnet or unknown (null) — fail-closed
|
|
100
|
+
const networkLabel = network === 'mainnet' ? 'production' : 'unknown network (fail-closed)';
|
|
101
|
+
throw new Error(
|
|
102
|
+
`ACTP_PRIVATE_KEY is not allowed in ${networkLabel}. Use ACTP_KEYSTORE_BASE64 instead.\n` +
|
|
103
|
+
'Run: actp deploy:env\n' +
|
|
104
|
+
'If this is testnet, set ACTP_NETWORK=testnet'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Auto-resolve private key: env var → base64 keystore → file keystore → undefined.
|
|
63
110
|
* Never logs or prints the key itself.
|
|
111
|
+
*
|
|
112
|
+
* @param stateDirectory - Directory containing .actp/ (defaults to cwd)
|
|
113
|
+
* @param options - Options including network for ACTP_PRIVATE_KEY policy
|
|
64
114
|
*/
|
|
65
115
|
export async function resolvePrivateKey(
|
|
66
|
-
stateDirectory?: string
|
|
116
|
+
stateDirectory?: string,
|
|
117
|
+
options?: ResolvePrivateKeyOptions
|
|
67
118
|
): Promise<string | undefined> {
|
|
68
|
-
// 1.
|
|
119
|
+
// 1. ACTP_PRIVATE_KEY (highest priority, policy-gated)
|
|
69
120
|
if (process.env.ACTP_PRIVATE_KEY) {
|
|
121
|
+
const network = getEffectiveNetwork(options);
|
|
122
|
+
enforcePrivateKeyPolicy(network);
|
|
123
|
+
|
|
70
124
|
if (_envCache && !isExpired(_envCache)) return _envCache.key;
|
|
71
125
|
|
|
72
126
|
const key = validateRawKey(process.env.ACTP_PRIVATE_KEY, 'ACTP_PRIVATE_KEY env var');
|
|
@@ -75,7 +129,54 @@ export async function resolvePrivateKey(
|
|
|
75
129
|
return key;
|
|
76
130
|
}
|
|
77
131
|
|
|
78
|
-
// 2.
|
|
132
|
+
// 2. ACTP_KEYSTORE_BASE64 (deployment-safe, preferred for production)
|
|
133
|
+
if (process.env.ACTP_KEYSTORE_BASE64) {
|
|
134
|
+
if (_base64Cache && !isExpired(_base64Cache)) return _base64Cache.key;
|
|
135
|
+
|
|
136
|
+
const raw = process.env.ACTP_KEYSTORE_BASE64.replace(/\s/g, '');
|
|
137
|
+
let decoded: string;
|
|
138
|
+
try {
|
|
139
|
+
decoded = Buffer.from(raw, 'base64').toString('utf-8');
|
|
140
|
+
} catch {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'ACTP_KEYSTORE_BASE64 is not valid base64.\n' +
|
|
143
|
+
'Run: actp deploy:env'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
JSON.parse(decoded);
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error(
|
|
151
|
+
'ACTP_KEYSTORE_BASE64 is not valid encrypted keystore JSON.\n' +
|
|
152
|
+
'Run: actp deploy:env'
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const password = process.env.ACTP_KEY_PASSWORD;
|
|
157
|
+
if (!password) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'ACTP_KEYSTORE_BASE64 is set but ACTP_KEY_PASSWORD is not set.\n' +
|
|
160
|
+
'Set it: export ACTP_KEY_PASSWORD="your-password"'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let wallet: Wallet;
|
|
165
|
+
try {
|
|
166
|
+
wallet = (await Wallet.fromEncryptedJson(decoded, password)) as Wallet;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// Sanitize: do not leak keystore content in error messages
|
|
169
|
+
const message = err instanceof Error ? err.message : 'unknown error';
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Failed to decrypt ACTP_KEYSTORE_BASE64: ${message}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_base64Cache = { key: wallet.privateKey, address: wallet.address, expiresAt: Date.now() + CACHE_TTL_MS };
|
|
176
|
+
return wallet.privateKey;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 3. Resolve keystore path
|
|
79
180
|
if (stateDirectory) {
|
|
80
181
|
validateStateDirectory(stateDirectory);
|
|
81
182
|
}
|
|
@@ -84,12 +185,12 @@ export async function resolvePrivateKey(
|
|
|
84
185
|
: path.join(process.cwd(), '.actp');
|
|
85
186
|
const keystorePath = path.resolve(actpDir, 'keystore.json');
|
|
86
187
|
|
|
87
|
-
//
|
|
188
|
+
// 4. Cache hit (keyed by resolved path, with TTL)
|
|
88
189
|
const cached = _cache.get(keystorePath);
|
|
89
190
|
if (cached && !isExpired(cached)) return cached.key;
|
|
90
191
|
if (cached) _cache.delete(keystorePath); // expired
|
|
91
192
|
|
|
92
|
-
//
|
|
193
|
+
// 5. Keystore file
|
|
93
194
|
if (!fs.existsSync(keystorePath)) return undefined;
|
|
94
195
|
|
|
95
196
|
const password = process.env.ACTP_KEY_PASSWORD;
|
|
@@ -109,12 +210,15 @@ export async function resolvePrivateKey(
|
|
|
109
210
|
|
|
110
211
|
/**
|
|
111
212
|
* Get cached address from last resolvePrivateKey() call.
|
|
112
|
-
* Works for
|
|
213
|
+
* Works for env-var, base64, and keystore resolution paths.
|
|
113
214
|
*/
|
|
114
215
|
export function getCachedAddress(stateDirectory?: string): string | undefined {
|
|
115
216
|
// Env var path
|
|
116
217
|
if (_envCache && !isExpired(_envCache)) return _envCache.address;
|
|
117
218
|
|
|
219
|
+
// Base64 path
|
|
220
|
+
if (_base64Cache && !isExpired(_base64Cache)) return _base64Cache.address;
|
|
221
|
+
|
|
118
222
|
// Keystore path — look up by resolved path
|
|
119
223
|
const actpDir = stateDirectory
|
|
120
224
|
? path.join(stateDirectory, '.actp')
|
|
@@ -132,4 +236,5 @@ export function getCachedAddress(stateDirectory?: string): string | undefined {
|
|
|
132
236
|
export function _clearCache(): void {
|
|
133
237
|
_cache.clear();
|
|
134
238
|
_envCache = null;
|
|
239
|
+
_base64Cache = null;
|
|
135
240
|
}
|