@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/dist/wallet/keystore.js
CHANGED
|
@@ -27,20 +27,24 @@ exports._clearCache = exports.getCachedAddress = exports.resolvePrivateKey = voi
|
|
|
27
27
|
/**
|
|
28
28
|
* Keystore auto-resolution for ACTP wallets.
|
|
29
29
|
*
|
|
30
|
-
* Resolution order:
|
|
31
|
-
* 1. ACTP_PRIVATE_KEY env var (
|
|
32
|
-
* 2.
|
|
33
|
-
* 3.
|
|
30
|
+
* Resolution order (AIP-13):
|
|
31
|
+
* 1. ACTP_PRIVATE_KEY env var (policy-gated: mainnet/unknown = hard fail)
|
|
32
|
+
* 2. ACTP_KEYSTORE_BASE64 + ACTP_KEY_PASSWORD (deployment-safe, preferred)
|
|
33
|
+
* 3. .actp/keystore.json decrypted with ACTP_KEY_PASSWORD
|
|
34
|
+
* 4. undefined (caller decides what to do)
|
|
34
35
|
*/
|
|
35
36
|
const fs = __importStar(require("fs"));
|
|
36
37
|
const path = __importStar(require("path"));
|
|
37
38
|
const ethers_1 = require("ethers");
|
|
39
|
+
const Logger_1 = require("../utils/Logger");
|
|
38
40
|
/** 30-minute TTL for cached private keys */
|
|
39
41
|
const CACHE_TTL_MS = 30 * 60 * 1000;
|
|
40
42
|
// Cache keyed by resolved keystorePath to support multiple stateDirectories
|
|
41
43
|
const _cache = new Map();
|
|
42
44
|
// Separate cache for env-var-resolved key (no path dependency)
|
|
43
45
|
let _envCache = null;
|
|
46
|
+
// Separate cache for base64-resolved key (no path dependency)
|
|
47
|
+
let _base64Cache = null;
|
|
44
48
|
function isExpired(entry) {
|
|
45
49
|
return Date.now() >= entry.expiresAt;
|
|
46
50
|
}
|
|
@@ -70,12 +74,47 @@ function validateRawKey(raw, source) {
|
|
|
70
74
|
return trimmed;
|
|
71
75
|
}
|
|
72
76
|
/**
|
|
73
|
-
*
|
|
77
|
+
* Determine the effective network for ACTP_PRIVATE_KEY policy.
|
|
78
|
+
* Falls back to ACTP_NETWORK env var. Null means unknown (fail-closed).
|
|
79
|
+
*/
|
|
80
|
+
function getEffectiveNetwork(options) {
|
|
81
|
+
return options?.network ?? process.env.ACTP_NETWORK ?? null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Enforce ACTP_PRIVATE_KEY policy based on network (AIP-13).
|
|
85
|
+
* - mainnet/unknown: hard fail
|
|
86
|
+
* - testnet: warn once (on first resolution, not cache hits)
|
|
87
|
+
* - mock: silent
|
|
88
|
+
*/
|
|
89
|
+
function enforcePrivateKeyPolicy(network) {
|
|
90
|
+
if (network === 'mock')
|
|
91
|
+
return;
|
|
92
|
+
if (network === 'testnet') {
|
|
93
|
+
// Warn once (only on first resolution — _envCache is null)
|
|
94
|
+
if (_envCache === null) {
|
|
95
|
+
Logger_1.sdkLogger.warn('ACTP_PRIVATE_KEY is deprecated. Use ACTP_KEYSTORE_BASE64 instead.\n' +
|
|
96
|
+
'Run: actp deploy:env');
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// mainnet or unknown (null) — fail-closed
|
|
101
|
+
const networkLabel = network === 'mainnet' ? 'production' : 'unknown network (fail-closed)';
|
|
102
|
+
throw new Error(`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
|
+
* Auto-resolve private key: env var → base64 keystore → file keystore → undefined.
|
|
74
108
|
* Never logs or prints the key itself.
|
|
109
|
+
*
|
|
110
|
+
* @param stateDirectory - Directory containing .actp/ (defaults to cwd)
|
|
111
|
+
* @param options - Options including network for ACTP_PRIVATE_KEY policy
|
|
75
112
|
*/
|
|
76
|
-
async function resolvePrivateKey(stateDirectory) {
|
|
77
|
-
// 1.
|
|
113
|
+
async function resolvePrivateKey(stateDirectory, options) {
|
|
114
|
+
// 1. ACTP_PRIVATE_KEY (highest priority, policy-gated)
|
|
78
115
|
if (process.env.ACTP_PRIVATE_KEY) {
|
|
116
|
+
const network = getEffectiveNetwork(options);
|
|
117
|
+
enforcePrivateKeyPolicy(network);
|
|
79
118
|
if (_envCache && !isExpired(_envCache))
|
|
80
119
|
return _envCache.key;
|
|
81
120
|
const key = validateRawKey(process.env.ACTP_PRIVATE_KEY, 'ACTP_PRIVATE_KEY env var');
|
|
@@ -83,7 +122,44 @@ async function resolvePrivateKey(stateDirectory) {
|
|
|
83
122
|
_envCache = { key, address, expiresAt: Date.now() + CACHE_TTL_MS };
|
|
84
123
|
return key;
|
|
85
124
|
}
|
|
86
|
-
// 2.
|
|
125
|
+
// 2. ACTP_KEYSTORE_BASE64 (deployment-safe, preferred for production)
|
|
126
|
+
if (process.env.ACTP_KEYSTORE_BASE64) {
|
|
127
|
+
if (_base64Cache && !isExpired(_base64Cache))
|
|
128
|
+
return _base64Cache.key;
|
|
129
|
+
const raw = process.env.ACTP_KEYSTORE_BASE64.replace(/\s/g, '');
|
|
130
|
+
let decoded;
|
|
131
|
+
try {
|
|
132
|
+
decoded = Buffer.from(raw, 'base64').toString('utf-8');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new Error('ACTP_KEYSTORE_BASE64 is not valid base64.\n' +
|
|
136
|
+
'Run: actp deploy:env');
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
JSON.parse(decoded);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new Error('ACTP_KEYSTORE_BASE64 is not valid encrypted keystore JSON.\n' +
|
|
143
|
+
'Run: actp deploy:env');
|
|
144
|
+
}
|
|
145
|
+
const password = process.env.ACTP_KEY_PASSWORD;
|
|
146
|
+
if (!password) {
|
|
147
|
+
throw new Error('ACTP_KEYSTORE_BASE64 is set but ACTP_KEY_PASSWORD is not set.\n' +
|
|
148
|
+
'Set it: export ACTP_KEY_PASSWORD="your-password"');
|
|
149
|
+
}
|
|
150
|
+
let wallet;
|
|
151
|
+
try {
|
|
152
|
+
wallet = (await ethers_1.Wallet.fromEncryptedJson(decoded, password));
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
// Sanitize: do not leak keystore content in error messages
|
|
156
|
+
const message = err instanceof Error ? err.message : 'unknown error';
|
|
157
|
+
throw new Error(`Failed to decrypt ACTP_KEYSTORE_BASE64: ${message}`);
|
|
158
|
+
}
|
|
159
|
+
_base64Cache = { key: wallet.privateKey, address: wallet.address, expiresAt: Date.now() + CACHE_TTL_MS };
|
|
160
|
+
return wallet.privateKey;
|
|
161
|
+
}
|
|
162
|
+
// 3. Resolve keystore path
|
|
87
163
|
if (stateDirectory) {
|
|
88
164
|
validateStateDirectory(stateDirectory);
|
|
89
165
|
}
|
|
@@ -91,13 +167,13 @@ async function resolvePrivateKey(stateDirectory) {
|
|
|
91
167
|
? path.join(stateDirectory, '.actp')
|
|
92
168
|
: path.join(process.cwd(), '.actp');
|
|
93
169
|
const keystorePath = path.resolve(actpDir, 'keystore.json');
|
|
94
|
-
//
|
|
170
|
+
// 4. Cache hit (keyed by resolved path, with TTL)
|
|
95
171
|
const cached = _cache.get(keystorePath);
|
|
96
172
|
if (cached && !isExpired(cached))
|
|
97
173
|
return cached.key;
|
|
98
174
|
if (cached)
|
|
99
175
|
_cache.delete(keystorePath); // expired
|
|
100
|
-
//
|
|
176
|
+
// 5. Keystore file
|
|
101
177
|
if (!fs.existsSync(keystorePath))
|
|
102
178
|
return undefined;
|
|
103
179
|
const password = process.env.ACTP_KEY_PASSWORD;
|
|
@@ -113,12 +189,15 @@ async function resolvePrivateKey(stateDirectory) {
|
|
|
113
189
|
exports.resolvePrivateKey = resolvePrivateKey;
|
|
114
190
|
/**
|
|
115
191
|
* Get cached address from last resolvePrivateKey() call.
|
|
116
|
-
* Works for
|
|
192
|
+
* Works for env-var, base64, and keystore resolution paths.
|
|
117
193
|
*/
|
|
118
194
|
function getCachedAddress(stateDirectory) {
|
|
119
195
|
// Env var path
|
|
120
196
|
if (_envCache && !isExpired(_envCache))
|
|
121
197
|
return _envCache.address;
|
|
198
|
+
// Base64 path
|
|
199
|
+
if (_base64Cache && !isExpired(_base64Cache))
|
|
200
|
+
return _base64Cache.address;
|
|
122
201
|
// Keystore path — look up by resolved path
|
|
123
202
|
const actpDir = stateDirectory
|
|
124
203
|
? path.join(stateDirectory, '.actp')
|
|
@@ -137,6 +216,7 @@ exports.getCachedAddress = getCachedAddress;
|
|
|
137
216
|
function _clearCache() {
|
|
138
217
|
_cache.clear();
|
|
139
218
|
_envCache = null;
|
|
219
|
+
_base64Cache = null;
|
|
140
220
|
}
|
|
141
221
|
exports._clearCache = _clearCache;
|
|
142
222
|
//# sourceMappingURL=keystore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keystore.js","sourceRoot":"","sources":["../../src/wallet/keystore.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA
|
|
1
|
+
{"version":3,"file":"keystore.js","sourceRoot":"","sources":["../../src/wallet/keystore.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/ACTPClient.ts
CHANGED
|
@@ -736,7 +736,7 @@ export class ACTPClient {
|
|
|
736
736
|
// Auto-detect private key from keystore / env var if not provided
|
|
737
737
|
if (!config.privateKey) {
|
|
738
738
|
const { resolvePrivateKey } = await import('./wallet/keystore');
|
|
739
|
-
const resolved = await resolvePrivateKey(config.stateDirectory);
|
|
739
|
+
const resolved = await resolvePrivateKey(config.stateDirectory, { network: config.mode });
|
|
740
740
|
if (resolved) {
|
|
741
741
|
config = { ...config, privateKey: resolved };
|
|
742
742
|
} else {
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy:check Command - Pre-deploy security audit (AIP-13)
|
|
3
|
+
*
|
|
4
|
+
* Scans the project for common deployment security issues:
|
|
5
|
+
* - Missing ignore files
|
|
6
|
+
* - Raw private keys in env files, Dockerfiles, CI configs
|
|
7
|
+
* - Symlinked keystore
|
|
8
|
+
* - Incorrect file permissions
|
|
9
|
+
*
|
|
10
|
+
* Exit code 0: all checks pass (warnings allowed)
|
|
11
|
+
* Exit code 1: at least one FAIL check
|
|
12
|
+
*
|
|
13
|
+
* @module cli/commands/deploy-check
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { Command } from 'commander';
|
|
19
|
+
import { Output, ExitCode } from '../utils/output';
|
|
20
|
+
import { getActpDir } from '../utils/config';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Types
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
type CheckSeverity = 'FAIL' | 'WARN';
|
|
27
|
+
type CheckResult = 'PASS' | 'FAIL' | 'WARN' | 'SKIP';
|
|
28
|
+
|
|
29
|
+
interface CheckOutput {
|
|
30
|
+
name: string;
|
|
31
|
+
result: CheckResult;
|
|
32
|
+
message?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Command Definition
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
export function createDeployCheckCommand(): Command {
|
|
40
|
+
const cmd = new Command('deploy:check')
|
|
41
|
+
.description('Pre-deploy security audit (AIP-13)')
|
|
42
|
+
.option('--json', 'Output as JSON')
|
|
43
|
+
.option('-q, --quiet', 'Only show failures')
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
const output = new Output(
|
|
46
|
+
options.json ? 'json' : options.quiet ? 'quiet' : 'human'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const exitCode = runDeployCheck(options, output);
|
|
51
|
+
process.exit(exitCode);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
output.errorResult({
|
|
54
|
+
code: 'DEPLOY_CHECK_FAILED',
|
|
55
|
+
message: (error as Error).message,
|
|
56
|
+
});
|
|
57
|
+
process.exit(ExitCode.ERROR);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return cmd;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Implementation
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/** Regex matching a raw private key pattern */
|
|
69
|
+
const RAW_KEY_PATTERN = /0x[0-9a-fA-F]{64}/;
|
|
70
|
+
const RAW_KEY_ENV_PATTERN = /ACTP_PRIVATE_KEY\s*=\s*0x/;
|
|
71
|
+
|
|
72
|
+
interface DeployCheckOptions {
|
|
73
|
+
json?: boolean;
|
|
74
|
+
quiet?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function runDeployCheck(options: DeployCheckOptions, output: Output): number {
|
|
78
|
+
const projectRoot = process.cwd();
|
|
79
|
+
const actpDir = getActpDir(projectRoot);
|
|
80
|
+
const results: CheckOutput[] = [];
|
|
81
|
+
let failCount = 0;
|
|
82
|
+
let warnCount = 0;
|
|
83
|
+
let passCount = 0;
|
|
84
|
+
|
|
85
|
+
// ── FAIL checks ──────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
// 1. .actp/ in .gitignore
|
|
88
|
+
results.push(checkIgnoreFile(projectRoot, '.gitignore', '.actp', 'FAIL'));
|
|
89
|
+
|
|
90
|
+
// 2. .actp/ in .dockerignore
|
|
91
|
+
results.push(checkIgnoreFile(projectRoot, '.dockerignore', '.actp', 'FAIL'));
|
|
92
|
+
|
|
93
|
+
// 3. No ACTP_PRIVATE_KEY in .env* files
|
|
94
|
+
results.push(checkNoRawKeysInEnvFiles(projectRoot));
|
|
95
|
+
|
|
96
|
+
// 4. No raw keys in Dockerfiles
|
|
97
|
+
results.push(checkNoRawKeysInFiles(
|
|
98
|
+
projectRoot,
|
|
99
|
+
['Dockerfile', 'Dockerfile.*', 'docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'],
|
|
100
|
+
'No raw keys in Dockerfiles'
|
|
101
|
+
));
|
|
102
|
+
|
|
103
|
+
// 5. No raw keys in CI/workflow files
|
|
104
|
+
results.push(checkNoRawKeysInCIFiles(projectRoot));
|
|
105
|
+
|
|
106
|
+
// 6. Keystore is not a symlink
|
|
107
|
+
results.push(checkKeystoreNotSymlink(actpDir));
|
|
108
|
+
|
|
109
|
+
// ── WARN checks ──────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
// 7. ACTP_KEYSTORE_BASE64 or keystore.json exists
|
|
112
|
+
results.push(checkKeystoreAvailable(actpDir));
|
|
113
|
+
|
|
114
|
+
// 8. Keystore file permissions (POSIX only)
|
|
115
|
+
results.push(checkKeystorePermissions(actpDir));
|
|
116
|
+
|
|
117
|
+
// ── Output ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
for (const check of results) {
|
|
120
|
+
if (check.result === 'FAIL') failCount++;
|
|
121
|
+
else if (check.result === 'WARN') warnCount++;
|
|
122
|
+
else if (check.result === 'PASS') passCount++;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.json) {
|
|
126
|
+
output.result({
|
|
127
|
+
checks: results,
|
|
128
|
+
summary: { passed: passCount, warnings: warnCount, failed: failCount },
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
output.print('actp deploy:check');
|
|
132
|
+
for (const check of results) {
|
|
133
|
+
if (options.quiet && check.result !== 'FAIL') continue;
|
|
134
|
+
const tag = `[${check.result}]`;
|
|
135
|
+
const msg = check.message ? ` ${check.message}` : '';
|
|
136
|
+
output.print(` ${tag} ${check.name}${msg}`);
|
|
137
|
+
}
|
|
138
|
+
output.print('');
|
|
139
|
+
output.print(` ${passCount} passed, ${warnCount} warnings, ${failCount} failed`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return failCount > 0 ? ExitCode.ERROR : ExitCode.SUCCESS;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Individual Checks
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
function checkIgnoreFile(
|
|
150
|
+
projectRoot: string,
|
|
151
|
+
fileName: string,
|
|
152
|
+
entry: string,
|
|
153
|
+
_severity: CheckSeverity
|
|
154
|
+
): CheckOutput {
|
|
155
|
+
const filePath = path.join(projectRoot, fileName);
|
|
156
|
+
const name = `${entry} in ${fileName}`;
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(filePath)) {
|
|
159
|
+
return { name, result: 'FAIL', message: `${fileName} does not exist` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
163
|
+
if (content.includes(entry)) {
|
|
164
|
+
return { name, result: 'PASS' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { name, result: 'FAIL', message: `${entry} not found in ${fileName}` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function checkNoRawKeysInEnvFiles(projectRoot: string): CheckOutput {
|
|
171
|
+
const name = 'No raw keys in .env files';
|
|
172
|
+
const envFiles = findMatchingFiles(projectRoot, ['.env', '.env.*', '.env.local', '.env.production']);
|
|
173
|
+
|
|
174
|
+
for (const file of envFiles) {
|
|
175
|
+
const content = safeReadFile(file);
|
|
176
|
+
if (content && (RAW_KEY_PATTERN.test(content) || RAW_KEY_ENV_PATTERN.test(content))) {
|
|
177
|
+
return { name, result: 'FAIL', message: `Raw key found in ${path.basename(file)}` };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { name, result: 'PASS' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function checkNoRawKeysInFiles(
|
|
185
|
+
projectRoot: string,
|
|
186
|
+
patterns: string[],
|
|
187
|
+
name: string
|
|
188
|
+
): CheckOutput {
|
|
189
|
+
const files = findMatchingFiles(projectRoot, patterns);
|
|
190
|
+
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
const content = safeReadFile(file);
|
|
193
|
+
if (content && RAW_KEY_PATTERN.test(content)) {
|
|
194
|
+
return { name, result: 'FAIL', message: `Raw key found in ${path.basename(file)}` };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { name, result: 'PASS' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function checkNoRawKeysInCIFiles(projectRoot: string): CheckOutput {
|
|
202
|
+
const name = 'No raw keys in CI/workflow files';
|
|
203
|
+
|
|
204
|
+
// Collect all CI-related files
|
|
205
|
+
const files: string[] = [];
|
|
206
|
+
|
|
207
|
+
// GitHub workflows
|
|
208
|
+
const workflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
209
|
+
if (fs.existsSync(workflowDir)) {
|
|
210
|
+
try {
|
|
211
|
+
const entries = fs.readdirSync(workflowDir);
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (entry.endsWith('.yml') || entry.endsWith('.yaml')) {
|
|
214
|
+
files.push(path.join(workflowDir, entry));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch { /* ignore read errors */ }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Platform config files
|
|
221
|
+
const platformFiles = [
|
|
222
|
+
'railway.json', 'fly.toml', 'vercel.json',
|
|
223
|
+
'pm2.config.js', 'pm2.config.cjs', 'ecosystem.config.js', 'ecosystem.config.cjs',
|
|
224
|
+
];
|
|
225
|
+
for (const pf of platformFiles) {
|
|
226
|
+
const fp = path.join(projectRoot, pf);
|
|
227
|
+
if (fs.existsSync(fp)) files.push(fp);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
const content = safeReadFile(file);
|
|
232
|
+
if (content && (RAW_KEY_PATTERN.test(content) || RAW_KEY_ENV_PATTERN.test(content))) {
|
|
233
|
+
return { name, result: 'FAIL', message: `Raw key found in ${path.relative(projectRoot, file)}` };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { name, result: 'PASS' };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function checkKeystoreNotSymlink(actpDir: string): CheckOutput {
|
|
241
|
+
const name = 'Keystore is not a symlink';
|
|
242
|
+
const keystorePath = path.join(actpDir, 'keystore.json');
|
|
243
|
+
|
|
244
|
+
if (!fs.existsSync(keystorePath)) {
|
|
245
|
+
return { name, result: 'PASS' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const stat = fs.lstatSync(keystorePath);
|
|
250
|
+
if (stat.isSymbolicLink()) {
|
|
251
|
+
return { name, result: 'FAIL', message: 'keystore.json is a symlink (security risk)' };
|
|
252
|
+
}
|
|
253
|
+
} catch { /* ignore */ }
|
|
254
|
+
|
|
255
|
+
return { name, result: 'PASS' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function checkKeystoreAvailable(actpDir: string): CheckOutput {
|
|
259
|
+
const name = 'Keystore available';
|
|
260
|
+
const keystorePath = path.join(actpDir, 'keystore.json');
|
|
261
|
+
|
|
262
|
+
if (process.env.ACTP_KEYSTORE_BASE64) {
|
|
263
|
+
return { name, result: 'PASS' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (fs.existsSync(keystorePath)) {
|
|
267
|
+
return { name, result: 'PASS' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { name, result: 'WARN', message: 'ACTP_KEYSTORE_BASE64 not set (OK for local dev)' };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function checkKeystorePermissions(actpDir: string): CheckOutput {
|
|
274
|
+
const name = 'Keystore file permissions';
|
|
275
|
+
const keystorePath = path.join(actpDir, 'keystore.json');
|
|
276
|
+
|
|
277
|
+
// Skip on Windows
|
|
278
|
+
if (process.platform === 'win32') {
|
|
279
|
+
return { name, result: 'SKIP', message: 'Permission check skipped on Windows' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!fs.existsSync(keystorePath)) {
|
|
283
|
+
return { name, result: 'PASS' };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const stat = fs.statSync(keystorePath);
|
|
288
|
+
const mode = stat.mode & 0o777;
|
|
289
|
+
if (mode === 0o600) {
|
|
290
|
+
return { name, result: 'PASS' };
|
|
291
|
+
}
|
|
292
|
+
return { name, result: 'WARN', message: `Permissions are 0o${mode.toString(8)} (expected 0o600)` };
|
|
293
|
+
} catch {
|
|
294
|
+
return { name, result: 'WARN', message: 'Could not check file permissions' };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Helpers
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
/** Directories to skip during recursive scan */
|
|
303
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.actp', 'dist', 'build', '.next', 'coverage']);
|
|
304
|
+
|
|
305
|
+
/** Max recursion depth to prevent runaway scans */
|
|
306
|
+
const MAX_SCAN_DEPTH = 5;
|
|
307
|
+
|
|
308
|
+
function findMatchingFiles(projectRoot: string, patterns: string[], maxDepth = MAX_SCAN_DEPTH): string[] {
|
|
309
|
+
const files: string[] = [];
|
|
310
|
+
walkDir(projectRoot, patterns, files, 0, maxDepth);
|
|
311
|
+
return files;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function walkDir(dir: string, patterns: string[], files: string[], depth: number, maxDepth: number): void {
|
|
315
|
+
if (depth > maxDepth) return;
|
|
316
|
+
|
|
317
|
+
let entries: string[];
|
|
318
|
+
try {
|
|
319
|
+
entries = fs.readdirSync(dir);
|
|
320
|
+
} catch { return; }
|
|
321
|
+
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
const fullPath = path.join(dir, entry);
|
|
324
|
+
|
|
325
|
+
// Match files against patterns
|
|
326
|
+
for (const pattern of patterns) {
|
|
327
|
+
if (matchPattern(entry, pattern)) {
|
|
328
|
+
try {
|
|
329
|
+
if (fs.statSync(fullPath).isFile()) {
|
|
330
|
+
files.push(fullPath);
|
|
331
|
+
}
|
|
332
|
+
} catch { /* ignore */ }
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Recurse into subdirectories (skip noise dirs)
|
|
338
|
+
if (!SKIP_DIRS.has(entry)) {
|
|
339
|
+
try {
|
|
340
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
341
|
+
walkDir(fullPath, patterns, files, depth + 1, maxDepth);
|
|
342
|
+
}
|
|
343
|
+
} catch { /* ignore */ }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function matchPattern(filename: string, pattern: string): boolean {
|
|
349
|
+
// Simple glob: support * and exact match
|
|
350
|
+
if (pattern === filename) return true;
|
|
351
|
+
if (pattern.includes('*')) {
|
|
352
|
+
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
353
|
+
return regex.test(filename);
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function safeReadFile(filePath: string): string | null {
|
|
359
|
+
try {
|
|
360
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy:env Command - Generate deployment environment variables (AIP-13)
|
|
3
|
+
*
|
|
4
|
+
* Reads .actp/keystore.json, base64-encodes it, and outputs env vars
|
|
5
|
+
* ready for any deployment platform (Railway, Vercel, Fly.io, Hetzner, etc.).
|
|
6
|
+
*
|
|
7
|
+
* @module cli/commands/deploy-env
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { Output, ExitCode } from '../utils/output';
|
|
14
|
+
import { getActpDir } from '../utils/config';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Command Definition
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export function createDeployEnvCommand(): Command {
|
|
21
|
+
const cmd = new Command('deploy:env')
|
|
22
|
+
.description('Generate deployment env vars from keystore (AIP-13)')
|
|
23
|
+
.option('--format <format>', 'Output format: shell (default), docker, json', 'shell')
|
|
24
|
+
.option('--json', 'Machine-readable JSON output')
|
|
25
|
+
.option('-q, --quiet', 'Output only the base64 string (for piping)')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
const output = new Output(
|
|
28
|
+
options.json ? 'json' : options.quiet ? 'quiet' : 'human'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
runDeployEnv(options, output);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
output.errorResult({
|
|
35
|
+
code: 'DEPLOY_ENV_FAILED',
|
|
36
|
+
message: (error as Error).message,
|
|
37
|
+
});
|
|
38
|
+
process.exit(ExitCode.ERROR);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return cmd;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Implementation
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
interface DeployEnvOptions {
|
|
50
|
+
format: string;
|
|
51
|
+
json?: boolean;
|
|
52
|
+
quiet?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runDeployEnv(options: DeployEnvOptions, output: Output): void {
|
|
56
|
+
const projectRoot = process.cwd();
|
|
57
|
+
const actpDir = getActpDir(projectRoot);
|
|
58
|
+
const keystorePath = path.join(actpDir, 'keystore.json');
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(keystorePath)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'No keystore found at ' + keystorePath + '\n' +
|
|
63
|
+
'Run "actp init" first to generate a wallet.'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const keystoreContent = fs.readFileSync(keystorePath, 'utf-8');
|
|
68
|
+
|
|
69
|
+
// Validate it's valid JSON
|
|
70
|
+
try {
|
|
71
|
+
JSON.parse(keystoreContent);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error('Keystore file is corrupted (not valid JSON).');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const base64 = Buffer.from(keystoreContent).toString('base64');
|
|
77
|
+
|
|
78
|
+
// Quiet mode: just the base64 string (for piping)
|
|
79
|
+
if (options.quiet) {
|
|
80
|
+
output.result({ keystoreBase64: base64 }, { quietKey: 'keystoreBase64' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// JSON mode
|
|
85
|
+
if (options.json || options.format === 'json') {
|
|
86
|
+
output.result({
|
|
87
|
+
keystoreBase64: base64,
|
|
88
|
+
passwordVar: 'ACTP_KEY_PASSWORD',
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Docker format
|
|
94
|
+
if (options.format === 'docker') {
|
|
95
|
+
output.print('# Add to your Dockerfile:');
|
|
96
|
+
output.print(`ENV ACTP_KEYSTORE_BASE64="${base64}"`);
|
|
97
|
+
output.print('ENV ACTP_KEY_PASSWORD="<your keystore password>"');
|
|
98
|
+
output.print('');
|
|
99
|
+
output.print('# WARNING: Prefer build args or secrets over ENV for sensitive values.');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Shell format (default)
|
|
104
|
+
output.print('# Set these environment variables on your deployment platform:');
|
|
105
|
+
output.print(`ACTP_KEYSTORE_BASE64=${base64}`);
|
|
106
|
+
output.print('ACTP_KEY_PASSWORD=<your keystore password>');
|
|
107
|
+
output.print('');
|
|
108
|
+
output.print('# SECURITY BEST PRACTICE:');
|
|
109
|
+
output.print('# Store ACTP_KEYSTORE_BASE64 and ACTP_KEY_PASSWORD in SEPARATE secret scopes');
|
|
110
|
+
output.print('# where your platform supports it (e.g., different secret groups, vaults, or teams).');
|
|
111
|
+
output.print('# This preserves the two-factor security model.');
|
|
112
|
+
output.print('#');
|
|
113
|
+
output.print('# WARNING: Never echo these values in CI/CD logs or commit them to git.');
|
|
114
|
+
output.print('');
|
|
115
|
+
output.print('# Platform-specific examples:');
|
|
116
|
+
output.print(`# Railway: railway variables set ACTP_KEYSTORE_BASE64="${base64}"`);
|
|
117
|
+
output.print('# Vercel: vercel env add ACTP_KEYSTORE_BASE64');
|
|
118
|
+
output.print(`# Fly.io: fly secrets set ACTP_KEYSTORE_BASE64="${base64}"`);
|
|
119
|
+
output.print('# Hetzner: Add to your .env on the server (ensure .env is in .gitignore)');
|
|
120
|
+
}
|