nodpay 0.2.35 → 0.2.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodpay",
3
- "version": "0.2.35",
3
+ "version": "0.2.36",
4
4
  "description": "NodPay CLI — propose on-chain payments from agent-human shared wallets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Unified environment variable loader for NodPay CLI.
3
+ *
4
+ * Priority (highest wins):
5
+ * 1. process.env (real env — set by caller, SafeClaw injection, container, etc.)
6
+ * 2. ~/.nodpay/.env file (convenience layer for local dev)
7
+ *
8
+ * The .env file is loaded into process.env WITHOUT overriding existing values.
9
+ * This matches standard dotenv behavior and Unix conventions:
10
+ * NODPAY_AGENT_KEY=0xabc npx nodpay propose → uses 0xabc, ignores file
11
+ *
12
+ * After loadDotEnv(), all code reads from process.env uniformly.
13
+ */
14
+
15
+ import { readFileSync, existsSync } from 'fs';
16
+ import { join } from 'path';
17
+
18
+ const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
19
+ const DEFAULT_ENV_PATH = join(HOME, '.nodpay', '.env');
20
+
21
+ /**
22
+ * Parse a .env file and load values into process.env.
23
+ * Does NOT override existing env vars (real env takes priority).
24
+ *
25
+ * @param {string} [envPath] - Path to .env file. Defaults to ~/.nodpay/.env
26
+ */
27
+ export function loadDotEnv(envPath = DEFAULT_ENV_PATH) {
28
+ if (!existsSync(envPath)) return;
29
+ try {
30
+ const lines = readFileSync(envPath, 'utf8').split('\n');
31
+ for (const line of lines) {
32
+ const trimmed = line.trim();
33
+ if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue;
34
+ const eqIdx = trimmed.indexOf('=');
35
+ const name = trimmed.slice(0, eqIdx).trim();
36
+ const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
37
+ // Only set if not already in process.env (real env wins)
38
+ if (!(name in process.env)) {
39
+ process.env[name] = value;
40
+ }
41
+ }
42
+ } catch {
43
+ // Non-fatal: .env file may not exist or be unreadable
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get an env var after .env has been loaded.
49
+ * Convenience wrapper — just reads process.env.
50
+ *
51
+ * @param {string} name - Environment variable name
52
+ * @param {string} [fallback] - Default value if not set
53
+ * @returns {string|undefined}
54
+ */
55
+ export function env(name, fallback) {
56
+ return process.env[name] || fallback;
57
+ }
58
+
59
+ export { HOME, DEFAULT_ENV_PATH };
@@ -14,41 +14,32 @@
14
14
  import { Wallet } from 'ethers';
15
15
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs';
16
16
  import { resolve, dirname, join } from 'path';
17
+ import { loadDotEnv, env, HOME } from './env.mjs';
17
18
 
18
19
  const args = process.argv.slice(2);
19
20
  const envFileIdx = args.indexOf('--env-file');
20
- const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
21
21
  const envFile = envFileIdx !== -1
22
22
  ? resolve(args[envFileIdx + 1])
23
23
  : join(HOME, '.nodpay', '.env');
24
24
 
25
25
  const ENV_VAR = 'NODPAY_AGENT_KEY';
26
26
 
27
- // Check if key already exists in the env file
28
- function findExistingKey() {
29
- if (!existsSync(envFile)) return null;
30
- const lines = readFileSync(envFile, 'utf8').split('\n');
31
- for (const line of lines) {
32
- const trimmed = line.trim();
33
- if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
34
- const [name, ...rest] = trimmed.split('=');
35
- if (name.trim() === ENV_VAR) {
36
- const value = rest.join('=').trim().replace(/^["']|["']$/g, '');
37
- if (value) return value;
38
- }
39
- }
40
- return null;
41
- }
27
+ // Load .env file into process.env (real env takes priority)
28
+ loadDotEnv(envFile);
42
29
 
43
- const existing = findExistingKey();
30
+ // Check if key already exists (process.env — from real env or .env file)
31
+ const existing = env(ENV_VAR);
44
32
 
45
33
  if (existing) {
46
34
  try {
47
35
  const wallet = new Wallet(existing);
48
36
  console.log(wallet.address);
49
- console.error(`${ENV_VAR} already configured in ${envFile}`);
37
+ const source = process.env[ENV_VAR] === existing && !existsSync(envFile)
38
+ ? 'environment variable'
39
+ : envFile;
40
+ console.error(`${ENV_VAR} already configured (${source})`);
50
41
  } catch {
51
- console.error(`${ENV_VAR} exists in ${envFile} but is invalid. Remove it and re-run.`);
42
+ console.error(`${ENV_VAR} is set but invalid. Check your env or ${envFile}.`);
52
43
  process.exit(1);
53
44
  }
54
45
  } else {
@@ -6,7 +6,7 @@
6
6
  * The agent signs first (1 of 2). The serialized SafeOperation is
7
7
  * output so the web app can have the user co-sign and submit.
8
8
  *
9
- * Agent key: read from .nodpay/.env or via --remote-signer (SafeClaw proxy).
9
+ * Agent key: from process.env.NODPAY_AGENT_KEY (real env > ~/.nodpay/.env file > --remote-signer).
10
10
  * Chain config: resolved via --chain from @nodpay/core networks registry.
11
11
  * Bundler: NodPay public proxy (override with OP_STORE_URL for self-hosted).
12
12
  *
@@ -27,15 +27,22 @@
27
27
 
28
28
  import { Safe4337Pack } from '@safe-global/relay-kit';
29
29
  import { ethers } from 'ethers';
30
- import { readFileSync, writeFileSync, mkdirSync } from 'fs';
30
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
31
31
  import { join, dirname } from 'path';
32
32
  import { fileURLToPath } from 'url';
33
33
  import { computeUserOpHash, ENTRYPOINT } from '@nodpay/core';
34
+ import { loadDotEnv, env, HOME } from './env.mjs';
34
35
 
35
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
36
37
  const PENDING_DIR = join(__dirname, '..', '.pending-txs');
37
38
  mkdirSync(PENDING_DIR, { recursive: true });
38
39
 
40
+ // Load ~/.nodpay/.env into process.env (real env takes priority, file is fallback)
41
+ const _envFileArg = process.argv.includes('--env-file')
42
+ ? process.argv[process.argv.indexOf('--env-file') + 1]
43
+ : undefined;
44
+ loadDotEnv(_envFileArg);
45
+
39
46
  // Resolve chain config: --chain flag auto-resolves from networks.json, env vars as fallback
40
47
  import { createRequire } from 'module';
41
48
  const require = createRequire(import.meta.url);
@@ -64,26 +71,10 @@ if (chainArg) {
64
71
  }
65
72
  const ENTRYPOINT_ADDRESS = ENTRYPOINT;
66
73
 
67
- const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
68
-
69
- // SECURITY: Read agent key from ~/.nodpay/.env file (chmod 600), not from
70
- // process.env or CLI args. The key is loaded at runtime by the script itself,
71
- // so it never passes through the LLM agent's context or conversation history.
72
- function loadAgentKey() {
73
- try {
74
- const envPath = join(HOME, '.nodpay', '.env');
75
- const lines = readFileSync(envPath, 'utf8').split('\n');
76
- for (const line of lines) {
77
- const trimmed = line.trim();
78
- if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
79
- const [name, ...rest] = trimmed.split('=');
80
- if (name.trim() === 'NODPAY_AGENT_KEY') {
81
- return rest.join('=').trim().replace(/^["']|["']$/g, '');
82
- }
83
- }
84
- } catch {}
85
- return null;
86
- }
74
+ // SECURITY: Agent key is read from process.env.NODPAY_AGENT_KEY.
75
+ // The .env file was loaded above (loadDotEnv) without overriding real env vars.
76
+ // Priority: real env (SafeClaw injection, container, explicit) > .env file.
77
+ // The key never appears in stdout or agent conversation history.
87
78
 
88
79
  const DEFAULT_SAFE = null; // always use --safe flag
89
80
 
@@ -91,21 +82,7 @@ const DEFAULT_SAFE = null; // always use --safe flag
91
82
  // agents don't need their own bundler API key. This is a thin relay — it
92
83
  // forwards the UserOp to a bundler service and returns the result. The proxy
93
84
  // only sees the already-signed (partial) UserOp; it cannot modify or execute it.
94
- // Optional overrides (OP_STORE_URL, WEB_APP_URL) also read from ~/.nodpay/.env.
95
- function loadDotEnvVar(name, fallback) {
96
- try {
97
- const envPath = join(HOME, '.nodpay', '.env');
98
- const lines = readFileSync(envPath, 'utf8').split('\n');
99
- for (const line of lines) {
100
- const trimmed = line.trim();
101
- if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
102
- const [k, ...rest] = trimmed.split('=');
103
- if (k.trim() === name) return rest.join('=').trim().replace(/^["']|["']$/g, '');
104
- }
105
- } catch {}
106
- return fallback;
107
- }
108
- const opStoreBase = loadDotEnvVar('OP_STORE_URL', 'https://nodpay.ai/api');
85
+ const opStoreBase = env('OP_STORE_URL', 'https://nodpay.ai/api');
109
86
  const BUNDLER_URL = `${opStoreBase}/bundler/${CHAIN_ID}`;
110
87
 
111
88
  // --- Agent key: remote signer (SafeClaw proxy) or local key (~/.nodpay/.env) ---
@@ -143,10 +120,10 @@ if (_remoteSignerUrl) {
143
120
  return (await signRes.json()).signature;
144
121
  };
145
122
  } else {
146
- // Local mode: read key from ~/.nodpay/.env (original behavior)
147
- const NODPAY_AGENT_KEY = loadAgentKey();
123
+ // Local mode: read key from process.env (loaded from real env or ~/.nodpay/.env)
124
+ const NODPAY_AGENT_KEY = env('NODPAY_AGENT_KEY');
148
125
  if (!NODPAY_AGENT_KEY) {
149
- console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY in ~/.nodpay/.env run npx nodpay keygen or use --remote-signer' }));
126
+ console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY set env var, add to ~/.nodpay/.env, or use --remote-signer' }));
150
127
  process.exit(1);
151
128
  }
152
129
  _localAgentKey = NODPAY_AGENT_KEY;
@@ -201,6 +178,18 @@ if (!ethers.isAddress(to)) {
201
178
 
202
179
  const SAFE_ADDRESS = safeOverride || DEFAULT_SAFE;
203
180
 
181
+ // Read optional wallet JSON for rpId (SafeClaw cross-origin passkey support)
182
+ let walletRpId = null;
183
+ if (SAFE_ADDRESS) {
184
+ const walletJsonPath = join(HOME, '.nodpay', 'wallets', `${SAFE_ADDRESS}.json`);
185
+ if (existsSync(walletJsonPath)) {
186
+ try {
187
+ const walletData = JSON.parse(readFileSync(walletJsonPath, 'utf8'));
188
+ if (walletData.rpId) walletRpId = walletData.rpId;
189
+ } catch {}
190
+ }
191
+ }
192
+
204
193
  if (!isCounterfactual && !SAFE_ADDRESS) {
205
194
  console.error(JSON.stringify({ error: 'Missing SAFE_ADDRESS. Use --safe <address> or set SAFE_ADDRESS env, or use --counterfactual.' }));
206
195
  process.exit(1);
@@ -588,12 +577,13 @@ try {
588
577
  result.opStoreError = storeData.error || `HTTP ${storeRes.status}`;
589
578
  }
590
579
  if (storeData.shortHash) {
591
- const webBase = loadDotEnvVar('WEB_APP_URL', 'https://nodpay.ai/');
580
+ const webBase = env('WEB_APP_URL', 'https://nodpay.ai/');
592
581
  // Build approve URL with passkey params for SafeClaw users (no localStorage)
593
582
  const approveParams = new URLSearchParams({ safeOpHash: storeData.safeOpHash });
594
583
  if (passkeyX) approveParams.set('px', passkeyX);
595
584
  if (passkeyY) approveParams.set('py', passkeyY);
596
585
  if (recoverySigner) approveParams.set('recovery', recoverySigner);
586
+ if (walletRpId) approveParams.set('rpId', walletRpId);
597
587
  approveUrl = `${webBase}approve?${approveParams.toString()}`;
598
588
  result.approveUrl = approveUrl;
599
589
  result.opStoreSafeOpHash = storeData.safeOpHash;