ethagent 3.0.1 → 3.1.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 +6 -1
- package/package.json +3 -1
- package/src/app/FirstRun.tsx +1 -24
- package/src/app/firstRunConfig.ts +26 -0
- package/src/auth/openaiOAuth/landingPage.ts +2 -11
- package/src/chat/ChatScreen.tsx +32 -117
- package/src/chat/MessageList.tsx +18 -260
- package/src/chat/chatEnvironment.ts +16 -0
- package/src/chat/chatTurnContext.ts +50 -0
- package/src/chat/chatTurnOrchestrator.ts +5 -112
- package/src/chat/chatTurnRows.ts +64 -0
- package/src/chat/commands.ts +3 -178
- package/src/chat/continuityEditReview.ts +42 -0
- package/src/chat/input/ChatInput.tsx +10 -144
- package/src/chat/input/chatInputHelpers.ts +62 -0
- package/src/chat/input/inputRendering.tsx +93 -0
- package/src/chat/messageMarkdown.ts +220 -0
- package/src/chat/messageRows.ts +43 -0
- package/src/chat/planImplementation.ts +62 -0
- package/src/chat/slashCommandHandlers.ts +165 -0
- package/src/chat/slashCommandViews.ts +120 -0
- package/src/cli/main.tsx +7 -0
- package/src/identity/continuity/challenges.ts +123 -0
- package/src/identity/continuity/envelope.ts +49 -1484
- package/src/identity/continuity/envelopeCreate.ts +322 -0
- package/src/identity/continuity/envelopeCrypto.ts +182 -0
- package/src/identity/continuity/envelopeParse.ts +441 -0
- package/src/identity/continuity/envelopeTypes.ts +204 -0
- package/src/identity/continuity/envelopeVersion.ts +1 -0
- package/src/identity/continuity/payloadNormalization.ts +183 -0
- package/src/identity/continuity/publicSkills.ts +5 -5
- package/src/identity/continuity/skills/loadSkills.ts +12 -69
- package/src/identity/continuity/skills/skillPaths.ts +76 -0
- package/src/identity/continuity/skillsNormalization.ts +119 -0
- package/src/identity/continuity/snapshotToken.ts +28 -0
- package/src/identity/hub/continuity/completion.ts +67 -0
- package/src/identity/hub/continuity/effects.ts +5 -62
- package/src/identity/hub/profile/effects.ts +6 -170
- package/src/identity/hub/profile/operatorSave.ts +202 -0
- package/src/identity/registry/erc8004/metadata.ts +31 -23
- package/src/identity/wallet/browserWallet/html.ts +1 -57
- package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
- package/src/identity/wallet/page/controller.ts +1 -1
- package/src/identity/wallet/page/errorView.ts +122 -0
- package/src/identity/wallet/page/view.ts +3 -114
- package/src/mcp/manager.ts +8 -66
- package/src/mcp/managerHelpers.ts +70 -0
- package/src/models/ModelPicker.tsx +69 -889
- package/src/models/huggingface.ts +20 -137
- package/src/models/huggingfaceStorage.ts +136 -0
- package/src/models/llamacpp.ts +37 -303
- package/src/models/llamacppCommands.ts +44 -0
- package/src/models/llamacppConfig.ts +34 -0
- package/src/models/llamacppDiscovery.ts +176 -0
- package/src/models/llamacppOutput.ts +65 -0
- package/src/models/modelPickerCatalogFlow.ts +56 -0
- package/src/models/modelPickerCredentials.ts +166 -0
- package/src/models/modelPickerData.ts +41 -0
- package/src/models/modelPickerDisplay.tsx +132 -0
- package/src/models/modelPickerHfFlow.ts +192 -0
- package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
- package/src/models/modelPickerTypes.ts +69 -0
- package/src/models/modelPickerUninstallFlow.ts +48 -0
- package/src/models/modelPickerViewHelpers.ts +174 -0
- package/src/providers/openai-chat.ts +5 -124
- package/src/providers/openaiChatWire.ts +124 -0
- package/src/runtime/providerTurn.ts +38 -0
- package/src/runtime/textToolParser.ts +161 -0
- package/src/runtime/toolIntent.ts +1 -1
- package/src/runtime/turn.ts +43 -499
- package/src/runtime/turnNudges.ts +223 -0
- package/src/runtime/turnTypes.ts +86 -0
- package/src/ui/terminalTitle.ts +30 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { ml_kem1024 } from '@noble/post-quantum/ml-kem.js'
|
|
3
|
+
import { recoverAddressFromSignature, toChecksumAddress } from '../crypto/eth.js'
|
|
4
|
+
import type { TransferSnapshotMetadata } from '../../storage/config.js'
|
|
5
|
+
import { normalizeContinuitySnapshotToken } from './snapshotToken.js'
|
|
6
|
+
import { CONTINUITY_SNAPSHOT_ENVELOPE_VERSION } from './envelopeVersion.js'
|
|
7
|
+
import {
|
|
8
|
+
continuityAadFor,
|
|
9
|
+
deriveContinuityAesKey,
|
|
10
|
+
deriveContinuityKemSeed,
|
|
11
|
+
deriveTransferSlotKey,
|
|
12
|
+
deriveWalletRestoreKemSeed,
|
|
13
|
+
deriveWalletSlotAesKey,
|
|
14
|
+
fromBase64,
|
|
15
|
+
sha256Hex,
|
|
16
|
+
toBase64,
|
|
17
|
+
transferPayloadAadFor,
|
|
18
|
+
transferSlotAadFor,
|
|
19
|
+
walletPayloadAadFor,
|
|
20
|
+
walletSlotAadFor,
|
|
21
|
+
} from './envelopeCrypto.js'
|
|
22
|
+
import {
|
|
23
|
+
normalizeContinuityPayload,
|
|
24
|
+
normalizeTransferSlot,
|
|
25
|
+
normalizeWalletSlot,
|
|
26
|
+
} from './payloadNormalization.js'
|
|
27
|
+
import {
|
|
28
|
+
ContinuitySnapshotOwnerMismatchError,
|
|
29
|
+
ContinuitySnapshotRestoreSlotMissingError,
|
|
30
|
+
ContinuityTransferSnapshotTargetMismatchError,
|
|
31
|
+
type ContinuitySnapshotEnvelope,
|
|
32
|
+
type ContinuitySnapshotPayload,
|
|
33
|
+
type TransferContinuitySnapshotEnvelope,
|
|
34
|
+
type TransferContinuitySnapshotSlot,
|
|
35
|
+
type WalletContinuitySnapshotEnvelope,
|
|
36
|
+
type WalletContinuitySnapshotSlot,
|
|
37
|
+
} from './envelopeTypes.js'
|
|
38
|
+
|
|
39
|
+
export function restoreContinuitySnapshotEnvelope(args: {
|
|
40
|
+
envelope: ContinuitySnapshotEnvelope
|
|
41
|
+
walletSignature: string
|
|
42
|
+
currentOwnerAddress?: string
|
|
43
|
+
}): ContinuitySnapshotPayload {
|
|
44
|
+
const envelope = normalizeContinuitySnapshotEnvelope(args.envelope)
|
|
45
|
+
if (isWalletContinuitySnapshotEnvelope(envelope)) {
|
|
46
|
+
return restoreWalletContinuitySnapshotEnvelope({
|
|
47
|
+
envelope,
|
|
48
|
+
walletSignature: args.walletSignature,
|
|
49
|
+
currentOwnerAddress: args.currentOwnerAddress,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
if (isTransferContinuitySnapshotEnvelope(envelope)) {
|
|
53
|
+
return restoreTransferContinuitySnapshotEnvelope({
|
|
54
|
+
envelope,
|
|
55
|
+
walletSignature: args.walletSignature,
|
|
56
|
+
currentOwnerAddress: args.currentOwnerAddress,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
assertSignatureForAddress(envelope.challenge, args.walletSignature, envelope.ownerAddress)
|
|
61
|
+
|
|
62
|
+
const salt = fromBase64(envelope.salt)
|
|
63
|
+
const kemSeed = deriveContinuityKemSeed(args.walletSignature, salt, envelope.ownerAddress)
|
|
64
|
+
const kemKeys = ml_kem1024.keygen(kemSeed)
|
|
65
|
+
const expectedPublicKey = toBase64(kemKeys.publicKey)
|
|
66
|
+
if (expectedPublicKey !== envelope.kemPublicKey) {
|
|
67
|
+
throw new Error('Wallet signature does not match this continuity snapshot')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sharedSecret = ml_kem1024.decapsulate(fromBase64(envelope.kemCiphertext), kemKeys.secretKey)
|
|
71
|
+
const key = deriveContinuityAesKey(args.walletSignature, sharedSecret, salt, envelope.ownerAddress)
|
|
72
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, fromBase64(envelope.nonce))
|
|
73
|
+
decipher.setAAD(continuityAadFor(envelope.ownerAddress, envelope.createdAt))
|
|
74
|
+
decipher.setAuthTag(fromBase64(envelope.tag))
|
|
75
|
+
|
|
76
|
+
let decoded: unknown
|
|
77
|
+
try {
|
|
78
|
+
const plaintext = Buffer.concat([
|
|
79
|
+
decipher.update(fromBase64(envelope.ciphertext)),
|
|
80
|
+
decipher.final(),
|
|
81
|
+
]).toString('utf8')
|
|
82
|
+
decoded = JSON.parse(plaintext)
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error('Could not decrypt continuity snapshot with the supplied wallet signature')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const payload = normalizeContinuityPayload(decoded)
|
|
88
|
+
assertPayloadMatchesEnvelope(payload, envelope.ownerAddress, envelope.createdAt)
|
|
89
|
+
return payload
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function assertContinuitySnapshotOwner(envelope: ContinuitySnapshotEnvelope, currentOwner: string): void {
|
|
93
|
+
const normalized = normalizeContinuitySnapshotEnvelope(envelope)
|
|
94
|
+
const owner = toChecksumAddress(currentOwner)
|
|
95
|
+
if (isWalletContinuitySnapshotEnvelope(normalized)) {
|
|
96
|
+
const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
|
|
97
|
+
if (snapshotOwner.toLowerCase() !== owner.toLowerCase()) {
|
|
98
|
+
throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, owner)
|
|
99
|
+
}
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (isTransferContinuitySnapshotEnvelope(normalized)) {
|
|
103
|
+
const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
|
|
104
|
+
const targetOwner = toChecksumAddress(normalized.targetAddress)
|
|
105
|
+
if (
|
|
106
|
+
owner.toLowerCase() !== snapshotOwner.toLowerCase()
|
|
107
|
+
&& owner.toLowerCase() !== targetOwner.toLowerCase()
|
|
108
|
+
) {
|
|
109
|
+
throw new ContinuityTransferSnapshotTargetMismatchError(snapshotOwner, targetOwner, owner)
|
|
110
|
+
}
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
|
|
114
|
+
if (snapshotOwner.toLowerCase() !== owner.toLowerCase()) {
|
|
115
|
+
throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, owner)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function serializeContinuitySnapshotEnvelope(envelope: ContinuitySnapshotEnvelope): string {
|
|
120
|
+
return JSON.stringify(normalizeContinuitySnapshotEnvelope(envelope), null, 2)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parseContinuitySnapshotEnvelope(raw: string | Uint8Array): ContinuitySnapshotEnvelope {
|
|
124
|
+
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw)
|
|
125
|
+
const parsed = JSON.parse(text) as unknown
|
|
126
|
+
return normalizeContinuitySnapshotEnvelope(parsed)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function transferSnapshotMetadataFromEnvelope(
|
|
130
|
+
envelope: ContinuitySnapshotEnvelope,
|
|
131
|
+
): TransferSnapshotMetadata | null {
|
|
132
|
+
const normalized = normalizeContinuitySnapshotEnvelope(envelope)
|
|
133
|
+
if (!isTransferContinuitySnapshotEnvelope(normalized)) return null
|
|
134
|
+
const slotCount = [normalized.slots.owner, normalized.slots.target]
|
|
135
|
+
.filter(slot => Boolean(slot?.encryptedKey && slot.address))
|
|
136
|
+
.length
|
|
137
|
+
if (slotCount < 2) return null
|
|
138
|
+
return {
|
|
139
|
+
kind: 'dual-wallet',
|
|
140
|
+
senderAddress: normalized.ownerAddress,
|
|
141
|
+
receiverAddress: normalized.targetAddress,
|
|
142
|
+
...(normalized.targetHandle ? { receiverHandle: normalized.targetHandle } : {}),
|
|
143
|
+
slotCount,
|
|
144
|
+
createdAt: normalized.createdAt,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function walletContinuitySnapshotSlotForAddress(
|
|
149
|
+
envelope: ContinuitySnapshotEnvelope,
|
|
150
|
+
address: string,
|
|
151
|
+
): WalletContinuitySnapshotSlot | null {
|
|
152
|
+
const normalized = normalizeContinuitySnapshotEnvelope(envelope)
|
|
153
|
+
if (!isWalletContinuitySnapshotEnvelope(normalized)) return null
|
|
154
|
+
const checksum = toChecksumAddress(address)
|
|
155
|
+
return normalized.slots.find(slot => slot.address.toLowerCase() === checksum.toLowerCase()) ?? null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function findRestorableAddressForSnapshot(
|
|
159
|
+
envelope: ContinuitySnapshotEnvelope,
|
|
160
|
+
candidates: ReadonlyArray<string>,
|
|
161
|
+
): string | null {
|
|
162
|
+
const normalized = normalizeContinuitySnapshotEnvelope(envelope)
|
|
163
|
+
const seen = new Set<string>()
|
|
164
|
+
const lowerCandidates: string[] = []
|
|
165
|
+
for (const candidate of candidates) {
|
|
166
|
+
if (!candidate) continue
|
|
167
|
+
const lower = candidate.toLowerCase()
|
|
168
|
+
if (seen.has(lower)) continue
|
|
169
|
+
seen.add(lower)
|
|
170
|
+
lowerCandidates.push(lower)
|
|
171
|
+
}
|
|
172
|
+
if (lowerCandidates.length === 0) return null
|
|
173
|
+
if (isWalletContinuitySnapshotEnvelope(normalized)) {
|
|
174
|
+
for (const slot of normalized.slots) {
|
|
175
|
+
if (lowerCandidates.includes(slot.address.toLowerCase())) return toChecksumAddress(slot.address)
|
|
176
|
+
}
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
if (isTransferContinuitySnapshotEnvelope(normalized)) {
|
|
180
|
+
const ownerLower = normalized.ownerAddress.toLowerCase()
|
|
181
|
+
const targetLower = normalized.targetAddress.toLowerCase()
|
|
182
|
+
if (lowerCandidates.includes(ownerLower)) return toChecksumAddress(normalized.ownerAddress)
|
|
183
|
+
if (lowerCandidates.includes(targetLower)) return toChecksumAddress(normalized.targetAddress)
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
const ownerLower = normalized.ownerAddress.toLowerCase()
|
|
187
|
+
return lowerCandidates.includes(ownerLower) ? toChecksumAddress(normalized.ownerAddress) : null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function isWalletContinuitySnapshotEnvelope(input: unknown): input is WalletContinuitySnapshotEnvelope {
|
|
191
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return false
|
|
192
|
+
const obj = input as Partial<WalletContinuitySnapshotEnvelope> & { crypto?: Partial<WalletContinuitySnapshotEnvelope['crypto']> }
|
|
193
|
+
return obj.version === 1
|
|
194
|
+
&& obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
195
|
+
&& typeof obj.ownerAddress === 'string'
|
|
196
|
+
&& typeof obj.createdAt === 'string'
|
|
197
|
+
&& !!obj.token
|
|
198
|
+
&& typeof obj.accessEpoch === 'number'
|
|
199
|
+
&& typeof obj.accessManifestHash === 'string'
|
|
200
|
+
&& obj.crypto?.decryptsWith === 'wallet-signature-slots'
|
|
201
|
+
&& typeof obj.payloadNonce === 'string'
|
|
202
|
+
&& typeof obj.payloadCiphertext === 'string'
|
|
203
|
+
&& typeof obj.payloadTag === 'string'
|
|
204
|
+
&& typeof obj.payloadHash === 'string'
|
|
205
|
+
&& Array.isArray(obj.slots)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function isTransferContinuitySnapshotEnvelope(input: unknown): input is TransferContinuitySnapshotEnvelope {
|
|
209
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return false
|
|
210
|
+
const obj = input as Partial<TransferContinuitySnapshotEnvelope> & { crypto?: Partial<TransferContinuitySnapshotEnvelope['crypto']> }
|
|
211
|
+
return obj.version === 1
|
|
212
|
+
&& obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
213
|
+
&& typeof obj.ownerAddress === 'string'
|
|
214
|
+
&& typeof obj.createdAt === 'string'
|
|
215
|
+
&& typeof obj.challenge === 'string'
|
|
216
|
+
&& !!obj.token
|
|
217
|
+
&& typeof obj.targetAddress === 'string'
|
|
218
|
+
&& obj.crypto?.decryptsWith === 'transfer-signature-slot'
|
|
219
|
+
&& typeof obj.payloadNonce === 'string'
|
|
220
|
+
&& typeof obj.payloadCiphertext === 'string'
|
|
221
|
+
&& typeof obj.payloadTag === 'string'
|
|
222
|
+
&& typeof obj.payloadHash === 'string'
|
|
223
|
+
&& !!obj.slots
|
|
224
|
+
&& !!obj.slots.owner
|
|
225
|
+
&& !!obj.slots.target
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function restoreTransferContinuitySnapshotEnvelope(args: {
|
|
229
|
+
envelope: TransferContinuitySnapshotEnvelope
|
|
230
|
+
walletSignature: string
|
|
231
|
+
currentOwnerAddress?: string
|
|
232
|
+
}): ContinuitySnapshotPayload {
|
|
233
|
+
const currentAddress = args.currentOwnerAddress
|
|
234
|
+
? toChecksumAddress(args.currentOwnerAddress)
|
|
235
|
+
: toChecksumAddress(recoverAddressFromSignature(args.envelope.challenge, args.walletSignature))
|
|
236
|
+
const slot = transferSlotForCurrentOwner(args.envelope, currentAddress)
|
|
237
|
+
assertSignatureForAddress(slot.challenge, args.walletSignature, slot.address)
|
|
238
|
+
|
|
239
|
+
let contentKey: Buffer
|
|
240
|
+
try {
|
|
241
|
+
const payloadAad = transferPayloadAadFor(args.envelope)
|
|
242
|
+
const slotKey = deriveTransferSlotKey(args.walletSignature, fromBase64(slot.salt), slot.address, slot.challenge)
|
|
243
|
+
const keyDecipher = crypto.createDecipheriv('aes-256-gcm', slotKey, fromBase64(slot.nonce))
|
|
244
|
+
keyDecipher.setAAD(transferSlotAadFor(slot, payloadAad))
|
|
245
|
+
keyDecipher.setAuthTag(fromBase64(slot.tag))
|
|
246
|
+
contentKey = Buffer.concat([
|
|
247
|
+
keyDecipher.update(fromBase64(slot.encryptedKey)),
|
|
248
|
+
keyDecipher.final(),
|
|
249
|
+
])
|
|
250
|
+
} catch {
|
|
251
|
+
throw new Error('Could not decrypt transfer snapshot key with the supplied wallet signature')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let decoded: unknown
|
|
255
|
+
try {
|
|
256
|
+
const payloadAad = transferPayloadAadFor(args.envelope)
|
|
257
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', contentKey, fromBase64(args.envelope.payloadNonce))
|
|
258
|
+
decipher.setAAD(payloadAad)
|
|
259
|
+
decipher.setAuthTag(fromBase64(args.envelope.payloadTag))
|
|
260
|
+
const plaintext = Buffer.concat([
|
|
261
|
+
decipher.update(fromBase64(args.envelope.payloadCiphertext)),
|
|
262
|
+
decipher.final(),
|
|
263
|
+
])
|
|
264
|
+
if (sha256Hex(plaintext) !== args.envelope.payloadHash) {
|
|
265
|
+
throw new Error('Transfer snapshot payload hash mismatch')
|
|
266
|
+
}
|
|
267
|
+
decoded = JSON.parse(plaintext.toString('utf8')) as unknown
|
|
268
|
+
} catch {
|
|
269
|
+
throw new Error('Could not decrypt continuity transfer snapshot with the supplied wallet signature')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const payload = normalizeContinuityPayload(decoded)
|
|
273
|
+
assertPayloadMatchesEnvelope(payload, args.envelope.ownerAddress, args.envelope.createdAt)
|
|
274
|
+
return payload
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function restoreWalletContinuitySnapshotEnvelope(args: {
|
|
278
|
+
envelope: WalletContinuitySnapshotEnvelope
|
|
279
|
+
walletSignature: string
|
|
280
|
+
currentOwnerAddress?: string
|
|
281
|
+
}): ContinuitySnapshotPayload {
|
|
282
|
+
const slot = walletSlotForRestore(args.envelope, args.walletSignature, args.currentOwnerAddress)
|
|
283
|
+
assertSignatureForAddress(slot.challenge, args.walletSignature, slot.address)
|
|
284
|
+
|
|
285
|
+
let contentKey: Buffer
|
|
286
|
+
try {
|
|
287
|
+
const salt = fromBase64(slot.salt)
|
|
288
|
+
const kemSeed = deriveWalletRestoreKemSeed(args.walletSignature, salt, slot.address, slot.challenge)
|
|
289
|
+
const kemKeys = ml_kem1024.keygen(kemSeed)
|
|
290
|
+
if (toBase64(kemKeys.publicKey) !== slot.kemPublicKey) {
|
|
291
|
+
throw new Error('Wallet restore key mismatch')
|
|
292
|
+
}
|
|
293
|
+
const sharedSecret = ml_kem1024.decapsulate(fromBase64(slot.kemCiphertext), kemKeys.secretKey)
|
|
294
|
+
const slotKey = deriveWalletSlotAesKey(sharedSecret, salt, slot.address, slot.challenge, args.envelope.accessManifestHash)
|
|
295
|
+
const keyDecipher = crypto.createDecipheriv('aes-256-gcm', slotKey, fromBase64(slot.nonce))
|
|
296
|
+
const payloadAad = walletPayloadAadFor(args.envelope)
|
|
297
|
+
keyDecipher.setAAD(walletSlotAadFor(slot, payloadAad))
|
|
298
|
+
keyDecipher.setAuthTag(fromBase64(slot.tag))
|
|
299
|
+
contentKey = Buffer.concat([
|
|
300
|
+
keyDecipher.update(fromBase64(slot.encryptedKey)),
|
|
301
|
+
keyDecipher.final(),
|
|
302
|
+
])
|
|
303
|
+
} catch {
|
|
304
|
+
throw new Error('Could not decrypt wallet restore snapshot key with the supplied wallet signature')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let decoded: unknown
|
|
308
|
+
try {
|
|
309
|
+
const payloadAad = walletPayloadAadFor(args.envelope)
|
|
310
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', contentKey, fromBase64(args.envelope.payloadNonce))
|
|
311
|
+
decipher.setAAD(payloadAad)
|
|
312
|
+
decipher.setAuthTag(fromBase64(args.envelope.payloadTag))
|
|
313
|
+
const plaintext = Buffer.concat([
|
|
314
|
+
decipher.update(fromBase64(args.envelope.payloadCiphertext)),
|
|
315
|
+
decipher.final(),
|
|
316
|
+
])
|
|
317
|
+
if (sha256Hex(plaintext) !== args.envelope.payloadHash) {
|
|
318
|
+
throw new Error('Wallet restore snapshot payload hash mismatch')
|
|
319
|
+
}
|
|
320
|
+
decoded = JSON.parse(plaintext.toString('utf8')) as unknown
|
|
321
|
+
} catch {
|
|
322
|
+
throw new Error('Could not decrypt wallet restore snapshot with the supplied wallet signature')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const payload = normalizeContinuityPayload(decoded)
|
|
326
|
+
assertPayloadMatchesEnvelope(payload, args.envelope.ownerAddress, args.envelope.createdAt)
|
|
327
|
+
return payload
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function walletSlotForRestore(
|
|
331
|
+
envelope: WalletContinuitySnapshotEnvelope,
|
|
332
|
+
walletSignature: string,
|
|
333
|
+
currentOwnerAddress?: string,
|
|
334
|
+
): WalletContinuitySnapshotSlot {
|
|
335
|
+
if (currentOwnerAddress) {
|
|
336
|
+
const address = toChecksumAddress(currentOwnerAddress)
|
|
337
|
+
const slot = envelope.slots.find(item => item.address.toLowerCase() === address.toLowerCase())
|
|
338
|
+
if (!slot) throw new ContinuitySnapshotRestoreSlotMissingError(address)
|
|
339
|
+
return slot
|
|
340
|
+
}
|
|
341
|
+
for (const slot of envelope.slots) {
|
|
342
|
+
try {
|
|
343
|
+
const recovered = recoverAddressFromSignature(slot.challenge, walletSignature)
|
|
344
|
+
if (recovered.toLowerCase() === slot.address.toLowerCase()) return slot
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
throw new ContinuitySnapshotRestoreSlotMissingError('unknown')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function transferSlotForCurrentOwner(
|
|
352
|
+
envelope: TransferContinuitySnapshotEnvelope,
|
|
353
|
+
currentOwner: string,
|
|
354
|
+
): TransferContinuitySnapshotSlot {
|
|
355
|
+
if (currentOwner.toLowerCase() === envelope.ownerAddress.toLowerCase()) return envelope.slots.owner
|
|
356
|
+
if (currentOwner.toLowerCase() === envelope.targetAddress.toLowerCase()) return envelope.slots.target
|
|
357
|
+
throw new ContinuityTransferSnapshotTargetMismatchError(envelope.ownerAddress, envelope.targetAddress, currentOwner)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function normalizeContinuitySnapshotEnvelope(input: unknown): ContinuitySnapshotEnvelope {
|
|
361
|
+
if (!isContinuitySnapshotEnvelope(input)) throw new Error('Invalid continuity snapshot envelope')
|
|
362
|
+
if (input.envelopeVersion !== CONTINUITY_SNAPSHOT_ENVELOPE_VERSION) {
|
|
363
|
+
throw new Error('Unsupported continuity snapshot envelope version')
|
|
364
|
+
}
|
|
365
|
+
if (isWalletContinuitySnapshotEnvelope(input)) {
|
|
366
|
+
if (input.crypto.kem !== 'ML-KEM-1024' || input.crypto.aead !== 'AES-256-GCM' || input.crypto.decryptsWith !== 'wallet-signature-slots') {
|
|
367
|
+
throw new Error('Unsupported continuity snapshot crypto suite')
|
|
368
|
+
}
|
|
369
|
+
const ownerAddress = toChecksumAddress(input.ownerAddress)
|
|
370
|
+
const token = normalizeContinuitySnapshotToken(input.token)
|
|
371
|
+
const slots = input.slots.map(normalizeWalletSlot)
|
|
372
|
+
if (slots.length === 0) throw new Error('Continuity wallet snapshot needs at least one slot')
|
|
373
|
+
return {
|
|
374
|
+
...input,
|
|
375
|
+
ownerAddress,
|
|
376
|
+
token,
|
|
377
|
+
slots,
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (isTransferContinuitySnapshotEnvelope(input)) {
|
|
381
|
+
if (input.crypto.aead !== 'AES-256-GCM' || input.crypto.decryptsWith !== 'transfer-signature-slot') {
|
|
382
|
+
throw new Error('Unsupported continuity snapshot crypto suite')
|
|
383
|
+
}
|
|
384
|
+
const ownerAddress = toChecksumAddress(input.ownerAddress)
|
|
385
|
+
const targetAddress = toChecksumAddress(input.targetAddress)
|
|
386
|
+
return {
|
|
387
|
+
...input,
|
|
388
|
+
ownerAddress,
|
|
389
|
+
targetAddress,
|
|
390
|
+
token: normalizeContinuitySnapshotToken(input.token),
|
|
391
|
+
slots: {
|
|
392
|
+
owner: normalizeTransferSlot(input.slots.owner, ownerAddress),
|
|
393
|
+
target: normalizeTransferSlot(input.slots.target, targetAddress),
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (input.crypto.kem !== 'ML-KEM-1024' || input.crypto.aead !== 'AES-256-GCM') {
|
|
398
|
+
throw new Error('Unsupported continuity snapshot crypto suite')
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
...input,
|
|
402
|
+
ownerAddress: toChecksumAddress(input.ownerAddress),
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isContinuitySnapshotEnvelope(input: unknown): input is ContinuitySnapshotEnvelope {
|
|
407
|
+
if (!input || typeof input !== 'object') return false
|
|
408
|
+
const obj = input as Record<string, unknown> & { walletSignature?: unknown; crypto?: unknown }
|
|
409
|
+
const base = obj.version === 1
|
|
410
|
+
&& obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
411
|
+
&& typeof obj.ownerAddress === 'string'
|
|
412
|
+
&& typeof obj.createdAt === 'string'
|
|
413
|
+
&& obj.walletSignature === undefined
|
|
414
|
+
&& !!obj.crypto
|
|
415
|
+
if (!base) return false
|
|
416
|
+
if (isWalletContinuitySnapshotEnvelope(input)) return true
|
|
417
|
+
if (isTransferContinuitySnapshotEnvelope(input)) return true
|
|
418
|
+
return typeof obj.challenge === 'string'
|
|
419
|
+
&& typeof obj.salt === 'string'
|
|
420
|
+
&& typeof obj.kemPublicKey === 'string'
|
|
421
|
+
&& typeof obj.kemCiphertext === 'string'
|
|
422
|
+
&& typeof obj.nonce === 'string'
|
|
423
|
+
&& typeof obj.ciphertext === 'string'
|
|
424
|
+
&& typeof obj.tag === 'string'
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function assertPayloadMatchesEnvelope(payload: ContinuitySnapshotPayload, ownerAddress: string, createdAt: string): void {
|
|
428
|
+
if (payload.ownerAddress.toLowerCase() !== ownerAddress.toLowerCase()) {
|
|
429
|
+
throw new Error('Continuity snapshot owner mismatch')
|
|
430
|
+
}
|
|
431
|
+
if (payload.createdAt !== createdAt) {
|
|
432
|
+
throw new Error('Continuity snapshot timestamp mismatch')
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function assertSignatureForAddress(challenge: string, signature: string, address: string): void {
|
|
437
|
+
const recovered = recoverAddressFromSignature(challenge, signature)
|
|
438
|
+
if (recovered.toLowerCase() !== address.toLowerCase()) {
|
|
439
|
+
throw new Error('Wallet signature does not match continuity snapshot owner')
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { ContinuitySnapshotToken } from './snapshotToken.js'
|
|
2
|
+
import { CONTINUITY_SNAPSHOT_ENVELOPE_VERSION } from './envelopeVersion.js'
|
|
3
|
+
|
|
4
|
+
export type ContinuityFiles = {
|
|
5
|
+
'SOUL.md': string
|
|
6
|
+
'MEMORY.md': string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ContinuitySkillsTree = Record<string, string>
|
|
10
|
+
|
|
11
|
+
export type ContinuityTranscriptSummary = {
|
|
12
|
+
sessionId?: string
|
|
13
|
+
createdAt?: string
|
|
14
|
+
summary: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ContinuityAgentSnapshot = {
|
|
18
|
+
chainId?: number
|
|
19
|
+
identityRegistryAddress?: string
|
|
20
|
+
agentId?: string
|
|
21
|
+
agentUri?: string
|
|
22
|
+
metadataCid?: string
|
|
23
|
+
name?: string
|
|
24
|
+
description?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ContinuitySnapshotPayload = {
|
|
28
|
+
version: 1
|
|
29
|
+
ownerAddress: string
|
|
30
|
+
createdAt: string
|
|
31
|
+
sequence?: number
|
|
32
|
+
agent: ContinuityAgentSnapshot
|
|
33
|
+
files: ContinuityFiles
|
|
34
|
+
skills?: ContinuitySkillsTree
|
|
35
|
+
transcript: ContinuityTranscriptSummary[]
|
|
36
|
+
state: Record<string, unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type TransferContinuitySnapshotSlot = {
|
|
40
|
+
address: string
|
|
41
|
+
challenge: string
|
|
42
|
+
salt: string
|
|
43
|
+
nonce: string
|
|
44
|
+
encryptedKey: string
|
|
45
|
+
tag: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type WalletContinuityRestoreAccessKey = {
|
|
49
|
+
address: string
|
|
50
|
+
challenge: string
|
|
51
|
+
salt: string
|
|
52
|
+
kemPublicKey: string
|
|
53
|
+
createdAt?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type WalletContinuitySnapshotSlot = {
|
|
57
|
+
address: string
|
|
58
|
+
challenge: string
|
|
59
|
+
salt: string
|
|
60
|
+
kemPublicKey: string
|
|
61
|
+
kemCiphertext: string
|
|
62
|
+
nonce: string
|
|
63
|
+
encryptedKey: string
|
|
64
|
+
tag: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SignatureContinuitySnapshotEnvelope = {
|
|
68
|
+
version: 1
|
|
69
|
+
envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
70
|
+
ownerAddress: string
|
|
71
|
+
createdAt: string
|
|
72
|
+
challenge: string
|
|
73
|
+
crypto: {
|
|
74
|
+
kem: 'ML-KEM-1024'
|
|
75
|
+
aead: 'AES-256-GCM'
|
|
76
|
+
kdf: 'HKDF-SHA256'
|
|
77
|
+
signature: 'EIP-191'
|
|
78
|
+
decryptsWith?: 'owner-signature'
|
|
79
|
+
}
|
|
80
|
+
salt: string
|
|
81
|
+
kemPublicKey: string
|
|
82
|
+
kemCiphertext: string
|
|
83
|
+
nonce: string
|
|
84
|
+
ciphertext: string
|
|
85
|
+
tag: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type WalletContinuitySnapshotEnvelope = {
|
|
89
|
+
version: 1
|
|
90
|
+
envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
91
|
+
ownerAddress: string
|
|
92
|
+
createdAt: string
|
|
93
|
+
token: ContinuitySnapshotToken
|
|
94
|
+
accessEpoch: number
|
|
95
|
+
accessManifestHash: string
|
|
96
|
+
crypto: {
|
|
97
|
+
kem: 'ML-KEM-1024'
|
|
98
|
+
aead: 'AES-256-GCM'
|
|
99
|
+
kdf: 'HKDF-SHA256'
|
|
100
|
+
signature: 'EIP-191'
|
|
101
|
+
decryptsWith: 'wallet-signature-slots'
|
|
102
|
+
}
|
|
103
|
+
payloadNonce: string
|
|
104
|
+
payloadCiphertext: string
|
|
105
|
+
payloadTag: string
|
|
106
|
+
payloadHash: string
|
|
107
|
+
slots: WalletContinuitySnapshotSlot[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type TransferContinuitySnapshotEnvelope = {
|
|
111
|
+
version: 1
|
|
112
|
+
envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
|
|
113
|
+
ownerAddress: string
|
|
114
|
+
createdAt: string
|
|
115
|
+
challenge: string
|
|
116
|
+
token: ContinuitySnapshotToken
|
|
117
|
+
targetAddress: string
|
|
118
|
+
targetHandle?: string
|
|
119
|
+
crypto: {
|
|
120
|
+
aead: 'AES-256-GCM'
|
|
121
|
+
kdf: 'HKDF-SHA256'
|
|
122
|
+
signature: 'EIP-191'
|
|
123
|
+
decryptsWith: 'transfer-signature-slot'
|
|
124
|
+
}
|
|
125
|
+
payloadNonce: string
|
|
126
|
+
payloadCiphertext: string
|
|
127
|
+
payloadTag: string
|
|
128
|
+
payloadHash: string
|
|
129
|
+
slots: {
|
|
130
|
+
owner: TransferContinuitySnapshotSlot
|
|
131
|
+
target: TransferContinuitySnapshotSlot
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ContinuitySnapshotEnvelope =
|
|
136
|
+
| SignatureContinuitySnapshotEnvelope
|
|
137
|
+
| WalletContinuitySnapshotEnvelope
|
|
138
|
+
| TransferContinuitySnapshotEnvelope
|
|
139
|
+
|
|
140
|
+
export type CreateContinuitySnapshotEnvelopeArgs = {
|
|
141
|
+
ownerAddress: string
|
|
142
|
+
walletSignature: string
|
|
143
|
+
payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
|
|
144
|
+
createdAt?: string
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type CreateTransferContinuitySnapshotEnvelopeArgs = {
|
|
149
|
+
ownerAddress: string
|
|
150
|
+
ownerWalletSignature: string
|
|
151
|
+
targetAddress: string
|
|
152
|
+
targetWalletSignature: string
|
|
153
|
+
targetHandle?: string
|
|
154
|
+
token: ContinuitySnapshotToken
|
|
155
|
+
payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
|
|
156
|
+
createdAt?: string
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type CreateWalletContinuitySnapshotEnvelopeArgs = {
|
|
161
|
+
ownerAddress: string
|
|
162
|
+
token: ContinuitySnapshotToken
|
|
163
|
+
signerAddress: string
|
|
164
|
+
signerWalletSignature: string
|
|
165
|
+
accessKeys: WalletContinuityRestoreAccessKey[]
|
|
166
|
+
accessEpoch?: number
|
|
167
|
+
payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
|
|
168
|
+
createdAt?: string
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type RestoreContinuitySnapshotEnvelopeArgs = {
|
|
173
|
+
envelope: ContinuitySnapshotEnvelope
|
|
174
|
+
walletSignature: string
|
|
175
|
+
currentOwnerAddress?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export class ContinuitySnapshotOwnerMismatchError extends Error {
|
|
179
|
+
constructor(
|
|
180
|
+
readonly snapshotOwner: string,
|
|
181
|
+
readonly currentOwner: string,
|
|
182
|
+
) {
|
|
183
|
+
super('Continuity snapshot is encrypted for another wallet')
|
|
184
|
+
this.name = 'ContinuitySnapshotOwnerMismatchError'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export class ContinuityTransferSnapshotTargetMismatchError extends Error {
|
|
189
|
+
constructor(
|
|
190
|
+
readonly snapshotOwner: string,
|
|
191
|
+
readonly targetOwner: string,
|
|
192
|
+
readonly currentOwner: string,
|
|
193
|
+
) {
|
|
194
|
+
super('Transfer snapshot receiver does not match the current token owner')
|
|
195
|
+
this.name = 'ContinuityTransferSnapshotTargetMismatchError'
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export class ContinuitySnapshotRestoreSlotMissingError extends Error {
|
|
200
|
+
constructor(readonly walletAddress: string) {
|
|
201
|
+
super('Restore slot missing')
|
|
202
|
+
this.name = 'ContinuitySnapshotRestoreSlotMissingError'
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CONTINUITY_SNAPSHOT_ENVELOPE_VERSION = 'ethagent-continuity-snapshot-v1'
|