@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.
Files changed (43) hide show
  1. package/dist/ACTPClient.js +1 -1
  2. package/dist/ACTPClient.js.map +1 -1
  3. package/dist/cli/commands/deploy-check.d.ts +24 -0
  4. package/dist/cli/commands/deploy-check.d.ts.map +1 -0
  5. package/dist/cli/commands/deploy-check.js +316 -0
  6. package/dist/cli/commands/deploy-check.js.map +1 -0
  7. package/dist/cli/commands/deploy-env.d.ts +19 -0
  8. package/dist/cli/commands/deploy-env.d.ts.map +1 -0
  9. package/dist/cli/commands/deploy-env.js +123 -0
  10. package/dist/cli/commands/deploy-env.js.map +1 -0
  11. package/dist/cli/commands/init.d.ts.map +1 -1
  12. package/dist/cli/commands/init.js +15 -1
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/cli/commands/publish.js +1 -1
  15. package/dist/cli/commands/publish.js.map +1 -1
  16. package/dist/cli/commands/register.js +1 -1
  17. package/dist/cli/commands/register.js.map +1 -1
  18. package/dist/cli/index.js +5 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/utils/config.d.ts +12 -0
  21. package/dist/cli/utils/config.d.ts.map +1 -1
  22. package/dist/cli/utils/config.js +61 -1
  23. package/dist/cli/utils/config.js.map +1 -1
  24. package/dist/level0/request.js +1 -1
  25. package/dist/level0/request.js.map +1 -1
  26. package/dist/level1/Agent.js +1 -1
  27. package/dist/level1/Agent.js.map +1 -1
  28. package/dist/wallet/keystore.d.ts +10 -3
  29. package/dist/wallet/keystore.d.ts.map +1 -1
  30. package/dist/wallet/keystore.js +91 -11
  31. package/dist/wallet/keystore.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/ACTPClient.ts +1 -1
  34. package/src/cli/commands/deploy-check.ts +364 -0
  35. package/src/cli/commands/deploy-env.ts +120 -0
  36. package/src/cli/commands/init.ts +15 -1
  37. package/src/cli/commands/publish.ts +1 -1
  38. package/src/cli/commands/register.ts +1 -1
  39. package/src/cli/index.ts +6 -0
  40. package/src/cli/utils/config.ts +68 -0
  41. package/src/level0/request.ts +1 -1
  42. package/src/level1/Agent.ts +1 -1
  43. package/src/wallet/keystore.ts +116 -11
@@ -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
  // ============================================================================
@@ -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
+ }
@@ -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
  /**
@@ -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
  }
@@ -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 (backward compat, highest priority)
6
- * 2. .actp/keystore.json decrypted with ACTP_KEY_PASSWORD
7
- * 3. undefined (caller decides what to do)
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
- * Auto-resolve private key: env var keystore → undefined.
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. Env var (highest priority, backward compat)
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. Resolve keystore path
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
- // 3. Cache hit (keyed by resolved path, with TTL)
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
- // 4. Keystore file
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 both env-var and keystore resolution paths.
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
  }