ethagent 0.2.1 → 1.0.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/LICENSE +21 -0
- package/README.md +114 -32
- package/bin/ethagent.js +11 -2
- package/package.json +25 -7
- package/src/app/FirstRun.tsx +412 -0
- package/src/app/hooks/useCancelRequest.ts +22 -0
- package/src/app/hooks/useDoublePress.ts +46 -0
- package/src/app/hooks/useExitOnCtrlC.ts +36 -0
- package/src/app/input/AppInputProvider.tsx +116 -0
- package/src/app/input/appInputParser.ts +279 -0
- package/src/app/keybindings/KeybindingProvider.tsx +134 -0
- package/src/app/keybindings/resolver.ts +42 -0
- package/src/app/keybindings/types.ts +26 -0
- package/src/chat/ChatBottomPane.tsx +280 -0
- package/src/chat/ChatInput.tsx +722 -0
- package/src/chat/ChatScreen.tsx +1575 -0
- package/src/chat/ContextLimitView.tsx +95 -0
- package/src/chat/ContinuityEditReviewView.tsx +48 -0
- package/src/chat/ConversationStack.tsx +47 -0
- package/src/chat/CopyPicker.tsx +52 -0
- package/src/chat/MessageList.tsx +609 -0
- package/src/chat/PermissionPrompt.tsx +153 -0
- package/src/chat/PermissionsView.tsx +159 -0
- package/src/chat/PlanApprovalView.tsx +91 -0
- package/src/chat/ResumeView.tsx +267 -0
- package/src/chat/RewindView.tsx +386 -0
- package/src/chat/SessionStatus.tsx +51 -0
- package/src/chat/TranscriptView.tsx +202 -0
- package/src/chat/chatInputState.ts +247 -0
- package/src/chat/chatPaste.ts +49 -0
- package/src/chat/chatScreenUtils.ts +187 -0
- package/src/chat/chatSessionState.ts +142 -0
- package/src/chat/chatTurnOrchestrator.ts +701 -0
- package/src/chat/commands.ts +673 -0
- package/src/chat/textCursor.ts +202 -0
- package/src/chat/toolResultDisplay.ts +8 -0
- package/src/chat/transcriptViewport.ts +247 -0
- package/src/cli/ResetConfirmView.tsx +61 -0
- package/src/cli/main.tsx +177 -0
- package/src/cli/preview.tsx +19 -0
- package/src/cli/reset.ts +106 -0
- package/src/identity/continuity/editor.ts +149 -0
- package/src/identity/continuity/envelope.ts +345 -0
- package/src/identity/continuity/history.ts +153 -0
- package/src/identity/continuity/privateEdit.ts +334 -0
- package/src/identity/continuity/publicSkills.ts +173 -0
- package/src/identity/continuity/snapshots.ts +183 -0
- package/src/identity/continuity/storage.ts +507 -0
- package/src/identity/crypto/backupEnvelope.ts +486 -0
- package/src/identity/crypto/eth.ts +137 -0
- package/src/identity/hub/IdentityHub.tsx +868 -0
- package/src/identity/hub/identityHubEffects.ts +1146 -0
- package/src/identity/hub/identityHubModel.ts +291 -0
- package/src/identity/hub/identityHubReducer.ts +212 -0
- package/src/identity/hub/screens/BusyScreen.tsx +26 -0
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +144 -0
- package/src/identity/hub/screens/CreateFlow.tsx +206 -0
- package/src/identity/hub/screens/DetailsScreen.tsx +64 -0
- package/src/identity/hub/screens/EditProfileFlow.tsx +145 -0
- package/src/identity/hub/screens/ErrorScreen.tsx +35 -0
- package/src/identity/hub/screens/IdentitySummary.tsx +70 -0
- package/src/identity/hub/screens/MenuScreen.tsx +117 -0
- package/src/identity/hub/screens/NetworkScreen.tsx +41 -0
- package/src/identity/hub/screens/RebackupStorageScreen.tsx +50 -0
- package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +85 -0
- package/src/identity/hub/screens/RestoreFlow.tsx +206 -0
- package/src/identity/hub/screens/StorageCredentialScreen.tsx +128 -0
- package/src/identity/hub/screens/WalletApprovalScreen.tsx +43 -0
- package/src/identity/profile/imagePicker.ts +180 -0
- package/src/identity/registry/erc8004.ts +1106 -0
- package/src/identity/registry/registryConfig.ts +69 -0
- package/src/identity/storage/ipfs.ts +212 -0
- package/src/identity/storage/pinataJwt.ts +53 -0
- package/src/identity/wallet/browserWallet.ts +393 -0
- package/src/identity/wallet/wallet-page/wallet.html +1082 -0
- package/src/mcp/approvals.ts +113 -0
- package/src/mcp/config.ts +235 -0
- package/src/mcp/manager.ts +541 -0
- package/src/mcp/names.ts +19 -0
- package/src/mcp/output.ts +96 -0
- package/src/models/ModelPicker.tsx +1446 -0
- package/src/models/catalog.ts +296 -0
- package/src/models/huggingface.ts +651 -0
- package/src/models/llamacpp.ts +810 -0
- package/src/models/llamacppPreflight.ts +150 -0
- package/src/models/modelDisplay.ts +105 -0
- package/src/models/modelPickerOptions.ts +421 -0
- package/src/models/modelRecommendation.ts +140 -0
- package/src/models/runtimeDetection.ts +81 -0
- package/src/models/uncensoredCatalog.ts +86 -0
- package/src/providers/anthropic.ts +259 -0
- package/src/providers/contracts.ts +62 -0
- package/src/providers/errors.ts +62 -0
- package/src/providers/gemini.ts +152 -0
- package/src/providers/openai-chat.ts +472 -0
- package/src/providers/registry.ts +42 -0
- package/src/providers/retry.ts +58 -0
- package/src/providers/sse.ts +93 -0
- package/src/runtime/compaction.ts +389 -0
- package/src/runtime/cwd.ts +43 -0
- package/src/runtime/sessionMode.ts +55 -0
- package/src/runtime/systemPrompt.ts +209 -0
- package/src/runtime/toolClaimGuards.ts +143 -0
- package/src/runtime/toolExecution.ts +304 -0
- package/src/runtime/toolIntent.ts +163 -0
- package/src/runtime/turn.ts +858 -0
- package/src/storage/atomicWrite.ts +68 -0
- package/src/storage/config.ts +189 -0
- package/src/storage/factoryReset.ts +130 -0
- package/src/storage/history.ts +58 -0
- package/src/storage/identity.ts +99 -0
- package/src/storage/permissions.ts +76 -0
- package/src/storage/rewind.ts +246 -0
- package/src/storage/secrets.ts +181 -0
- package/src/storage/sessionExport.ts +49 -0
- package/src/storage/sessions.ts +482 -0
- package/src/tools/bashSafety.ts +174 -0
- package/src/tools/bashTool.ts +140 -0
- package/src/tools/changeDirectoryTool.ts +213 -0
- package/src/tools/contracts.ts +179 -0
- package/src/tools/deleteFileTool.ts +111 -0
- package/src/tools/editTool.ts +160 -0
- package/src/tools/editUtils.ts +170 -0
- package/src/tools/listDirectoryTool.ts +55 -0
- package/src/tools/mcpResourceTools.ts +95 -0
- package/src/tools/permissionRules.ts +85 -0
- package/src/tools/privateContinuityEditTool.ts +178 -0
- package/src/tools/privateContinuityReadTool.ts +107 -0
- package/src/tools/readTool.ts +85 -0
- package/src/tools/registry.ts +67 -0
- package/src/tools/writeFileTool.ts +142 -0
- package/src/ui/BrandSplash.tsx +193 -0
- package/src/ui/ProgressBar.tsx +34 -0
- package/src/ui/Select.tsx +143 -0
- package/src/ui/Spinner.tsx +269 -0
- package/src/ui/Surface.tsx +47 -0
- package/src/ui/TextInput.tsx +97 -0
- package/src/ui/theme.ts +59 -0
- package/src/utils/clipboard.ts +216 -0
- package/src/utils/markdownSegments.ts +51 -0
- package/src/utils/messages.ts +35 -0
- package/src/utils/withRetry.ts +280 -0
- package/src/cli.tsx +0 -147
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { getAddress, type Address, type Hex } from 'viem'
|
|
7
|
+
import { recoverAddressFromSignature } from '../crypto/eth.js'
|
|
8
|
+
|
|
9
|
+
const WALLET_PAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'wallet-page')
|
|
10
|
+
const WALLET_HTML = loadWalletHtml()
|
|
11
|
+
|
|
12
|
+
type ReadyHandler = (session: BrowserWalletReady) => void
|
|
13
|
+
|
|
14
|
+
export type BrowserWalletReady = {
|
|
15
|
+
url: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type SignatureRequest = {
|
|
19
|
+
chainId: number
|
|
20
|
+
expectedAccount?: Address
|
|
21
|
+
message?: string
|
|
22
|
+
messageForAccount?: (account: Address) => string
|
|
23
|
+
timeoutMs?: number
|
|
24
|
+
onReady?: ReadyHandler
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type TransactionRequest = {
|
|
28
|
+
chainId: number
|
|
29
|
+
expectedAccount: Address
|
|
30
|
+
to: Address
|
|
31
|
+
data: Hex
|
|
32
|
+
value?: Hex
|
|
33
|
+
timeoutMs?: number
|
|
34
|
+
onReady?: ReadyHandler
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type SignAndTransactionRequest<TPrepared> = {
|
|
38
|
+
chainId: number
|
|
39
|
+
expectedAccount?: Address
|
|
40
|
+
message?: string
|
|
41
|
+
messageForAccount?: (account: Address) => string
|
|
42
|
+
timeoutMs?: number
|
|
43
|
+
onReady?: ReadyHandler
|
|
44
|
+
prepareTransaction: (wallet: BrowserWalletSignature) => Promise<{
|
|
45
|
+
to: Address
|
|
46
|
+
data: Hex
|
|
47
|
+
value?: Hex
|
|
48
|
+
prepared: TPrepared
|
|
49
|
+
}>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type AccountRequest = {
|
|
53
|
+
timeoutMs?: number
|
|
54
|
+
onReady?: ReadyHandler
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type BrowserWalletSignature = {
|
|
58
|
+
account: Address
|
|
59
|
+
message: string
|
|
60
|
+
signature: Hex
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type BrowserWalletTransaction = {
|
|
64
|
+
account: Address
|
|
65
|
+
txHash: Hex
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type BrowserWalletSignAndTransaction<TPrepared> = BrowserWalletSignature & {
|
|
69
|
+
txHash: Hex
|
|
70
|
+
prepared: TPrepared
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type BrowserWalletAccount = {
|
|
74
|
+
account: Address
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function requestBrowserWalletAccount(args: AccountRequest = {}): Promise<BrowserWalletAccount> {
|
|
78
|
+
return await startBrowserWalletServer<BrowserWalletAccount>({
|
|
79
|
+
title: 'ethagent wallet connection',
|
|
80
|
+
timeoutMs: args.timeoutMs,
|
|
81
|
+
onReady: args.onReady,
|
|
82
|
+
payload: {
|
|
83
|
+
kind: 'account',
|
|
84
|
+
},
|
|
85
|
+
complete: body => {
|
|
86
|
+
const account = parseAccount(body.account)
|
|
87
|
+
return { account }
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function requestBrowserWalletSignature(args: SignatureRequest): Promise<BrowserWalletSignature> {
|
|
93
|
+
if (!args.message && !args.messageForAccount) throw new Error('wallet signature request needs a message')
|
|
94
|
+
return await startBrowserWalletServer<BrowserWalletSignature>({
|
|
95
|
+
title: 'ethagent wallet signature',
|
|
96
|
+
timeoutMs: args.timeoutMs,
|
|
97
|
+
onReady: args.onReady,
|
|
98
|
+
payload: {
|
|
99
|
+
kind: 'sign',
|
|
100
|
+
chainIdHex: chainIdHex(args.chainId),
|
|
101
|
+
message: args.message,
|
|
102
|
+
},
|
|
103
|
+
prepare: body => {
|
|
104
|
+
const account = parseAccount(body.account)
|
|
105
|
+
if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
|
|
106
|
+
throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
|
|
107
|
+
}
|
|
108
|
+
const message = args.messageForAccount ? args.messageForAccount(account) : args.message!
|
|
109
|
+
return { message }
|
|
110
|
+
},
|
|
111
|
+
complete: body => {
|
|
112
|
+
const account = parseAccount(body.account)
|
|
113
|
+
const message = typeof body.message === 'string' ? body.message : ''
|
|
114
|
+
const signature = parseHex(body.signature, 'wallet signature')
|
|
115
|
+
if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
|
|
116
|
+
throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
|
|
117
|
+
}
|
|
118
|
+
const recovered = recoverAddressFromSignature(message, signature)
|
|
119
|
+
if (recovered.toLowerCase() !== account.toLowerCase()) {
|
|
120
|
+
throw new Error('wallet signature does not match connected account')
|
|
121
|
+
}
|
|
122
|
+
return { account, message, signature }
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function sendBrowserWalletTransaction(args: TransactionRequest): Promise<BrowserWalletTransaction> {
|
|
128
|
+
return await startBrowserWalletServer<BrowserWalletTransaction>({
|
|
129
|
+
title: 'ethagent wallet transaction',
|
|
130
|
+
timeoutMs: args.timeoutMs,
|
|
131
|
+
onReady: args.onReady,
|
|
132
|
+
payload: {
|
|
133
|
+
kind: 'transaction',
|
|
134
|
+
chainIdHex: chainIdHex(args.chainId),
|
|
135
|
+
expectedAccount: args.expectedAccount,
|
|
136
|
+
tx: {
|
|
137
|
+
to: args.to,
|
|
138
|
+
data: args.data,
|
|
139
|
+
...(args.value ? { value: args.value } : {}),
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
complete: body => {
|
|
143
|
+
const account = parseAccount(body.account)
|
|
144
|
+
if (account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
|
|
145
|
+
throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
|
|
146
|
+
}
|
|
147
|
+
return { account, txHash: parseHex(body.txHash, 'transaction hash') }
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function requestBrowserWalletSignatureAndTransaction<TPrepared>(
|
|
153
|
+
args: SignAndTransactionRequest<TPrepared>,
|
|
154
|
+
): Promise<BrowserWalletSignAndTransaction<TPrepared>> {
|
|
155
|
+
if (!args.message && !args.messageForAccount) throw new Error('wallet signature request needs a message')
|
|
156
|
+
|
|
157
|
+
let prepared:
|
|
158
|
+
| {
|
|
159
|
+
account: Address
|
|
160
|
+
message: string
|
|
161
|
+
signature: Hex
|
|
162
|
+
tx: { to: Address; data: Hex; value?: Hex }
|
|
163
|
+
prepared: TPrepared
|
|
164
|
+
}
|
|
165
|
+
| null = null
|
|
166
|
+
|
|
167
|
+
return await startBrowserWalletServer<BrowserWalletSignAndTransaction<TPrepared>>({
|
|
168
|
+
title: 'ethagent wallet approval',
|
|
169
|
+
timeoutMs: args.timeoutMs,
|
|
170
|
+
onReady: args.onReady,
|
|
171
|
+
payload: {
|
|
172
|
+
kind: 'sign-transaction',
|
|
173
|
+
chainIdHex: chainIdHex(args.chainId),
|
|
174
|
+
message: args.message,
|
|
175
|
+
},
|
|
176
|
+
prepare: body => {
|
|
177
|
+
const account = parseAccount(body.account)
|
|
178
|
+
if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
|
|
179
|
+
throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
|
|
180
|
+
}
|
|
181
|
+
const message = args.messageForAccount ? args.messageForAccount(account) : args.message!
|
|
182
|
+
return { message }
|
|
183
|
+
},
|
|
184
|
+
prepareTransaction: async body => {
|
|
185
|
+
const account = parseAccount(body.account)
|
|
186
|
+
const message = typeof body.message === 'string' ? body.message : ''
|
|
187
|
+
const signature = parseHex(body.signature, 'wallet signature')
|
|
188
|
+
if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
|
|
189
|
+
throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
|
|
190
|
+
}
|
|
191
|
+
const recovered = recoverAddressFromSignature(message, signature)
|
|
192
|
+
if (recovered.toLowerCase() !== account.toLowerCase()) {
|
|
193
|
+
throw new Error('wallet signature does not match connected account')
|
|
194
|
+
}
|
|
195
|
+
const next = await args.prepareTransaction({ account, message, signature })
|
|
196
|
+
prepared = {
|
|
197
|
+
account,
|
|
198
|
+
message,
|
|
199
|
+
signature,
|
|
200
|
+
tx: {
|
|
201
|
+
to: next.to,
|
|
202
|
+
data: next.data,
|
|
203
|
+
...(next.value ? { value: next.value } : {}),
|
|
204
|
+
},
|
|
205
|
+
prepared: next.prepared,
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
tx: prepared.tx,
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
complete: body => {
|
|
212
|
+
if (!prepared) throw new Error('wallet transaction was not prepared')
|
|
213
|
+
const account = parseAccount(body.account)
|
|
214
|
+
if (account.toLowerCase() !== prepared.account.toLowerCase()) {
|
|
215
|
+
throw new Error(`connected wallet ${account} does not match owner ${prepared.account}`)
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
account,
|
|
219
|
+
message: prepared.message,
|
|
220
|
+
signature: prepared.signature,
|
|
221
|
+
txHash: parseHex(body.txHash, 'transaction hash'),
|
|
222
|
+
prepared: prepared.prepared,
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function startBrowserWalletServer<T>(args: {
|
|
229
|
+
title: string
|
|
230
|
+
payload: Record<string, unknown>
|
|
231
|
+
timeoutMs?: number
|
|
232
|
+
onReady?: ReadyHandler
|
|
233
|
+
prepare?: (body: Record<string, unknown>) => Record<string, unknown>
|
|
234
|
+
prepareTransaction?: (body: Record<string, unknown>) => Promise<Record<string, unknown>>
|
|
235
|
+
complete: (body: Record<string, unknown>) => T
|
|
236
|
+
}): Promise<T> {
|
|
237
|
+
const sessionToken = randomUUID()
|
|
238
|
+
const timeoutMs = args.timeoutMs ?? 5 * 60_000
|
|
239
|
+
|
|
240
|
+
return new Promise<T>((resolve, reject) => {
|
|
241
|
+
let settled = false
|
|
242
|
+
const finish = (fn: () => void): void => {
|
|
243
|
+
if (settled) return
|
|
244
|
+
settled = true
|
|
245
|
+
clearTimeout(timer)
|
|
246
|
+
server.close()
|
|
247
|
+
fn()
|
|
248
|
+
}
|
|
249
|
+
const fail = (err: unknown): void => finish(() => reject(err instanceof Error ? err : new Error(String(err))))
|
|
250
|
+
|
|
251
|
+
const server = http.createServer((req, res) => {
|
|
252
|
+
void handleRequest(req, res).catch(err => {
|
|
253
|
+
respondJson(res, 500, { ok: false, error: (err as Error).message })
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const timer = setTimeout(() => {
|
|
258
|
+
fail(new Error('browser wallet request timed out'))
|
|
259
|
+
}, timeoutMs)
|
|
260
|
+
|
|
261
|
+
const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse): Promise<void> => {
|
|
262
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
|
|
263
|
+
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/ethagent')) {
|
|
264
|
+
respondHtml(res, walletPage(args.title, sessionToken, args.payload))
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
if (req.method === 'POST' && (url.pathname === '/prepare' || url.pathname === '/ethagent/prepare')) {
|
|
268
|
+
const body = await readJson(req)
|
|
269
|
+
assertSessionToken(body, sessionToken)
|
|
270
|
+
if (!args.prepare) {
|
|
271
|
+
respondJson(res, 400, { ok: false, error: 'this wallet request does not have a prepare step' })
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
respondJson(res, 200, { ok: true, ...args.prepare(body) })
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
if (req.method === 'POST' && (url.pathname === '/prepare-transaction' || url.pathname === '/ethagent/prepare-transaction')) {
|
|
278
|
+
const body = await readJson(req)
|
|
279
|
+
assertSessionToken(body, sessionToken)
|
|
280
|
+
if (!args.prepareTransaction) {
|
|
281
|
+
respondJson(res, 400, { ok: false, error: 'this wallet request does not prepare transactions' })
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
respondJson(res, 200, { ok: true, ...(await args.prepareTransaction(body)) })
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
if (req.method === 'POST' && (url.pathname === '/complete' || url.pathname === '/ethagent/complete')) {
|
|
288
|
+
const body = await readJson(req)
|
|
289
|
+
assertSessionToken(body, sessionToken)
|
|
290
|
+
const result = args.complete(body)
|
|
291
|
+
respondJson(res, 200, { ok: true })
|
|
292
|
+
finish(() => resolve(result))
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (req.method === 'POST' && (url.pathname === '/cancel' || url.pathname === '/ethagent/cancel')) {
|
|
296
|
+
const body = await readJson(req)
|
|
297
|
+
assertSessionToken(body, sessionToken)
|
|
298
|
+
respondJson(res, 200, { ok: true })
|
|
299
|
+
fail(new Error('browser wallet request was cancelled'))
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
respondJson(res, 404, { ok: false, error: 'wallet session not found' })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
server.once('error', fail)
|
|
306
|
+
server.listen(0, '127.0.0.1', () => {
|
|
307
|
+
const address = server.address()
|
|
308
|
+
if (!address || typeof address === 'string') {
|
|
309
|
+
fail(new Error('could not start browser wallet server'))
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
const url = `http://localhost:${address.port}/`
|
|
313
|
+
args.onReady?.({ url })
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function readJson(req: http.IncomingMessage): Promise<Record<string, unknown>> {
|
|
319
|
+
const chunks: Buffer[] = []
|
|
320
|
+
for await (const chunk of req) {
|
|
321
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
322
|
+
}
|
|
323
|
+
const raw = Buffer.concat(chunks).toString('utf8')
|
|
324
|
+
const parsed = raw ? JSON.parse(raw) as unknown : {}
|
|
325
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('request body must be a JSON object')
|
|
326
|
+
return parsed as Record<string, unknown>
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function respondHtml(res: http.ServerResponse, body: string): void {
|
|
330
|
+
res.writeHead(200, {
|
|
331
|
+
'content-type': 'text/html; charset=utf-8',
|
|
332
|
+
'cache-control': 'no-store',
|
|
333
|
+
})
|
|
334
|
+
res.end(body)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function respondJson(res: http.ServerResponse, status: number, body: Record<string, unknown>): void {
|
|
338
|
+
res.writeHead(status, {
|
|
339
|
+
'content-type': 'application/json; charset=utf-8',
|
|
340
|
+
'cache-control': 'no-store',
|
|
341
|
+
})
|
|
342
|
+
res.end(JSON.stringify(body))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseAccount(value: unknown): Address {
|
|
346
|
+
if (typeof value !== 'string') throw new Error('wallet account is missing')
|
|
347
|
+
return getAddress(value)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function parseHex(value: unknown, label: string): Hex {
|
|
351
|
+
if (typeof value !== 'string' || !/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} is invalid`)
|
|
352
|
+
return value as Hex
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function assertSessionToken(body: Record<string, unknown>, sessionToken: string): void {
|
|
356
|
+
if (body.sessionToken !== sessionToken) throw new Error('wallet session token is invalid')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function chainIdHex(chainId: number): Hex {
|
|
360
|
+
return `0x${chainId.toString(16)}` as Hex
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function loadWalletHtml(): string {
|
|
364
|
+
try {
|
|
365
|
+
return readFileSync(join(WALLET_PAGE_DIR, 'wallet.html'), 'utf8')
|
|
366
|
+
} catch (err) {
|
|
367
|
+
const sourcePath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', 'src', 'identity', 'wallet', 'wallet-page', 'wallet.html')
|
|
368
|
+
try {
|
|
369
|
+
return readFileSync(sourcePath, 'utf8')
|
|
370
|
+
} catch {
|
|
371
|
+
throw err
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function __testWalletPage(title: string, sessionToken: string, payload: Record<string, unknown>): string {
|
|
377
|
+
return walletPage(title, sessionToken, payload)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function walletPage(title: string, sessionToken: string, payload: Record<string, unknown>): string {
|
|
381
|
+
const config = JSON.stringify({ sessionToken, ...payload }).replaceAll('<', '\\u003c')
|
|
382
|
+
const injection = `<script>window.__WALLET_CONFIG__ = ${config};</script>`
|
|
383
|
+
return WALLET_HTML
|
|
384
|
+
.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(title)}</title>`)
|
|
385
|
+
.replace('<head>', `<head>\n ${injection}`)
|
|
386
|
+
}
|
|
387
|
+
function escapeHtml(value: string): string {
|
|
388
|
+
return value
|
|
389
|
+
.replaceAll('&', '&')
|
|
390
|
+
.replaceAll('<', '<')
|
|
391
|
+
.replaceAll('>', '>')
|
|
392
|
+
.replaceAll('"', '"')
|
|
393
|
+
}
|