perp-cli 0.9.8 → 0.12.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/README.md +30 -25
- package/dist/_stderr-filter.d.ts +0 -0
- package/dist/_stderr-filter.js +30 -0
- package/dist/agent-wallet/expiry.d.ts +15 -0
- package/dist/agent-wallet/expiry.js +26 -0
- package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
- package/dist/agent-wallet/lighter-keystore.js +232 -0
- package/dist/agent-wallet/passphrase.d.ts +13 -0
- package/dist/agent-wallet/passphrase.js +54 -0
- package/dist/agent-wallet/signing-strategy.d.ts +67 -0
- package/dist/agent-wallet/signing-strategy.js +51 -0
- package/dist/agent-wallet/store.d.ts +37 -0
- package/dist/agent-wallet/store.js +172 -0
- package/dist/api/public/_http.d.ts +15 -0
- package/dist/api/public/_http.js +24 -0
- package/dist/api/public/hyperliquid.js +36 -24
- package/dist/api/public/lighter.d.ts +7 -1
- package/dist/api/public/lighter.js +85 -37
- package/dist/api/public/pacifica.js +47 -18
- package/dist/bot/engine.js +23 -4
- package/dist/bot/presets.js +3 -3
- package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
- package/dist/bot/strategies/funding-arb-strategy.js +41 -176
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bridge-engine.js +102 -49
- package/dist/cli-spec.js +1 -2
- package/dist/commands/account.js +2 -500
- package/dist/commands/agent.d.ts +8 -2
- package/dist/commands/agent.js +1903 -366
- package/dist/commands/alerts.js +3 -1
- package/dist/commands/arb/index.js +37 -7
- package/dist/commands/arb-auto.js +44 -11
- package/dist/commands/arb.js +83 -34
- package/dist/commands/bot.d.ts +1 -1
- package/dist/commands/bot.js +20 -16
- package/dist/commands/bridge.d.ts +1 -1
- package/dist/commands/bridge.js +4 -4
- package/dist/commands/funds.d.ts +1 -1
- package/dist/commands/funds.js +18 -68
- package/dist/commands/jobs.d.ts +1 -1
- package/dist/commands/jobs.js +2 -2
- package/dist/commands/manage.d.ts +1 -1
- package/dist/commands/manage.js +12 -84
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +2 -2
- package/dist/commands/portfolio.d.ts +19 -3
- package/dist/commands/portfolio.js +747 -163
- package/dist/commands/rebalance.d.ts +1 -1
- package/dist/commands/rebalance.js +18 -12
- package/dist/commands/risk.js +2 -6
- package/dist/commands/settings.js +30 -53
- package/dist/commands/trade.js +16 -15
- package/dist/commands/wallet.d.ts +2 -1
- package/dist/commands/wallet.js +1122 -176
- package/dist/config.d.ts +9 -0
- package/dist/config.js +55 -9
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +82 -2
- package/dist/event-stream.js +12 -1
- package/dist/exchanges/aster-typed-data.d.ts +156 -0
- package/dist/exchanges/aster-typed-data.js +189 -0
- package/dist/exchanges/aster.d.ts +60 -11
- package/dist/exchanges/aster.js +278 -136
- package/dist/exchanges/hyperliquid.d.ts +14 -0
- package/dist/exchanges/hyperliquid.js +88 -34
- package/dist/exchanges/lighter-spot.js +19 -5
- package/dist/exchanges/lighter.d.ts +66 -0
- package/dist/exchanges/lighter.js +213 -31
- package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
- package/dist/exchanges/pacifica-typed-data.js +74 -0
- package/dist/exchanges/pacifica.d.ts +29 -1
- package/dist/exchanges/pacifica.js +94 -6
- package/dist/funding/rates.js +59 -21
- package/dist/guardrail/contracts.d.ts +14 -0
- package/dist/guardrail/contracts.js +72 -0
- package/dist/guardrail/perp-guardrail.d.ts +21 -0
- package/dist/guardrail/perp-guardrail.js +145 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +307 -362
- package/dist/jobs.js +3 -1
- package/dist/mcp-server.js +92 -88
- package/dist/position-history.js +47 -4
- package/dist/settings.d.ts +69 -3
- package/dist/settings.js +3 -2
- package/dist/signer/index.d.ts +2 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/ows-evm.d.ts +32 -0
- package/dist/signer/ows-evm.js +86 -0
- package/dist/signer/ows-loader.d.ts +2 -0
- package/dist/signer/ows-loader.js +6 -0
- package/dist/signer/ows-solana.d.ts +25 -0
- package/dist/signer/ows-solana.js +56 -0
- package/dist/smart-order.d.ts +8 -2
- package/dist/smart-order.js +46 -11
- package/dist/strategies/funding-arb.js +29 -8
- package/dist/trade-validator.js +30 -5
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +19 -0
- package/package.json +4 -2
- package/skills/perp-cli/SKILL.md +23 -14
- package/skills/perp-cli/references/agent-operations.md +6 -4
- package/skills/perp-cli/references/commands.md +55 -34
- package/skills/perp-cli/scripts/preflight.sh +4 -4
- package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
- package/dist/__tests__/alert-logic.test.d.ts +0 -1
- package/dist/__tests__/alert-logic.test.js +0 -107
- package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
- package/dist/__tests__/arb-auto-3dex.test.js +0 -397
- package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
- package/dist/__tests__/arb-history-stats.test.js +0 -176
- package/dist/__tests__/arb-logic.test.d.ts +0 -1
- package/dist/__tests__/arb-logic.test.js +0 -84
- package/dist/__tests__/arb-manage.test.d.ts +0 -1
- package/dist/__tests__/arb-manage.test.js +0 -253
- package/dist/__tests__/arb-new-features.test.d.ts +0 -1
- package/dist/__tests__/arb-new-features.test.js +0 -457
- package/dist/__tests__/arb-sizing.test.d.ts +0 -1
- package/dist/__tests__/arb-sizing.test.js +0 -48
- package/dist/__tests__/arb-state.test.d.ts +0 -1
- package/dist/__tests__/arb-state.test.js +0 -284
- package/dist/__tests__/arb-userflow.test.d.ts +0 -1
- package/dist/__tests__/arb-userflow.test.js +0 -945
- package/dist/__tests__/arb-utils.test.d.ts +0 -1
- package/dist/__tests__/arb-utils.test.js +0 -264
- package/dist/__tests__/bot-conditions.test.d.ts +0 -1
- package/dist/__tests__/bot-conditions.test.js +0 -341
- package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
- package/dist/__tests__/client-id-tracker.test.js +0 -137
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
- package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
- package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
- package/dist/__tests__/commands/order-intent.test.js +0 -600
- package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
- package/dist/__tests__/commands/trade-commands.test.js +0 -821
- package/dist/__tests__/config.test.d.ts +0 -1
- package/dist/__tests__/config.test.js +0 -86
- package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
- package/dist/__tests__/cross-chain-margin.test.js +0 -287
- package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
- package/dist/__tests__/dex-asset-map.test.js +0 -191
- package/dist/__tests__/errors.test.d.ts +0 -1
- package/dist/__tests__/errors.test.js +0 -110
- package/dist/__tests__/event-stream.test.d.ts +0 -1
- package/dist/__tests__/event-stream.test.js +0 -276
- package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
- package/dist/__tests__/exchanges/interface.test.js +0 -132
- package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
- package/dist/__tests__/exchanges/mock-adapter.js +0 -137
- package/dist/__tests__/execution-log.test.d.ts +0 -1
- package/dist/__tests__/execution-log.test.js +0 -106
- package/dist/__tests__/funding-calc.test.d.ts +0 -1
- package/dist/__tests__/funding-calc.test.js +0 -71
- package/dist/__tests__/funding-history.test.d.ts +0 -1
- package/dist/__tests__/funding-history.test.js +0 -343
- package/dist/__tests__/funding-rates.test.d.ts +0 -1
- package/dist/__tests__/funding-rates.test.js +0 -342
- package/dist/__tests__/funding.test.d.ts +0 -1
- package/dist/__tests__/funding.test.js +0 -173
- package/dist/__tests__/gap-logic.test.d.ts +0 -1
- package/dist/__tests__/gap-logic.test.js +0 -43
- package/dist/__tests__/hip3-dex.test.d.ts +0 -1
- package/dist/__tests__/hip3-dex.test.js +0 -234
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
- package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
- package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge.integration.test.js +0 -309
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
- package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/lighter.integration.test.js +0 -53
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
- package/dist/__tests__/liquidity.test.d.ts +0 -1
- package/dist/__tests__/liquidity.test.js +0 -225
- package/dist/__tests__/plan-executor.test.d.ts +0 -1
- package/dist/__tests__/plan-executor.test.js +0 -314
- package/dist/__tests__/position-history.test.d.ts +0 -1
- package/dist/__tests__/position-history.test.js +0 -367
- package/dist/__tests__/retry.test.d.ts +0 -1
- package/dist/__tests__/retry.test.js +0 -310
- package/dist/__tests__/risk-assessment.test.d.ts +0 -1
- package/dist/__tests__/risk-assessment.test.js +0 -288
- package/dist/__tests__/security-adversarial.test.d.ts +0 -1
- package/dist/__tests__/security-adversarial.test.js +0 -574
- package/dist/__tests__/strategies.test.d.ts +0 -1
- package/dist/__tests__/strategies.test.js +0 -539
- package/dist/__tests__/trade-execution.test.d.ts +0 -1
- package/dist/__tests__/trade-execution.test.js +0 -129
- package/dist/__tests__/trade-validator.test.d.ts +0 -1
- package/dist/__tests__/trade-validator.test.js +0 -655
- package/dist/__tests__/utils.test.d.ts +0 -1
- package/dist/__tests__/utils.test.js +0 -76
- package/dist/arb/reconcile.d.ts +0 -28
- package/dist/arb/reconcile.js +0 -162
- package/dist/commands/alert.d.ts +0 -2
- package/dist/commands/alert.js +0 -421
- package/dist/commands/analytics.d.ts +0 -3
- package/dist/commands/analytics.js +0 -311
- package/dist/commands/arb-manage.d.ts +0 -5
- package/dist/commands/arb-manage.js +0 -5
- package/dist/commands/dashboard.d.ts +0 -3
- package/dist/commands/dashboard.js +0 -176
- package/dist/commands/deposit.d.ts +0 -4
- package/dist/commands/deposit.js +0 -573
- package/dist/commands/dex.d.ts +0 -3
- package/dist/commands/dex.js +0 -120
- package/dist/commands/env.d.ts +0 -2
- package/dist/commands/env.js +0 -136
- package/dist/commands/funding.d.ts +0 -3
- package/dist/commands/funding.js +0 -554
- package/dist/commands/gap.d.ts +0 -2
- package/dist/commands/gap.js +0 -305
- package/dist/commands/health.d.ts +0 -2
- package/dist/commands/health.js +0 -67
- package/dist/commands/multileg.d.ts +0 -16
- package/dist/commands/multileg.js +0 -174
- package/dist/commands/pnl.d.ts +0 -12
- package/dist/commands/pnl.js +0 -243
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -202
- package/dist/commands/stream.d.ts +0 -5
- package/dist/commands/stream.js +0 -200
- package/dist/commands/withdraw.d.ts +0 -3
- package/dist/commands/withdraw.js +0 -187
- package/dist/exchanges/edgex.d.ts +0 -62
- package/dist/exchanges/edgex.js +0 -552
- package/dist/exchanges/nado.d.ts +0 -52
- package/dist/exchanges/nado.js +0 -223
- package/dist/funding/index.d.ts +0 -3
- package/dist/funding/index.js +0 -3
- package/dist/retry.d.ts +0 -74
- package/dist/retry.js +0 -129
- package/dist/ws/market-feed.d.ts +0 -74
- package/dist/ws/market-feed.js +0 -227
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npx -y perp-cli --json portfolio
|
|
|
19
19
|
- **4 Exchanges** — trade, bridge, arbitrage across Pacifica, Hyperliquid, Lighter, Aster
|
|
20
20
|
- **Funding Rate Arb** — perp-perp + spot-perp scan & one-command dual-leg execution
|
|
21
21
|
- **Portfolio** — single call returns balances, positions, risk level across all exchanges
|
|
22
|
-
- **Funds** — deposit, withdraw,
|
|
22
|
+
- **Funds** — deposit, withdraw, transfer, multi-provider bridge (cctp/relay/debridge), inter-exchange rebalance — all in one group
|
|
23
23
|
- **Bots** — TWAP, grid, DCA, trailing-stop with background job management
|
|
24
24
|
- **Agent-First Design** — `--json`, `--fields`, `--ndjson`, `--dry-run`, runtime schema introspection
|
|
25
25
|
- **Safety** — pre-trade validation, response sanitization, client-id deduplication
|
|
@@ -42,7 +42,7 @@ perp wallet show
|
|
|
42
42
|
|
|
43
43
|
Same EVM key works for both Hyperliquid and Lighter.
|
|
44
44
|
|
|
45
|
-
> **Lighter API Key Index:** Indexes 0–3 are reserved by Lighter's frontend (web/mobile). perp-cli defaults to index `4`. Override with `LIGHTER_API_KEY_INDEX` env var or
|
|
45
|
+
> **Lighter API Key Index:** Indexes 0–3 are reserved by Lighter's frontend (web/mobile). perp-cli defaults to index `4`. Override with `LIGHTER_API_KEY_INDEX` env var, or use `perp wallet agent approve lighter --api-key-index <n>` for managed agent slots. Valid range: 4–254.
|
|
46
46
|
|
|
47
47
|
## Command Groups
|
|
48
48
|
|
|
@@ -52,9 +52,8 @@ Same EVM key works for both Hyperliquid and Lighter.
|
|
|
52
52
|
| `account` | Balance, positions, orders, margin |
|
|
53
53
|
| `trade` | Market/limit/stop orders, close, scale, split execution |
|
|
54
54
|
| `arb` | Funding rate arb — scan, exec, close, monitor (perp-perp & spot-perp) |
|
|
55
|
-
| `
|
|
56
|
-
| `funds` | Deposit, withdraw, transfer,
|
|
57
|
-
| `bridge` | Cross-chain USDC bridge (deBridge DLN) |
|
|
55
|
+
| `strategy` | 19 bot algorithms (grid, dca, twap, APEX, REFLECT, presets) + nested scripted plans |
|
|
56
|
+
| `funds` | Deposit, withdraw, transfer, cross-chain bridge (multi-provider), inter-exchange rebalance |
|
|
58
57
|
| `risk` | Risk limits, liquidation distance, guardrails |
|
|
59
58
|
| `wallet` | Multi-wallet management & on-chain balances |
|
|
60
59
|
| `history` | Execution log, PnL, performance breakdown |
|
|
@@ -63,9 +62,7 @@ Same EVM key works for both Hyperliquid and Lighter.
|
|
|
63
62
|
| `dashboard` | Live web dashboard |
|
|
64
63
|
| `settings` | CLI settings (referrals, defaults) |
|
|
65
64
|
| `backtest` | Strategy backtesting |
|
|
66
|
-
| `
|
|
67
|
-
| `rebalance` | Cross-exchange balance management |
|
|
68
|
-
| `jobs` | Background job management (tmux) |
|
|
65
|
+
| `background` | Background process supervisor (tmux sessions for strategies, alerts, etc.) |
|
|
69
66
|
| `alerts` | Telegram funding rate alerts with background daemon |
|
|
70
67
|
| `agent` | Schema introspection, capabilities, health check |
|
|
71
68
|
| `setup` | Interactive setup wizard (alias: `init`) |
|
|
@@ -117,28 +114,39 @@ perp --json arb exec <SYM> spot:<exch> <perpEx> <$> # spot+perp entry
|
|
|
117
114
|
perp --json arb config # show arb defaults
|
|
118
115
|
perp --json arb history # past arb trade performance (alias: log)
|
|
119
116
|
|
|
120
|
-
# Funds (deposit, withdraw, transfer)
|
|
117
|
+
# Funds (deposit, withdraw, transfer, bridge, rebalance)
|
|
121
118
|
perp --json funds deposit hyperliquid 100
|
|
122
119
|
perp --json funds withdraw pacifica 50
|
|
123
|
-
perp --json funds transfer 100 <ADDRESS> # HL internal transfer
|
|
120
|
+
perp --json funds transfer 100 <ADDRESS> # HL internal transfer (instant)
|
|
121
|
+
perp --json funds bridge quote --from solana --to arbitrum --amount 100
|
|
122
|
+
perp --json funds bridge send --from solana --to arbitrum --amount 100 # auto-cheapest provider
|
|
123
|
+
perp --json funds bridge exchange --from pacifica --to hyperliquid --amount 100
|
|
124
|
+
perp --json funds rebalance check # balances across exchanges
|
|
125
|
+
perp --json funds rebalance plan # compute optimal moves
|
|
126
|
+
perp --json funds rebalance execute --auto-bridge # withdraw → bridge → deposit
|
|
124
127
|
perp --json funds info # all routes & limits
|
|
125
128
|
|
|
126
129
|
# Risk
|
|
127
130
|
perp --json risk limits --max-leverage 5
|
|
128
131
|
perp --json risk liquidation-distance
|
|
129
132
|
|
|
130
|
-
#
|
|
131
|
-
perp
|
|
132
|
-
perp
|
|
133
|
-
perp
|
|
134
|
-
perp
|
|
135
|
-
perp
|
|
136
|
-
perp --json
|
|
137
|
-
perp --json
|
|
138
|
-
|
|
139
|
-
#
|
|
140
|
-
perp --json
|
|
141
|
-
|
|
133
|
+
# Strategies (19 bot algorithms + nested scripted plans)
|
|
134
|
+
perp strategy list-strategies # list all available strategies
|
|
135
|
+
perp strategy run <strategy> [symbol] # run any strategy
|
|
136
|
+
perp strategy apex [symbol] # APEX autonomous orchestrator
|
|
137
|
+
perp strategy reflect # trading performance analysis
|
|
138
|
+
perp strategy preset-list # list strategy presets
|
|
139
|
+
perp --json strategy twap <SYM> buy <SIZE> 30m
|
|
140
|
+
perp --json strategy grid <SYM> --range 5 --grids 10 --size 100
|
|
141
|
+
perp strategy plan example # scripted multi-step plan format
|
|
142
|
+
perp --json strategy plan validate <FILE> # validate scripted plan
|
|
143
|
+
perp --json strategy plan execute <FILE> --dry-run # dry-run scripted plan
|
|
144
|
+
|
|
145
|
+
# Background process supervisor (tmux)
|
|
146
|
+
perp background list # list running jobs
|
|
147
|
+
perp background stop <ID> # stop a job
|
|
148
|
+
perp background logs <ID> -f # follow logs
|
|
149
|
+
|
|
142
150
|
```
|
|
143
151
|
|
|
144
152
|
## Telegram Alerts
|
|
@@ -231,9 +239,6 @@ Built following [agent-first CLI principles](https://justin.poehnelt.com/posts/r
|
|
|
231
239
|
perp --json portfolio
|
|
232
240
|
# → { "ok": true, "data": {...}, "meta": { "timestamp": "..." } }
|
|
233
241
|
|
|
234
|
-
# Runtime schema introspection (don't guess commands — query this)
|
|
235
|
-
perp --json agent schema
|
|
236
|
-
|
|
237
242
|
# Filter output to specific fields (saves tokens)
|
|
238
243
|
perp --json --fields totalEquity,risk portfolio
|
|
239
244
|
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Install a stderr filter that drops known noisy SDK warnings when --json
|
|
3
|
+
// or --ndjson is set. Module body runs at import time — must be the FIRST
|
|
4
|
+
// import in src/index.ts so it activates before SDK module bodies.
|
|
5
|
+
if (process.argv.includes("--json") || process.argv.includes("--ndjson")) {
|
|
6
|
+
const _suppressPatterns = [
|
|
7
|
+
/'--chain evm' is deprecated/,
|
|
8
|
+
/\[hyperliquid\] Failed to load asset map/,
|
|
9
|
+
/\[lighter\] Account index not available/,
|
|
10
|
+
/Failed to initialize SymbolConversion/,
|
|
11
|
+
/HyperliquidAPIError/,
|
|
12
|
+
];
|
|
13
|
+
// Wrap at the lowest level (_write on the Writable) so we catch writes
|
|
14
|
+
// even if a downstream module replaces process.stderr.write itself.
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
const stderr = process.stderr;
|
|
17
|
+
const _origStderrWriteRaw = stderr._write.bind(stderr);
|
|
18
|
+
stderr._write = function (chunk, encoding, callback) {
|
|
19
|
+
const s = typeof chunk === "string"
|
|
20
|
+
? chunk
|
|
21
|
+
: (chunk instanceof Uint8Array || Buffer.isBuffer(chunk))
|
|
22
|
+
? Buffer.from(chunk).toString()
|
|
23
|
+
: "";
|
|
24
|
+
if (_suppressPatterns.some(re => re.test(s))) {
|
|
25
|
+
callback();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
return _origStderrWriteRaw(chunk, encoding, callback);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentMeta } from "../settings.js";
|
|
2
|
+
/**
|
|
3
|
+
* Returns true if the agent's expiry timestamp is in the past.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isExpired(meta: AgentMeta): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Returns the number of days (float) until the agent expires.
|
|
8
|
+
* Negative if already expired.
|
|
9
|
+
*/
|
|
10
|
+
export declare function daysUntilExpiry(meta: AgentMeta): number;
|
|
11
|
+
/**
|
|
12
|
+
* Returns a human-readable expiry string, e.g.
|
|
13
|
+
* "expires in 7 days (2026-05-05)" or "expired 3 days ago (2026-04-25)".
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatExpiry(meta: AgentMeta): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the agent's expiry timestamp is in the past.
|
|
3
|
+
*/
|
|
4
|
+
export function isExpired(meta) {
|
|
5
|
+
return Date.now() >= new Date(meta.expiresAt).getTime();
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Returns the number of days (float) until the agent expires.
|
|
9
|
+
* Negative if already expired.
|
|
10
|
+
*/
|
|
11
|
+
export function daysUntilExpiry(meta) {
|
|
12
|
+
const ms = new Date(meta.expiresAt).getTime() - Date.now();
|
|
13
|
+
return ms / (1000 * 60 * 60 * 24);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Returns a human-readable expiry string, e.g.
|
|
17
|
+
* "expires in 7 days (2026-05-05)" or "expired 3 days ago (2026-04-25)".
|
|
18
|
+
*/
|
|
19
|
+
export function formatExpiry(meta) {
|
|
20
|
+
const days = daysUntilExpiry(meta);
|
|
21
|
+
const dateStr = new Date(meta.expiresAt).toISOString().slice(0, 10);
|
|
22
|
+
if (days < 0) {
|
|
23
|
+
return `expired ${Math.abs(days).toFixed(0)} days ago (${dateStr})`;
|
|
24
|
+
}
|
|
25
|
+
return `expires in ${days.toFixed(0)} days (${dateStr})`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Pure: deterministic absolute path for the keystore file. */
|
|
2
|
+
export declare function lighterKeystorePath(accountIndex: number, slot: number): string;
|
|
3
|
+
/**
|
|
4
|
+
* Encrypt a 40-byte Lighter L2 private key (hex) and write it to
|
|
5
|
+
* `~/.perp/lighter-agents/<accountIndex>-<slot>.json` with mode 0600.
|
|
6
|
+
*
|
|
7
|
+
* Returns the absolute path of the written file.
|
|
8
|
+
*/
|
|
9
|
+
export declare function saveLighterKey(accountIndex: number, slot: number, privateKeyHex: string, passphrase: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Read + decrypt a stored Lighter L2 private key. Returns the hex string
|
|
12
|
+
* without `0x` prefix.
|
|
13
|
+
*
|
|
14
|
+
* Throws (no fallback):
|
|
15
|
+
* - LIGHTER_KEYSTORE_NOT_FOUND if the file is missing
|
|
16
|
+
* - LIGHTER_KEYSTORE_DECRYPT_FAILED if the passphrase is wrong / file corrupt
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadLighterKey(accountIndex: number, slot: number, passphrase: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Idempotent delete. Returns true if a file was removed, false if absent.
|
|
21
|
+
*/
|
|
22
|
+
export declare function deleteLighterKey(accountIndex: number, slot: number): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* One-time bridge from legacy env-based storage to the keystore.
|
|
25
|
+
*
|
|
26
|
+
* Triggered on a single, narrow condition: env has `LIGHTER_API_KEY` AND the
|
|
27
|
+
* target keystore file does not yet exist. After successful save, clears the
|
|
28
|
+
* three legacy env entries from `~/.perp/.env` so they cannot be re-read.
|
|
29
|
+
*
|
|
30
|
+
* NOT a fallback (SSOT Rule #2): runs at most once per install (post-clear, the
|
|
31
|
+
* trigger condition is permanently false). Single-direction (env → keystore);
|
|
32
|
+
* never reads from keystore back to env.
|
|
33
|
+
*/
|
|
34
|
+
export declare function migrateFromEnvIfPresent(): void;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lighter L2 agent keystore (SSOT Rule #3 — encrypted at-rest).
|
|
3
|
+
*
|
|
4
|
+
* Lighter's L2 signing key is a 40-byte secp256k1 private key — a non-OWS-native
|
|
5
|
+
* curve, so it cannot live in `~/.ows/wallets/` like Aster/HL/PAC agent keypairs.
|
|
6
|
+
* Per the FIXME(2d-spike) in src/commands/agent.ts and SSOT Rule #3 in CLAUDE.md,
|
|
7
|
+
* we persist these keys at `~/.perp/lighter-agents/<accountIndex>-<slot>.json`
|
|
8
|
+
* using the same AES-256-GCM + scrypt encryption scheme OWS uses.
|
|
9
|
+
*
|
|
10
|
+
* Empty passphrase ("") is acceptable — mirrors the HL/PAC pattern. Security
|
|
11
|
+
* model = file mode 0600 + obfuscation, NOT cryptographic strength against a
|
|
12
|
+
* local attacker. A non-empty passphrase upgrades the threat model.
|
|
13
|
+
*
|
|
14
|
+
* No fallbacks (SSOT Rule #2): missing files / wrong passphrase throw with
|
|
15
|
+
* remediation pointing the user at `perp wallet agent approve lighter --rotate`.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, chmodSync, } from "fs";
|
|
18
|
+
import { resolve } from "path";
|
|
19
|
+
import { randomBytes, scryptSync, createCipheriv, createDecipheriv, } from "node:crypto";
|
|
20
|
+
import { PerpError } from "../errors.js";
|
|
21
|
+
const KDF_PARAMS = { dklen: 32, n: 65536, p: 1, r: 8 };
|
|
22
|
+
const SCRYPT_OPTS = { N: KDF_PARAMS.n, r: KDF_PARAMS.r, p: KDF_PARAMS.p, maxmem: 128 * 1024 * 1024 };
|
|
23
|
+
const REMEDIATION = "perp wallet agent approve lighter --rotate";
|
|
24
|
+
function lighterAgentsDir() {
|
|
25
|
+
return resolve(process.env["HOME"] ?? "~", ".perp", "lighter-agents");
|
|
26
|
+
}
|
|
27
|
+
/** Pure: deterministic absolute path for the keystore file. */
|
|
28
|
+
export function lighterKeystorePath(accountIndex, slot) {
|
|
29
|
+
return resolve(lighterAgentsDir(), `${accountIndex}-${slot}.json`);
|
|
30
|
+
}
|
|
31
|
+
function ensureDir() {
|
|
32
|
+
const dir = lighterAgentsDir();
|
|
33
|
+
if (!existsSync(dir)) {
|
|
34
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// best-effort tighten permissions if dir exists with looser mode
|
|
38
|
+
try {
|
|
39
|
+
chmodSync(dir, 0o700);
|
|
40
|
+
}
|
|
41
|
+
catch { /* non-fatal */ }
|
|
42
|
+
}
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
function stripHexPrefix(hex) {
|
|
46
|
+
return hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Encrypt a 40-byte Lighter L2 private key (hex) and write it to
|
|
50
|
+
* `~/.perp/lighter-agents/<accountIndex>-<slot>.json` with mode 0600.
|
|
51
|
+
*
|
|
52
|
+
* Returns the absolute path of the written file.
|
|
53
|
+
*/
|
|
54
|
+
export function saveLighterKey(accountIndex, slot, privateKeyHex, passphrase) {
|
|
55
|
+
const dir = ensureDir();
|
|
56
|
+
const path = resolve(dir, `${accountIndex}-${slot}.json`);
|
|
57
|
+
const cleanHex = stripHexPrefix(privateKeyHex).toLowerCase();
|
|
58
|
+
if (!/^[0-9a-f]+$/.test(cleanHex) || cleanHex.length === 0) {
|
|
59
|
+
throw new PerpError("INVALID_PARAMS", "Lighter L2 private key must be hex", {
|
|
60
|
+
remediation: REMEDIATION,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const salt = randomBytes(32);
|
|
64
|
+
const iv = randomBytes(12);
|
|
65
|
+
const dk = scryptSync(Buffer.from(passphrase, "utf-8"), salt, KDF_PARAMS.dklen, SCRYPT_OPTS);
|
|
66
|
+
const cipher = createCipheriv("aes-256-gcm", dk, iv);
|
|
67
|
+
const ciphertext = Buffer.concat([
|
|
68
|
+
cipher.update(Buffer.from(cleanHex, "utf-8")),
|
|
69
|
+
cipher.final(),
|
|
70
|
+
]);
|
|
71
|
+
const authTag = cipher.getAuthTag();
|
|
72
|
+
const file = {
|
|
73
|
+
version: 1,
|
|
74
|
+
accountIndex,
|
|
75
|
+
slot,
|
|
76
|
+
crypto: {
|
|
77
|
+
cipher: "aes-256-gcm",
|
|
78
|
+
cipherparams: { iv: iv.toString("hex") },
|
|
79
|
+
ciphertext: ciphertext.toString("hex"),
|
|
80
|
+
auth_tag: authTag.toString("hex"),
|
|
81
|
+
kdf: "scrypt",
|
|
82
|
+
kdfparams: {
|
|
83
|
+
dklen: KDF_PARAMS.dklen,
|
|
84
|
+
n: KDF_PARAMS.n,
|
|
85
|
+
p: KDF_PARAMS.p,
|
|
86
|
+
r: KDF_PARAMS.r,
|
|
87
|
+
salt: salt.toString("hex"),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
createdAt: new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
writeFileSync(path, JSON.stringify(file, null, 2), { mode: 0o600 });
|
|
93
|
+
// Re-tighten in case writeFileSync overwrote an existing file with a looser umask
|
|
94
|
+
try {
|
|
95
|
+
chmodSync(path, 0o600);
|
|
96
|
+
}
|
|
97
|
+
catch { /* non-fatal */ }
|
|
98
|
+
return path;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read + decrypt a stored Lighter L2 private key. Returns the hex string
|
|
102
|
+
* without `0x` prefix.
|
|
103
|
+
*
|
|
104
|
+
* Throws (no fallback):
|
|
105
|
+
* - LIGHTER_KEYSTORE_NOT_FOUND if the file is missing
|
|
106
|
+
* - LIGHTER_KEYSTORE_DECRYPT_FAILED if the passphrase is wrong / file corrupt
|
|
107
|
+
*/
|
|
108
|
+
export function loadLighterKey(accountIndex, slot, passphrase) {
|
|
109
|
+
// One-time data migration: if a legacy `LIGHTER_API_KEY` env var is set and
|
|
110
|
+
// the new keystore file does not exist yet, materialize the keystore from env
|
|
111
|
+
// before continuing. This is NOT a runtime fallback — it's a single-pass
|
|
112
|
+
// upgrade path that runs at most once per install.
|
|
113
|
+
migrateFromEnvIfPresent();
|
|
114
|
+
const path = lighterKeystorePath(accountIndex, slot);
|
|
115
|
+
if (!existsSync(path)) {
|
|
116
|
+
throw new PerpError("KEY_NOT_FOUND", `LIGHTER_KEYSTORE_NOT_FOUND: ${path}`, { remediation: REMEDIATION });
|
|
117
|
+
}
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
throw new PerpError("SIGNATURE_FAILED", `LIGHTER_KEYSTORE_DECRYPT_FAILED: corrupt JSON — ${e instanceof Error ? e.message : String(e)}`, { remediation: REMEDIATION });
|
|
124
|
+
}
|
|
125
|
+
if (parsed.version !== 1 || parsed.crypto?.cipher !== "aes-256-gcm" || parsed.crypto?.kdf !== "scrypt") {
|
|
126
|
+
throw new PerpError("SIGNATURE_FAILED", "LIGHTER_KEYSTORE_DECRYPT_FAILED: unsupported version or cipher", { remediation: REMEDIATION });
|
|
127
|
+
}
|
|
128
|
+
const salt = Buffer.from(parsed.crypto.kdfparams.salt, "hex");
|
|
129
|
+
const iv = Buffer.from(parsed.crypto.cipherparams.iv, "hex");
|
|
130
|
+
const ciphertext = Buffer.from(parsed.crypto.ciphertext, "hex");
|
|
131
|
+
const authTag = Buffer.from(parsed.crypto.auth_tag, "hex");
|
|
132
|
+
const dk = scryptSync(Buffer.from(passphrase, "utf-8"), salt, parsed.crypto.kdfparams.dklen, {
|
|
133
|
+
N: parsed.crypto.kdfparams.n,
|
|
134
|
+
r: parsed.crypto.kdfparams.r,
|
|
135
|
+
p: parsed.crypto.kdfparams.p,
|
|
136
|
+
maxmem: 128 * 1024 * 1024,
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
const decipher = createDecipheriv("aes-256-gcm", dk, iv);
|
|
140
|
+
decipher.setAuthTag(authTag);
|
|
141
|
+
const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
142
|
+
return plain.toString("utf-8");
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
throw new PerpError("SIGNATURE_FAILED", `LIGHTER_KEYSTORE_DECRYPT_FAILED: ${e instanceof Error ? e.message : String(e)}`, { remediation: REMEDIATION });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Idempotent delete. Returns true if a file was removed, false if absent.
|
|
150
|
+
*/
|
|
151
|
+
export function deleteLighterKey(accountIndex, slot) {
|
|
152
|
+
const path = lighterKeystorePath(accountIndex, slot);
|
|
153
|
+
if (!existsSync(path))
|
|
154
|
+
return false;
|
|
155
|
+
unlinkSync(path);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* One-time bridge from legacy env-based storage to the keystore.
|
|
160
|
+
*
|
|
161
|
+
* Triggered on a single, narrow condition: env has `LIGHTER_API_KEY` AND the
|
|
162
|
+
* target keystore file does not yet exist. After successful save, clears the
|
|
163
|
+
* three legacy env entries from `~/.perp/.env` so they cannot be re-read.
|
|
164
|
+
*
|
|
165
|
+
* NOT a fallback (SSOT Rule #2): runs at most once per install (post-clear, the
|
|
166
|
+
* trigger condition is permanently false). Single-direction (env → keystore);
|
|
167
|
+
* never reads from keystore back to env.
|
|
168
|
+
*/
|
|
169
|
+
export function migrateFromEnvIfPresent() {
|
|
170
|
+
const envKey = process.env["LIGHTER_API_KEY"];
|
|
171
|
+
if (!envKey || envKey.length === 0)
|
|
172
|
+
return;
|
|
173
|
+
const accountIndexStr = process.env["LIGHTER_ACCOUNT_INDEX"];
|
|
174
|
+
const apiKeyIndexStr = process.env["LIGHTER_API_KEY_INDEX"];
|
|
175
|
+
const accountIndex = accountIndexStr ? parseInt(accountIndexStr, 10) : NaN;
|
|
176
|
+
const slot = apiKeyIndexStr ? parseInt(apiKeyIndexStr, 10) : NaN;
|
|
177
|
+
// If we lack the indices needed to address the keystore slot, bail silently.
|
|
178
|
+
// Migration only runs when the env trio is fully populated.
|
|
179
|
+
if (!Number.isFinite(accountIndex) || accountIndex < 0)
|
|
180
|
+
return;
|
|
181
|
+
if (!Number.isFinite(slot) || slot < 0)
|
|
182
|
+
return;
|
|
183
|
+
const target = lighterKeystorePath(accountIndex, slot);
|
|
184
|
+
if (existsSync(target))
|
|
185
|
+
return;
|
|
186
|
+
saveLighterKey(accountIndex, slot, envKey, "");
|
|
187
|
+
process.stderr.write(`[lighter] migrated LIGHTER_API_KEY env → ~/.perp/lighter-agents/${accountIndex}-${slot}.json (SSOT Rule #3)\n`);
|
|
188
|
+
// Clear the legacy env entries from ~/.perp/.env so the trigger never fires
|
|
189
|
+
// again. setEnvVar(name, "") rewrites the file with the empty value.
|
|
190
|
+
try {
|
|
191
|
+
// Inline lazy import to avoid a circular dep at module-load time.
|
|
192
|
+
// setEnvVar is sync; the dynamic import is deliberately resolved lazily.
|
|
193
|
+
void clearEnvLegacyKeys();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// best-effort — file may be locked or read-only; the in-memory delete below
|
|
197
|
+
// still prevents this process from re-triggering migration.
|
|
198
|
+
}
|
|
199
|
+
delete process.env["LIGHTER_API_KEY"];
|
|
200
|
+
delete process.env["LIGHTER_ACCOUNT_INDEX"];
|
|
201
|
+
delete process.env["LIGHTER_API_KEY_INDEX"];
|
|
202
|
+
}
|
|
203
|
+
// Async helper deliberately split out so migrateFromEnvIfPresent stays sync.
|
|
204
|
+
function clearEnvLegacyKeys() {
|
|
205
|
+
// Use the same .env parser/writer the rest of the CLI uses so we don't
|
|
206
|
+
// accidentally drop comments or other entries.
|
|
207
|
+
// Sync read + write via fs.
|
|
208
|
+
const PERP_DIR = resolve(process.env["HOME"] ?? "~", ".perp");
|
|
209
|
+
const ENV_FILE = resolve(PERP_DIR, ".env");
|
|
210
|
+
if (!existsSync(ENV_FILE))
|
|
211
|
+
return;
|
|
212
|
+
const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
|
|
213
|
+
const out = [];
|
|
214
|
+
const dropKeys = new Set([
|
|
215
|
+
"LIGHTER_API_KEY",
|
|
216
|
+
"LIGHTER_ACCOUNT_INDEX",
|
|
217
|
+
"LIGHTER_API_KEY_INDEX",
|
|
218
|
+
]);
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const trimmed = line.trim();
|
|
221
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
222
|
+
out.push(line);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const eqIdx = trimmed.indexOf("=");
|
|
226
|
+
if (eqIdx > 0 && dropKeys.has(trimmed.slice(0, eqIdx))) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
out.push(line);
|
|
230
|
+
}
|
|
231
|
+
writeFileSync(ENV_FILE, out.join("\n"), { mode: 0o600 });
|
|
232
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve master passphrase from (in precedence order):
|
|
3
|
+
* 1. opts.flag (--passphrase)
|
|
4
|
+
* 2. process.env.OWS_PASSPHRASE
|
|
5
|
+
* 3. stdin (only if process.stdin.isTTY === false)
|
|
6
|
+
*
|
|
7
|
+
* Returns null if no path provides one AND stdin IS TTY (caller may prompt).
|
|
8
|
+
* Throws PASSPHRASE_REQUIRED with remediation if no path provides one AND
|
|
9
|
+
* stdin is non-TTY (AC-16).
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolvePassphrase(opts: {
|
|
12
|
+
flag?: string;
|
|
13
|
+
}): Promise<string | null>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PerpError } from "../errors.js";
|
|
2
|
+
const REMEDIATION = "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe (echo $PP | perp ...)";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve master passphrase from (in precedence order):
|
|
5
|
+
* 1. opts.flag (--passphrase)
|
|
6
|
+
* 2. process.env.OWS_PASSPHRASE
|
|
7
|
+
* 3. stdin (only if process.stdin.isTTY === false)
|
|
8
|
+
*
|
|
9
|
+
* Returns null if no path provides one AND stdin IS TTY (caller may prompt).
|
|
10
|
+
* Throws PASSPHRASE_REQUIRED with remediation if no path provides one AND
|
|
11
|
+
* stdin is non-TTY (AC-16).
|
|
12
|
+
*/
|
|
13
|
+
export async function resolvePassphrase(opts) {
|
|
14
|
+
// 1. CLI flag (allow empty string when explicitly passed via --passphrase '')
|
|
15
|
+
if (opts.flag !== undefined) {
|
|
16
|
+
return opts.flag;
|
|
17
|
+
}
|
|
18
|
+
// 2. Environment variable (allow empty string when explicitly set; treat
|
|
19
|
+
// unset as absent)
|
|
20
|
+
if ("OWS_PASSPHRASE" in process.env) {
|
|
21
|
+
return process.env["OWS_PASSPHRASE"] ?? "";
|
|
22
|
+
}
|
|
23
|
+
// 3. stdin pipe — only when stdin is not a TTY
|
|
24
|
+
if (process.stdin.isTTY === false) {
|
|
25
|
+
const piped = await readStdin();
|
|
26
|
+
if (piped !== null) {
|
|
27
|
+
return piped;
|
|
28
|
+
}
|
|
29
|
+
// stdin was non-TTY but empty — throw rather than falling through to prompt
|
|
30
|
+
throw new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY", {
|
|
31
|
+
remediation: REMEDIATION,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// stdin IS TTY — caller decides whether to prompt interactively
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read all bytes from stdin until EOF, trim trailing newlines.
|
|
39
|
+
* Returns null if stdin produced zero bytes.
|
|
40
|
+
*/
|
|
41
|
+
async function readStdin() {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
process.stdin.setEncoding("utf-8");
|
|
45
|
+
process.stdin.on("data", (chunk) => {
|
|
46
|
+
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf-8"));
|
|
47
|
+
});
|
|
48
|
+
process.stdin.on("end", () => {
|
|
49
|
+
const raw = chunks.join("").replace(/\r?\n$/, "");
|
|
50
|
+
resolve(raw.length > 0 ? raw : null);
|
|
51
|
+
});
|
|
52
|
+
process.stdin.on("error", reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AgentMeta } from "../settings.js";
|
|
2
|
+
/**
|
|
3
|
+
* Interface for agent-key signing strategies.
|
|
4
|
+
* The AsterAdapter consumes this interface, never a concrete class.
|
|
5
|
+
*
|
|
6
|
+
* Signature shape mirrors EvmSigner.signTypedData(domain, types, message) so
|
|
7
|
+
* call sites can cast to `EvmSigner & AgentSigningStrategy` and dispatch
|
|
8
|
+
* uniformly. The return type differs from EvmSigner (which returns string):
|
|
9
|
+
* AgentSigningStrategy returns { signature, r, s, v } so callers can pull
|
|
10
|
+
* raw r/s/v if needed.
|
|
11
|
+
*/
|
|
12
|
+
export interface AgentSigningStrategy {
|
|
13
|
+
signTypedData(domain: Record<string, unknown>, types: Record<string, Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
}>>, message: Record<string, unknown>): Promise<{
|
|
17
|
+
signature: string;
|
|
18
|
+
r: string;
|
|
19
|
+
s: string;
|
|
20
|
+
v: number;
|
|
21
|
+
}>;
|
|
22
|
+
getAddress(): string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Strategy: pass the OWS api-key token in the passphrase slot.
|
|
26
|
+
* This is the default (Option A γ-path). Validity depends on Step 0a spike.
|
|
27
|
+
* Wraps OwsEvmSigner.create(walletName, owsKeyToken).
|
|
28
|
+
*/
|
|
29
|
+
export declare class TokenAsPassphraseStrategy implements AgentSigningStrategy {
|
|
30
|
+
private _walletName;
|
|
31
|
+
private _owsKeyToken;
|
|
32
|
+
private _address;
|
|
33
|
+
constructor(walletName: string, owsKeyToken: string, address: string);
|
|
34
|
+
getAddress(): string;
|
|
35
|
+
signTypedData(domain: Record<string, unknown>, types: Record<string, Array<{
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
}>>, message: Record<string, unknown>): Promise<{
|
|
39
|
+
signature: string;
|
|
40
|
+
r: string;
|
|
41
|
+
s: string;
|
|
42
|
+
v: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Strategy: read the token from the OWS keyfile by api-key id and pass it
|
|
47
|
+
* explicitly. Stub until Step 0a determines whether this path is needed.
|
|
48
|
+
*/
|
|
49
|
+
export declare class ExplicitTokenStrategy implements AgentSigningStrategy {
|
|
50
|
+
constructor(_walletName: string, _apiKeyId: string, _address: string);
|
|
51
|
+
getAddress(): string;
|
|
52
|
+
signTypedData(_domain: Record<string, unknown>, _types: Record<string, Array<{
|
|
53
|
+
name: string;
|
|
54
|
+
type: string;
|
|
55
|
+
}>>, _message: Record<string, unknown>): Promise<{
|
|
56
|
+
signature: string;
|
|
57
|
+
r: string;
|
|
58
|
+
s: string;
|
|
59
|
+
v: number;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Factory: returns the appropriate signing strategy for the given agent meta +
|
|
64
|
+
* OWS api-key token. Defaults to TokenAsPassphraseStrategy per Option A γ-path.
|
|
65
|
+
* Will switch based on Step 0a spike outcome.
|
|
66
|
+
*/
|
|
67
|
+
export declare function agentSigningStrategyFor(meta: AgentMeta, owsKeyToken: string): AgentSigningStrategy;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy: pass the OWS api-key token in the passphrase slot.
|
|
3
|
+
* This is the default (Option A γ-path). Validity depends on Step 0a spike.
|
|
4
|
+
* Wraps OwsEvmSigner.create(walletName, owsKeyToken).
|
|
5
|
+
*/
|
|
6
|
+
export class TokenAsPassphraseStrategy {
|
|
7
|
+
_walletName;
|
|
8
|
+
_owsKeyToken;
|
|
9
|
+
_address;
|
|
10
|
+
constructor(walletName, owsKeyToken, address) {
|
|
11
|
+
this._walletName = walletName;
|
|
12
|
+
this._owsKeyToken = owsKeyToken;
|
|
13
|
+
this._address = address;
|
|
14
|
+
}
|
|
15
|
+
getAddress() {
|
|
16
|
+
return this._address;
|
|
17
|
+
}
|
|
18
|
+
async signTypedData(domain, types, message) {
|
|
19
|
+
// Lazy import to avoid loading OWS bindings unless this strategy is used
|
|
20
|
+
const { OwsEvmSigner } = await import("../signer/ows-evm.js");
|
|
21
|
+
const signer = OwsEvmSigner.create(this._walletName, this._owsKeyToken);
|
|
22
|
+
const hexSig = await signer.signTypedData(domain, types, message);
|
|
23
|
+
// Parse r, s, v from the 65-byte hex signature
|
|
24
|
+
const sig = hexSig.startsWith("0x") ? hexSig.slice(2) : hexSig;
|
|
25
|
+
const r = `0x${sig.slice(0, 64)}`;
|
|
26
|
+
const s = `0x${sig.slice(64, 128)}`;
|
|
27
|
+
const v = parseInt(sig.slice(128, 130), 16);
|
|
28
|
+
return { signature: hexSig, r, s, v };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Strategy: read the token from the OWS keyfile by api-key id and pass it
|
|
33
|
+
* explicitly. Stub until Step 0a determines whether this path is needed.
|
|
34
|
+
*/
|
|
35
|
+
export class ExplicitTokenStrategy {
|
|
36
|
+
constructor(_walletName, _apiKeyId, _address) { }
|
|
37
|
+
getAddress() {
|
|
38
|
+
throw new Error("ExplicitTokenStrategy.getAddress(): not implemented — pending Step 0a spike outcome");
|
|
39
|
+
}
|
|
40
|
+
async signTypedData(_domain, _types, _message) {
|
|
41
|
+
throw new Error("ExplicitTokenStrategy.signTypedData(): not implemented — pending Step 0a spike outcome");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Factory: returns the appropriate signing strategy for the given agent meta +
|
|
46
|
+
* OWS api-key token. Defaults to TokenAsPassphraseStrategy per Option A γ-path.
|
|
47
|
+
* Will switch based on Step 0a spike outcome.
|
|
48
|
+
*/
|
|
49
|
+
export function agentSigningStrategyFor(meta, owsKeyToken) {
|
|
50
|
+
return new TokenAsPassphraseStrategy(meta.agentWalletName, owsKeyToken, meta.agentEvmAddress);
|
|
51
|
+
}
|