ethagent 3.0.2 → 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.
Files changed (69) hide show
  1. package/README.md +6 -1
  2. package/package.json +3 -1
  3. package/src/app/FirstRun.tsx +1 -24
  4. package/src/app/firstRunConfig.ts +26 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +2 -11
  6. package/src/chat/ChatScreen.tsx +15 -116
  7. package/src/chat/MessageList.tsx +18 -260
  8. package/src/chat/chatEnvironment.ts +16 -0
  9. package/src/chat/chatTurnContext.ts +50 -0
  10. package/src/chat/chatTurnOrchestrator.ts +5 -112
  11. package/src/chat/chatTurnRows.ts +64 -0
  12. package/src/chat/commands.ts +3 -178
  13. package/src/chat/continuityEditReview.ts +42 -0
  14. package/src/chat/input/ChatInput.tsx +10 -144
  15. package/src/chat/input/chatInputHelpers.ts +62 -0
  16. package/src/chat/input/inputRendering.tsx +93 -0
  17. package/src/chat/messageMarkdown.ts +220 -0
  18. package/src/chat/messageRows.ts +43 -0
  19. package/src/chat/planImplementation.ts +62 -0
  20. package/src/chat/slashCommandHandlers.ts +165 -0
  21. package/src/chat/slashCommandViews.ts +120 -0
  22. package/src/identity/continuity/challenges.ts +123 -0
  23. package/src/identity/continuity/envelope.ts +49 -1484
  24. package/src/identity/continuity/envelopeCreate.ts +322 -0
  25. package/src/identity/continuity/envelopeCrypto.ts +182 -0
  26. package/src/identity/continuity/envelopeParse.ts +441 -0
  27. package/src/identity/continuity/envelopeTypes.ts +204 -0
  28. package/src/identity/continuity/envelopeVersion.ts +1 -0
  29. package/src/identity/continuity/payloadNormalization.ts +183 -0
  30. package/src/identity/continuity/skills/loadSkills.ts +12 -69
  31. package/src/identity/continuity/skills/skillPaths.ts +76 -0
  32. package/src/identity/continuity/skillsNormalization.ts +119 -0
  33. package/src/identity/continuity/snapshotToken.ts +28 -0
  34. package/src/identity/hub/continuity/completion.ts +67 -0
  35. package/src/identity/hub/continuity/effects.ts +5 -62
  36. package/src/identity/hub/profile/effects.ts +6 -170
  37. package/src/identity/hub/profile/operatorSave.ts +202 -0
  38. package/src/identity/wallet/browserWallet/html.ts +1 -57
  39. package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
  40. package/src/identity/wallet/page/controller.ts +1 -1
  41. package/src/identity/wallet/page/errorView.ts +122 -0
  42. package/src/identity/wallet/page/view.ts +3 -114
  43. package/src/mcp/manager.ts +8 -66
  44. package/src/mcp/managerHelpers.ts +70 -0
  45. package/src/models/ModelPicker.tsx +69 -889
  46. package/src/models/huggingface.ts +20 -137
  47. package/src/models/huggingfaceStorage.ts +136 -0
  48. package/src/models/llamacpp.ts +37 -303
  49. package/src/models/llamacppCommands.ts +44 -0
  50. package/src/models/llamacppConfig.ts +34 -0
  51. package/src/models/llamacppDiscovery.ts +176 -0
  52. package/src/models/llamacppOutput.ts +65 -0
  53. package/src/models/modelPickerCatalogFlow.ts +56 -0
  54. package/src/models/modelPickerCredentials.ts +166 -0
  55. package/src/models/modelPickerData.ts +41 -0
  56. package/src/models/modelPickerDisplay.tsx +132 -0
  57. package/src/models/modelPickerHfFlow.ts +192 -0
  58. package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
  59. package/src/models/modelPickerTypes.ts +69 -0
  60. package/src/models/modelPickerUninstallFlow.ts +48 -0
  61. package/src/models/modelPickerViewHelpers.ts +174 -0
  62. package/src/providers/openai-chat.ts +5 -124
  63. package/src/providers/openaiChatWire.ts +124 -0
  64. package/src/runtime/providerTurn.ts +38 -0
  65. package/src/runtime/textToolParser.ts +161 -0
  66. package/src/runtime/toolIntent.ts +1 -1
  67. package/src/runtime/turn.ts +43 -499
  68. package/src/runtime/turnNudges.ts +223 -0
  69. package/src/runtime/turnTypes.ts +86 -0
@@ -1,1484 +1,49 @@
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
-
6
- export const CONTINUITY_SNAPSHOT_ENVELOPE_VERSION = 'ethagent-continuity-snapshot-v1'
7
-
8
- export type ContinuityFiles = {
9
- 'SOUL.md': string
10
- 'MEMORY.md': string
11
- }
12
-
13
- export type ContinuitySkillsTree = Record<string, string>
14
-
15
- const PRIVATE_SKILL_FILE_RE = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+$/
16
- const PRIVATE_SKILL_LAST_SEG_FILE_RE = /^[A-Za-z0-9._-]+\.[A-Za-z0-9]+$/
17
- const LEGACY_NESTED_SKILL_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\/.+$/
18
- const LEGACY_FLAT_NAME_MD_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\.md$/i
19
- const MAX_PRIVATE_SKILL_ENTRIES = 500
20
- const MAX_PRIVATE_SKILL_BODY_BYTES = 256 * 1024
21
- const MAX_PRIVATE_SKILL_PATH_LEN = 256
22
-
23
- type ContinuityTranscriptSummary = {
24
- sessionId?: string
25
- createdAt?: string
26
- summary: string
27
- }
28
-
29
- export type ContinuityAgentSnapshot = {
30
- chainId?: number
31
- identityRegistryAddress?: string
32
- agentId?: string
33
- agentUri?: string
34
- metadataCid?: string
35
- name?: string
36
- description?: string
37
- }
38
-
39
- type ContinuitySnapshotPayload = {
40
- version: 1
41
- ownerAddress: string
42
- createdAt: string
43
- sequence?: number
44
- agent: ContinuityAgentSnapshot
45
- files: ContinuityFiles
46
- skills?: ContinuitySkillsTree
47
- transcript: ContinuityTranscriptSummary[]
48
- state: Record<string, unknown>
49
- }
50
-
51
- type ContinuitySnapshotToken = {
52
- chainId: number
53
- identityRegistryAddress: string
54
- agentId: string
55
- }
56
-
57
- type TransferContinuitySnapshotSlot = {
58
- address: string
59
- challenge: string
60
- salt: string
61
- nonce: string
62
- encryptedKey: string
63
- tag: string
64
- }
65
-
66
- export type WalletContinuityRestoreAccessKey = {
67
- address: string
68
- challenge: string
69
- salt: string
70
- kemPublicKey: string
71
- createdAt?: string
72
- }
73
-
74
- type WalletContinuitySnapshotSlot = {
75
- address: string
76
- challenge: string
77
- salt: string
78
- kemPublicKey: string
79
- kemCiphertext: string
80
- nonce: string
81
- encryptedKey: string
82
- tag: string
83
- }
84
-
85
- type SignatureContinuitySnapshotEnvelope = {
86
- version: 1
87
- envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
88
- ownerAddress: string
89
- createdAt: string
90
- challenge: string
91
- crypto: {
92
- kem: 'ML-KEM-1024'
93
- aead: 'AES-256-GCM'
94
- kdf: 'HKDF-SHA256'
95
- signature: 'EIP-191'
96
- decryptsWith?: 'owner-signature'
97
- }
98
- salt: string
99
- kemPublicKey: string
100
- kemCiphertext: string
101
- nonce: string
102
- ciphertext: string
103
- tag: string
104
- }
105
-
106
- type WalletContinuitySnapshotEnvelope = {
107
- version: 1
108
- envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
109
- ownerAddress: string
110
- createdAt: string
111
- token: ContinuitySnapshotToken
112
- accessEpoch: number
113
- accessManifestHash: string
114
- crypto: {
115
- kem: 'ML-KEM-1024'
116
- aead: 'AES-256-GCM'
117
- kdf: 'HKDF-SHA256'
118
- signature: 'EIP-191'
119
- decryptsWith: 'wallet-signature-slots'
120
- }
121
- payloadNonce: string
122
- payloadCiphertext: string
123
- payloadTag: string
124
- payloadHash: string
125
- slots: WalletContinuitySnapshotSlot[]
126
- }
127
-
128
- export type TransferContinuitySnapshotEnvelope = {
129
- version: 1
130
- envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
131
- ownerAddress: string
132
- createdAt: string
133
- challenge: string
134
- token: ContinuitySnapshotToken
135
- targetAddress: string
136
- targetHandle?: string
137
- crypto: {
138
- aead: 'AES-256-GCM'
139
- kdf: 'HKDF-SHA256'
140
- signature: 'EIP-191'
141
- decryptsWith: 'transfer-signature-slot'
142
- }
143
- payloadNonce: string
144
- payloadCiphertext: string
145
- payloadTag: string
146
- payloadHash: string
147
- slots: {
148
- owner: TransferContinuitySnapshotSlot
149
- target: TransferContinuitySnapshotSlot
150
- }
151
- }
152
-
153
- export type ContinuitySnapshotEnvelope =
154
- | SignatureContinuitySnapshotEnvelope
155
- | WalletContinuitySnapshotEnvelope
156
- | TransferContinuitySnapshotEnvelope
157
-
158
- type CreateContinuitySnapshotEnvelopeArgs = {
159
- ownerAddress: string
160
- walletSignature: string
161
- payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
162
- createdAt?: string
163
- }
164
- }
165
-
166
- type CreateTransferContinuitySnapshotEnvelopeArgs = {
167
- ownerAddress: string
168
- ownerWalletSignature: string
169
- targetAddress: string
170
- targetWalletSignature: string
171
- targetHandle?: string
172
- token: ContinuitySnapshotToken
173
- payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
174
- createdAt?: string
175
- }
176
- }
177
-
178
- type CreateWalletContinuitySnapshotEnvelopeArgs = {
179
- ownerAddress: string
180
- token: ContinuitySnapshotToken
181
- signerAddress: string
182
- signerWalletSignature: string
183
- accessKeys: WalletContinuityRestoreAccessKey[]
184
- accessEpoch?: number
185
- payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
186
- createdAt?: string
187
- }
188
- }
189
-
190
- type RestoreContinuitySnapshotEnvelopeArgs = {
191
- envelope: ContinuitySnapshotEnvelope
192
- walletSignature: string
193
- currentOwnerAddress?: string
194
- }
195
-
196
- export class ContinuitySnapshotOwnerMismatchError extends Error {
197
- constructor(
198
- readonly snapshotOwner: string,
199
- readonly currentOwner: string,
200
- ) {
201
- super('Continuity snapshot is encrypted for another wallet')
202
- this.name = 'ContinuitySnapshotOwnerMismatchError'
203
- }
204
- }
205
-
206
- export class ContinuityTransferSnapshotTargetMismatchError extends Error {
207
- constructor(
208
- readonly snapshotOwner: string,
209
- readonly targetOwner: string,
210
- readonly currentOwner: string,
211
- ) {
212
- super('Transfer snapshot receiver does not match the current token owner')
213
- this.name = 'ContinuityTransferSnapshotTargetMismatchError'
214
- }
215
- }
216
-
217
- export class ContinuitySnapshotRestoreSlotMissingError extends Error {
218
- constructor(readonly walletAddress: string) {
219
- super('Restore slot missing')
220
- this.name = 'ContinuitySnapshotRestoreSlotMissingError'
221
- }
222
- }
223
-
224
- const CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES = [
225
- 'Save or Restore Identity Files',
226
- 'Action: encrypt or decrypt local identity files',
227
- 'Private: SOUL.md, MEMORY.md, skills',
228
- 'Public: public skills and profile',
229
- 'Safety: no transaction, spending, or approvals',
230
- 'Version: 2',
231
- ] as const
232
-
233
- export function createContinuitySnapshotChallenge(ownerAddress: string): string {
234
- const checksum = toChecksumAddress(ownerAddress)
235
- return [
236
- CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES[0],
237
- `Owner: ${checksum}`,
238
- ...CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES.slice(1),
239
- ].join('\n')
240
- }
241
-
242
- export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_LEGACY = 'Prepare Transfer Restore Snapshot'
243
- export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_SENDER = 'Prepare Transfer Snapshot · Sender Restore Slot'
244
- export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_RECEIVER = 'Prepare Transfer Snapshot · Receiver Restore Slot'
245
-
246
- export function createTransferContinuitySnapshotChallenge(args: {
247
- token: ContinuitySnapshotToken
248
- ownerAddress: string
249
- targetAddress: string
250
- role?: 'sender' | 'receiver'
251
- }): string {
252
- const token = normalizeContinuitySnapshotToken(args.token)
253
- const ownerAddress = toChecksumAddress(args.ownerAddress)
254
- const targetAddress = toChecksumAddress(args.targetAddress)
255
- const header = args.role === 'sender'
256
- ? TRANSFER_SNAPSHOT_CHALLENGE_HEADER_SENDER
257
- : args.role === 'receiver'
258
- ? TRANSFER_SNAPSHOT_CHALLENGE_HEADER_RECEIVER
259
- : TRANSFER_SNAPSHOT_CHALLENGE_HEADER_LEGACY
260
- return [
261
- header,
262
- `ERC-8004 Chain ID: ${token.chainId}`,
263
- `ERC-8004 Registry: ${token.identityRegistryAddress}`,
264
- `ERC-8004 Token ID: ${token.agentId}`,
265
- `Sender Owner: ${ownerAddress}`,
266
- `Receiver Owner: ${targetAddress}`,
267
- 'Action: encrypt or decrypt local identity files for this token transfer',
268
- 'Private: SOUL.md, MEMORY.md, skills',
269
- 'Public: public skills and profile',
270
- 'Safety: no transaction, spending, or approvals',
271
- 'Version: 2',
272
- ].join('\n')
273
- }
274
-
275
- export type WalletChallengePurpose =
276
- | 'create-agent'
277
- | 'update-snapshot'
278
- | 'update-ens-snapshot'
279
- | 'clear-ens-snapshot'
280
- | 'update-profile-snapshot'
281
- | 'update-operators-snapshot'
282
- | 'refetch-snapshot'
283
- | 'operator-proof'
284
- | 'restore-owner'
285
- | 'restore-operator'
286
- | 'transfer-prepare-sender'
287
- | 'transfer-prepare-receiver'
288
-
289
- const WALLET_CHALLENGE_V2_COPY: Record<WalletChallengePurpose, { title: string; action: string }> = {
290
- 'create-agent': { title: 'Create Agent Snapshot Key', action: 'Action: encrypt the new agent snapshot for owner restore' },
291
- 'update-snapshot': { title: 'Save Snapshot Encryption Key', action: 'Action: encrypt the updated agent snapshot' },
292
- 'update-ens-snapshot': { title: 'Update ENS in Agent Snapshot', action: 'Action: encrypt the snapshot with the new ENS name. No onchain ENS records change.' },
293
- 'clear-ens-snapshot': { title: 'Unlink ENS from Agent', action: 'Action: encrypt the snapshot with no ENS name. No onchain ENS records change.' },
294
- 'update-profile-snapshot': { title: 'Update Public Profile Snapshot Key', action: 'Action: encrypt the snapshot with the updated profile' },
295
- 'update-operators-snapshot': { title: 'Update Operator Wallets Snapshot Key', action: 'Action: encrypt the snapshot with the updated operator list' },
296
- 'refetch-snapshot': { title: 'Refetch Latest Snapshot', action: 'Action: decrypt the latest published snapshot' },
297
- 'operator-proof': { title: 'Authorize Operator Wallet Restore Access', action: 'Action: prove this operator wallet can decrypt future snapshots' },
298
- 'restore-owner': { title: 'Restore Agent with Owner Wallet', action: 'Action: decrypt the snapshot for the owner wallet' },
299
- 'restore-operator': { title: 'Restore Agent with Operator Wallet', action: 'Action: decrypt the snapshot for the authorized operator wallet' },
300
- 'transfer-prepare-sender': { title: 'Prepare Token Transfer (Sender)', action: 'Action: encrypt the transfer snapshot for the receiver' },
301
- 'transfer-prepare-receiver': { title: 'Receive Token Transfer (Receiver)', action: 'Action: prepare receiver decryption for the transfer snapshot' },
302
- }
303
-
304
- export function createWalletRestoreAccessChallenge(args: {
305
- token: ContinuitySnapshotToken
306
- ownerAddress: string
307
- walletAddress: string
308
- accessEpoch?: number
309
- purpose?: WalletChallengePurpose
310
- }): string {
311
- const token = normalizeContinuitySnapshotToken(args.token)
312
- const ownerAddress = toChecksumAddress(args.ownerAddress)
313
- const walletAddress = toChecksumAddress(args.walletAddress)
314
- if (args.purpose) {
315
- const copy = WALLET_CHALLENGE_V2_COPY[args.purpose]
316
- return [
317
- copy.title,
318
- `ERC-8004 Chain ID: ${token.chainId}`,
319
- `ERC-8004 Registry: ${token.identityRegistryAddress}`,
320
- `ERC-8004 Token ID: ${token.agentId}`,
321
- `Owner: ${ownerAddress}`,
322
- `Wallet: ${walletAddress}`,
323
- `Access Epoch: ${args.accessEpoch ?? 1}`,
324
- copy.action,
325
- 'Private: SOUL.md, MEMORY.md, skills',
326
- 'Safety: no transaction, spending, or approvals',
327
- 'Version: 3',
328
- ].join('\n')
329
- }
330
- return [
331
- 'Authorize Wallet Restore Access',
332
- `ERC-8004 Chain ID: ${token.chainId}`,
333
- `ERC-8004 Registry: ${token.identityRegistryAddress}`,
334
- `ERC-8004 Token ID: ${token.agentId}`,
335
- `Owner: ${ownerAddress}`,
336
- `Wallet: ${walletAddress}`,
337
- `Access Epoch: ${args.accessEpoch ?? 1}`,
338
- 'Action: create a restore key for encrypted identity snapshots',
339
- 'Private: SOUL.md, MEMORY.md, skills',
340
- 'Safety: no transaction, spending, or approvals',
341
- 'Version: 2',
342
- ].join('\n')
343
- }
344
-
345
- export function createWalletRestoreAccessKey(args: {
346
- token: ContinuitySnapshotToken
347
- ownerAddress: string
348
- walletAddress: string
349
- walletSignature: string
350
- accessEpoch?: number
351
- createdAt?: string
352
- salt?: string
353
- purpose?: WalletChallengePurpose
354
- }): WalletContinuityRestoreAccessKey {
355
- const walletAddress = toChecksumAddress(args.walletAddress)
356
- const challenge = createWalletRestoreAccessChallenge({
357
- token: args.token,
358
- ownerAddress: args.ownerAddress,
359
- walletAddress,
360
- accessEpoch: args.accessEpoch,
361
- purpose: args.purpose,
362
- })
363
- assertSignatureForAddress(challenge, args.walletSignature, walletAddress)
364
- const salt = args.salt ? fromBase64(args.salt) : crypto.randomBytes(32)
365
- const kemSeed = deriveWalletRestoreKemSeed(args.walletSignature, salt, walletAddress, challenge)
366
- const kemKeys = ml_kem1024.keygen(kemSeed)
367
- return {
368
- address: walletAddress,
369
- challenge,
370
- salt: toBase64(salt),
371
- kemPublicKey: toBase64(kemKeys.publicKey),
372
- ...(args.createdAt ? { createdAt: args.createdAt } : {}),
373
- }
374
- }
375
-
376
- export function createContinuitySnapshotEnvelope(args: CreateContinuitySnapshotEnvelopeArgs): SignatureContinuitySnapshotEnvelope {
377
- const ownerAddress = toChecksumAddress(args.ownerAddress)
378
- const challenge = createContinuitySnapshotChallenge(ownerAddress)
379
- assertSignatureForAddress(challenge, args.walletSignature, ownerAddress)
380
-
381
- const createdAt = args.payload.createdAt ?? new Date().toISOString()
382
- const payload = continuityPayloadFromArgs({
383
- ownerAddress,
384
- createdAt,
385
- payload: args.payload,
386
- })
387
-
388
- const salt = crypto.randomBytes(32)
389
- const kemSeed = deriveContinuityKemSeed(args.walletSignature, salt, ownerAddress)
390
- const kemKeys = ml_kem1024.keygen(kemSeed)
391
- const kem = ml_kem1024.encapsulate(kemKeys.publicKey)
392
- const key = deriveContinuityAesKey(args.walletSignature, kem.sharedSecret, salt, ownerAddress)
393
- const nonce = crypto.randomBytes(12)
394
- const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
395
- cipher.setAAD(continuityAadFor(ownerAddress, createdAt))
396
- const plaintext = Buffer.from(JSON.stringify(payload), 'utf8')
397
- const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()])
398
- const tag = cipher.getAuthTag()
399
-
400
- return {
401
- version: 1,
402
- envelopeVersion: CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
403
- ownerAddress,
404
- createdAt,
405
- challenge,
406
- crypto: {
407
- kem: 'ML-KEM-1024',
408
- aead: 'AES-256-GCM',
409
- kdf: 'HKDF-SHA256',
410
- signature: 'EIP-191',
411
- decryptsWith: 'owner-signature',
412
- },
413
- salt: toBase64(salt),
414
- kemPublicKey: toBase64(kemKeys.publicKey),
415
- kemCiphertext: toBase64(kem.cipherText),
416
- nonce: toBase64(nonce),
417
- ciphertext: toBase64(encrypted),
418
- tag: toBase64(tag),
419
- }
420
- }
421
-
422
- export function createWalletContinuitySnapshotEnvelope(
423
- args: CreateWalletContinuitySnapshotEnvelopeArgs,
424
- ): WalletContinuitySnapshotEnvelope {
425
- const ownerAddress = toChecksumAddress(args.ownerAddress)
426
- const signerAddress = toChecksumAddress(args.signerAddress)
427
- const token = normalizeContinuitySnapshotToken(args.token)
428
- const accessEpoch = args.accessEpoch ?? 1
429
- const accessKeys = normalizeWalletRestoreAccessKeys(args.accessKeys)
430
- if (accessKeys.length === 0) throw new Error('At least one restore access key is required')
431
- const signerKey = accessKeys.find(key => key.address.toLowerCase() === signerAddress.toLowerCase())
432
- if (!signerKey) throw new Error('Snapshot signer is not an authorized restore wallet')
433
- assertSignatureForAddress(signerKey.challenge, args.signerWalletSignature, signerAddress)
434
-
435
- const createdAt = args.payload.createdAt ?? new Date().toISOString()
436
- const payload = continuityPayloadFromArgs({
437
- ownerAddress,
438
- createdAt,
439
- payload: {
440
- ...args.payload,
441
- agent: normalizeAgentSnapshot({
442
- ...args.payload.agent,
443
- chainId: args.payload.agent.chainId ?? token.chainId,
444
- identityRegistryAddress: args.payload.agent.identityRegistryAddress ?? token.identityRegistryAddress,
445
- agentId: args.payload.agent.agentId ?? token.agentId,
446
- }),
447
- },
448
- })
449
- const plaintext = Buffer.from(JSON.stringify(payload), 'utf8')
450
- const contentKey = crypto.randomBytes(32)
451
- const payloadNonce = crypto.randomBytes(12)
452
- const accessManifestHash = walletAccessManifestHash({ ownerAddress, token, accessEpoch, accessKeys })
453
- const payloadAad = walletPayloadAadFor({ ownerAddress, createdAt, token, accessEpoch, accessManifestHash })
454
- const cipher = crypto.createCipheriv('aes-256-gcm', contentKey, payloadNonce)
455
- cipher.setAAD(payloadAad)
456
- const payloadCiphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
457
- const payloadTag = cipher.getAuthTag()
458
-
459
- return {
460
- version: 1,
461
- envelopeVersion: CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
462
- ownerAddress,
463
- createdAt,
464
- token,
465
- accessEpoch,
466
- accessManifestHash,
467
- crypto: {
468
- kem: 'ML-KEM-1024',
469
- aead: 'AES-256-GCM',
470
- kdf: 'HKDF-SHA256',
471
- signature: 'EIP-191',
472
- decryptsWith: 'wallet-signature-slots',
473
- },
474
- payloadNonce: toBase64(payloadNonce),
475
- payloadCiphertext: toBase64(payloadCiphertext),
476
- payloadTag: toBase64(payloadTag),
477
- payloadHash: sha256Hex(plaintext),
478
- slots: accessKeys.map(key => createWalletSlot({ accessKey: key, contentKey, payloadAad, accessManifestHash })),
479
- }
480
- }
481
-
482
- export function createTransferContinuitySnapshotEnvelope(
483
- args: CreateTransferContinuitySnapshotEnvelopeArgs,
484
- ): TransferContinuitySnapshotEnvelope {
485
- const ownerAddress = toChecksumAddress(args.ownerAddress)
486
- const targetAddress = toChecksumAddress(args.targetAddress)
487
- if (ownerAddress.toLowerCase() === targetAddress.toLowerCase()) {
488
- throw new Error('Receiver wallet must be different from sender wallet')
489
- }
490
- const token = normalizeContinuitySnapshotToken(args.token)
491
- const senderChallenge = createTransferContinuitySnapshotChallenge({ token, ownerAddress, targetAddress, role: 'sender' })
492
- const receiverChallenge = createTransferContinuitySnapshotChallenge({ token, ownerAddress, targetAddress, role: 'receiver' })
493
- const challenge = createTransferContinuitySnapshotChallenge({ token, ownerAddress, targetAddress })
494
- assertSignatureForAddress(senderChallenge, args.ownerWalletSignature, ownerAddress)
495
- assertSignatureForAddress(receiverChallenge, args.targetWalletSignature, targetAddress)
496
-
497
- const createdAt = args.payload.createdAt ?? new Date().toISOString()
498
- const payload = continuityPayloadFromArgs({
499
- ownerAddress,
500
- createdAt,
501
- payload: {
502
- ...args.payload,
503
- agent: normalizeAgentSnapshot({
504
- ...args.payload.agent,
505
- chainId: args.payload.agent.chainId ?? token.chainId,
506
- identityRegistryAddress: args.payload.agent.identityRegistryAddress ?? token.identityRegistryAddress,
507
- agentId: args.payload.agent.agentId ?? token.agentId,
508
- }),
509
- },
510
- })
511
- const plaintext = Buffer.from(JSON.stringify(payload), 'utf8')
512
- const contentKey = crypto.randomBytes(32)
513
- const payloadNonce = crypto.randomBytes(12)
514
- const payloadAad = transferPayloadAadFor({ ownerAddress, targetAddress, createdAt, token })
515
- const cipher = crypto.createCipheriv('aes-256-gcm', contentKey, payloadNonce)
516
- cipher.setAAD(payloadAad)
517
- const payloadCiphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
518
- const payloadTag = cipher.getAuthTag()
519
-
520
- const ownerSlot = createTransferSlot({
521
- address: ownerAddress,
522
- challenge: senderChallenge,
523
- walletSignature: args.ownerWalletSignature,
524
- contentKey,
525
- payloadAad,
526
- })
527
- const targetSlot = createTransferSlot({
528
- address: targetAddress,
529
- challenge: receiverChallenge,
530
- walletSignature: args.targetWalletSignature,
531
- contentKey,
532
- payloadAad,
533
- })
534
-
535
- return {
536
- version: 1,
537
- envelopeVersion: CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
538
- ownerAddress,
539
- createdAt,
540
- challenge,
541
- token,
542
- targetAddress,
543
- ...(args.targetHandle ? { targetHandle: args.targetHandle } : {}),
544
- crypto: {
545
- aead: 'AES-256-GCM',
546
- kdf: 'HKDF-SHA256',
547
- signature: 'EIP-191',
548
- decryptsWith: 'transfer-signature-slot',
549
- },
550
- payloadNonce: toBase64(payloadNonce),
551
- payloadCiphertext: toBase64(payloadCiphertext),
552
- payloadTag: toBase64(payloadTag),
553
- payloadHash: sha256Hex(plaintext),
554
- slots: {
555
- owner: ownerSlot,
556
- target: targetSlot,
557
- },
558
- }
559
- }
560
-
561
- export function restoreContinuitySnapshotEnvelope(args: RestoreContinuitySnapshotEnvelopeArgs): ContinuitySnapshotPayload {
562
- const envelope = normalizeContinuitySnapshotEnvelope(args.envelope)
563
- if (isWalletContinuitySnapshotEnvelope(envelope)) {
564
- return restoreWalletContinuitySnapshotEnvelope({
565
- envelope,
566
- walletSignature: args.walletSignature,
567
- currentOwnerAddress: args.currentOwnerAddress,
568
- })
569
- }
570
- if (isTransferContinuitySnapshotEnvelope(envelope)) {
571
- return restoreTransferContinuitySnapshotEnvelope({
572
- envelope,
573
- walletSignature: args.walletSignature,
574
- currentOwnerAddress: args.currentOwnerAddress,
575
- })
576
- }
577
-
578
- assertSignatureForAddress(envelope.challenge, args.walletSignature, envelope.ownerAddress)
579
-
580
- const salt = fromBase64(envelope.salt)
581
- const kemSeed = deriveContinuityKemSeed(args.walletSignature, salt, envelope.ownerAddress)
582
- const kemKeys = ml_kem1024.keygen(kemSeed)
583
- const expectedPublicKey = toBase64(kemKeys.publicKey)
584
- if (expectedPublicKey !== envelope.kemPublicKey) {
585
- throw new Error('Wallet signature does not match this continuity snapshot')
586
- }
587
-
588
- const sharedSecret = ml_kem1024.decapsulate(fromBase64(envelope.kemCiphertext), kemKeys.secretKey)
589
- const key = deriveContinuityAesKey(args.walletSignature, sharedSecret, salt, envelope.ownerAddress)
590
- const decipher = crypto.createDecipheriv('aes-256-gcm', key, fromBase64(envelope.nonce))
591
- decipher.setAAD(continuityAadFor(envelope.ownerAddress, envelope.createdAt))
592
- decipher.setAuthTag(fromBase64(envelope.tag))
593
-
594
- let decoded: unknown
595
- try {
596
- const plaintext = Buffer.concat([
597
- decipher.update(fromBase64(envelope.ciphertext)),
598
- decipher.final(),
599
- ]).toString('utf8')
600
- decoded = JSON.parse(plaintext)
601
- } catch {
602
- throw new Error('Could not decrypt continuity snapshot with the supplied wallet signature')
603
- }
604
-
605
- const payload = normalizeContinuityPayload(decoded)
606
- assertPayloadMatchesEnvelope(payload, envelope.ownerAddress, envelope.createdAt)
607
- return payload
608
- }
609
-
610
- export function assertContinuitySnapshotOwner(envelope: ContinuitySnapshotEnvelope, currentOwner: string): void {
611
- const normalized = normalizeContinuitySnapshotEnvelope(envelope)
612
- const owner = toChecksumAddress(currentOwner)
613
- if (isWalletContinuitySnapshotEnvelope(normalized)) {
614
- const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
615
- if (snapshotOwner.toLowerCase() !== owner.toLowerCase()) {
616
- throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, owner)
617
- }
618
- return
619
- }
620
- if (isTransferContinuitySnapshotEnvelope(normalized)) {
621
- const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
622
- const targetOwner = toChecksumAddress(normalized.targetAddress)
623
- if (
624
- owner.toLowerCase() !== snapshotOwner.toLowerCase()
625
- && owner.toLowerCase() !== targetOwner.toLowerCase()
626
- ) {
627
- throw new ContinuityTransferSnapshotTargetMismatchError(snapshotOwner, targetOwner, owner)
628
- }
629
- return
630
- }
631
- const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
632
- if (snapshotOwner.toLowerCase() !== owner.toLowerCase()) {
633
- throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, owner)
634
- }
635
- }
636
-
637
- export function serializeContinuitySnapshotEnvelope(envelope: ContinuitySnapshotEnvelope): string {
638
- return JSON.stringify(normalizeContinuitySnapshotEnvelope(envelope), null, 2)
639
- }
640
-
641
- export function parseContinuitySnapshotEnvelope(raw: string | Uint8Array): ContinuitySnapshotEnvelope {
642
- const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw)
643
- const parsed = JSON.parse(text) as unknown
644
- return normalizeContinuitySnapshotEnvelope(parsed)
645
- }
646
-
647
- export function transferSnapshotMetadataFromEnvelope(
648
- envelope: ContinuitySnapshotEnvelope,
649
- ): TransferSnapshotMetadata | null {
650
- const normalized = normalizeContinuitySnapshotEnvelope(envelope)
651
- if (!isTransferContinuitySnapshotEnvelope(normalized)) return null
652
- const slotCount = [normalized.slots.owner, normalized.slots.target]
653
- .filter(slot => Boolean(slot?.encryptedKey && slot.address))
654
- .length
655
- if (slotCount < 2) return null
656
- return {
657
- kind: 'dual-wallet',
658
- senderAddress: normalized.ownerAddress,
659
- receiverAddress: normalized.targetAddress,
660
- ...(normalized.targetHandle ? { receiverHandle: normalized.targetHandle } : {}),
661
- slotCount,
662
- createdAt: normalized.createdAt,
663
- }
664
- }
665
-
666
- export function walletContinuitySnapshotSlotForAddress(
667
- envelope: ContinuitySnapshotEnvelope,
668
- address: string,
669
- ): WalletContinuitySnapshotSlot | null {
670
- const normalized = normalizeContinuitySnapshotEnvelope(envelope)
671
- if (!isWalletContinuitySnapshotEnvelope(normalized)) return null
672
- const checksum = toChecksumAddress(address)
673
- return normalized.slots.find(slot => slot.address.toLowerCase() === checksum.toLowerCase()) ?? null
674
- }
675
-
676
- export function findRestorableAddressForSnapshot(
677
- envelope: ContinuitySnapshotEnvelope,
678
- candidates: ReadonlyArray<string>,
679
- ): string | null {
680
- const normalized = normalizeContinuitySnapshotEnvelope(envelope)
681
- const seen = new Set<string>()
682
- const lowerCandidates: string[] = []
683
- for (const candidate of candidates) {
684
- if (!candidate) continue
685
- const lower = candidate.toLowerCase()
686
- if (seen.has(lower)) continue
687
- seen.add(lower)
688
- lowerCandidates.push(lower)
689
- }
690
- if (lowerCandidates.length === 0) return null
691
- if (isWalletContinuitySnapshotEnvelope(normalized)) {
692
- for (const slot of normalized.slots) {
693
- if (lowerCandidates.includes(slot.address.toLowerCase())) return toChecksumAddress(slot.address)
694
- }
695
- return null
696
- }
697
- if (isTransferContinuitySnapshotEnvelope(normalized)) {
698
- const ownerLower = normalized.ownerAddress.toLowerCase()
699
- const targetLower = normalized.targetAddress.toLowerCase()
700
- if (lowerCandidates.includes(ownerLower)) return toChecksumAddress(normalized.ownerAddress)
701
- if (lowerCandidates.includes(targetLower)) return toChecksumAddress(normalized.targetAddress)
702
- return null
703
- }
704
- const ownerLower = normalized.ownerAddress.toLowerCase()
705
- return lowerCandidates.includes(ownerLower) ? toChecksumAddress(normalized.ownerAddress) : null
706
- }
707
-
708
- export function isWalletContinuitySnapshotEnvelope(input: unknown): input is WalletContinuitySnapshotEnvelope {
709
- if (!input || typeof input !== 'object' || Array.isArray(input)) return false
710
- const obj = input as Partial<WalletContinuitySnapshotEnvelope> & { crypto?: Partial<WalletContinuitySnapshotEnvelope['crypto']> }
711
- return obj.version === 1
712
- && obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
713
- && typeof obj.ownerAddress === 'string'
714
- && typeof obj.createdAt === 'string'
715
- && !!obj.token
716
- && typeof obj.accessEpoch === 'number'
717
- && typeof obj.accessManifestHash === 'string'
718
- && obj.crypto?.decryptsWith === 'wallet-signature-slots'
719
- && typeof obj.payloadNonce === 'string'
720
- && typeof obj.payloadCiphertext === 'string'
721
- && typeof obj.payloadTag === 'string'
722
- && typeof obj.payloadHash === 'string'
723
- && Array.isArray(obj.slots)
724
- }
725
-
726
- function isTransferContinuitySnapshotEnvelope(input: unknown): input is TransferContinuitySnapshotEnvelope {
727
- if (!input || typeof input !== 'object' || Array.isArray(input)) return false
728
- const obj = input as Partial<TransferContinuitySnapshotEnvelope> & { crypto?: Partial<TransferContinuitySnapshotEnvelope['crypto']> }
729
- return obj.version === 1
730
- && obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
731
- && typeof obj.ownerAddress === 'string'
732
- && typeof obj.createdAt === 'string'
733
- && typeof obj.challenge === 'string'
734
- && !!obj.token
735
- && typeof obj.targetAddress === 'string'
736
- && obj.crypto?.decryptsWith === 'transfer-signature-slot'
737
- && typeof obj.payloadNonce === 'string'
738
- && typeof obj.payloadCiphertext === 'string'
739
- && typeof obj.payloadTag === 'string'
740
- && typeof obj.payloadHash === 'string'
741
- && !!obj.slots
742
- && !!obj.slots.owner
743
- && !!obj.slots.target
744
- }
745
-
746
- function restoreTransferContinuitySnapshotEnvelope(args: {
747
- envelope: TransferContinuitySnapshotEnvelope
748
- walletSignature: string
749
- currentOwnerAddress?: string
750
- }): ContinuitySnapshotPayload {
751
- const currentAddress = args.currentOwnerAddress
752
- ? toChecksumAddress(args.currentOwnerAddress)
753
- : toChecksumAddress(recoverAddressFromSignature(args.envelope.challenge, args.walletSignature))
754
- const slot = transferSlotForCurrentOwner(args.envelope, currentAddress)
755
- assertSignatureForAddress(slot.challenge, args.walletSignature, slot.address)
756
-
757
- let contentKey: Buffer
758
- try {
759
- const payloadAad = transferPayloadAadFor(args.envelope)
760
- const slotKey = deriveTransferSlotKey(args.walletSignature, fromBase64(slot.salt), slot.address, slot.challenge)
761
- const keyDecipher = crypto.createDecipheriv('aes-256-gcm', slotKey, fromBase64(slot.nonce))
762
- keyDecipher.setAAD(transferSlotAadFor(slot, payloadAad))
763
- keyDecipher.setAuthTag(fromBase64(slot.tag))
764
- contentKey = Buffer.concat([
765
- keyDecipher.update(fromBase64(slot.encryptedKey)),
766
- keyDecipher.final(),
767
- ])
768
- } catch {
769
- throw new Error('Could not decrypt transfer snapshot key with the supplied wallet signature')
770
- }
771
-
772
- let decoded: unknown
773
- try {
774
- const payloadAad = transferPayloadAadFor(args.envelope)
775
- const decipher = crypto.createDecipheriv('aes-256-gcm', contentKey, fromBase64(args.envelope.payloadNonce))
776
- decipher.setAAD(payloadAad)
777
- decipher.setAuthTag(fromBase64(args.envelope.payloadTag))
778
- const plaintext = Buffer.concat([
779
- decipher.update(fromBase64(args.envelope.payloadCiphertext)),
780
- decipher.final(),
781
- ])
782
- if (sha256Hex(plaintext) !== args.envelope.payloadHash) {
783
- throw new Error('Transfer snapshot payload hash mismatch')
784
- }
785
- decoded = JSON.parse(plaintext.toString('utf8')) as unknown
786
- } catch {
787
- throw new Error('Could not decrypt continuity transfer snapshot with the supplied wallet signature')
788
- }
789
-
790
- const payload = normalizeContinuityPayload(decoded)
791
- assertPayloadMatchesEnvelope(payload, args.envelope.ownerAddress, args.envelope.createdAt)
792
- return payload
793
- }
794
-
795
- function restoreWalletContinuitySnapshotEnvelope(args: {
796
- envelope: WalletContinuitySnapshotEnvelope
797
- walletSignature: string
798
- currentOwnerAddress?: string
799
- }): ContinuitySnapshotPayload {
800
- const slot = walletSlotForRestore(args.envelope, args.walletSignature, args.currentOwnerAddress)
801
- assertSignatureForAddress(slot.challenge, args.walletSignature, slot.address)
802
-
803
- let contentKey: Buffer
804
- try {
805
- const salt = fromBase64(slot.salt)
806
- const kemSeed = deriveWalletRestoreKemSeed(args.walletSignature, salt, slot.address, slot.challenge)
807
- const kemKeys = ml_kem1024.keygen(kemSeed)
808
- if (toBase64(kemKeys.publicKey) !== slot.kemPublicKey) {
809
- throw new Error('Wallet restore key mismatch')
810
- }
811
- const sharedSecret = ml_kem1024.decapsulate(fromBase64(slot.kemCiphertext), kemKeys.secretKey)
812
- const slotKey = deriveWalletSlotAesKey(sharedSecret, salt, slot.address, slot.challenge, args.envelope.accessManifestHash)
813
- const keyDecipher = crypto.createDecipheriv('aes-256-gcm', slotKey, fromBase64(slot.nonce))
814
- const payloadAad = walletPayloadAadFor(args.envelope)
815
- keyDecipher.setAAD(walletSlotAadFor(slot, payloadAad))
816
- keyDecipher.setAuthTag(fromBase64(slot.tag))
817
- contentKey = Buffer.concat([
818
- keyDecipher.update(fromBase64(slot.encryptedKey)),
819
- keyDecipher.final(),
820
- ])
821
- } catch {
822
- throw new Error('Could not decrypt wallet restore snapshot key with the supplied wallet signature')
823
- }
824
-
825
- let decoded: unknown
826
- try {
827
- const payloadAad = walletPayloadAadFor(args.envelope)
828
- const decipher = crypto.createDecipheriv('aes-256-gcm', contentKey, fromBase64(args.envelope.payloadNonce))
829
- decipher.setAAD(payloadAad)
830
- decipher.setAuthTag(fromBase64(args.envelope.payloadTag))
831
- const plaintext = Buffer.concat([
832
- decipher.update(fromBase64(args.envelope.payloadCiphertext)),
833
- decipher.final(),
834
- ])
835
- if (sha256Hex(plaintext) !== args.envelope.payloadHash) {
836
- throw new Error('Wallet restore snapshot payload hash mismatch')
837
- }
838
- decoded = JSON.parse(plaintext.toString('utf8')) as unknown
839
- } catch {
840
- throw new Error('Could not decrypt wallet restore snapshot with the supplied wallet signature')
841
- }
842
-
843
- const payload = normalizeContinuityPayload(decoded)
844
- assertPayloadMatchesEnvelope(payload, args.envelope.ownerAddress, args.envelope.createdAt)
845
- return payload
846
- }
847
-
848
- function walletSlotForRestore(
849
- envelope: WalletContinuitySnapshotEnvelope,
850
- walletSignature: string,
851
- currentOwnerAddress?: string,
852
- ): WalletContinuitySnapshotSlot {
853
- if (currentOwnerAddress) {
854
- const address = toChecksumAddress(currentOwnerAddress)
855
- const slot = envelope.slots.find(item => item.address.toLowerCase() === address.toLowerCase())
856
- if (!slot) throw new ContinuitySnapshotRestoreSlotMissingError(address)
857
- return slot
858
- }
859
- for (const slot of envelope.slots) {
860
- try {
861
- const recovered = recoverAddressFromSignature(slot.challenge, walletSignature)
862
- if (recovered.toLowerCase() === slot.address.toLowerCase()) return slot
863
- } catch {
864
- }
865
- }
866
- throw new ContinuitySnapshotRestoreSlotMissingError('unknown')
867
- }
868
-
869
- function transferSlotForCurrentOwner(
870
- envelope: TransferContinuitySnapshotEnvelope,
871
- currentOwner: string,
872
- ): TransferContinuitySnapshotSlot {
873
- if (currentOwner.toLowerCase() === envelope.ownerAddress.toLowerCase()) return envelope.slots.owner
874
- if (currentOwner.toLowerCase() === envelope.targetAddress.toLowerCase()) return envelope.slots.target
875
- throw new ContinuityTransferSnapshotTargetMismatchError(envelope.ownerAddress, envelope.targetAddress, currentOwner)
876
- }
877
-
878
- function createTransferSlot(args: {
879
- address: string
880
- challenge: string
881
- walletSignature: string
882
- contentKey: Uint8Array
883
- payloadAad: Buffer
884
- }): TransferContinuitySnapshotSlot {
885
- const address = toChecksumAddress(args.address)
886
- const salt = crypto.randomBytes(32)
887
- const nonce = crypto.randomBytes(12)
888
- const slotKey = deriveTransferSlotKey(args.walletSignature, salt, address, args.challenge)
889
- const cipher = crypto.createCipheriv('aes-256-gcm', slotKey, nonce)
890
- const slot: TransferContinuitySnapshotSlot = {
891
- address,
892
- challenge: args.challenge,
893
- salt: toBase64(salt),
894
- nonce: toBase64(nonce),
895
- encryptedKey: '',
896
- tag: '',
897
- }
898
- cipher.setAAD(transferSlotAadFor(slot, args.payloadAad))
899
- const encryptedKey = Buffer.concat([cipher.update(args.contentKey), cipher.final()])
900
- return {
901
- ...slot,
902
- encryptedKey: toBase64(encryptedKey),
903
- tag: toBase64(cipher.getAuthTag()),
904
- }
905
- }
906
-
907
- function createWalletSlot(args: {
908
- accessKey: WalletContinuityRestoreAccessKey
909
- contentKey: Uint8Array
910
- payloadAad: Buffer
911
- accessManifestHash: string
912
- }): WalletContinuitySnapshotSlot {
913
- const accessKey = normalizeWalletRestoreAccessKey(args.accessKey)
914
- const publicKey = fromBase64(accessKey.kemPublicKey)
915
- const kem = ml_kem1024.encapsulate(publicKey)
916
- const salt = fromBase64(accessKey.salt)
917
- const nonce = crypto.randomBytes(12)
918
- const slotKey = deriveWalletSlotAesKey(kem.sharedSecret, salt, accessKey.address, accessKey.challenge, args.accessManifestHash)
919
- const cipher = crypto.createCipheriv('aes-256-gcm', slotKey, nonce)
920
- const slot: WalletContinuitySnapshotSlot = {
921
- address: accessKey.address,
922
- challenge: accessKey.challenge,
923
- salt: accessKey.salt,
924
- kemPublicKey: accessKey.kemPublicKey,
925
- kemCiphertext: toBase64(kem.cipherText),
926
- nonce: toBase64(nonce),
927
- encryptedKey: '',
928
- tag: '',
929
- }
930
- cipher.setAAD(walletSlotAadFor(slot, args.payloadAad))
931
- const encryptedKey = Buffer.concat([cipher.update(args.contentKey), cipher.final()])
932
- return {
933
- ...slot,
934
- encryptedKey: toBase64(encryptedKey),
935
- tag: toBase64(cipher.getAuthTag()),
936
- }
937
- }
938
-
939
- function normalizeContinuitySnapshotEnvelope(input: unknown): ContinuitySnapshotEnvelope {
940
- if (!isContinuitySnapshotEnvelope(input)) throw new Error('Invalid continuity snapshot envelope')
941
- if (input.envelopeVersion !== CONTINUITY_SNAPSHOT_ENVELOPE_VERSION) {
942
- throw new Error('Unsupported continuity snapshot envelope version')
943
- }
944
- if (isWalletContinuitySnapshotEnvelope(input)) {
945
- if (input.crypto.kem !== 'ML-KEM-1024' || input.crypto.aead !== 'AES-256-GCM' || input.crypto.decryptsWith !== 'wallet-signature-slots') {
946
- throw new Error('Unsupported continuity snapshot crypto suite')
947
- }
948
- const ownerAddress = toChecksumAddress(input.ownerAddress)
949
- const token = normalizeContinuitySnapshotToken(input.token)
950
- const slots = input.slots.map(normalizeWalletSlot)
951
- if (slots.length === 0) throw new Error('Continuity wallet snapshot needs at least one slot')
952
- return {
953
- ...input,
954
- ownerAddress,
955
- token,
956
- slots,
957
- }
958
- }
959
- if (isTransferContinuitySnapshotEnvelope(input)) {
960
- if (input.crypto.aead !== 'AES-256-GCM' || input.crypto.decryptsWith !== 'transfer-signature-slot') {
961
- throw new Error('Unsupported continuity snapshot crypto suite')
962
- }
963
- const ownerAddress = toChecksumAddress(input.ownerAddress)
964
- const targetAddress = toChecksumAddress(input.targetAddress)
965
- return {
966
- ...input,
967
- ownerAddress,
968
- targetAddress,
969
- token: normalizeContinuitySnapshotToken(input.token),
970
- slots: {
971
- owner: normalizeTransferSlot(input.slots.owner, ownerAddress),
972
- target: normalizeTransferSlot(input.slots.target, targetAddress),
973
- },
974
- }
975
- }
976
- if (input.crypto.kem !== 'ML-KEM-1024' || input.crypto.aead !== 'AES-256-GCM') {
977
- throw new Error('Unsupported continuity snapshot crypto suite')
978
- }
979
- return {
980
- ...input,
981
- ownerAddress: toChecksumAddress(input.ownerAddress),
982
- }
983
- }
984
-
985
- function isContinuitySnapshotEnvelope(input: unknown): input is ContinuitySnapshotEnvelope {
986
- if (!input || typeof input !== 'object') return false
987
- const obj = input as Record<string, unknown> & { walletSignature?: unknown; crypto?: unknown }
988
- const base = obj.version === 1
989
- && obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
990
- && typeof obj.ownerAddress === 'string'
991
- && typeof obj.createdAt === 'string'
992
- && obj.walletSignature === undefined
993
- && !!obj.crypto
994
- if (!base) return false
995
- if (isWalletContinuitySnapshotEnvelope(input)) return true
996
- if (isTransferContinuitySnapshotEnvelope(input)) return true
997
- return typeof obj.challenge === 'string'
998
- && typeof obj.salt === 'string'
999
- && typeof obj.kemPublicKey === 'string'
1000
- && typeof obj.kemCiphertext === 'string'
1001
- && typeof obj.nonce === 'string'
1002
- && typeof obj.ciphertext === 'string'
1003
- && typeof obj.tag === 'string'
1004
- }
1005
-
1006
- function continuityPayloadFromArgs(args: {
1007
- ownerAddress: string
1008
- createdAt: string
1009
- payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & { createdAt?: string }
1010
- }): ContinuitySnapshotPayload {
1011
- const skills = normalizeContinuitySkills(args.payload.skills)
1012
- return {
1013
- version: 1,
1014
- ownerAddress: args.ownerAddress,
1015
- createdAt: args.createdAt,
1016
- ...(args.payload.sequence !== undefined ? { sequence: args.payload.sequence } : {}),
1017
- agent: normalizeAgentSnapshot(args.payload.agent),
1018
- files: normalizeContinuityFiles(args.payload.files),
1019
- ...(skills ? { skills } : {}),
1020
- transcript: normalizeTranscript(args.payload.transcript),
1021
- state: normalizeState(args.payload.state),
1022
- }
1023
- }
1024
-
1025
- function normalizeContinuityPayload(input: unknown): ContinuitySnapshotPayload {
1026
- if (!input || typeof input !== 'object') throw new Error('Continuity snapshot payload is invalid')
1027
- const obj = input as Partial<ContinuitySnapshotPayload>
1028
- if (obj.version !== 1) throw new Error('Continuity snapshot payload version is invalid')
1029
- if (typeof obj.ownerAddress !== 'string') throw new Error('Continuity snapshot owner is invalid')
1030
- if (typeof obj.createdAt !== 'string') throw new Error('Continuity snapshot timestamp is invalid')
1031
- const skills = normalizeContinuitySkills(obj.skills)
1032
- return {
1033
- version: 1,
1034
- ownerAddress: toChecksumAddress(obj.ownerAddress),
1035
- createdAt: obj.createdAt,
1036
- ...(typeof obj.sequence === 'number' && Number.isSafeInteger(obj.sequence) ? { sequence: obj.sequence } : {}),
1037
- agent: normalizeAgentSnapshot(obj.agent),
1038
- files: normalizeContinuityFiles(obj.files),
1039
- ...(skills ? { skills } : {}),
1040
- transcript: normalizeTranscript(obj.transcript),
1041
- state: normalizeState(obj.state),
1042
- }
1043
- }
1044
-
1045
- export function normalizeContinuitySkills(input: unknown): ContinuitySkillsTree | undefined {
1046
- if (input === undefined || input === null) return undefined
1047
- if (typeof input !== 'object' || Array.isArray(input)) return undefined
1048
- const obj = input as Record<string, unknown>
1049
- const out: ContinuitySkillsTree = {}
1050
- let count = 0
1051
- const tryInsert = (key: string, rawValue: unknown): void => {
1052
- if (count >= MAX_PRIVATE_SKILL_ENTRIES) return
1053
- if (typeof rawValue !== 'string') return
1054
- if (key.length === 0 || key.length > MAX_PRIVATE_SKILL_PATH_LEN) return
1055
- if (key.includes('\0')) return
1056
- if (key.includes('..')) return
1057
- if (key.startsWith('/')) return
1058
- if (/^[A-Za-z]:/.test(key)) return
1059
- if (!isAcceptableSkillKey(key)) return
1060
- if (Buffer.byteLength(rawValue, 'utf8') > MAX_PRIVATE_SKILL_BODY_BYTES) return
1061
- if (out[key] !== undefined) return
1062
- out[key] = rawValue
1063
- count++
1064
- }
1065
- const legacyRoots = new Set<string>()
1066
- const realSkillFolders = new Set<string>()
1067
- for (const rawKey of Object.keys(obj)) {
1068
- const key = rawKey.replace(/\\/g, '/')
1069
- const segments = key.split('/')
1070
- if (segments.length === 3 && segments[2] === 'SKILL.md' && segments[0] && segments[1]) {
1071
- legacyRoots.add(`${segments[0]}/${segments[1]}`)
1072
- }
1073
- if (segments.length === 2 && segments[1] === 'SKILL.md' && segments[0]) {
1074
- realSkillFolders.add(segments[0])
1075
- }
1076
- }
1077
- for (const [rawKey, rawValue] of Object.entries(obj)) {
1078
- const key = rawKey.replace(/\\/g, '/')
1079
- if (!isCanonicalFlatKey(key)) continue
1080
- if (isUnderLegacyRoot(key, legacyRoots)) continue
1081
- if (!keyHasRealSkillFolder(key, realSkillFolders)) continue
1082
- tryInsert(key, rawValue)
1083
- }
1084
- for (const [rawKey, rawValue] of Object.entries(obj)) {
1085
- const key = rawKey.replace(/\\/g, '/')
1086
- if (
1087
- isCanonicalFlatKey(key)
1088
- && !isUnderLegacyRoot(key, legacyRoots)
1089
- && keyHasRealSkillFolder(key, realSkillFolders)
1090
- ) continue
1091
- const upgraded = upgradeLegacySkillKey(key, legacyRoots)
1092
- if (!upgraded) continue
1093
- tryInsert(upgraded, rawValue)
1094
- }
1095
- return count > 0 ? out : undefined
1096
- }
1097
-
1098
- function isUnderLegacyRoot(key: string, legacyRoots: Set<string>): boolean {
1099
- for (const root of legacyRoots) {
1100
- if (key === `${root}/SKILL.md`) return true
1101
- if (key.startsWith(`${root}/`)) return true
1102
- }
1103
- return false
1104
- }
1105
-
1106
- function keyHasRealSkillFolder(key: string, realSkillFolders: Set<string>): boolean {
1107
- const first = key.split('/')[0]
1108
- if (!first) return false
1109
- return realSkillFolders.has(first)
1110
- }
1111
-
1112
- function isCanonicalFlatKey(key: string): boolean {
1113
- if (!PRIVATE_SKILL_FILE_RE.test(key)) return false
1114
- const segments = key.split('/')
1115
- if (segments.length < 2) return false
1116
- const last = segments[segments.length - 1]!
1117
- if (last === 'SKILL.md') return segments.length === 2
1118
- return PRIVATE_SKILL_LAST_SEG_FILE_RE.test(last)
1119
- }
1120
-
1121
- function isAcceptableSkillKey(key: string): boolean {
1122
- return isCanonicalFlatKey(key)
1123
- }
1124
-
1125
- function upgradeLegacySkillKey(key: string, legacyRoots: Set<string>): string | null {
1126
- for (const root of legacyRoots) {
1127
- if (key === `${root}/SKILL.md` || key.startsWith(`${root}/`)) {
1128
- const [first, second] = root.split('/')
1129
- if (!first || !second) continue
1130
- const rest = key.slice(root.length + 1)
1131
- const flattened = `${first}-${second}/${rest}`
1132
- return isCanonicalFlatKey(flattened) ? flattened : null
1133
- }
1134
- }
1135
- if (LEGACY_FLAT_NAME_MD_RE.test(key)) {
1136
- const [category, file] = key.split('/')
1137
- if (!category || !file) return null
1138
- const slug = file.replace(/\.md$/i, '')
1139
- if (!slug) return null
1140
- const flattened = `${category}-${slug}/SKILL.md`
1141
- return isCanonicalFlatKey(flattened) ? flattened : null
1142
- }
1143
- if (LEGACY_NESTED_SKILL_RE.test(key)) {
1144
- const segments = key.split('/')
1145
- if (segments.length < 3) return null
1146
- const [first, second, ...rest] = segments
1147
- if (!first || !second || rest.length === 0) return null
1148
- const flattened = `${first}-${second}/${rest.join('/')}`
1149
- return isCanonicalFlatKey(flattened) ? flattened : null
1150
- }
1151
- if (isCanonicalFlatKey(key)) return key
1152
- return null
1153
- }
1154
-
1155
- function normalizeAgentSnapshot(input: unknown): ContinuityAgentSnapshot {
1156
- if (!input || typeof input !== 'object' || Array.isArray(input)) return {}
1157
- const obj = input as Record<string, unknown>
1158
- return {
1159
- ...(typeof obj.chainId === 'number' && Number.isSafeInteger(obj.chainId) && obj.chainId > 0 ? { chainId: obj.chainId } : {}),
1160
- ...(typeof obj.identityRegistryAddress === 'string' ? { identityRegistryAddress: obj.identityRegistryAddress } : {}),
1161
- ...(typeof obj.agentId === 'string' ? { agentId: obj.agentId } : {}),
1162
- ...(typeof obj.agentUri === 'string' ? { agentUri: obj.agentUri } : {}),
1163
- ...(typeof obj.metadataCid === 'string' ? { metadataCid: obj.metadataCid } : {}),
1164
- ...(typeof obj.name === 'string' ? { name: obj.name } : {}),
1165
- ...(typeof obj.description === 'string' ? { description: obj.description } : {}),
1166
- }
1167
- }
1168
-
1169
- function normalizeContinuityFiles(input: unknown): ContinuityFiles {
1170
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
1171
- throw new Error('Continuity snapshot files are invalid')
1172
- }
1173
- const obj = input as Partial<ContinuityFiles>
1174
- if (typeof obj['SOUL.md'] !== 'string') throw new Error('SOUL.md is missing from continuity snapshot')
1175
- if (typeof obj['MEMORY.md'] !== 'string') throw new Error('MEMORY.md is missing from continuity snapshot')
1176
- return {
1177
- 'SOUL.md': obj['SOUL.md'],
1178
- 'MEMORY.md': obj['MEMORY.md'],
1179
- }
1180
- }
1181
-
1182
- function normalizeTranscript(input: unknown): ContinuityTranscriptSummary[] {
1183
- if (!Array.isArray(input)) return []
1184
- return input.flatMap(item => {
1185
- if (!item || typeof item !== 'object' || Array.isArray(item)) return []
1186
- const obj = item as Partial<ContinuityTranscriptSummary>
1187
- if (typeof obj.summary !== 'string' || !obj.summary.trim()) return []
1188
- return [{
1189
- ...(typeof obj.sessionId === 'string' ? { sessionId: obj.sessionId } : {}),
1190
- ...(typeof obj.createdAt === 'string' ? { createdAt: obj.createdAt } : {}),
1191
- summary: obj.summary,
1192
- }]
1193
- })
1194
- }
1195
-
1196
- function normalizeState(input: unknown): Record<string, unknown> {
1197
- if (!input || typeof input !== 'object' || Array.isArray(input)) return {}
1198
- return input as Record<string, unknown>
1199
- }
1200
-
1201
- function normalizeContinuitySnapshotToken(input: unknown): ContinuitySnapshotToken {
1202
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
1203
- throw new Error('Continuity snapshot token is invalid')
1204
- }
1205
- const obj = input as Partial<ContinuitySnapshotToken>
1206
- if (typeof obj.chainId !== 'number' || !Number.isSafeInteger(obj.chainId) || obj.chainId <= 0) {
1207
- throw new Error('Continuity snapshot token chain is invalid')
1208
- }
1209
- if (typeof obj.identityRegistryAddress !== 'string') {
1210
- throw new Error('Continuity snapshot token registry is invalid')
1211
- }
1212
- if (typeof obj.agentId !== 'string' || !/^\d+$/.test(obj.agentId)) {
1213
- throw new Error('Continuity snapshot token id is invalid')
1214
- }
1215
- return {
1216
- chainId: obj.chainId,
1217
- identityRegistryAddress: toChecksumAddress(obj.identityRegistryAddress),
1218
- agentId: obj.agentId,
1219
- }
1220
- }
1221
-
1222
- function normalizeTransferSlot(input: unknown, expectedAddress: string): TransferContinuitySnapshotSlot {
1223
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
1224
- throw new Error('Continuity transfer slot is invalid')
1225
- }
1226
- const obj = input as Partial<TransferContinuitySnapshotSlot>
1227
- if (typeof obj.address !== 'string') throw new Error('Continuity transfer slot address is invalid')
1228
- const address = toChecksumAddress(obj.address)
1229
- if (address.toLowerCase() !== expectedAddress.toLowerCase()) {
1230
- throw new Error('Continuity transfer slot address mismatch')
1231
- }
1232
- if (typeof obj.challenge !== 'string') throw new Error('Continuity transfer slot challenge is invalid')
1233
- if (typeof obj.salt !== 'string') throw new Error('Continuity transfer slot salt is invalid')
1234
- if (typeof obj.nonce !== 'string') throw new Error('Continuity transfer slot nonce is invalid')
1235
- if (typeof obj.encryptedKey !== 'string') throw new Error('Continuity transfer slot key is invalid')
1236
- if (typeof obj.tag !== 'string') throw new Error('Continuity transfer slot tag is invalid')
1237
- return {
1238
- address,
1239
- challenge: obj.challenge,
1240
- salt: obj.salt,
1241
- nonce: obj.nonce,
1242
- encryptedKey: obj.encryptedKey,
1243
- tag: obj.tag,
1244
- }
1245
- }
1246
-
1247
- function normalizeWalletRestoreAccessKeys(input: unknown): WalletContinuityRestoreAccessKey[] {
1248
- if (!Array.isArray(input)) return []
1249
- const out: WalletContinuityRestoreAccessKey[] = []
1250
- const seen = new Set<string>()
1251
- for (const item of input) {
1252
- const key = normalizeWalletRestoreAccessKey(item)
1253
- const dedupe = key.address.toLowerCase()
1254
- if (seen.has(dedupe)) continue
1255
- seen.add(dedupe)
1256
- out.push(key)
1257
- }
1258
- return out.sort((a, b) => a.address.toLowerCase().localeCompare(b.address.toLowerCase()))
1259
- }
1260
-
1261
- function normalizeWalletRestoreAccessKey(input: unknown): WalletContinuityRestoreAccessKey {
1262
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
1263
- throw new Error('Wallet restore access key is invalid')
1264
- }
1265
- const obj = input as Partial<WalletContinuityRestoreAccessKey>
1266
- if (typeof obj.address !== 'string') throw new Error('Wallet restore access address is invalid')
1267
- if (typeof obj.challenge !== 'string') throw new Error('Wallet restore access challenge is invalid')
1268
- if (typeof obj.salt !== 'string') throw new Error('Wallet restore access salt is invalid')
1269
- if (typeof obj.kemPublicKey !== 'string') throw new Error('Wallet restore access public key is invalid')
1270
- return {
1271
- address: toChecksumAddress(obj.address),
1272
- challenge: obj.challenge,
1273
- salt: obj.salt,
1274
- kemPublicKey: obj.kemPublicKey,
1275
- ...(typeof obj.createdAt === 'string' ? { createdAt: obj.createdAt } : {}),
1276
- }
1277
- }
1278
-
1279
- function normalizeWalletSlot(input: unknown): WalletContinuitySnapshotSlot {
1280
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
1281
- throw new Error('Continuity wallet slot is invalid')
1282
- }
1283
- const obj = input as Partial<WalletContinuitySnapshotSlot>
1284
- if (typeof obj.address !== 'string') throw new Error('Continuity wallet slot address is invalid')
1285
- if (typeof obj.challenge !== 'string') throw new Error('Continuity wallet slot challenge is invalid')
1286
- if (typeof obj.salt !== 'string') throw new Error('Continuity wallet slot salt is invalid')
1287
- if (typeof obj.kemPublicKey !== 'string') throw new Error('Continuity wallet slot public key is invalid')
1288
- if (typeof obj.kemCiphertext !== 'string') throw new Error('Continuity wallet slot ciphertext is invalid')
1289
- if (typeof obj.nonce !== 'string') throw new Error('Continuity wallet slot nonce is invalid')
1290
- if (typeof obj.encryptedKey !== 'string') throw new Error('Continuity wallet slot key is invalid')
1291
- if (typeof obj.tag !== 'string') throw new Error('Continuity wallet slot tag is invalid')
1292
- return {
1293
- address: toChecksumAddress(obj.address),
1294
- challenge: obj.challenge,
1295
- salt: obj.salt,
1296
- kemPublicKey: obj.kemPublicKey,
1297
- kemCiphertext: obj.kemCiphertext,
1298
- nonce: obj.nonce,
1299
- encryptedKey: obj.encryptedKey,
1300
- tag: obj.tag,
1301
- }
1302
- }
1303
-
1304
- function assertPayloadMatchesEnvelope(payload: ContinuitySnapshotPayload, ownerAddress: string, createdAt: string): void {
1305
- if (payload.ownerAddress.toLowerCase() !== ownerAddress.toLowerCase()) {
1306
- throw new Error('Continuity snapshot owner mismatch')
1307
- }
1308
- if (payload.createdAt !== createdAt) {
1309
- throw new Error('Continuity snapshot timestamp mismatch')
1310
- }
1311
- }
1312
-
1313
- function assertSignatureForAddress(challenge: string, signature: string, address: string): void {
1314
- const recovered = recoverAddressFromSignature(challenge, signature)
1315
- if (recovered.toLowerCase() !== address.toLowerCase()) {
1316
- throw new Error('Wallet signature does not match continuity snapshot owner')
1317
- }
1318
- }
1319
-
1320
- function deriveContinuityKemSeed(walletSignature: string, salt: Uint8Array, ownerAddress: string): Uint8Array {
1321
- return hkdf(
1322
- Buffer.from(walletSignature, 'utf8'),
1323
- salt,
1324
- `ethagent:${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}:ml-kem1024:${ownerAddress.toLowerCase()}`,
1325
- 64,
1326
- )
1327
- }
1328
-
1329
- function deriveContinuityAesKey(
1330
- walletSignature: string,
1331
- sharedSecret: Uint8Array,
1332
- salt: Uint8Array,
1333
- ownerAddress: string,
1334
- ): Buffer {
1335
- return Buffer.from(hkdf(
1336
- Buffer.concat([
1337
- Buffer.from(walletSignature, 'utf8'),
1338
- Buffer.from('\n', 'utf8'),
1339
- Buffer.from(sharedSecret),
1340
- ]),
1341
- salt,
1342
- `ethagent:${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}:aes-256-gcm:${ownerAddress.toLowerCase()}`,
1343
- 32,
1344
- ))
1345
- }
1346
-
1347
- function deriveTransferSlotKey(walletSignature: string, salt: Uint8Array, address: string, challenge: string): Buffer {
1348
- return Buffer.from(hkdf(
1349
- Buffer.from(walletSignature, 'utf8'),
1350
- salt,
1351
- `ethagent:${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}:transfer-slot:${address.toLowerCase()}:${sha256Hex(challenge)}`,
1352
- 32,
1353
- ))
1354
- }
1355
-
1356
- function deriveWalletRestoreKemSeed(walletSignature: string, salt: Uint8Array, address: string, challenge: string): Uint8Array {
1357
- return hkdf(
1358
- Buffer.from(walletSignature, 'utf8'),
1359
- salt,
1360
- `ethagent:${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}:wallet-restore-kem:${address.toLowerCase()}:${sha256Hex(challenge)}`,
1361
- 64,
1362
- )
1363
- }
1364
-
1365
- function deriveWalletSlotAesKey(
1366
- sharedSecret: Uint8Array,
1367
- salt: Uint8Array,
1368
- address: string,
1369
- challenge: string,
1370
- accessManifestHash: string,
1371
- ): Buffer {
1372
- return Buffer.from(hkdf(
1373
- sharedSecret,
1374
- salt,
1375
- `ethagent:${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}:wallet-slot:${address.toLowerCase()}:${sha256Hex(challenge)}:${accessManifestHash}`,
1376
- 32,
1377
- ))
1378
- }
1379
-
1380
- function continuityAadFor(ownerAddress: string, createdAt: string): Buffer {
1381
- return Buffer.from(`${CONTINUITY_SNAPSHOT_ENVELOPE_VERSION}\n${ownerAddress.toLowerCase()}\n${createdAt}`, 'utf8')
1382
- }
1383
-
1384
- function walletAccessManifestHash(args: {
1385
- ownerAddress: string
1386
- token: ContinuitySnapshotToken
1387
- accessEpoch: number
1388
- accessKeys: WalletContinuityRestoreAccessKey[]
1389
- }): string {
1390
- const token = normalizeContinuitySnapshotToken(args.token)
1391
- const accessKeys = normalizeWalletRestoreAccessKeys(args.accessKeys)
1392
- return sha256Hex(JSON.stringify({
1393
- ownerAddress: toChecksumAddress(args.ownerAddress).toLowerCase(),
1394
- token: {
1395
- chainId: token.chainId,
1396
- identityRegistryAddress: token.identityRegistryAddress.toLowerCase(),
1397
- agentId: token.agentId,
1398
- },
1399
- accessEpoch: args.accessEpoch,
1400
- wallets: accessKeys.map(key => ({
1401
- address: key.address.toLowerCase(),
1402
- challengeHash: sha256Hex(key.challenge),
1403
- salt: key.salt,
1404
- kemPublicKey: key.kemPublicKey,
1405
- })),
1406
- }))
1407
- }
1408
-
1409
- function walletPayloadAadFor(args: {
1410
- ownerAddress: string
1411
- createdAt: string
1412
- token: ContinuitySnapshotToken
1413
- accessEpoch: number
1414
- accessManifestHash: string
1415
- }): Buffer {
1416
- const token = normalizeContinuitySnapshotToken(args.token)
1417
- return Buffer.from([
1418
- CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
1419
- 'wallet-signature-slots',
1420
- args.ownerAddress.toLowerCase(),
1421
- args.createdAt,
1422
- String(token.chainId),
1423
- token.identityRegistryAddress.toLowerCase(),
1424
- token.agentId,
1425
- String(args.accessEpoch),
1426
- args.accessManifestHash,
1427
- ].join('\n'), 'utf8')
1428
- }
1429
-
1430
- function transferPayloadAadFor(args: {
1431
- ownerAddress: string
1432
- targetAddress: string
1433
- createdAt: string
1434
- token: ContinuitySnapshotToken
1435
- }): Buffer {
1436
- const token = normalizeContinuitySnapshotToken(args.token)
1437
- return Buffer.from([
1438
- CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
1439
- 'transfer',
1440
- args.ownerAddress.toLowerCase(),
1441
- args.targetAddress.toLowerCase(),
1442
- args.createdAt,
1443
- String(token.chainId),
1444
- token.identityRegistryAddress.toLowerCase(),
1445
- token.agentId,
1446
- ].join('\n'), 'utf8')
1447
- }
1448
-
1449
- function walletSlotAadFor(slot: Pick<WalletContinuitySnapshotSlot, 'address' | 'challenge' | 'kemPublicKey' | 'kemCiphertext'>, payloadAad: Buffer): Buffer {
1450
- return Buffer.concat([
1451
- payloadAad,
1452
- Buffer.from([
1453
- '',
1454
- 'wallet-slot',
1455
- slot.address.toLowerCase(),
1456
- sha256Hex(slot.challenge),
1457
- slot.kemPublicKey,
1458
- slot.kemCiphertext,
1459
- ].join('\n'), 'utf8'),
1460
- ])
1461
- }
1462
-
1463
- function transferSlotAadFor(slot: Pick<TransferContinuitySnapshotSlot, 'address' | 'challenge'>, payloadAad: Buffer): Buffer {
1464
- return Buffer.concat([
1465
- payloadAad,
1466
- Buffer.from(`\nslot\n${slot.address.toLowerCase()}\n${sha256Hex(slot.challenge)}`, 'utf8'),
1467
- ])
1468
- }
1469
-
1470
- function hkdf(ikm: Uint8Array, salt: Uint8Array, info: string, length: number): Uint8Array {
1471
- return new Uint8Array(crypto.hkdfSync('sha256', ikm, salt, Buffer.from(info, 'utf8'), length))
1472
- }
1473
-
1474
- function sha256Hex(value: string | Uint8Array): string {
1475
- return crypto.createHash('sha256').update(value).digest('hex')
1476
- }
1477
-
1478
- function toBase64(bytes: Uint8Array): string {
1479
- return Buffer.from(bytes).toString('base64')
1480
- }
1481
-
1482
- function fromBase64(value: string): Uint8Array {
1483
- return new Uint8Array(Buffer.from(value, 'base64'))
1484
- }
1
+ export {
2
+ createContinuitySnapshotChallenge,
3
+ createTransferContinuitySnapshotChallenge,
4
+ createWalletRestoreAccessChallenge,
5
+ TRANSFER_SNAPSHOT_CHALLENGE_HEADER_LEGACY,
6
+ TRANSFER_SNAPSHOT_CHALLENGE_HEADER_RECEIVER,
7
+ TRANSFER_SNAPSHOT_CHALLENGE_HEADER_SENDER,
8
+ } from './challenges.js'
9
+ export type { WalletChallengePurpose } from './challenges.js'
10
+
11
+ export { CONTINUITY_SNAPSHOT_ENVELOPE_VERSION } from './envelopeVersion.js'
12
+
13
+ export { normalizeContinuitySkills } from './skillsNormalization.js'
14
+
15
+ export type {
16
+ ContinuityFiles,
17
+ ContinuitySkillsTree,
18
+ ContinuityAgentSnapshot,
19
+ ContinuitySnapshotPayload,
20
+ TransferContinuitySnapshotSlot,
21
+ WalletContinuityRestoreAccessKey,
22
+ WalletContinuitySnapshotSlot,
23
+ TransferContinuitySnapshotEnvelope,
24
+ ContinuitySnapshotEnvelope,
25
+ } from './envelopeTypes.js'
26
+
27
+ export {
28
+ ContinuitySnapshotOwnerMismatchError,
29
+ ContinuityTransferSnapshotTargetMismatchError,
30
+ ContinuitySnapshotRestoreSlotMissingError,
31
+ } from './envelopeTypes.js'
32
+
33
+ export {
34
+ createWalletRestoreAccessKey,
35
+ createContinuitySnapshotEnvelope,
36
+ createWalletContinuitySnapshotEnvelope,
37
+ createTransferContinuitySnapshotEnvelope,
38
+ } from './envelopeCreate.js'
39
+
40
+ export {
41
+ restoreContinuitySnapshotEnvelope,
42
+ assertContinuitySnapshotOwner,
43
+ serializeContinuitySnapshotEnvelope,
44
+ parseContinuitySnapshotEnvelope,
45
+ transferSnapshotMetadataFromEnvelope,
46
+ walletContinuitySnapshotSlotForAddress,
47
+ findRestorableAddressForSnapshot,
48
+ isWalletContinuitySnapshotEnvelope,
49
+ } from './envelopeParse.js'