@aroha-sdk/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@aroha-sdk/core",
3
+ "version": "1.0.0",
4
+ "description": "Aroha core: identity, crypto, messaging, transport",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./identity": "./dist/identity/index.js",
11
+ "./crypto": "./dist/crypto/index.js",
12
+ "./messages": "./dist/messages/index.js",
13
+ "./transport": "./dist/transport/index.js"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "test": "vitest run",
18
+ "dev": "tsc -p tsconfig.json --watch"
19
+ },
20
+ "dependencies": {
21
+ "@noble/curves": "^1.9.7",
22
+ "@noble/ed25519": "^2.1.0",
23
+ "@noble/hashes": "^1.4.0",
24
+ "undici": "^6.27.0",
25
+ "uuid": "^10.0.0",
26
+ "ws": "^8.17.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.14.0",
30
+ "@types/uuid": "^10.0.0",
31
+ "@types/ws": "^8.5.10",
32
+ "typescript": "^5.4.5",
33
+ "vitest": "^1.6.0"
34
+ }
35
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Aroha Protocol Conformance Suite — Layer 0: DID & Identity
3
+ *
4
+ * Tests the MUST requirements from docs/conformance/test-matrix.md rows L0-01 to L0-04.
5
+ * Each test maps to one test matrix row and is labelled accordingly.
6
+ */
7
+
8
+ import { describe, it, expect } from "vitest";
9
+ import * as ed from "@noble/ed25519";
10
+ import { sha512 } from "@noble/hashes/sha512";
11
+ import {
12
+ generateDID,
13
+ rotateDIDKey,
14
+ toMultibase,
15
+ fromMultibase,
16
+ } from "../identity/did.js";
17
+ import { buildEnvelope, validateEnvelope, newCorrelationId } from "../messages/envelope.js";
18
+ import { NonceRegistry } from "../messages/nonce.js";
19
+
20
+ ed.etc.sha512Sync = (...m: Parameters<typeof sha512>) => sha512(...m);
21
+
22
+ // ─── L0-01: Valid did:aroha: DID backed by Ed25519 keypair ───────────────────
23
+
24
+ describe("L0-01 [MUST] Agent generates a valid did:aroha: DID backed by Ed25519 keypair", () => {
25
+ it("generates a DID with the correct prefix", async () => {
26
+ const agent = await generateDID("test-agent", "https://example.com/aroha");
27
+ expect(agent.did).toMatch(/^did:aroha:/);
28
+ });
29
+
30
+ it("public key round-trips through multibase encoding", async () => {
31
+ const agent = await generateDID("multibase-test", "https://example.com/aroha");
32
+ const encoded = toMultibase(agent.publicKey);
33
+ const decoded = fromMultibase(encoded);
34
+ expect(decoded).toEqual(agent.publicKey);
35
+ });
36
+
37
+ it("DID document contains the public key in the first verificationMethod", async () => {
38
+ const agent = await generateDID("doc-test", "https://example.com/aroha");
39
+ expect(agent.document.verificationMethod).toHaveLength(1);
40
+ const vm = agent.document.verificationMethod[0];
41
+ expect(vm.type).toBe("Ed25519VerificationKey2020");
42
+ const recovered = fromMultibase(vm.publicKeyMultibase);
43
+ expect(recovered).toEqual(agent.publicKey);
44
+ });
45
+
46
+ it("public key verifies a signature made with the private key", async () => {
47
+ const agent = await generateDID("sig-test", "https://example.com/aroha");
48
+ const message = new TextEncoder().encode("hello aroha");
49
+ const sig = await ed.signAsync(message, agent.privateKey);
50
+ const valid = await ed.verifyAsync(sig, message, agent.publicKey);
51
+ expect(valid).toBe(true);
52
+ });
53
+ });
54
+
55
+ // ─── L0-02: DID document conforms to W3C DID Core 1.0 ───────────────────────
56
+
57
+ describe("L0-02 [MUST] DID document conforms to W3C DID Core 1.0", () => {
58
+ it("includes the required @context entries", async () => {
59
+ const agent = await generateDID("ctx-test", "https://example.com/aroha");
60
+ const doc = agent.document;
61
+ expect(doc["@context"]).toContain("https://www.w3.org/ns/did/v1");
62
+ expect(doc["@context"]).toContain(
63
+ "https://w3id.org/security/suites/ed25519-2020/v1"
64
+ );
65
+ });
66
+
67
+ it("id field equals the DID", async () => {
68
+ const agent = await generateDID("id-test", "https://example.com/aroha");
69
+ expect(agent.document.id).toBe(agent.did);
70
+ });
71
+
72
+ it("authentication references the key id", async () => {
73
+ const agent = await generateDID("auth-test", "https://example.com/aroha");
74
+ const doc = agent.document;
75
+ const keyId = `${agent.did}#key-1`;
76
+ expect(doc.authentication).toContain(keyId);
77
+ });
78
+
79
+ it("assertionMethod references the key id", async () => {
80
+ const agent = await generateDID("assertion-test", "https://example.com/aroha");
81
+ const doc = agent.document;
82
+ const keyId = `${agent.did}#key-1`;
83
+ expect(doc.assertionMethod).toContain(keyId);
84
+ });
85
+
86
+ it("service array contains an ArohaEndpoint entry", async () => {
87
+ const endpoint = "https://agent.example.com/aroha";
88
+ const agent = await generateDID("svc-test", endpoint);
89
+ const svc = agent.document.service.find((s) => s.type === "ArohaEndpoint");
90
+ expect(svc).toBeDefined();
91
+ expect(svc!.serviceEndpoint).toBe(endpoint);
92
+ });
93
+
94
+ it("verificationMethod controller equals the DID", async () => {
95
+ const agent = await generateDID("ctrl-test", "https://example.com/aroha");
96
+ const vm = agent.document.verificationMethod[0];
97
+ expect(vm.controller).toBe(agent.did);
98
+ });
99
+ });
100
+
101
+ // ─── L0-03: Key rotation without DID change ──────────────────────────────────
102
+
103
+ describe("L0-03 [MUST] Agent can rotate keys without changing DID", () => {
104
+ it("DID is unchanged after rotation", async () => {
105
+ const original = await generateDID("rotate-test", "https://example.com/aroha");
106
+ const rotated = await rotateDIDKey(original, 0); // 0h cooldown for tests
107
+ expect(rotated.did).toBe(original.did);
108
+ });
109
+
110
+ it("new keypair is different from the original", async () => {
111
+ const original = await generateDID("rotate-keys-test", "https://example.com/aroha");
112
+ const rotated = await rotateDIDKey(original, 0);
113
+ expect(rotated.privateKey).not.toEqual(original.privateKey);
114
+ expect(rotated.publicKey).not.toEqual(original.publicKey);
115
+ });
116
+
117
+ it("rotation record is stored in keyHistory", async () => {
118
+ const original = await generateDID("history-test", "https://example.com/aroha");
119
+ const rotated = await rotateDIDKey(original, 0);
120
+ expect(rotated.document.keyHistory).toBeDefined();
121
+ expect(rotated.document.keyHistory!.length).toBeGreaterThan(0);
122
+ });
123
+
124
+ it("new public key is recorded in the DID document after rotation", async () => {
125
+ const original = await generateDID("new-key-test", "https://example.com/aroha");
126
+ const rotated = await rotateDIDKey(original, 0);
127
+ const recoveredKey = fromMultibase(
128
+ rotated.document.verificationMethod[0].publicKeyMultibase
129
+ );
130
+ expect(recoveredKey).toEqual(rotated.publicKey);
131
+ });
132
+
133
+ it("new key signs and verifies correctly after rotation", async () => {
134
+ const original = await generateDID("sign-after-rotate", "https://example.com/aroha");
135
+ const rotated = await rotateDIDKey(original, 0);
136
+ const message = new TextEncoder().encode("post-rotation message");
137
+ const sig = await ed.signAsync(message, rotated.privateKey);
138
+ const valid = await ed.verifyAsync(sig, message, rotated.publicKey);
139
+ expect(valid).toBe(true);
140
+ });
141
+ });
142
+
143
+ // ─── L0-04: Reject messages signed with unknown DID ──────────────────────────
144
+
145
+ describe("L0-04 [MUST] Agent rejects messages signed with unknown/wrong DID key", () => {
146
+ it("rejects an envelope when the verifying key does not match the signing key", async () => {
147
+ const sender = await generateDID("l0-04-sender", "https://example.com/aroha");
148
+ const stranger = await generateDID("l0-04-stranger", "https://example.com/aroha");
149
+ const myDID = "did:aroha:l0-04-receiver";
150
+ const reg = new NonceRegistry();
151
+
152
+ const env = await buildEnvelope(
153
+ "ArohaRequest",
154
+ sender.did,
155
+ myDID,
156
+ { capability: "test", params: {} },
157
+ newCorrelationId(),
158
+ sender.privateKey
159
+ );
160
+
161
+ // Validate with the wrong public key (stranger's key instead of sender's)
162
+ const result = await validateEnvelope(env, stranger.publicKey, myDID, reg);
163
+ expect(result.valid).toBe(false);
164
+ });
165
+
166
+ it("accepts an envelope when the correct sender key is used", async () => {
167
+ const sender = await generateDID("l0-04-valid", "https://example.com/aroha");
168
+ const myDID = "did:aroha:l0-04-valid-recv";
169
+ const reg = new NonceRegistry();
170
+
171
+ const env = await buildEnvelope(
172
+ "ArohaRequest",
173
+ sender.did,
174
+ myDID,
175
+ { capability: "test", params: {} },
176
+ newCorrelationId(),
177
+ sender.privateKey
178
+ );
179
+
180
+ const result = await validateEnvelope(env, sender.publicKey, myDID, reg);
181
+ expect(result.valid).toBe(true);
182
+ });
183
+ });
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Aroha Protocol Conformance Suite — Layer 1: Signed Envelopes
3
+ *
4
+ * Tests the MUST requirements from docs/conformance/test-matrix.md rows L1-01 to L1-05.
5
+ * L1-06 (TLS enforcement) is a transport-level control verified during deployment;
6
+ * it cannot be meaningfully tested in unit scope — a note is included.
7
+ */
8
+
9
+ import { describe, it, expect } from "vitest";
10
+ import * as ed from "@noble/ed25519";
11
+ import { sha512 } from "@noble/hashes/sha512";
12
+ import { buildEnvelope, validateEnvelope, newCorrelationId } from "../messages/envelope.js";
13
+ import { NonceRegistry } from "../messages/nonce.js";
14
+ import { generateDID } from "../identity/did.js";
15
+
16
+ ed.etc.sha512Sync = (...m: Parameters<typeof sha512>) => sha512(...m);
17
+
18
+ async function makeAgent(id: string) {
19
+ return generateDID(id, `https://${id}.example.com/aroha`);
20
+ }
21
+
22
+ // ─── L1-01: Every outbound message is signed ─────────────────────────────────
23
+
24
+ describe("L1-01 [MUST] Every outbound message is signed with sender's Ed25519 key", () => {
25
+ it("buildEnvelope attaches a proof block", async () => {
26
+ const sender = await makeAgent("l1-01-sender");
27
+ const env = await buildEnvelope(
28
+ "ArohaRequest",
29
+ sender.did,
30
+ "did:aroha:l1-01-recv",
31
+ { capability: "ping", params: {} },
32
+ newCorrelationId(),
33
+ sender.privateKey
34
+ );
35
+ expect(env.proof).toBeDefined();
36
+ expect(env.proof!.type).toBe("Ed25519Signature2020");
37
+ expect(env.proof!.proofValue).toBeTruthy();
38
+ });
39
+
40
+ it("proof verification key id points to sender's DID key", async () => {
41
+ const sender = await makeAgent("l1-01-key-id");
42
+ const env = await buildEnvelope(
43
+ "ArohaRequest",
44
+ sender.did,
45
+ "did:aroha:l1-01-key-id-recv",
46
+ { capability: "ping", params: {} },
47
+ newCorrelationId(),
48
+ sender.privateKey
49
+ );
50
+ expect(env.proof!.verificationMethod).toContain(sender.did);
51
+ });
52
+
53
+ it("proof verifies successfully against the sender's public key", async () => {
54
+ const sender = await makeAgent("l1-01-verify");
55
+ const myDID = "did:aroha:l1-01-verify-recv";
56
+ const reg = new NonceRegistry();
57
+
58
+ const env = await buildEnvelope(
59
+ "ArohaRequest",
60
+ sender.did,
61
+ myDID,
62
+ { capability: "ping", params: {} },
63
+ newCorrelationId(),
64
+ sender.privateKey
65
+ );
66
+
67
+ const result = await validateEnvelope(env, sender.publicKey, myDID, reg);
68
+ expect(result.valid).toBe(true);
69
+ });
70
+ });
71
+
72
+ // ─── L1-02: Receiver rejects messages with invalid signatures ─────────────────
73
+
74
+ describe("L1-02 [MUST] Receiver rejects messages with invalid signatures", () => {
75
+ it("rejects a tampered proofValue", async () => {
76
+ const sender = await makeAgent("l1-02-tamper");
77
+ const myDID = "did:aroha:l1-02-tamper-recv";
78
+ const reg = new NonceRegistry();
79
+
80
+ const env = await buildEnvelope(
81
+ "ArohaRequest",
82
+ sender.did,
83
+ myDID,
84
+ { capability: "ping", params: {} },
85
+ newCorrelationId(),
86
+ sender.privateKey
87
+ );
88
+
89
+ const tampered = {
90
+ ...env,
91
+ proof: { ...env.proof!, proofValue: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" },
92
+ };
93
+
94
+ const result = await validateEnvelope(tampered, sender.publicKey, myDID, reg);
95
+ expect(result.valid).toBe(false);
96
+ });
97
+
98
+ it("rejects when signed with a different private key than the declared sender", async () => {
99
+ const declaredSender = await makeAgent("l1-02-declared");
100
+ const actualSigner = await makeAgent("l1-02-actual");
101
+ const myDID = "did:aroha:l1-02-wrong-key-recv";
102
+ const reg = new NonceRegistry();
103
+
104
+ // Signed with actualSigner's key but claims to be from declaredSender
105
+ const env = await buildEnvelope(
106
+ "ArohaRequest",
107
+ declaredSender.did,
108
+ myDID,
109
+ { capability: "ping", params: {} },
110
+ newCorrelationId(),
111
+ actualSigner.privateKey // wrong key
112
+ );
113
+
114
+ // Receiver looks up declaredSender's public key — signature won't verify
115
+ const result = await validateEnvelope(env, declaredSender.publicKey, myDID, reg);
116
+ expect(result.valid).toBe(false);
117
+ });
118
+ });
119
+
120
+ // ─── L1-03: Receiver rejects replayed messages ───────────────────────────────
121
+
122
+ describe("L1-03 [MUST] Receiver rejects replayed messages (timestamp + nonce)", () => {
123
+ it("rejects an identical envelope submitted twice", async () => {
124
+ const sender = await makeAgent("l1-03-replay");
125
+ const myDID = "did:aroha:l1-03-replay-recv";
126
+ const reg = new NonceRegistry();
127
+
128
+ const env = await buildEnvelope(
129
+ "ArohaRequest",
130
+ sender.did,
131
+ myDID,
132
+ { capability: "ping", params: {} },
133
+ newCorrelationId(),
134
+ sender.privateKey
135
+ );
136
+
137
+ const first = await validateEnvelope(env, sender.publicKey, myDID, reg);
138
+ expect(first.valid).toBe(true);
139
+
140
+ const second = await validateEnvelope(env, sender.publicKey, myDID, reg);
141
+ expect(second.valid).toBe(false);
142
+ expect((second as { valid: false; reason: string }).reason).toMatch(/[Rr]eplay/);
143
+ });
144
+
145
+ it("accepts two different envelopes (different nonces) from the same sender", async () => {
146
+ const sender = await makeAgent("l1-03-two-valid");
147
+ const myDID = "did:aroha:l1-03-two-recv";
148
+ const reg = new NonceRegistry();
149
+
150
+ const env1 = await buildEnvelope(
151
+ "ArohaRequest",
152
+ sender.did,
153
+ myDID,
154
+ { capability: "ping", params: {} },
155
+ newCorrelationId(),
156
+ sender.privateKey
157
+ );
158
+ const env2 = await buildEnvelope(
159
+ "ArohaRequest",
160
+ sender.did,
161
+ myDID,
162
+ { capability: "ping", params: {} },
163
+ newCorrelationId(),
164
+ sender.privateKey
165
+ );
166
+
167
+ const r1 = await validateEnvelope(env1, sender.publicKey, myDID, reg);
168
+ const r2 = await validateEnvelope(env2, sender.publicKey, myDID, reg);
169
+ expect(r1.valid).toBe(true);
170
+ expect(r2.valid).toBe(true);
171
+ });
172
+ });
173
+
174
+ // ─── L1-04: Envelope includes expires field; expired envelopes rejected ───────
175
+
176
+ describe("L1-04 [MUST] Envelope includes expires field; receiver rejects expired envelopes", () => {
177
+ it("buildEnvelope sets expires to a future timestamp", async () => {
178
+ const sender = await makeAgent("l1-04-expires");
179
+ const env = await buildEnvelope(
180
+ "ArohaRequest",
181
+ sender.did,
182
+ "did:aroha:l1-04-recv",
183
+ { capability: "ping", params: {} },
184
+ newCorrelationId(),
185
+ sender.privateKey
186
+ );
187
+ expect(env.expires).toBeDefined();
188
+ expect(new Date(env.expires) > new Date()).toBe(true);
189
+ });
190
+
191
+ it("rejects an envelope where expires is in the past", async () => {
192
+ const sender = await makeAgent("l1-04-past");
193
+ const myDID = "did:aroha:l1-04-past-recv";
194
+ const reg = new NonceRegistry();
195
+
196
+ // ttlSeconds = -10 creates an already-expired envelope
197
+ const env = await buildEnvelope(
198
+ "ArohaRequest",
199
+ sender.did,
200
+ myDID,
201
+ { capability: "ping", params: {} },
202
+ newCorrelationId(),
203
+ sender.privateKey,
204
+ -10
205
+ );
206
+
207
+ const result = await validateEnvelope(env, sender.publicKey, myDID, reg);
208
+ expect(result.valid).toBe(false);
209
+ expect((result as { valid: false; reason: string }).reason).toMatch(/expir/i);
210
+ });
211
+
212
+ it("accepts an envelope with expires well in the future", async () => {
213
+ const sender = await makeAgent("l1-04-future");
214
+ const myDID = "did:aroha:l1-04-future-recv";
215
+ const reg = new NonceRegistry();
216
+
217
+ const env = await buildEnvelope(
218
+ "ArohaRequest",
219
+ sender.did,
220
+ myDID,
221
+ { capability: "ping", params: {} },
222
+ newCorrelationId(),
223
+ sender.privateKey,
224
+ 3600 // 1 hour TTL
225
+ );
226
+
227
+ const result = await validateEnvelope(env, sender.publicKey, myDID, reg);
228
+ expect(result.valid).toBe(true);
229
+ });
230
+ });
231
+
232
+ // ─── L1-05: Envelope schema validates against Aroha 1.0 message spec ─────────
233
+
234
+ describe("L1-05 [MUST] Envelope schema validates against Aroha 1.0 message spec", () => {
235
+ it("built envelope contains all required top-level fields", async () => {
236
+ const sender = await makeAgent("l1-05-schema");
237
+ const env = await buildEnvelope(
238
+ "ArohaRequest",
239
+ sender.did,
240
+ "did:aroha:l1-05-recv",
241
+ { capability: "test", params: { x: 1 } },
242
+ newCorrelationId(),
243
+ sender.privateKey
244
+ );
245
+
246
+ // Required fields per Aroha 1.0 spec §3 (envelope schema)
247
+ expect(env["@context"]).toBeDefined();
248
+ expect(env.id).toMatch(/^urn:uuid:/);
249
+ expect(env.type).toBe("ArohaRequest");
250
+ expect(env.from).toMatch(/^did:aroha:/);
251
+ expect(env.to).toMatch(/^did:aroha:/);
252
+ expect(env.created).toBeTruthy();
253
+ expect(env.expires).toBeTruthy();
254
+ expect(env.nonce).toBeTruthy();
255
+ expect(env.correlationId).toBeTruthy();
256
+ expect(env.body).toBeDefined();
257
+ expect(env.proof).toBeDefined();
258
+ });
259
+
260
+ it("@context includes the Aroha protocol context", async () => {
261
+ const sender = await makeAgent("l1-05-ctx");
262
+ const env = await buildEnvelope(
263
+ "ArohaResponse",
264
+ sender.did,
265
+ "did:aroha:l1-05-ctx-recv",
266
+ { capability: "test", result: {} },
267
+ newCorrelationId(),
268
+ sender.privateKey
269
+ );
270
+ expect(env["@context"]).toContain("https://aroha-labs.com/contexts/v1");
271
+ });
272
+
273
+ it("correlationId links related messages in a saga", async () => {
274
+ const sender = await makeAgent("l1-05-cid");
275
+ const cid = newCorrelationId();
276
+
277
+ const req = await buildEnvelope(
278
+ "ArohaRequest",
279
+ sender.did,
280
+ "did:aroha:l1-05-cid-recv",
281
+ { capability: "test", params: {} },
282
+ cid,
283
+ sender.privateKey
284
+ );
285
+ const resp = await buildEnvelope(
286
+ "ArohaResponse",
287
+ "did:aroha:l1-05-cid-recv",
288
+ sender.did,
289
+ { capability: "test", result: {} },
290
+ cid, // same correlationId
291
+ sender.privateKey
292
+ );
293
+
294
+ expect(req.correlationId).toBe(cid);
295
+ expect(resp.correlationId).toBe(cid);
296
+ });
297
+
298
+ it("all valid Aroha message types can be built without error", async () => {
299
+ const sender = await makeAgent("l1-05-types");
300
+ const cid = newCorrelationId();
301
+ const recvDID = "did:aroha:l1-05-types-recv";
302
+
303
+ const types = [
304
+ ["ArohaRequest", { capability: "c", params: {} }],
305
+ ["ArohaResponse", { capability: "c", result: {} }],
306
+ ["ArohaError", { code: "Aroha_INTERNAL_ERROR", message: "test", retryable: false }],
307
+ ] as const;
308
+
309
+ for (const [type, body] of types) {
310
+ const env = await buildEnvelope(
311
+ type as Parameters<typeof buildEnvelope>[0],
312
+ sender.did,
313
+ recvDID,
314
+ body as Parameters<typeof buildEnvelope>[3],
315
+ cid,
316
+ sender.privateKey
317
+ );
318
+ expect(env.type).toBe(type);
319
+ }
320
+ });
321
+ });
322
+
323
+ // ─── L1-06: TLS 1.3+ enforcement (deployment-level control) ──────────────────
324
+
325
+ describe("L1-06 [MUST] TLS 1.3+ enforced for all connections", () => {
326
+ it("is verified at deployment, not unit scope — minVersion TLSv1.3 must be set in server options", () => {
327
+ // TLS version enforcement cannot be meaningfully verified in a unit test.
328
+ // The reference implementation sets minVersion: 'TLSv1.3' in the HTTPS server
329
+ // options (docs/security/hardening.md §2) and nginx config (ssl_protocols TLSv1.3).
330
+ // This test documents the requirement and confirms the constant is correct.
331
+ const requiredMinTLSVersion = "TLSv1.3";
332
+ expect(requiredMinTLSVersion).toBe("TLSv1.3");
333
+ });
334
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as ed from "@noble/ed25519";
3
+ import { sha512 } from "@noble/hashes/sha512";
4
+ import { encryptBody, decryptBody } from "./encryption.js";
5
+
6
+ ed.etc.sha512Sync = (...m: Parameters<typeof sha512>) => sha512(...m);
7
+
8
+ describe("encryptBody / decryptBody", () => {
9
+ it("round-trips a plaintext object", async () => {
10
+ const priv = ed.utils.randomPrivateKey();
11
+ const pub = await ed.getPublicKeyAsync(priv);
12
+ const plaintext = { capability: "book-flight", params: { origin: "JFK", dest: "LAX" } };
13
+
14
+ const encrypted = await encryptBody(plaintext, pub);
15
+
16
+ expect(encrypted.alg).toBe("ECDH-ES+A256GCM");
17
+ expect(encrypted.ciphertext).toBeTruthy();
18
+ expect(encrypted.tag).toBeTruthy();
19
+
20
+ const decrypted = await decryptBody(encrypted, priv);
21
+ expect(decrypted).toEqual(plaintext);
22
+ });
23
+
24
+ it("produces different ciphertext each time (random IV)", async () => {
25
+ const priv = ed.utils.randomPrivateKey();
26
+ const pub = await ed.getPublicKeyAsync(priv);
27
+ const plaintext = { hello: "world" };
28
+
29
+ const a = await encryptBody(plaintext, pub);
30
+ const b = await encryptBody(plaintext, pub);
31
+
32
+ expect(a.ciphertext).not.toBe(b.ciphertext);
33
+ expect(a.iv).not.toBe(b.iv);
34
+ });
35
+
36
+ it("fails to decrypt with a different private key", async () => {
37
+ const priv1 = ed.utils.randomPrivateKey();
38
+ const pub1 = await ed.getPublicKeyAsync(priv1);
39
+ const priv2 = ed.utils.randomPrivateKey();
40
+
41
+ const encrypted = await encryptBody({ secret: "data" }, pub1);
42
+
43
+ await expect(decryptBody(encrypted, priv2)).rejects.toThrow();
44
+ });
45
+ });