cairn-p2p 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/index.cjs +1883 -0
- package/dist/index.d.cts +572 -0
- package/dist/index.d.ts +572 -0
- package/dist/index.js +1827 -0
- package/eslint.config.js +24 -0
- package/package.json +54 -0
- package/src/channel.ts +277 -0
- package/src/config.ts +161 -0
- package/src/crypto/aead.ts +80 -0
- package/src/crypto/double-ratchet.ts +355 -0
- package/src/crypto/exchange.ts +51 -0
- package/src/crypto/hkdf.ts +33 -0
- package/src/crypto/identity.ts +84 -0
- package/src/crypto/index.ts +20 -0
- package/src/crypto/noise.ts +415 -0
- package/src/crypto/sas.ts +36 -0
- package/src/crypto/spake2.ts +169 -0
- package/src/discovery/index.ts +38 -0
- package/src/discovery/manager.ts +138 -0
- package/src/discovery/rendezvous.ts +189 -0
- package/src/discovery/tracker.ts +251 -0
- package/src/errors.ts +166 -0
- package/src/index.ts +57 -0
- package/src/mesh/index.ts +48 -0
- package/src/mesh/relay.ts +100 -0
- package/src/mesh/routing-table.ts +196 -0
- package/src/node.ts +619 -0
- package/src/pairing/adapter.ts +51 -0
- package/src/pairing/index.ts +40 -0
- package/src/pairing/link.ts +127 -0
- package/src/pairing/payload.ts +98 -0
- package/src/pairing/pin.ts +115 -0
- package/src/pairing/psk.ts +49 -0
- package/src/pairing/qr.ts +52 -0
- package/src/pairing/rate-limit.ts +134 -0
- package/src/pairing/sas-flow.ts +45 -0
- package/src/pairing/state-machine.ts +438 -0
- package/src/pairing/unpairing.ts +50 -0
- package/src/protocol/custom-handler.ts +52 -0
- package/src/protocol/envelope.ts +138 -0
- package/src/protocol/index.ts +36 -0
- package/src/protocol/message-types.ts +74 -0
- package/src/protocol/version.ts +98 -0
- package/src/server/index.ts +67 -0
- package/src/server/management.ts +285 -0
- package/src/server/store-forward.ts +266 -0
- package/src/session/backoff.ts +58 -0
- package/src/session/heartbeat.ts +79 -0
- package/src/session/index.ts +26 -0
- package/src/session/message-queue.ts +133 -0
- package/src/session/network-monitor.ts +130 -0
- package/src/session/state-machine.ts +122 -0
- package/src/session.ts +223 -0
- package/src/transport/fallback.ts +475 -0
- package/src/transport/index.ts +46 -0
- package/src/transport/libp2p-node.ts +158 -0
- package/src/transport/nat.ts +348 -0
- package/tests/conformance/cbor-vectors.test.ts +250 -0
- package/tests/integration/pairing-session.test.ts +317 -0
- package/tests/unit/config-api.test.ts +310 -0
- package/tests/unit/crypto.test.ts +407 -0
- package/tests/unit/discovery.test.ts +618 -0
- package/tests/unit/double-ratchet.test.ts +185 -0
- package/tests/unit/mesh.test.ts +349 -0
- package/tests/unit/noise.test.ts +346 -0
- package/tests/unit/pairing-extras.test.ts +402 -0
- package/tests/unit/pairing.test.ts +572 -0
- package/tests/unit/protocol.test.ts +438 -0
- package/tests/unit/reconnection.test.ts +402 -0
- package/tests/unit/scaffolding.test.ts +142 -0
- package/tests/unit/server.test.ts +492 -0
- package/tests/unit/sessions.test.ts +595 -0
- package/tests/unit/transport.test.ts +604 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { NoiseXXHandshake } from '../../src/crypto/noise.js';
|
|
3
|
+
import type { HandshakeResult, StepOutput } from '../../src/crypto/noise.js';
|
|
4
|
+
import { IdentityKeypair } from '../../src/crypto/identity.js';
|
|
5
|
+
import { deriveNumericSas, deriveEmojiSas, EMOJI_TABLE } from '../../src/crypto/sas.js';
|
|
6
|
+
import { Spake2 } from '../../src/crypto/spake2.js';
|
|
7
|
+
import { CairnError } from '../../src/errors.js';
|
|
8
|
+
|
|
9
|
+
/** Run a complete Noise XX handshake and return both results. */
|
|
10
|
+
async function runHandshake(
|
|
11
|
+
pakeSecret?: Uint8Array,
|
|
12
|
+
): Promise<[HandshakeResult, HandshakeResult]> {
|
|
13
|
+
const alice = await IdentityKeypair.generate();
|
|
14
|
+
const bob = await IdentityKeypair.generate();
|
|
15
|
+
return runHandshakeWithIdentities(alice, bob, pakeSecret);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function runHandshakeWithIdentities(
|
|
19
|
+
alice: IdentityKeypair,
|
|
20
|
+
bob: IdentityKeypair,
|
|
21
|
+
pakeSecret?: Uint8Array,
|
|
22
|
+
): Promise<[HandshakeResult, HandshakeResult]> {
|
|
23
|
+
const initiator = new NoiseXXHandshake('initiator', alice, pakeSecret);
|
|
24
|
+
const responder = new NoiseXXHandshake('responder', bob, pakeSecret);
|
|
25
|
+
|
|
26
|
+
// Initiator sends msg1
|
|
27
|
+
const out1 = initiator.step();
|
|
28
|
+
expect(out1.type).toBe('send_message');
|
|
29
|
+
const msg1 = (out1 as { type: 'send_message'; data: Uint8Array }).data;
|
|
30
|
+
|
|
31
|
+
// Responder receives msg1, sends msg2
|
|
32
|
+
const out2 = responder.step(msg1);
|
|
33
|
+
expect(out2.type).toBe('send_message');
|
|
34
|
+
const msg2 = (out2 as { type: 'send_message'; data: Uint8Array }).data;
|
|
35
|
+
|
|
36
|
+
// Initiator receives msg2, sends msg3
|
|
37
|
+
const out3 = initiator.step(msg2);
|
|
38
|
+
expect(out3.type).toBe('send_message');
|
|
39
|
+
const msg3 = (out3 as { type: 'send_message'; data: Uint8Array }).data;
|
|
40
|
+
|
|
41
|
+
// Initiator result is available via getResult()
|
|
42
|
+
const initiatorResult = initiator.getResult();
|
|
43
|
+
|
|
44
|
+
// Responder receives msg3 -> complete
|
|
45
|
+
const out4 = responder.step(msg3);
|
|
46
|
+
expect(out4.type).toBe('complete');
|
|
47
|
+
const responderResult = (out4 as { type: 'complete'; result: HandshakeResult }).result;
|
|
48
|
+
|
|
49
|
+
return [initiatorResult, responderResult];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('Noise XX Handshake', () => {
|
|
53
|
+
it('produces matching session keys', async () => {
|
|
54
|
+
const [initResult, respResult] = await runHandshake();
|
|
55
|
+
expect(initResult.sessionKey).toEqual(respResult.sessionKey);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reveals remote static keys', async () => {
|
|
59
|
+
const alice = await IdentityKeypair.generate();
|
|
60
|
+
const bob = await IdentityKeypair.generate();
|
|
61
|
+
const alicePub = alice.publicKey();
|
|
62
|
+
const bobPub = bob.publicKey();
|
|
63
|
+
|
|
64
|
+
const [initResult, respResult] = await runHandshakeWithIdentities(alice, bob);
|
|
65
|
+
|
|
66
|
+
expect(initResult.remoteStatic).toEqual(bobPub);
|
|
67
|
+
expect(respResult.remoteStatic).toEqual(alicePub);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('transcript hashes match', async () => {
|
|
71
|
+
const [initResult, respResult] = await runHandshake();
|
|
72
|
+
expect(initResult.transcriptHash).toEqual(respResult.transcriptHash);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('different handshakes produce different session keys', async () => {
|
|
76
|
+
const [result1] = await runHandshake();
|
|
77
|
+
const [result2] = await runHandshake();
|
|
78
|
+
expect(result1.sessionKey).not.toEqual(result2.sessionKey);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('message 1 is 32 bytes (ephemeral public key)', async () => {
|
|
82
|
+
const alice = await IdentityKeypair.generate();
|
|
83
|
+
const initiator = new NoiseXXHandshake('initiator', alice);
|
|
84
|
+
const out = initiator.step();
|
|
85
|
+
expect(out.type).toBe('send_message');
|
|
86
|
+
expect((out as { type: 'send_message'; data: Uint8Array }).data.length).toBe(32);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('session key is 32 bytes', async () => {
|
|
90
|
+
const [initResult] = await runHandshake();
|
|
91
|
+
expect(initResult.sessionKey.length).toBe(32);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('transcript hash is 32 bytes', async () => {
|
|
95
|
+
const [initResult] = await runHandshake();
|
|
96
|
+
expect(initResult.transcriptHash.length).toBe(32);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('Noise XX with PAKE secret', () => {
|
|
101
|
+
it('completes with matching PAKE secrets', async () => {
|
|
102
|
+
const pake = new Uint8Array(32).fill(42);
|
|
103
|
+
const [initResult, respResult] = await runHandshake(pake);
|
|
104
|
+
expect(initResult.sessionKey).toEqual(respResult.sessionKey);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('mismatched PAKE secrets cause failure', async () => {
|
|
108
|
+
const alice = await IdentityKeypair.generate();
|
|
109
|
+
const bob = await IdentityKeypair.generate();
|
|
110
|
+
|
|
111
|
+
const initiator = new NoiseXXHandshake('initiator', alice, new Uint8Array(32).fill(1));
|
|
112
|
+
const responder = new NoiseXXHandshake('responder', bob, new Uint8Array(32).fill(2));
|
|
113
|
+
|
|
114
|
+
const msg1 = (initiator.step() as { type: 'send_message'; data: Uint8Array }).data;
|
|
115
|
+
const msg2 = (responder.step(msg1) as { type: 'send_message'; data: Uint8Array }).data;
|
|
116
|
+
const msg3 = (initiator.step(msg2) as { type: 'send_message'; data: Uint8Array }).data;
|
|
117
|
+
|
|
118
|
+
// Responder should fail to decrypt msg3 because PAKE secrets differ
|
|
119
|
+
expect(() => responder.step(msg3)).toThrow(CairnError);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Noise XX error handling', () => {
|
|
124
|
+
it('initiator rejects input at start', async () => {
|
|
125
|
+
const alice = await IdentityKeypair.generate();
|
|
126
|
+
const initiator = new NoiseXXHandshake('initiator', alice);
|
|
127
|
+
expect(() => initiator.step(new Uint8Array(32))).toThrow(CairnError);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('responder rejects no input', async () => {
|
|
131
|
+
const bob = await IdentityKeypair.generate();
|
|
132
|
+
const responder = new NoiseXXHandshake('responder', bob);
|
|
133
|
+
expect(() => responder.step()).toThrow(CairnError);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('message 1 wrong length rejected', async () => {
|
|
137
|
+
const bob = await IdentityKeypair.generate();
|
|
138
|
+
const responder = new NoiseXXHandshake('responder', bob);
|
|
139
|
+
expect(() => responder.step(new Uint8Array(16))).toThrow(CairnError);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('message 2 too short rejected', async () => {
|
|
143
|
+
const alice = await IdentityKeypair.generate();
|
|
144
|
+
const bob = await IdentityKeypair.generate();
|
|
145
|
+
|
|
146
|
+
const initiator = new NoiseXXHandshake('initiator', alice);
|
|
147
|
+
const responder = new NoiseXXHandshake('responder', bob);
|
|
148
|
+
|
|
149
|
+
const msg1 = (initiator.step() as { type: 'send_message'; data: Uint8Array }).data;
|
|
150
|
+
responder.step(msg1);
|
|
151
|
+
|
|
152
|
+
expect(() => initiator.step(new Uint8Array(10))).toThrow(CairnError);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('message 3 too short rejected', async () => {
|
|
156
|
+
const alice = await IdentityKeypair.generate();
|
|
157
|
+
const bob = await IdentityKeypair.generate();
|
|
158
|
+
|
|
159
|
+
const initiator = new NoiseXXHandshake('initiator', alice);
|
|
160
|
+
const responder = new NoiseXXHandshake('responder', bob);
|
|
161
|
+
|
|
162
|
+
const msg1 = (initiator.step() as { type: 'send_message'; data: Uint8Array }).data;
|
|
163
|
+
responder.step(msg1);
|
|
164
|
+
|
|
165
|
+
expect(() => responder.step(new Uint8Array(5))).toThrow(CairnError);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('tampered message 2 rejected', async () => {
|
|
169
|
+
const alice = await IdentityKeypair.generate();
|
|
170
|
+
const bob = await IdentityKeypair.generate();
|
|
171
|
+
|
|
172
|
+
const initiator = new NoiseXXHandshake('initiator', alice);
|
|
173
|
+
const responder = new NoiseXXHandshake('responder', bob);
|
|
174
|
+
|
|
175
|
+
const msg1 = (initiator.step() as { type: 'send_message'; data: Uint8Array }).data;
|
|
176
|
+
const msg2 = (responder.step(msg1) as { type: 'send_message'; data: Uint8Array }).data;
|
|
177
|
+
|
|
178
|
+
// Tamper with encrypted portion
|
|
179
|
+
if (msg2.length > 40) msg2[40] ^= 0xff;
|
|
180
|
+
expect(() => initiator.step(msg2)).toThrow(CairnError);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('tampered message 3 rejected', async () => {
|
|
184
|
+
const alice = await IdentityKeypair.generate();
|
|
185
|
+
const bob = await IdentityKeypair.generate();
|
|
186
|
+
|
|
187
|
+
const initiator = new NoiseXXHandshake('initiator', alice);
|
|
188
|
+
const responder = new NoiseXXHandshake('responder', bob);
|
|
189
|
+
|
|
190
|
+
const msg1 = (initiator.step() as { type: 'send_message'; data: Uint8Array }).data;
|
|
191
|
+
const msg2 = (responder.step(msg1) as { type: 'send_message'; data: Uint8Array }).data;
|
|
192
|
+
const msg3 = (initiator.step(msg2) as { type: 'send_message'; data: Uint8Array }).data;
|
|
193
|
+
|
|
194
|
+
msg3[0] ^= 0xff;
|
|
195
|
+
expect(() => responder.step(msg3)).toThrow(CairnError);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('step after complete rejected', async () => {
|
|
199
|
+
const [, respResult] = await runHandshake();
|
|
200
|
+
// Create a new completed handshake and test further steps
|
|
201
|
+
const alice = await IdentityKeypair.generate();
|
|
202
|
+
const bob = await IdentityKeypair.generate();
|
|
203
|
+
|
|
204
|
+
const initiator = new NoiseXXHandshake('initiator', alice);
|
|
205
|
+
const responder = new NoiseXXHandshake('responder', bob);
|
|
206
|
+
|
|
207
|
+
const msg1 = (initiator.step() as { type: 'send_message'; data: Uint8Array }).data;
|
|
208
|
+
const msg2 = (responder.step(msg1) as { type: 'send_message'; data: Uint8Array }).data;
|
|
209
|
+
const msg3 = (initiator.step(msg2) as { type: 'send_message'; data: Uint8Array }).data;
|
|
210
|
+
responder.step(msg3);
|
|
211
|
+
|
|
212
|
+
expect(() => responder.step()).toThrow(CairnError);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('SAS derivation', () => {
|
|
217
|
+
it('numeric SAS matches between peers', async () => {
|
|
218
|
+
const [initResult, respResult] = await runHandshake();
|
|
219
|
+
const initSas = deriveNumericSas(initResult.transcriptHash);
|
|
220
|
+
const respSas = deriveNumericSas(respResult.transcriptHash);
|
|
221
|
+
expect(initSas).toBe(respSas);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('emoji SAS matches between peers', async () => {
|
|
225
|
+
const [initResult, respResult] = await runHandshake();
|
|
226
|
+
const initEmoji = deriveEmojiSas(initResult.transcriptHash);
|
|
227
|
+
const respEmoji = deriveEmojiSas(respResult.transcriptHash);
|
|
228
|
+
expect(initEmoji).toEqual(respEmoji);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('numeric SAS is 6 digits', () => {
|
|
232
|
+
const hash = new Uint8Array(32).fill(42);
|
|
233
|
+
const sas = deriveNumericSas(hash);
|
|
234
|
+
expect(sas.length).toBe(6);
|
|
235
|
+
expect(sas).toMatch(/^\d{6}$/);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('numeric SAS is zero-padded', () => {
|
|
239
|
+
// We can't easily force a specific result, but let's verify format
|
|
240
|
+
for (let i = 0; i < 10; i++) {
|
|
241
|
+
const hash = new Uint8Array(32).fill(i);
|
|
242
|
+
const sas = deriveNumericSas(hash);
|
|
243
|
+
expect(sas.length).toBe(6);
|
|
244
|
+
expect(sas).toMatch(/^\d{6}$/);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('numeric SAS is deterministic', () => {
|
|
249
|
+
const hash = new Uint8Array(32).fill(99);
|
|
250
|
+
expect(deriveNumericSas(hash)).toBe(deriveNumericSas(hash));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('different transcripts produce different SAS', () => {
|
|
254
|
+
const hash1 = new Uint8Array(32).fill(1);
|
|
255
|
+
const hash2 = new Uint8Array(32).fill(2);
|
|
256
|
+
expect(deriveNumericSas(hash1)).not.toBe(deriveNumericSas(hash2));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('emoji SAS returns 4 entries', () => {
|
|
260
|
+
const hash = new Uint8Array(32).fill(42);
|
|
261
|
+
const emojis = deriveEmojiSas(hash);
|
|
262
|
+
expect(emojis.length).toBe(4);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('emoji SAS is deterministic', () => {
|
|
266
|
+
const hash = new Uint8Array(32).fill(99);
|
|
267
|
+
expect(deriveEmojiSas(hash)).toEqual(deriveEmojiSas(hash));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('emoji SAS entries are from table', () => {
|
|
271
|
+
const hash = new Uint8Array(32).fill(77);
|
|
272
|
+
const emojis = deriveEmojiSas(hash);
|
|
273
|
+
for (const emoji of emojis) {
|
|
274
|
+
expect(EMOJI_TABLE).toContain(emoji);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('emoji table has 64 entries', () => {
|
|
279
|
+
expect(EMOJI_TABLE.length).toBe(64);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('SPAKE2', () => {
|
|
284
|
+
it('same password produces matching keys', () => {
|
|
285
|
+
const password = new TextEncoder().encode('test-password-42');
|
|
286
|
+
const alice = Spake2.startA(password);
|
|
287
|
+
const bob = Spake2.startB(password);
|
|
288
|
+
|
|
289
|
+
const aliceKey = alice.finish(bob.outboundMsg);
|
|
290
|
+
const bobKey = bob.finish(alice.outboundMsg);
|
|
291
|
+
|
|
292
|
+
expect(aliceKey).toEqual(bobKey);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('different passwords produce different keys', () => {
|
|
296
|
+
const alice = Spake2.startA(new TextEncoder().encode('password-1'));
|
|
297
|
+
const bob = Spake2.startB(new TextEncoder().encode('password-2'));
|
|
298
|
+
|
|
299
|
+
const aliceKey = alice.finish(bob.outboundMsg);
|
|
300
|
+
const bobKey = bob.finish(alice.outboundMsg);
|
|
301
|
+
|
|
302
|
+
expect(aliceKey).not.toEqual(bobKey);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('output key is 32 bytes', () => {
|
|
306
|
+
const password = new TextEncoder().encode('test');
|
|
307
|
+
const alice = Spake2.startA(password);
|
|
308
|
+
const bob = Spake2.startB(password);
|
|
309
|
+
const key = alice.finish(bob.outboundMsg);
|
|
310
|
+
expect(key.length).toBe(32);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('outbound message is 32 bytes (compressed Ed25519 point)', () => {
|
|
314
|
+
const password = new TextEncoder().encode('test');
|
|
315
|
+
const alice = Spake2.startA(password);
|
|
316
|
+
expect(alice.outboundMsg.length).toBe(32);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('different sessions produce different outbound messages', () => {
|
|
320
|
+
const password = new TextEncoder().encode('same-password');
|
|
321
|
+
const a1 = Spake2.startA(password);
|
|
322
|
+
const a2 = Spake2.startA(password);
|
|
323
|
+
expect(a1.outboundMsg).not.toEqual(a2.outboundMsg);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('SPAKE2 key integrates with Noise XX as PAKE secret', async () => {
|
|
327
|
+
const password = new TextEncoder().encode('pairing-pin-42');
|
|
328
|
+
|
|
329
|
+
// Both sides run SPAKE2 to derive a shared key
|
|
330
|
+
const spakeA = Spake2.startA(password);
|
|
331
|
+
const spakeB = Spake2.startB(password);
|
|
332
|
+
|
|
333
|
+
const pakeKeyA = spakeA.finish(spakeB.outboundMsg);
|
|
334
|
+
const pakeKeyB = spakeB.finish(spakeA.outboundMsg);
|
|
335
|
+
|
|
336
|
+
expect(pakeKeyA).toEqual(pakeKeyB);
|
|
337
|
+
|
|
338
|
+
// Use the SPAKE2 key as PAKE secret in Noise XX
|
|
339
|
+
const alice = await IdentityKeypair.generate();
|
|
340
|
+
const bob = await IdentityKeypair.generate();
|
|
341
|
+
|
|
342
|
+
const [initResult, respResult] = await runHandshakeWithIdentities(alice, bob, pakeKeyA);
|
|
343
|
+
|
|
344
|
+
expect(initResult.sessionKey).toEqual(respResult.sessionKey);
|
|
345
|
+
});
|
|
346
|
+
});
|