@lawrenceliang-btc/atel-sdk 0.8.7

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -0
  3. package/bin/atel.mjs +2692 -0
  4. package/bin/tunnel-manager.mjs +171 -0
  5. package/dist/anchor/base.d.ts +21 -0
  6. package/dist/anchor/base.js +26 -0
  7. package/dist/anchor/bsc.d.ts +20 -0
  8. package/dist/anchor/bsc.js +25 -0
  9. package/dist/anchor/evm.d.ts +99 -0
  10. package/dist/anchor/evm.js +262 -0
  11. package/dist/anchor/index.d.ts +173 -0
  12. package/dist/anchor/index.js +165 -0
  13. package/dist/anchor/mock.d.ts +43 -0
  14. package/dist/anchor/mock.js +100 -0
  15. package/dist/anchor/solana.d.ts +95 -0
  16. package/dist/anchor/solana.js +298 -0
  17. package/dist/auditor/index.d.ts +54 -0
  18. package/dist/auditor/index.js +141 -0
  19. package/dist/collaboration/index.d.ts +146 -0
  20. package/dist/collaboration/index.js +237 -0
  21. package/dist/crypto/index.d.ts +162 -0
  22. package/dist/crypto/index.js +231 -0
  23. package/dist/endpoint/index.d.ts +147 -0
  24. package/dist/endpoint/index.js +390 -0
  25. package/dist/envelope/index.d.ts +104 -0
  26. package/dist/envelope/index.js +156 -0
  27. package/dist/executor/index.d.ts +71 -0
  28. package/dist/executor/index.js +398 -0
  29. package/dist/gateway/index.d.ts +278 -0
  30. package/dist/gateway/index.js +520 -0
  31. package/dist/graph/index.d.ts +215 -0
  32. package/dist/graph/index.js +524 -0
  33. package/dist/handshake/index.d.ts +166 -0
  34. package/dist/handshake/index.js +287 -0
  35. package/dist/identity/index.d.ts +155 -0
  36. package/dist/identity/index.js +250 -0
  37. package/dist/index.d.ts +23 -0
  38. package/dist/index.js +28 -0
  39. package/dist/negotiation/index.d.ts +133 -0
  40. package/dist/negotiation/index.js +160 -0
  41. package/dist/network/index.d.ts +78 -0
  42. package/dist/network/index.js +207 -0
  43. package/dist/orchestrator/index.d.ts +190 -0
  44. package/dist/orchestrator/index.js +297 -0
  45. package/dist/policy/index.d.ts +100 -0
  46. package/dist/policy/index.js +206 -0
  47. package/dist/proof/index.d.ts +220 -0
  48. package/dist/proof/index.js +541 -0
  49. package/dist/registry/index.d.ts +98 -0
  50. package/dist/registry/index.js +129 -0
  51. package/dist/rollback/index.d.ts +76 -0
  52. package/dist/rollback/index.js +91 -0
  53. package/dist/schema/capability-schema.json +52 -0
  54. package/dist/schema/index.d.ts +128 -0
  55. package/dist/schema/index.js +163 -0
  56. package/dist/schema/task-schema.json +69 -0
  57. package/dist/score/index.d.ts +174 -0
  58. package/dist/score/index.js +275 -0
  59. package/dist/service/index.d.ts +34 -0
  60. package/dist/service/index.js +273 -0
  61. package/dist/service/server.d.ts +7 -0
  62. package/dist/service/server.js +22 -0
  63. package/dist/trace/index.d.ts +217 -0
  64. package/dist/trace/index.js +446 -0
  65. package/dist/trust/index.d.ts +84 -0
  66. package/dist/trust/index.js +107 -0
  67. package/dist/trust-sync/index.d.ts +30 -0
  68. package/dist/trust-sync/index.js +57 -0
  69. package/package.json +71 -0
  70. package/skill/SKILL.md +363 -0
  71. package/skill/references/commercial.md +184 -0
  72. package/skill/references/executor.md +356 -0
  73. package/skill/references/networking.md +64 -0
  74. package/skill/references/onchain.md +73 -0
  75. package/skill/references/security.md +96 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Module: Handshake Protocol
3
+ *
4
+ * Mutual identity verification + encrypted session establishment.
5
+ * Uses challenge-response with Ed25519 signatures and X25519 key exchange.
6
+ *
7
+ * Flow:
8
+ * A → B: handshake_init {did_a, pubkey_a, enc_pubkey_a, challenge_a}
9
+ * B → A: handshake_ack {did_b, pubkey_b, enc_pubkey_b, challenge_b, sign(challenge_a)}
10
+ * A → B: handshake_confirm {sign(challenge_b)}
11
+ * ✅ Encrypted session established (X25519 shared secret derived)
12
+ */
13
+ import { randomUUID, randomBytes } from 'node:crypto';
14
+ import { verify, sign as didSign, parseDID } from '../identity/index.js';
15
+ import { createMessage, verifyMessage, } from '../envelope/index.js';
16
+ import { generateEncryptionKeyPair, deriveSharedKey, EncryptionManager, } from '../crypto/index.js';
17
+ // ─── Custom Errors ───────────────────────────────────────────────
18
+ export class HandshakeError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'HandshakeError';
22
+ }
23
+ }
24
+ // ─── Wallet Proof Helpers ─────────────────────────────────────────
25
+ /** Create a signed wallet bundle proving DID ownership of wallet addresses */
26
+ export function createWalletBundle(addresses, secretKey) {
27
+ // Canonical JSON: sorted keys, no undefined values
28
+ const clean = {};
29
+ for (const [k, v] of Object.entries(addresses)) {
30
+ if (v)
31
+ clean[k] = v;
32
+ }
33
+ const proof = didSign(clean, secretKey);
34
+ return { addresses: clean, proof };
35
+ }
36
+ /** Verify a wallet bundle's DID signature */
37
+ export function verifyWalletBundle(bundle, publicKey) {
38
+ if (!bundle || !bundle.proof || !bundle.addresses)
39
+ return false;
40
+ const clean = {};
41
+ for (const [k, v] of Object.entries(bundle.addresses)) {
42
+ if (v)
43
+ clean[k] = v;
44
+ }
45
+ return verify(clean, bundle.proof, publicKey);
46
+ }
47
+ // ─── Handshake Manager ───────────────────────────────────────────
48
+ /**
49
+ * Manages handshake flows, active sessions, and encryption.
50
+ *
51
+ * Supports both initiator and responder roles.
52
+ * Automatically establishes E2E encryption during handshake.
53
+ */
54
+ export class HandshakeManager {
55
+ identity;
56
+ sessionTtlSec;
57
+ challengeBytes;
58
+ enableEncryption;
59
+ sessions = new Map();
60
+ pendingChallenges = new Map();
61
+ pendingEncKeys = new Map();
62
+ pendingInitPayloads = new Map();
63
+ /** Encryption manager for E2E encrypted communication */
64
+ encryption;
65
+ constructor(identity, config) {
66
+ this.identity = identity;
67
+ this.sessionTtlSec = config?.sessionTtlSec ?? 3600;
68
+ this.challengeBytes = config?.challengeBytes ?? 32;
69
+ this.enableEncryption = config?.enableEncryption ?? true;
70
+ this.encryption = new EncryptionManager();
71
+ }
72
+ // ── Initiator Side ───────────────────────────────────────────
73
+ /**
74
+ * Create a handshake_init message (Step 1).
75
+ */
76
+ createInit(remoteDid, wallets) {
77
+ const challenge = randomBytes(this.challengeBytes).toString('hex');
78
+ this.pendingChallenges.set(remoteDid, challenge);
79
+ // Generate ephemeral X25519 key pair for this handshake
80
+ const encKeyPair = generateEncryptionKeyPair();
81
+ this.pendingEncKeys.set(remoteDid, encKeyPair);
82
+ // Sign wallet addresses if provided
83
+ const walletBundle = wallets ? createWalletBundle(wallets, this.identity.secretKey) : undefined;
84
+ return createMessage({
85
+ type: 'handshake_init',
86
+ from: this.identity.did,
87
+ to: remoteDid,
88
+ payload: {
89
+ did: this.identity.did,
90
+ publicKey: Buffer.from(this.identity.publicKey).toString('base64'),
91
+ encPublicKey: Buffer.from(encKeyPair.publicKey).toString('base64'),
92
+ challenge,
93
+ wallets,
94
+ walletBundle,
95
+ },
96
+ secretKey: this.identity.secretKey,
97
+ });
98
+ }
99
+ /**
100
+ * Process a handshake_ack message and create handshake_confirm (Step 3).
101
+ */
102
+ processAck(ackMessage) {
103
+ const payload = ackMessage.payload;
104
+ // Verify the ack message signature
105
+ const remotePubKey = Uint8Array.from(Buffer.from(payload.publicKey, 'base64'));
106
+ const msgResult = verifyMessage(ackMessage, remotePubKey, { skipTimestampCheck: false });
107
+ if (!msgResult.valid) {
108
+ throw new HandshakeError(`Handshake ack verification failed: ${msgResult.error}`);
109
+ }
110
+ // Verify the challenge response
111
+ const ourChallenge = this.pendingChallenges.get(payload.did);
112
+ if (!ourChallenge) {
113
+ throw new HandshakeError(`No pending handshake with ${payload.did}`);
114
+ }
115
+ if (!verify(ourChallenge, payload.challengeResponse, remotePubKey)) {
116
+ throw new HandshakeError('Challenge response verification failed');
117
+ }
118
+ this.pendingChallenges.delete(payload.did);
119
+ // Establish E2E encryption
120
+ let encrypted = false;
121
+ if (this.enableEncryption && payload.encPublicKey) {
122
+ const remoteEncPubKey = Uint8Array.from(Buffer.from(payload.encPublicKey, 'base64'));
123
+ this.encryption.createSession(payload.did, remoteEncPubKey);
124
+ // Override with our pending key pair for correct DH
125
+ const ourEncKeyPair = this.pendingEncKeys.get(payload.did);
126
+ if (ourEncKeyPair) {
127
+ // Re-derive using our actual ephemeral key
128
+ const sharedKey = deriveSharedKey(ourEncKeyPair.secretKey, remoteEncPubKey);
129
+ this.encryption.destroySession(payload.did);
130
+ // Manually create session with correct keys
131
+ this.encryption.createSessionWithKeys(payload.did, ourEncKeyPair, remoteEncPubKey, sharedKey);
132
+ this.pendingEncKeys.delete(payload.did);
133
+ encrypted = true;
134
+ }
135
+ }
136
+ // Sign their challenge
137
+ const challengeResponse = this.identity.sign(payload.challenge);
138
+ const confirm = createMessage({
139
+ type: 'handshake_confirm',
140
+ from: this.identity.did,
141
+ to: payload.did,
142
+ payload: { challengeResponse },
143
+ secretKey: this.identity.secretKey,
144
+ });
145
+ const session = this.createSession(payload.did, remotePubKey, encrypted, payload.capabilities, payload.wallets, payload.walletBundle);
146
+ return { confirm, session };
147
+ }
148
+ // ── Responder Side ───────────────────────────────────────────
149
+ /**
150
+ * Process a handshake_init message and create handshake_ack (Step 2).
151
+ */
152
+ processInit(initMessage, wallets) {
153
+ const payload = initMessage.payload;
154
+ // Verify the init message signature
155
+ const remotePubKey = Uint8Array.from(Buffer.from(payload.publicKey, 'base64'));
156
+ const msgResult = verifyMessage(initMessage, remotePubKey, { skipTimestampCheck: false });
157
+ if (!msgResult.valid) {
158
+ throw new HandshakeError(`Handshake init verification failed: ${msgResult.error}`);
159
+ }
160
+ // Verify DID matches public key
161
+ const didPubKey = parseDID(payload.did);
162
+ if (Buffer.from(didPubKey).toString('base64') !== payload.publicKey) {
163
+ throw new HandshakeError('DID does not match provided public key');
164
+ }
165
+ // Generate our challenge
166
+ const ourChallenge = randomBytes(this.challengeBytes).toString('hex');
167
+ this.pendingChallenges.set(payload.did, ourChallenge);
168
+ this.pendingInitPayloads.set(payload.did, payload);
169
+ // Generate ephemeral X25519 key pair and set up encryption
170
+ const encKeyPair = generateEncryptionKeyPair();
171
+ if (this.enableEncryption && payload.encPublicKey) {
172
+ const remoteEncPubKey = Uint8Array.from(Buffer.from(payload.encPublicKey, 'base64'));
173
+ const sharedKey = deriveSharedKey(encKeyPair.secretKey, remoteEncPubKey);
174
+ this.encryption.createSessionWithKeys(payload.did, encKeyPair, remoteEncPubKey, sharedKey);
175
+ }
176
+ // Sign their challenge
177
+ const challengeResponse = this.identity.sign(payload.challenge);
178
+ // Sign wallet addresses if provided
179
+ const walletBundle = wallets ? createWalletBundle(wallets, this.identity.secretKey) : undefined;
180
+ return createMessage({
181
+ type: 'handshake_ack',
182
+ from: this.identity.did,
183
+ to: payload.did,
184
+ payload: {
185
+ did: this.identity.did,
186
+ publicKey: Buffer.from(this.identity.publicKey).toString('base64'),
187
+ encPublicKey: Buffer.from(encKeyPair.publicKey).toString('base64'),
188
+ challenge: ourChallenge,
189
+ challengeResponse,
190
+ wallets,
191
+ walletBundle,
192
+ },
193
+ secretKey: this.identity.secretKey,
194
+ });
195
+ }
196
+ /**
197
+ * Process a handshake_confirm message (Step 3, responder side).
198
+ */
199
+ processConfirm(confirmMessage, initiatorPublicKey, initiatorCapabilities, initiatorWallets, initiatorWalletBundle) {
200
+ const payload = confirmMessage.payload;
201
+ const msgResult = verifyMessage(confirmMessage, initiatorPublicKey, { skipTimestampCheck: false });
202
+ if (!msgResult.valid) {
203
+ throw new HandshakeError(`Handshake confirm verification failed: ${msgResult.error}`);
204
+ }
205
+ const ourChallenge = this.pendingChallenges.get(confirmMessage.from);
206
+ if (!ourChallenge) {
207
+ throw new HandshakeError(`No pending handshake with ${confirmMessage.from}`);
208
+ }
209
+ if (!verify(ourChallenge, payload.challengeResponse, initiatorPublicKey)) {
210
+ throw new HandshakeError('Challenge response verification failed');
211
+ }
212
+ this.pendingChallenges.delete(confirmMessage.from);
213
+ // Use stored init payload if explicit params not provided
214
+ const storedInit = this.pendingInitPayloads.get(confirmMessage.from);
215
+ this.pendingInitPayloads.delete(confirmMessage.from);
216
+ const caps = initiatorCapabilities ?? storedInit?.capabilities;
217
+ const wallets = initiatorWallets ?? storedInit?.wallets;
218
+ const bundle = initiatorWalletBundle ?? storedInit?.walletBundle;
219
+ const encrypted = this.encryption.hasSession(confirmMessage.from);
220
+ return this.createSession(confirmMessage.from, initiatorPublicKey, encrypted, caps, wallets, bundle);
221
+ }
222
+ // ── Session Management ───────────────────────────────────────
223
+ getSession(remoteDid) {
224
+ const session = this.sessions.get(remoteDid);
225
+ if (!session)
226
+ return undefined;
227
+ if (new Date(session.expiresAt).getTime() < Date.now()) {
228
+ session.state = 'expired';
229
+ this.sessions.delete(remoteDid);
230
+ this.encryption.destroySession(remoteDid);
231
+ return undefined;
232
+ }
233
+ return session;
234
+ }
235
+ hasActiveSession(remoteDid) {
236
+ return this.getSession(remoteDid) !== undefined;
237
+ }
238
+ getActiveSessions() {
239
+ const now = Date.now();
240
+ const active = [];
241
+ for (const [did, session] of this.sessions) {
242
+ if (new Date(session.expiresAt).getTime() < now) {
243
+ session.state = 'expired';
244
+ this.sessions.delete(did);
245
+ this.encryption.destroySession(did);
246
+ }
247
+ else {
248
+ active.push(session);
249
+ }
250
+ }
251
+ return active;
252
+ }
253
+ terminateSession(remoteDid) {
254
+ this.sessions.delete(remoteDid);
255
+ this.encryption.destroySession(remoteDid);
256
+ }
257
+ // ── Private ──────────────────────────────────────────────────
258
+ createSession(remoteDid, remotePublicKey, encrypted, remoteCapabilities, remoteWallets, remoteWalletBundle) {
259
+ const now = new Date();
260
+ const expiresAt = new Date(now.getTime() + this.sessionTtlSec * 1000);
261
+ // Verify wallet bundle if provided — cryptographic proof of ownership
262
+ let walletsVerified = false;
263
+ let finalWallets = remoteWallets;
264
+ if (remoteWalletBundle) {
265
+ walletsVerified = verifyWalletBundle(remoteWalletBundle, remotePublicKey);
266
+ if (walletsVerified) {
267
+ // Use addresses from verified bundle (authoritative)
268
+ finalWallets = remoteWalletBundle.addresses;
269
+ }
270
+ }
271
+ const session = {
272
+ sessionId: randomUUID(),
273
+ localDid: this.identity.did,
274
+ remoteDid,
275
+ remotePublicKey,
276
+ encrypted,
277
+ remoteCapabilities,
278
+ remoteWallets: finalWallets,
279
+ remoteWalletsVerified: walletsVerified,
280
+ createdAt: now.toISOString(),
281
+ expiresAt: expiresAt.toISOString(),
282
+ state: 'active',
283
+ };
284
+ this.sessions.set(remoteDid, session);
285
+ return session;
286
+ }
287
+ }
@@ -0,0 +1,155 @@
1
+ export declare class IdentityError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class SignatureError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ export interface KeyPair {
8
+ publicKey: Uint8Array;
9
+ secretKey: Uint8Array;
10
+ }
11
+ export interface AgentIdentityData {
12
+ agent_id: string;
13
+ publicKey: Uint8Array;
14
+ secretKey: Uint8Array;
15
+ did: string;
16
+ }
17
+ /** Optional metadata for an agent identity */
18
+ export interface AgentMetadata {
19
+ /** Human-readable name */
20
+ name?: string;
21
+ /** Description of the agent's purpose */
22
+ description?: string;
23
+ /** List of capabilities the agent supports */
24
+ capabilities?: string[];
25
+ /** Agent version string */
26
+ version?: string;
27
+ /** Additional custom metadata */
28
+ [key: string]: unknown;
29
+ }
30
+ /**
31
+ * Generate an Ed25519 key pair using tweetnacl.
32
+ * @returns A KeyPair containing 32-byte publicKey and 64-byte secretKey.
33
+ */
34
+ export declare function generateKeyPair(): KeyPair;
35
+ /**
36
+ * Create a DID (Decentralized Identifier) from a public key.
37
+ * Format: "did:atel:ed25519:<base58(publicKey)>"
38
+ * @param publicKey - The 32-byte Ed25519 public key.
39
+ * @returns The DID string.
40
+ */
41
+ export declare function createDID(publicKey: Uint8Array): string;
42
+ /**
43
+ * Parse a DID string and extract the public key bytes.
44
+ * Supports both formats:
45
+ * - "did:atel:ed25519:<base58>" (current)
46
+ * - "did:atel:<base58>" (legacy, for backward compatibility)
47
+ * @param did - A DID string.
48
+ * @returns The decoded 32-byte public key.
49
+ */
50
+ export declare function parseDID(did: string): Uint8Array;
51
+ /**
52
+ * Deterministic JSON serialization — keys sorted recursively.
53
+ * Ensures identical payloads produce identical byte sequences for signing.
54
+ * @param obj - The object to serialize.
55
+ * @returns A deterministic JSON string.
56
+ */
57
+ export declare function serializePayload(obj: unknown): string;
58
+ /**
59
+ * Sign a payload with an Ed25519 secret key.
60
+ * The payload is first deterministically serialized, then signed.
61
+ * @param payload - The object or string to sign.
62
+ * @param secretKey - The 64-byte Ed25519 secret key.
63
+ * @returns A base64-encoded detached signature.
64
+ */
65
+ export declare function sign(payload: unknown, secretKey: Uint8Array): string;
66
+ /**
67
+ * Verify a detached Ed25519 signature against a payload.
68
+ * @param payload - The original object or string that was signed.
69
+ * @param signature - The base64-encoded signature to verify.
70
+ * @param publicKey - The 32-byte Ed25519 public key.
71
+ * @returns True if the signature is valid.
72
+ */
73
+ export declare function verify(payload: unknown, signature: string, publicKey: Uint8Array): boolean;
74
+ /**
75
+ * Encapsulates an agent's cryptographic identity.
76
+ * Provides key generation, DID creation, signing, and verification.
77
+ */
78
+ export declare class AgentIdentity {
79
+ readonly agent_id: string;
80
+ readonly publicKey: Uint8Array;
81
+ readonly secretKey: Uint8Array;
82
+ readonly did: string;
83
+ /** Optional metadata describing the agent */
84
+ readonly metadata?: AgentMetadata;
85
+ /**
86
+ * Create an AgentIdentity from an existing key pair.
87
+ * @param params - Optional agent_id, key pair, and metadata. Generates new keys if omitted.
88
+ */
89
+ constructor(params?: {
90
+ agent_id?: string;
91
+ publicKey?: Uint8Array;
92
+ secretKey?: Uint8Array;
93
+ metadata?: AgentMetadata;
94
+ });
95
+ /**
96
+ * Sign a payload using this agent's secret key.
97
+ * @param payload - The data to sign.
98
+ * @returns Base64-encoded signature.
99
+ */
100
+ sign(payload: unknown): string;
101
+ /**
102
+ * Verify a signature against this agent's public key.
103
+ * @param payload - The original data.
104
+ * @param signature - The base64 signature to verify.
105
+ * @returns True if valid.
106
+ */
107
+ verify(payload: unknown, signature: string): boolean;
108
+ /**
109
+ * Export identity data (excluding secret key) for sharing.
110
+ */
111
+ toPublic(): {
112
+ agent_id: string;
113
+ did: string;
114
+ publicKey: string;
115
+ metadata?: AgentMetadata;
116
+ };
117
+ }
118
+ /**
119
+ * Proof of key rotation: signed by both old and new keys.
120
+ * This allows verifiers to confirm the rotation was authorized by the original identity.
121
+ */
122
+ export interface KeyRotationProof {
123
+ /** The agent's original DID (old key) */
124
+ oldDid: string;
125
+ /** The agent's new DID (new key) */
126
+ newDid: string;
127
+ /** New public key (base64) */
128
+ newPublicKey: string;
129
+ /** ISO 8601 timestamp of rotation */
130
+ timestamp: string;
131
+ /** Signature of {oldDid, newDid, newPublicKey, timestamp} by OLD secret key */
132
+ oldSignature: string;
133
+ /** Signature of {oldDid, newDid, newPublicKey, timestamp} by NEW secret key */
134
+ newSignature: string;
135
+ }
136
+ /**
137
+ * Rotate an agent's identity key pair.
138
+ * Generates a new key pair and produces a rotation proof signed by both old and new keys.
139
+ * The proof can be anchored on-chain and submitted to the Registry.
140
+ *
141
+ * @param oldIdentity - The current identity (with secret key).
142
+ * @returns The new identity and the rotation proof.
143
+ */
144
+ export declare function rotateKey(oldIdentity: AgentIdentity): {
145
+ newIdentity: AgentIdentity;
146
+ proof: KeyRotationProof;
147
+ };
148
+ /**
149
+ * Verify a key rotation proof.
150
+ * Checks that both old and new keys signed the rotation data.
151
+ *
152
+ * @param proof - The rotation proof to verify.
153
+ * @returns True if both signatures are valid.
154
+ */
155
+ export declare function verifyKeyRotation(proof: KeyRotationProof): boolean;
@@ -0,0 +1,250 @@
1
+ import nacl from 'tweetnacl';
2
+ import bs58 from 'bs58';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ // ─── Custom Errors ───────────────────────────────────────────────
5
+ export class IdentityError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'IdentityError';
9
+ }
10
+ }
11
+ export class SignatureError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = 'SignatureError';
15
+ }
16
+ }
17
+ // ─── Core Functions ──────────────────────────────────────────────
18
+ /**
19
+ * Generate an Ed25519 key pair using tweetnacl.
20
+ * @returns A KeyPair containing 32-byte publicKey and 64-byte secretKey.
21
+ */
22
+ export function generateKeyPair() {
23
+ const kp = nacl.sign.keyPair();
24
+ return { publicKey: kp.publicKey, secretKey: kp.secretKey };
25
+ }
26
+ /**
27
+ * Create a DID (Decentralized Identifier) from a public key.
28
+ * Format: "did:atel:ed25519:<base58(publicKey)>"
29
+ * @param publicKey - The 32-byte Ed25519 public key.
30
+ * @returns The DID string.
31
+ */
32
+ export function createDID(publicKey) {
33
+ if (publicKey.length !== 32) {
34
+ throw new IdentityError(`Invalid public key length: expected 32, got ${publicKey.length}`);
35
+ }
36
+ const encoded = bs58.encode(publicKey);
37
+ return `did:atel:ed25519:${encoded}`;
38
+ }
39
+ /**
40
+ * Parse a DID string and extract the public key bytes.
41
+ * Supports both formats:
42
+ * - "did:atel:ed25519:<base58>" (current)
43
+ * - "did:atel:<base58>" (legacy, for backward compatibility)
44
+ * @param did - A DID string.
45
+ * @returns The decoded 32-byte public key.
46
+ */
47
+ export function parseDID(did) {
48
+ const parts = did.split(':');
49
+ let base58Part;
50
+ if (parts.length === 4 && parts[0] === 'did' && parts[1] === 'atel' && parts[2] === 'ed25519') {
51
+ // New format: did:atel:ed25519:<base58>
52
+ base58Part = parts[3];
53
+ }
54
+ else if (parts.length === 3 && parts[0] === 'did' && parts[1] === 'atel') {
55
+ // Legacy format: did:atel:<base58>
56
+ base58Part = parts[2];
57
+ }
58
+ else {
59
+ throw new IdentityError(`Invalid DID format: ${did}`);
60
+ }
61
+ try {
62
+ const decoded = bs58.decode(base58Part);
63
+ if (decoded.length !== 32) {
64
+ throw new IdentityError(`Invalid public key in DID: expected 32 bytes, got ${decoded.length}`);
65
+ }
66
+ return decoded;
67
+ }
68
+ catch (e) {
69
+ if (e instanceof IdentityError)
70
+ throw e;
71
+ throw new IdentityError(`Failed to decode DID: ${e.message}`);
72
+ }
73
+ }
74
+ /**
75
+ * Deterministic JSON serialization — keys sorted recursively.
76
+ * Ensures identical payloads produce identical byte sequences for signing.
77
+ * @param obj - The object to serialize.
78
+ * @returns A deterministic JSON string.
79
+ */
80
+ export function serializePayload(obj) {
81
+ return JSON.stringify(obj, (_key, value) => {
82
+ if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Uint8Array)) {
83
+ const sorted = {};
84
+ for (const k of Object.keys(value).sort()) {
85
+ sorted[k] = value[k];
86
+ }
87
+ return sorted;
88
+ }
89
+ return value;
90
+ });
91
+ }
92
+ /**
93
+ * Sign a payload with an Ed25519 secret key.
94
+ * The payload is first deterministically serialized, then signed.
95
+ * @param payload - The object or string to sign.
96
+ * @param secretKey - The 64-byte Ed25519 secret key.
97
+ * @returns A base64-encoded detached signature.
98
+ */
99
+ export function sign(payload, secretKey) {
100
+ if (secretKey.length !== 64) {
101
+ throw new SignatureError(`Invalid secret key length: expected 64, got ${secretKey.length}`);
102
+ }
103
+ const message = typeof payload === 'string' ? payload : serializePayload(payload);
104
+ const messageBytes = new TextEncoder().encode(message);
105
+ const signature = nacl.sign.detached(messageBytes, secretKey);
106
+ return Buffer.from(signature).toString('base64');
107
+ }
108
+ /**
109
+ * Verify a detached Ed25519 signature against a payload.
110
+ * @param payload - The original object or string that was signed.
111
+ * @param signature - The base64-encoded signature to verify.
112
+ * @param publicKey - The 32-byte Ed25519 public key.
113
+ * @returns True if the signature is valid.
114
+ */
115
+ export function verify(payload, signature, publicKey) {
116
+ if (publicKey.length !== 32) {
117
+ throw new SignatureError(`Invalid public key length: expected 32, got ${publicKey.length}`);
118
+ }
119
+ try {
120
+ const message = typeof payload === 'string' ? payload : serializePayload(payload);
121
+ const messageBytes = new TextEncoder().encode(message);
122
+ const sigBytes = Uint8Array.from(Buffer.from(signature, 'base64'));
123
+ return nacl.sign.detached.verify(messageBytes, sigBytes, publicKey);
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ // ─── AgentIdentity Class ─────────────────────────────────────────
130
+ /**
131
+ * Encapsulates an agent's cryptographic identity.
132
+ * Provides key generation, DID creation, signing, and verification.
133
+ */
134
+ export class AgentIdentity {
135
+ agent_id;
136
+ publicKey;
137
+ secretKey;
138
+ did;
139
+ /** Optional metadata describing the agent */
140
+ metadata;
141
+ /**
142
+ * Create an AgentIdentity from an existing key pair.
143
+ * @param params - Optional agent_id, key pair, and metadata. Generates new keys if omitted.
144
+ */
145
+ constructor(params) {
146
+ if (params?.publicKey && params?.secretKey) {
147
+ this.publicKey = params.publicKey;
148
+ this.secretKey = params.secretKey;
149
+ }
150
+ else {
151
+ const kp = generateKeyPair();
152
+ this.publicKey = kp.publicKey;
153
+ this.secretKey = kp.secretKey;
154
+ }
155
+ this.agent_id = params?.agent_id ?? uuidv4();
156
+ this.did = createDID(this.publicKey);
157
+ this.metadata = params?.metadata;
158
+ }
159
+ /**
160
+ * Sign a payload using this agent's secret key.
161
+ * @param payload - The data to sign.
162
+ * @returns Base64-encoded signature.
163
+ */
164
+ sign(payload) {
165
+ return sign(payload, this.secretKey);
166
+ }
167
+ /**
168
+ * Verify a signature against this agent's public key.
169
+ * @param payload - The original data.
170
+ * @param signature - The base64 signature to verify.
171
+ * @returns True if valid.
172
+ */
173
+ verify(payload, signature) {
174
+ return verify(payload, signature, this.publicKey);
175
+ }
176
+ /**
177
+ * Export identity data (excluding secret key) for sharing.
178
+ */
179
+ toPublic() {
180
+ const pub = {
181
+ agent_id: this.agent_id,
182
+ did: this.did,
183
+ publicKey: Buffer.from(this.publicKey).toString('base64'),
184
+ };
185
+ if (this.metadata) {
186
+ pub.metadata = this.metadata;
187
+ }
188
+ return pub;
189
+ }
190
+ }
191
+ /**
192
+ * Rotate an agent's identity key pair.
193
+ * Generates a new key pair and produces a rotation proof signed by both old and new keys.
194
+ * The proof can be anchored on-chain and submitted to the Registry.
195
+ *
196
+ * @param oldIdentity - The current identity (with secret key).
197
+ * @returns The new identity and the rotation proof.
198
+ */
199
+ export function rotateKey(oldIdentity) {
200
+ const newKp = generateKeyPair();
201
+ const newIdentity = new AgentIdentity({
202
+ agent_id: oldIdentity.agent_id,
203
+ publicKey: newKp.publicKey,
204
+ secretKey: newKp.secretKey,
205
+ metadata: oldIdentity.metadata,
206
+ });
207
+ const rotationData = {
208
+ oldDid: oldIdentity.did,
209
+ newDid: newIdentity.did,
210
+ newPublicKey: Buffer.from(newKp.publicKey).toString('base64'),
211
+ timestamp: new Date().toISOString(),
212
+ };
213
+ const signable = serializePayload(rotationData);
214
+ const oldSignature = sign(signable, oldIdentity.secretKey);
215
+ const newSignature = sign(signable, newIdentity.secretKey);
216
+ return {
217
+ newIdentity,
218
+ proof: {
219
+ ...rotationData,
220
+ oldSignature,
221
+ newSignature,
222
+ },
223
+ };
224
+ }
225
+ /**
226
+ * Verify a key rotation proof.
227
+ * Checks that both old and new keys signed the rotation data.
228
+ *
229
+ * @param proof - The rotation proof to verify.
230
+ * @returns True if both signatures are valid.
231
+ */
232
+ export function verifyKeyRotation(proof) {
233
+ try {
234
+ const oldPk = parseDID(proof.oldDid);
235
+ const newPk = parseDID(proof.newDid);
236
+ const rotationData = {
237
+ oldDid: proof.oldDid,
238
+ newDid: proof.newDid,
239
+ newPublicKey: proof.newPublicKey,
240
+ timestamp: proof.timestamp,
241
+ };
242
+ const signable = serializePayload(rotationData);
243
+ const oldValid = verify(signable, proof.oldSignature, oldPk);
244
+ const newValid = verify(signable, proof.newSignature, newPk);
245
+ return oldValid && newValid;
246
+ }
247
+ catch {
248
+ return false;
249
+ }
250
+ }