@openlv/core 0.2.0 → 0.3.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/dist/encryption/asymmetric.d.ts +16 -17
- package/dist/encryption/asymmetric.js +59 -63
- package/dist/encryption/asymmetric.js.map +1 -1
- package/dist/encryption/asymmetric.test.js +45 -25
- package/dist/encryption/asymmetric.test.js.map +1 -1
- package/dist/encryption/base64.d.ts +1 -1
- package/dist/encryption/base64.js +1 -8
- package/dist/encryption/base64.js.map +1 -1
- package/dist/encryption/handshake.d.ts +3 -10
- package/dist/encryption/handshake.js +9 -18
- package/dist/encryption/handshake.js.map +1 -1
- package/dist/encryption/handshake.test.js +8 -8
- package/dist/encryption/handshake.test.js.map +1 -1
- package/dist/encryption/index.d.ts +1 -1
- package/dist/encryption/index.js +1 -1
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/scope.d.ts +19 -0
- package/dist/scope.js +84 -0
- package/dist/scope.js.map +1 -0
- package/dist/scope.test.d.ts +1 -0
- package/dist/scope.test.js +140 -0
- package/dist/scope.test.js.map +1 -0
- package/dist/session.d.ts +5 -7
- package/dist/session.js.map +1 -1
- package/dist/url/index.js +1 -1
- package/dist/url/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/encryption/hash.d.ts +0 -13
- package/dist/encryption/hash.js +0 -27
- package/dist/encryption/hash.js.map +0 -1
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
export type
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export type PublicKey = Uint8Array<ArrayBuffer>;
|
|
2
|
+
export type KeyPair = {
|
|
3
|
+
publicKey: PublicKey;
|
|
4
|
+
secretKey: Uint8Array;
|
|
5
5
|
};
|
|
6
|
-
export type
|
|
7
|
-
|
|
8
|
-
decrypt: (
|
|
6
|
+
export type Cipher = {
|
|
7
|
+
encrypt: (message: string) => Promise<string>;
|
|
8
|
+
decrypt: (ciphertext: string) => Promise<string>;
|
|
9
9
|
};
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
encryptionKey: EncryptionKey;
|
|
15
|
-
decryptionKey: DecryptionKey;
|
|
10
|
+
export type AcceptedPeer = {
|
|
11
|
+
peerKey: PublicKey;
|
|
12
|
+
cipher: Cipher;
|
|
13
|
+
message: string;
|
|
16
14
|
};
|
|
17
|
-
export declare const generateKeyPair: () =>
|
|
18
|
-
export declare const
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
export declare const generateKeyPair: () => KeyPair;
|
|
16
|
+
export declare const parsePublicKey: (encoded: string) => PublicKey | undefined;
|
|
17
|
+
export declare const hashPublicKey: (publicKey: PublicKey) => Promise<string>;
|
|
18
|
+
export declare const createPeerCipher: (keyPair: KeyPair, peerKey: PublicKey) => Promise<Cipher>;
|
|
19
|
+
export declare const acceptPeer: (keyPair: KeyPair, ciphertext: string) => Promise<AcceptedPeer>;
|
|
@@ -1,82 +1,78 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { gcm } from "@noble/ciphers/aes.js";
|
|
2
|
+
import { bytesToHex, bytesToUtf8, concatBytes, equalBytes, utf8ToBytes, } from "@noble/ciphers/utils.js";
|
|
2
3
|
import { x25519 } from "@noble/curves/ed25519.js";
|
|
3
4
|
import { fromBase64, toBase64 } from "./base64.js";
|
|
4
5
|
const PUBLIC_KEY_BYTES = 32;
|
|
5
|
-
const NONCE_BYTES =
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const NONCE_BYTES = 12;
|
|
7
|
+
const HASH_LENGTH = 16;
|
|
8
|
+
const KEY_INFO = new TextEncoder().encode("openlv/1 peer-key");
|
|
9
|
+
export const generateKeyPair = () => {
|
|
10
|
+
const secretKey = x25519.utils.randomSecretKey();
|
|
11
|
+
return {
|
|
12
|
+
publicKey: Uint8Array.from(x25519.getPublicKey(secretKey)),
|
|
13
|
+
secretKey,
|
|
14
|
+
};
|
|
12
15
|
};
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
export const parsePublicKey = (encoded) => {
|
|
17
|
+
try {
|
|
18
|
+
const publicKey = fromBase64(encoded);
|
|
19
|
+
return publicKey.length === PUBLIC_KEY_BYTES ? publicKey : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
17
23
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
24
|
+
};
|
|
25
|
+
export const hashPublicKey = async (publicKey) => {
|
|
26
|
+
const hash = await crypto.subtle.digest("SHA-256", publicKey);
|
|
27
|
+
return bytesToHex(new Uint8Array(hash)).slice(0, HASH_LENGTH);
|
|
28
|
+
};
|
|
29
|
+
const deriveKeys = async (keyPair, peerKey) => {
|
|
30
|
+
const shared = await crypto.subtle.importKey("raw", Uint8Array.from(x25519.getSharedSecret(keyPair.secretKey, peerKey)), "HKDF", false, ["deriveBits"]);
|
|
31
|
+
const deriveDirectionKey = async (sender, recipient) => {
|
|
32
|
+
const key = await crypto.subtle.deriveBits({
|
|
33
|
+
name: "HKDF",
|
|
34
|
+
hash: "SHA-256",
|
|
35
|
+
salt: new Uint8Array(),
|
|
36
|
+
info: Uint8Array.from(concatBytes(KEY_INFO, sender, recipient)),
|
|
37
|
+
}, shared, 256);
|
|
38
|
+
return new Uint8Array(key);
|
|
39
|
+
};
|
|
21
40
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
ciphertext,
|
|
41
|
+
sendKey: await deriveDirectionKey(keyPair.publicKey, peerKey),
|
|
42
|
+
receiveKey: await deriveDirectionKey(peerKey, keyPair.publicKey),
|
|
25
43
|
};
|
|
26
44
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
const decodeCiphertext = (ciphertext) => {
|
|
46
|
+
const bytes = fromBase64(ciphertext);
|
|
47
|
+
if (bytes.length <= PUBLIC_KEY_BYTES + NONCE_BYTES) {
|
|
48
|
+
throw new Error("Ciphertext is malformed");
|
|
30
49
|
}
|
|
31
|
-
const serializedPublicKey = toBase64(publicKey);
|
|
32
50
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const ephemeralPublic = x25519.getPublicKey(ephemeralSecret);
|
|
37
|
-
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
38
|
-
const { seal } = secretbox(x25519.getSharedSecret(ephemeralSecret, publicKey), nonce);
|
|
39
|
-
const ciphertext = seal(new TextEncoder().encode(message));
|
|
40
|
-
return composePayload(ephemeralPublic, nonce, ciphertext);
|
|
41
|
-
},
|
|
51
|
+
peerKey: bytes.slice(0, PUBLIC_KEY_BYTES),
|
|
52
|
+
nonce: bytes.slice(PUBLIC_KEY_BYTES, PUBLIC_KEY_BYTES + NONCE_BYTES),
|
|
53
|
+
ciphertext: bytes.slice(PUBLIC_KEY_BYTES + NONCE_BYTES),
|
|
42
54
|
};
|
|
43
55
|
};
|
|
44
|
-
export const
|
|
45
|
-
|
|
46
|
-
const serializedSecretKey = toBase64(secretKey);
|
|
56
|
+
export const createPeerCipher = async (keyPair, peerKey) => {
|
|
57
|
+
const { sendKey, receiveKey } = await deriveKeys(keyPair, peerKey);
|
|
47
58
|
return {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
encrypt: async (message) => {
|
|
60
|
+
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
61
|
+
const ciphertext = gcm(sendKey, nonce).encrypt(utf8ToBytes(message));
|
|
62
|
+
return toBase64(concatBytes(keyPair.publicKey, nonce, ciphertext));
|
|
63
|
+
},
|
|
64
|
+
decrypt: async (ciphertext) => {
|
|
65
|
+
const decoded = decodeCiphertext(ciphertext);
|
|
66
|
+
if (!equalBytes(decoded.peerKey, peerKey)) {
|
|
67
|
+
throw new Error("Ciphertext is not from the peer");
|
|
52
68
|
}
|
|
53
|
-
|
|
54
|
-
const { open } = secretbox(x25519.getSharedSecret(secretKey, ephemeralPublicKey), nonce);
|
|
55
|
-
return new TextDecoder().decode(open(ciphertext));
|
|
69
|
+
return bytesToUtf8(gcm(receiveKey, decoded.nonce).decrypt(decoded.ciphertext));
|
|
56
70
|
},
|
|
57
71
|
};
|
|
58
72
|
};
|
|
59
|
-
export const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
return {
|
|
63
|
-
encryptionKey: await createEncryptionKey(publicKey),
|
|
64
|
-
decryptionKey: await createDecryptionKey(secretKey),
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
export const initEncryptionKeys = async (initParameters) => {
|
|
68
|
-
const { encryptionKey, decryptionKey } = await generateKeyPair();
|
|
69
|
-
// Note: the peer key is deliberately left undefined until discovered --
|
|
70
|
-
// falling back to our own key would silently encrypt to ourselves.
|
|
71
|
-
const publicKey = initParameters && "publicKey" in initParameters
|
|
72
|
-
? initParameters.publicKey
|
|
73
|
-
: undefined;
|
|
74
|
-
return {
|
|
75
|
-
encryptionKey,
|
|
76
|
-
decryptionKey,
|
|
77
|
-
relyingEncryptionKey: publicKey
|
|
78
|
-
? await parseEncryptionKey(publicKey)
|
|
79
|
-
: undefined,
|
|
80
|
-
};
|
|
73
|
+
export const acceptPeer = async (keyPair, ciphertext) => {
|
|
74
|
+
const { peerKey } = decodeCiphertext(ciphertext);
|
|
75
|
+
const cipher = await createPeerCipher(keyPair, peerKey);
|
|
76
|
+
return { peerKey, cipher, message: await cipher.decrypt(ciphertext) };
|
|
81
77
|
};
|
|
82
78
|
//# sourceMappingURL=asymmetric.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asymmetric.js","sourceRoot":"","sources":["../../src/encryption/asymmetric.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"asymmetric.js","sourceRoot":"","sources":["../../src/encryption/asymmetric.ts"],"names":[],"mappings":"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","sourcesContent":["import { gcm } from \"@noble/ciphers/aes.js\";\nimport {\n bytesToHex,\n bytesToUtf8,\n concatBytes,\n equalBytes,\n utf8ToBytes,\n} from \"@noble/ciphers/utils.js\";\nimport { x25519 } from \"@noble/curves/ed25519.js\";\n\nimport { fromBase64, toBase64 } from \"./base64.js\";\n\nconst PUBLIC_KEY_BYTES = 32;\nconst NONCE_BYTES = 12;\nconst HASH_LENGTH = 16;\nconst KEY_INFO = new TextEncoder().encode(\"openlv/1 peer-key\");\n\nexport type PublicKey = Uint8Array<ArrayBuffer>;\n\nexport type KeyPair = {\n publicKey: PublicKey;\n secretKey: Uint8Array;\n};\n\nexport type Cipher = {\n encrypt: (message: string) => Promise<string>;\n decrypt: (ciphertext: string) => Promise<string>;\n};\n\nexport type AcceptedPeer = {\n peerKey: PublicKey;\n cipher: Cipher;\n message: string;\n};\n\ntype DecodedCiphertext = {\n peerKey: PublicKey;\n nonce: Uint8Array;\n ciphertext: Uint8Array;\n};\n\nexport const generateKeyPair = (): KeyPair => {\n const secretKey = x25519.utils.randomSecretKey();\n\n return {\n publicKey: Uint8Array.from(x25519.getPublicKey(secretKey)),\n secretKey,\n };\n};\n\nexport const parsePublicKey = (encoded: string): PublicKey | undefined => {\n try {\n const publicKey = fromBase64(encoded);\n\n return publicKey.length === PUBLIC_KEY_BYTES ? publicKey : undefined;\n }\n catch {\n return undefined;\n }\n};\n\nexport const hashPublicKey = async (publicKey: PublicKey): Promise<string> => {\n const hash = await crypto.subtle.digest(\"SHA-256\", publicKey);\n\n return bytesToHex(new Uint8Array(hash)).slice(0, HASH_LENGTH);\n};\n\nconst deriveKeys = async (\n keyPair: KeyPair,\n peerKey: PublicKey,\n): Promise<{ sendKey: Uint8Array; receiveKey: Uint8Array; }> => {\n const shared = await crypto.subtle.importKey(\n \"raw\",\n Uint8Array.from(x25519.getSharedSecret(keyPair.secretKey, peerKey)),\n \"HKDF\",\n false,\n [\"deriveBits\"],\n );\n const deriveDirectionKey = async (sender: PublicKey, recipient: PublicKey) => {\n const key = await crypto.subtle.deriveBits(\n {\n name: \"HKDF\",\n hash: \"SHA-256\",\n salt: new Uint8Array(),\n info: Uint8Array.from(concatBytes(KEY_INFO, sender, recipient)),\n },\n shared,\n 256,\n );\n\n return new Uint8Array(key);\n };\n\n return {\n sendKey: await deriveDirectionKey(keyPair.publicKey, peerKey),\n receiveKey: await deriveDirectionKey(peerKey, keyPair.publicKey),\n };\n};\n\nconst decodeCiphertext = (ciphertext: string): DecodedCiphertext => {\n const bytes = fromBase64(ciphertext);\n\n if (bytes.length <= PUBLIC_KEY_BYTES + NONCE_BYTES) {\n throw new Error(\"Ciphertext is malformed\");\n }\n\n return {\n peerKey: bytes.slice(0, PUBLIC_KEY_BYTES),\n nonce: bytes.slice(PUBLIC_KEY_BYTES, PUBLIC_KEY_BYTES + NONCE_BYTES),\n ciphertext: bytes.slice(PUBLIC_KEY_BYTES + NONCE_BYTES),\n };\n};\n\nexport const createPeerCipher = async (\n keyPair: KeyPair,\n peerKey: PublicKey,\n): Promise<Cipher> => {\n const { sendKey, receiveKey } = await deriveKeys(keyPair, peerKey);\n\n return {\n encrypt: async (message) => {\n const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));\n const ciphertext = gcm(sendKey, nonce).encrypt(utf8ToBytes(message));\n\n return toBase64(concatBytes(keyPair.publicKey, nonce, ciphertext));\n },\n decrypt: async (ciphertext) => {\n const decoded = decodeCiphertext(ciphertext);\n\n if (!equalBytes(decoded.peerKey, peerKey)) {\n throw new Error(\"Ciphertext is not from the peer\");\n }\n\n return bytesToUtf8(gcm(receiveKey, decoded.nonce).decrypt(decoded.ciphertext));\n },\n };\n};\n\nexport const acceptPeer = async (\n keyPair: KeyPair,\n ciphertext: string,\n): Promise<AcceptedPeer> => {\n const { peerKey } = decodeCiphertext(ciphertext);\n const cipher = await createPeerCipher(keyPair, peerKey);\n\n return { peerKey, cipher, message: await cipher.decrypt(ciphertext) };\n};\n"]}
|
|
@@ -1,36 +1,56 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
expect(keyPair.
|
|
8
|
-
expect(keyPair.
|
|
2
|
+
import { createPeerCipher, generateKeyPair } from "./asymmetric.js";
|
|
3
|
+
import { fromBase64, toBase64 } from "./base64.js";
|
|
4
|
+
describe("Peer encryption", () => {
|
|
5
|
+
test("should generate a key pair", () => {
|
|
6
|
+
const keyPair = generateKeyPair();
|
|
7
|
+
expect(keyPair.publicKey).toHaveLength(32);
|
|
8
|
+
expect(keyPair.secretKey).toHaveLength(32);
|
|
9
9
|
});
|
|
10
10
|
test("should encrypt and decrypt a message", async () => {
|
|
11
|
-
const
|
|
11
|
+
const alice = generateKeyPair();
|
|
12
|
+
const bob = generateKeyPair();
|
|
12
13
|
const message = "test";
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
expect(decrypted).toBe(message);
|
|
17
|
-
});
|
|
18
|
-
test("should encrypt and decrypt a message with a parsed key", async () => {
|
|
19
|
-
const { encryptionKey, decryptionKey } = await generateKeyPair();
|
|
20
|
-
const message = "test";
|
|
21
|
-
const key = encryptionKey.toString();
|
|
22
|
-
const parsedKey = await parseEncryptionKey(key);
|
|
23
|
-
const parsedEncrypted = await parsedKey.encrypt(message);
|
|
24
|
-
const decodedParsedEncrypted = await decryptionKey.decrypt(parsedEncrypted);
|
|
25
|
-
expect(decodedParsedEncrypted).toBe(message);
|
|
14
|
+
const toBob = await createPeerCipher(alice, bob.publicKey);
|
|
15
|
+
const fromAlice = await createPeerCipher(bob, alice.publicKey);
|
|
16
|
+
await expect(fromAlice.decrypt(await toBob.encrypt(message))).resolves.toBe(message);
|
|
26
17
|
});
|
|
27
18
|
test("should be able to encrypt large blob", async () => {
|
|
28
|
-
const
|
|
19
|
+
const keyPair = generateKeyPair();
|
|
20
|
+
const cipher = await createPeerCipher(keyPair, keyPair.publicKey);
|
|
29
21
|
const message = Array.from({ length: 1024 * 1024 }).fill("a")
|
|
30
22
|
.join("");
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
await expect(cipher.decrypt(await cipher.encrypt(message))).resolves.toBe(message);
|
|
24
|
+
});
|
|
25
|
+
test("should decrypt the shared test vector", async () => {
|
|
26
|
+
// Same bytes as the Rust test: sender secret 0x01 * 32, receiver secret 0x02 * 32, nonce 0x03 * 12.
|
|
27
|
+
const keyPair = {
|
|
28
|
+
publicKey: fromBase64("zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk="),
|
|
29
|
+
secretKey: fromBase64("AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI="),
|
|
30
|
+
};
|
|
31
|
+
const peerKey = fromBase64("pOCSkrZRwni5dyxWn1+puxPZBrRqtoyd+dwrRAn4ogk=");
|
|
32
|
+
const cipher = await createPeerCipher(keyPair, peerKey);
|
|
33
|
+
const ciphertext = "pOCSkrZRwni5dyxWn1+puxPZBrRqtoyd+dwrRAn4ogkDAwMDAwMDAwMDAwOL3klAuRZ1a2kM5EryzNjgXIUN6C2hj8Att8QbUqi3iEg5GNHlNL2Q3C//";
|
|
34
|
+
await expect(cipher.decrypt(ciphertext)).resolves.toBe("openlv peer-key test vector");
|
|
35
|
+
});
|
|
36
|
+
test("should reject a ciphertext from a different peer key", async () => {
|
|
37
|
+
const alice = generateKeyPair();
|
|
38
|
+
const bob = generateKeyPair();
|
|
39
|
+
const mallory = generateKeyPair();
|
|
40
|
+
const fromMallory = await createPeerCipher(mallory, bob.publicKey);
|
|
41
|
+
const expectingMallory = await createPeerCipher(bob, mallory.publicKey);
|
|
42
|
+
const expectingAlice = await createPeerCipher(bob, alice.publicKey);
|
|
43
|
+
const ciphertext = await fromMallory.encrypt("test");
|
|
44
|
+
await expect(expectingMallory.decrypt(ciphertext)).resolves.toBe("test");
|
|
45
|
+
await expect(expectingAlice.decrypt(ciphertext)).rejects.toThrow("Ciphertext is not from the peer");
|
|
46
|
+
});
|
|
47
|
+
test("should reject a ciphertext reflected back to its sender", async () => {
|
|
48
|
+
const alice = generateKeyPair();
|
|
49
|
+
const bob = generateKeyPair();
|
|
50
|
+
const withBob = await createPeerCipher(alice, bob.publicKey);
|
|
51
|
+
const reflected = fromBase64(await withBob.encrypt("test"));
|
|
52
|
+
reflected.set(bob.publicKey, 0);
|
|
53
|
+
await expect(withBob.decrypt(toBase64(reflected))).rejects.toThrow();
|
|
34
54
|
});
|
|
35
55
|
});
|
|
36
56
|
//# sourceMappingURL=asymmetric.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asymmetric.test.js","sourceRoot":"","sources":["../../src/encryption/asymmetric.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"asymmetric.test.js","sourceRoot":"","sources":["../../src/encryption/asymmetric.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, test } from \"vitest\";\n\nimport { createPeerCipher, generateKeyPair } from \"./asymmetric.js\";\nimport { fromBase64, toBase64 } from \"./base64.js\";\n\ndescribe(\"Peer encryption\", () => {\n test(\"should generate a key pair\", () => {\n const keyPair = generateKeyPair();\n\n expect(keyPair.publicKey).toHaveLength(32);\n expect(keyPair.secretKey).toHaveLength(32);\n });\n\n test(\"should encrypt and decrypt a message\", async () => {\n const alice = generateKeyPair();\n const bob = generateKeyPair();\n const message = \"test\";\n\n const toBob = await createPeerCipher(alice, bob.publicKey);\n const fromAlice = await createPeerCipher(bob, alice.publicKey);\n\n await expect(fromAlice.decrypt(await toBob.encrypt(message))).resolves.toBe(message);\n });\n\n test(\"should be able to encrypt large blob\", async () => {\n const keyPair = generateKeyPair();\n const cipher = await createPeerCipher(keyPair, keyPair.publicKey);\n const message = Array.from({ length: 1024 * 1024 }).fill(\"a\")\n .join(\"\");\n\n await expect(cipher.decrypt(await cipher.encrypt(message))).resolves.toBe(message);\n });\n\n test(\"should decrypt the shared test vector\", async () => {\n // Same bytes as the Rust test: sender secret 0x01 * 32, receiver secret 0x02 * 32, nonce 0x03 * 12.\n const keyPair = {\n publicKey: fromBase64(\"zo060cy2M+x7cMF4FKXHbs0CloUFDTRHRboFhw5YfVk=\"),\n secretKey: fromBase64(\"AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=\"),\n };\n const peerKey = fromBase64(\"pOCSkrZRwni5dyxWn1+puxPZBrRqtoyd+dwrRAn4ogk=\");\n const cipher = await createPeerCipher(keyPair, peerKey);\n const ciphertext = \"pOCSkrZRwni5dyxWn1+puxPZBrRqtoyd+dwrRAn4ogkDAwMDAwMDAwMDAwOL3klAuRZ1a2kM5EryzNjgXIUN6C2hj8Att8QbUqi3iEg5GNHlNL2Q3C//\";\n\n await expect(cipher.decrypt(ciphertext)).resolves.toBe(\"openlv peer-key test vector\");\n });\n\n test(\"should reject a ciphertext from a different peer key\", async () => {\n const alice = generateKeyPair();\n const bob = generateKeyPair();\n const mallory = generateKeyPair();\n const fromMallory = await createPeerCipher(mallory, bob.publicKey);\n const expectingMallory = await createPeerCipher(bob, mallory.publicKey);\n const expectingAlice = await createPeerCipher(bob, alice.publicKey);\n const ciphertext = await fromMallory.encrypt(\"test\");\n\n await expect(expectingMallory.decrypt(ciphertext)).resolves.toBe(\"test\");\n await expect(expectingAlice.decrypt(ciphertext)).rejects.toThrow(\"Ciphertext is not from the peer\");\n });\n\n test(\"should reject a ciphertext reflected back to its sender\", async () => {\n const alice = generateKeyPair();\n const bob = generateKeyPair();\n const withBob = await createPeerCipher(alice, bob.publicKey);\n const reflected = fromBase64(await withBob.encrypt(\"test\"));\n\n reflected.set(bob.publicKey, 0);\n\n await expect(withBob.decrypt(toBase64(reflected))).rejects.toThrow();\n });\n});\n"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const toBase64: (data: Uint8Array) => string;
|
|
2
|
-
export declare const fromBase64: (value: string) => Uint8Array
|
|
2
|
+
export declare const fromBase64: (value: string) => Uint8Array<ArrayBuffer>;
|
|
@@ -5,12 +5,5 @@ export const toBase64 = (data) => {
|
|
|
5
5
|
}
|
|
6
6
|
return btoa(binary);
|
|
7
7
|
};
|
|
8
|
-
export const fromBase64 = (value) =>
|
|
9
|
-
const binary = atob(value);
|
|
10
|
-
const bytes = new Uint8Array(binary.length);
|
|
11
|
-
for (let index = 0; index < binary.length; index += 1) {
|
|
12
|
-
bytes[index] = binary.codePointAt(index);
|
|
13
|
-
}
|
|
14
|
-
return bytes;
|
|
15
|
-
};
|
|
8
|
+
export const fromBase64 = (value) => Uint8Array.from(atob(value), char => char.codePointAt(0) ?? 0);
|
|
16
9
|
//# sourceMappingURL=base64.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base64.js","sourceRoot":"","sources":["../../src/encryption/base64.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAgB,EAAU,EAAE;IACnD,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;QACzB,MAAM,IAAI,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAa,
|
|
1
|
+
{"version":3,"file":"base64.js","sourceRoot":"","sources":["../../src/encryption/base64.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAgB,EAAU,EAAE;IACnD,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;QACzB,MAAM,IAAI,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAa,EAA2B,EAAE,CACnE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC","sourcesContent":["export const toBase64 = (data: Uint8Array): string => {\n let binary = \"\";\n\n for (const datum of data) {\n binary += String.fromCodePoint(datum);\n }\n\n return btoa(binary);\n};\n\nexport const fromBase64 = (value: string): Uint8Array<ArrayBuffer> =>\n Uint8Array.from(atob(value), char => char.codePointAt(0) ?? 0);\n"]}
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
decrypt: (message: string) => Promise<string>;
|
|
5
|
-
};
|
|
6
|
-
export declare const deriveSymmetricKey: (k: string) => Promise<SymmetricKey>;
|
|
7
|
-
/**
|
|
8
|
-
* Generate a 16-byte random shared key and return as hex
|
|
9
|
-
*/
|
|
10
|
-
export declare const generateHandshakeKey: () => Promise<SymmetricKey>;
|
|
1
|
+
import type { Cipher } from "./asymmetric.js";
|
|
2
|
+
export declare const createHandshakeCipher: (k: string) => Promise<Cipher>;
|
|
3
|
+
export declare const generateHandshakeKey: () => string;
|
|
@@ -1,40 +1,31 @@
|
|
|
1
|
-
import { bytesToHex, hexToBytes } from "@noble/ciphers/utils.js";
|
|
1
|
+
import { bytesToHex, concatBytes, hexToBytes } from "@noble/ciphers/utils.js";
|
|
2
|
+
import { fromBase64, toBase64 } from "./base64.js";
|
|
2
3
|
const HANDSHAKE_KEY_BYTES = 16;
|
|
3
4
|
const IV_BYTES = 12;
|
|
4
|
-
export const
|
|
5
|
+
export const createHandshakeCipher = async (k) => {
|
|
5
6
|
const baseKey = Uint8Array.from(hexToBytes(k));
|
|
6
7
|
if (baseKey.length !== HANDSHAKE_KEY_BYTES) {
|
|
7
8
|
throw new Error(`Handshake key must be ${HANDSHAKE_KEY_BYTES} bytes, got ${baseKey.length}`);
|
|
8
9
|
}
|
|
9
10
|
const sharedSecret = await crypto.subtle.importKey("raw", baseKey, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
10
11
|
return {
|
|
11
|
-
toString: () => k,
|
|
12
12
|
encrypt: async (message) => {
|
|
13
13
|
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
14
14
|
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, sharedSecret, new TextEncoder().encode(message));
|
|
15
|
-
|
|
16
|
-
combined.set(iv);
|
|
17
|
-
combined.set(new Uint8Array(encrypted), iv.length);
|
|
18
|
-
return btoa(String.fromCharCode.apply(null, Array.from(combined)));
|
|
15
|
+
return toBase64(concatBytes(iv, new Uint8Array(encrypted)));
|
|
19
16
|
},
|
|
20
|
-
decrypt: async (
|
|
21
|
-
const combined =
|
|
22
|
-
.split("")
|
|
23
|
-
.map(char => char.codePointAt(0)));
|
|
17
|
+
decrypt: async (ciphertext) => {
|
|
18
|
+
const combined = fromBase64(ciphertext);
|
|
24
19
|
const iv = combined.slice(0, IV_BYTES);
|
|
25
20
|
const encrypted = combined.slice(IV_BYTES);
|
|
26
21
|
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, sharedSecret, encrypted);
|
|
27
|
-
|
|
28
|
-
return decoder.decode(decrypted);
|
|
22
|
+
return new TextDecoder().decode(decrypted);
|
|
29
23
|
},
|
|
30
24
|
};
|
|
31
25
|
};
|
|
32
|
-
|
|
33
|
-
* Generate a 16-byte random shared key and return as hex
|
|
34
|
-
*/
|
|
35
|
-
export const generateHandshakeKey = async () => {
|
|
26
|
+
export const generateHandshakeKey = () => {
|
|
36
27
|
const array = new Uint8Array(HANDSHAKE_KEY_BYTES);
|
|
37
28
|
crypto.getRandomValues(array);
|
|
38
|
-
return
|
|
29
|
+
return bytesToHex(array);
|
|
39
30
|
};
|
|
40
31
|
//# sourceMappingURL=handshake.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../../src/encryption/handshake.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../../src/encryption/handshake.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAC/B,MAAM,QAAQ,GAAG,EAAE,CAAC;AAEpB,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EAAE,CAAS,EAAmB,EAAE;IACxE,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAE/C,IAAI,OAAO,CAAC,MAAM,KAAK,mBAAmB,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,yBAAyB,mBAAmB,eAAe,OAAO,CAAC,MAAM,EAAE,CAC5E,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAChD,KAAK,EACL,OAAO,EACP,EAAE,IAAI,EAAE,SAAS,EAAE,EACnB,KAAK,EACL,CAAC,SAAS,EAAE,SAAS,CAAC,CACvB,CAAC;IAEF,OAAO;QACL,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACzB,MAAM,EAAE,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC5D,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CAC3C,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,EACvB,YAAY,EACZ,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAClC,CAAC;YAEF,OAAO,QAAQ,CAAC,WAAW,CAAC,EAAE,EAAE,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9D,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;YAC5B,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;YACxC,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;YACvC,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC3C,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CAC3C,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,EACvB,YAAY,EACZ,SAAS,CACV,CAAC;YAEF,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAW,EAAE;IAC/C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,mBAAmB,CAAC,CAAC;IAElD,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAE9B,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC,CAAC","sourcesContent":["import { bytesToHex, concatBytes, hexToBytes } from \"@noble/ciphers/utils.js\";\n\nimport type { Cipher } from \"./asymmetric.js\";\nimport { fromBase64, toBase64 } from \"./base64.js\";\n\nconst HANDSHAKE_KEY_BYTES = 16;\nconst IV_BYTES = 12;\n\nexport const createHandshakeCipher = async (k: string): Promise<Cipher> => {\n const baseKey = Uint8Array.from(hexToBytes(k));\n\n if (baseKey.length !== HANDSHAKE_KEY_BYTES) {\n throw new Error(\n `Handshake key must be ${HANDSHAKE_KEY_BYTES} bytes, got ${baseKey.length}`,\n );\n }\n\n const sharedSecret = await crypto.subtle.importKey(\n \"raw\",\n baseKey,\n { name: \"AES-GCM\" },\n false,\n [\"encrypt\", \"decrypt\"],\n );\n\n return {\n encrypt: async (message) => {\n const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n const encrypted = await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv },\n sharedSecret,\n new TextEncoder().encode(message),\n );\n\n return toBase64(concatBytes(iv, new Uint8Array(encrypted)));\n },\n decrypt: async (ciphertext) => {\n const combined = fromBase64(ciphertext);\n const iv = combined.slice(0, IV_BYTES);\n const encrypted = combined.slice(IV_BYTES);\n const decrypted = await crypto.subtle.decrypt(\n { name: \"AES-GCM\", iv },\n sharedSecret,\n encrypted,\n );\n\n return new TextDecoder().decode(decrypted);\n },\n };\n};\n\nexport const generateHandshakeKey = (): string => {\n const array = new Uint8Array(HANDSHAKE_KEY_BYTES);\n\n crypto.getRandomValues(array);\n\n return bytesToHex(array);\n};\n"]}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { createHandshakeCipher, generateHandshakeKey } from "./handshake.js";
|
|
3
3
|
describe("Handshake", async () => {
|
|
4
|
-
const
|
|
4
|
+
const k = generateHandshakeKey();
|
|
5
|
+
const cipher = await createHandshakeCipher(k);
|
|
5
6
|
test("should generate a 16-byte random shared key", () => {
|
|
6
|
-
expect(
|
|
7
|
-
expect(key.toString()).toHaveLength(32);
|
|
7
|
+
expect(k).toHaveLength(32);
|
|
8
8
|
});
|
|
9
9
|
test("should encrypt and decrypt a message", async () => {
|
|
10
10
|
const message = "test";
|
|
11
|
-
const encrypted = await
|
|
12
|
-
const decrypted = await
|
|
11
|
+
const encrypted = await cipher.encrypt(message);
|
|
12
|
+
const decrypted = await cipher.decrypt(encrypted);
|
|
13
13
|
expect(decrypted).toBe(message);
|
|
14
14
|
});
|
|
15
15
|
test("should derive symmetric key from decoded hex bytes", async () => {
|
|
16
|
-
const key = await
|
|
16
|
+
const key = await createHandshakeCipher("000102030405060708090a0b0c0d0e0f");
|
|
17
17
|
const encrypted = await key.encrypt("hello");
|
|
18
|
-
const rederived = await
|
|
18
|
+
const rederived = await createHandshakeCipher("000102030405060708090a0b0c0d0e0f");
|
|
19
19
|
await expect(rederived.decrypt(encrypted)).resolves.toBe("hello");
|
|
20
20
|
});
|
|
21
21
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handshake.test.js","sourceRoot":"","sources":["../../src/encryption/handshake.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"handshake.test.js","sourceRoot":"","sources":["../../src/encryption/handshake.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,QAAQ,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE;IAC/B,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;IACjC,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAE9C,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,OAAO,GAAG,MAAM,CAAC;QACvB,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAChD,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAElD,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,GAAG,GAAG,MAAM,qBAAqB,CAAC,kCAAkC,CAAC,CAAC;QAC5E,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,MAAM,qBAAqB,CAAC,kCAAkC,CAAC,CAAC;QAElF,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, test } from \"vitest\";\n\nimport { createHandshakeCipher, generateHandshakeKey } from \"./handshake.js\";\n\ndescribe(\"Handshake\", async () => {\n const k = generateHandshakeKey();\n const cipher = await createHandshakeCipher(k);\n\n test(\"should generate a 16-byte random shared key\", () => {\n expect(k).toHaveLength(32);\n });\n\n test(\"should encrypt and decrypt a message\", async () => {\n const message = \"test\";\n const encrypted = await cipher.encrypt(message);\n const decrypted = await cipher.decrypt(encrypted);\n\n expect(decrypted).toBe(message);\n });\n\n test(\"should derive symmetric key from decoded hex bytes\", async () => {\n const key = await createHandshakeCipher(\"000102030405060708090a0b0c0d0e0f\");\n const encrypted = await key.encrypt(\"hello\");\n const rederived = await createHandshakeCipher(\"000102030405060708090a0b0c0d0e0f\");\n\n await expect(rederived.decrypt(encrypted)).resolves.toBe(\"hello\");\n });\n});\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** biome-ignore-all lint/performance/noReExportAll: package entrypoint */
|
|
2
2
|
/** biome-ignore-all lint/performance/noBarrelFile: package entrypoint */
|
|
3
3
|
export * from "./asymmetric.js";
|
|
4
|
+
export * from "./base64.js";
|
|
4
5
|
export * from "./handshake.js";
|
|
5
|
-
export * from "./hash.js";
|
|
6
6
|
export * from "./random.js";
|
package/dist/encryption/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** biome-ignore-all lint/performance/noReExportAll: package entrypoint */
|
|
2
2
|
/** biome-ignore-all lint/performance/noBarrelFile: package entrypoint */
|
|
3
3
|
export * from "./asymmetric.js";
|
|
4
|
+
export * from "./base64.js";
|
|
4
5
|
export * from "./handshake.js";
|
|
5
|
-
export * from "./hash.js";
|
|
6
6
|
export * from "./random.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/encryption/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,yEAAyE;AAEzE,cAAc,iBAAiB,CAAC;AAChC,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/encryption/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,yEAAyE;AAEzE,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC","sourcesContent":["/** biome-ignore-all lint/performance/noReExportAll: package entrypoint */\n/** biome-ignore-all lint/performance/noBarrelFile: package entrypoint */\n\nexport * from \"./asymmetric.js\";\nexport * from \"./base64.js\";\nexport * from \"./handshake.js\";\nexport * from \"./random.js\";\n"]}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,yEAAyE;AAEzE,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC","sourcesContent":["/** biome-ignore-all lint/performance/noReExportAll: package entrypoint */\n/** biome-ignore-all lint/performance/noBarrelFile: package entrypoint */\n\nexport * from \"./log.js\";\nexport * from \"./make.js\";\nexport * from \"./observable.js\";\nexport * from \"./session.js\";\nexport * from \"./url/index.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,yEAAyE;AAEzE,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC","sourcesContent":["/** biome-ignore-all lint/performance/noReExportAll: package entrypoint */\n/** biome-ignore-all lint/performance/noBarrelFile: package entrypoint */\n\nexport * from \"./log.js\";\nexport * from \"./make.js\";\nexport * from \"./observable.js\";\nexport * from \"./scope.js\";\nexport * from \"./session.js\";\nexport * from \"./url/index.js\";\n"]}
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EventEmitter } from "eventemitter3";
|
|
2
|
+
import type { Observable } from "./observable.js";
|
|
3
|
+
export type Cleanup = () => void | Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Owns cleanups, listeners and timers for one lifetime. `close` runs every
|
|
6
|
+
* cleanup once, newest first; a cleanup that throws does not stop the others,
|
|
7
|
+
* and `close` then rejects with all their errors. A closed scope takes no new
|
|
8
|
+
* work: `add` runs the cleanup at once, the rest do nothing.
|
|
9
|
+
*/
|
|
10
|
+
export type Scope = {
|
|
11
|
+
add: (cleanup: Cleanup) => void;
|
|
12
|
+
listen: <Events extends EventEmitter.ValidEventTypes, Event extends EventEmitter.EventNames<Events>>(emitter: EventEmitter<Events>, event: Event, listener: EventEmitter.EventListener<Events, Event>) => void;
|
|
13
|
+
timeout: (delayMs: number, run: () => void) => void;
|
|
14
|
+
repeat: (intervalMs: number, run: () => void | Promise<void>) => void;
|
|
15
|
+
child: () => Scope;
|
|
16
|
+
closeWhen: <Value>(observable: Observable<Value>, isMatch: (value: Value) => boolean) => void;
|
|
17
|
+
close: () => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export declare const createScope: () => Scope;
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createLogger } from "./log.js";
|
|
2
|
+
const log = createLogger("scope");
|
|
3
|
+
// `run` starts before the first await, so the caller sees its synchronous effects at once
|
|
4
|
+
const runLogged = async (run, failure) => {
|
|
5
|
+
try {
|
|
6
|
+
await run();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
log(failure, error);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export const createScope = () => {
|
|
13
|
+
// Newest first, the order `close` runs them in
|
|
14
|
+
const cleanups = [];
|
|
15
|
+
let closing;
|
|
16
|
+
const remove = (cleanup) => {
|
|
17
|
+
const index = cleanups.indexOf(cleanup);
|
|
18
|
+
if (index !== -1)
|
|
19
|
+
cleanups.splice(index, 1);
|
|
20
|
+
};
|
|
21
|
+
const add = (cleanup) => {
|
|
22
|
+
if (closing) {
|
|
23
|
+
void runLogged(cleanup, "cleanup failed");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
cleanups.unshift(cleanup);
|
|
27
|
+
};
|
|
28
|
+
const close = () => {
|
|
29
|
+
closing ?? (closing = (async () => {
|
|
30
|
+
const pending = [...cleanups];
|
|
31
|
+
cleanups.length = 0;
|
|
32
|
+
const results = await Promise.allSettled(pending.map(async (cleanup) => cleanup()));
|
|
33
|
+
const errors = results.flatMap(result => (result.status === "rejected" ? [result.reason] : []));
|
|
34
|
+
if (errors.length > 0)
|
|
35
|
+
throw new AggregateError(errors, "Failed to close scope");
|
|
36
|
+
})());
|
|
37
|
+
return closing;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
add,
|
|
41
|
+
listen: (emitter, event, listener) => {
|
|
42
|
+
if (closing)
|
|
43
|
+
return;
|
|
44
|
+
emitter.on(event, listener);
|
|
45
|
+
add(() => {
|
|
46
|
+
emitter.off(event, listener);
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
timeout: (delayMs, run) => {
|
|
50
|
+
if (closing)
|
|
51
|
+
return;
|
|
52
|
+
const cancel = () => clearTimeout(timer);
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
remove(cancel);
|
|
55
|
+
run();
|
|
56
|
+
}, delayMs);
|
|
57
|
+
add(cancel);
|
|
58
|
+
},
|
|
59
|
+
repeat: (intervalMs, run) => {
|
|
60
|
+
if (closing)
|
|
61
|
+
return;
|
|
62
|
+
const timer = setInterval(() => void runLogged(run, "repeated run failed"), intervalMs);
|
|
63
|
+
add(() => clearInterval(timer));
|
|
64
|
+
void runLogged(run, "repeated run failed");
|
|
65
|
+
},
|
|
66
|
+
child: () => {
|
|
67
|
+
const child = createScope();
|
|
68
|
+
add(child.close);
|
|
69
|
+
// A child that closes on its own leaves its parent
|
|
70
|
+
child.add(() => remove(child.close));
|
|
71
|
+
return child;
|
|
72
|
+
},
|
|
73
|
+
closeWhen: (observable, isMatch) => {
|
|
74
|
+
if (closing)
|
|
75
|
+
return;
|
|
76
|
+
add(observable.subscribe((value) => {
|
|
77
|
+
if (isMatch(value))
|
|
78
|
+
void runLogged(close, "close failed");
|
|
79
|
+
}));
|
|
80
|
+
},
|
|
81
|
+
close,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EventEmitter } from \"eventemitter3\";\n\nimport { createLogger } from \"./log.js\";\nimport type { Observable } from \"./observable.js\";\n\nconst log = createLogger(\"scope\");\n\nexport type Cleanup = () => void | Promise<void>;\n\n/**\n * Owns cleanups, listeners and timers for one lifetime. `close` runs every\n * cleanup once, newest first; a cleanup that throws does not stop the others,\n * and `close` then rejects with all their errors. A closed scope takes no new\n * work: `add` runs the cleanup at once, the rest do nothing.\n */\nexport type Scope = {\n add: (cleanup: Cleanup) => void;\n listen: <\n Events extends EventEmitter.ValidEventTypes,\n Event extends EventEmitter.EventNames<Events>,\n >(\n emitter: EventEmitter<Events>,\n event: Event,\n listener: EventEmitter.EventListener<Events, Event>,\n ) => void;\n // Runs `run` once after `delayMs`, unless the scope closes first\n timeout: (delayMs: number, run: () => void) => void;\n // Runs `run` now, then every `intervalMs`; a failed run is logged and the next one tries again\n repeat: (intervalMs: number, run: () => void | Promise<void>) => void;\n // A scope with a shorter lifetime, closed with this one at the latest\n child: () => Scope;\n // Closes this scope once `observable` holds a value that `isMatch` accepts, the current one included\n closeWhen: <Value>(observable: Observable<Value>, isMatch: (value: Value) => boolean) => void;\n close: () => Promise<void>;\n};\n\n// `run` starts before the first await, so the caller sees its synchronous effects at once\nconst runLogged = async (run: () => void | Promise<void>, failure: string) => {\n try {\n await run();\n }\n catch (error) {\n log(failure, error);\n }\n};\n\nexport const createScope = (): Scope => {\n // Newest first, the order `close` runs them in\n const cleanups: Cleanup[] = [];\n let closing: Promise<void> | undefined;\n\n const remove = (cleanup: Cleanup) => {\n const index = cleanups.indexOf(cleanup);\n\n if (index !== -1) cleanups.splice(index, 1);\n };\n\n const add = (cleanup: Cleanup) => {\n if (closing) {\n void runLogged(cleanup, \"cleanup failed\");\n\n return;\n }\n\n cleanups.unshift(cleanup);\n };\n\n const close = () => {\n closing ??= (async () => {\n const pending = [...cleanups];\n\n cleanups.length = 0;\n const results = await Promise.allSettled(pending.map(async cleanup => cleanup()));\n const errors = results.flatMap(result => (result.status === \"rejected\" ? [result.reason] : []));\n\n if (errors.length > 0) throw new AggregateError(errors, \"Failed to close scope\");\n })();\n\n return closing;\n };\n\n return {\n add,\n listen: (emitter, event, listener) => {\n if (closing) return;\n\n emitter.on(event, listener);\n add(() => {\n emitter.off(event, listener);\n });\n },\n timeout: (delayMs, run) => {\n if (closing) return;\n\n const cancel = () => clearTimeout(timer);\n const timer = setTimeout(() => {\n remove(cancel);\n run();\n }, delayMs);\n\n add(cancel);\n },\n repeat: (intervalMs, run) => {\n if (closing) return;\n\n const timer = setInterval(() => void runLogged(run, \"repeated run failed\"), intervalMs);\n\n add(() => clearInterval(timer));\n void runLogged(run, \"repeated run failed\");\n },\n child: () => {\n const child = createScope();\n\n add(child.close);\n // A child that closes on its own leaves its parent\n child.add(() => remove(child.close));\n\n return child;\n },\n closeWhen: (observable, isMatch) => {\n if (closing) return;\n\n add(observable.subscribe((value) => {\n if (isMatch(value)) void runLogged(close, \"close failed\");\n }));\n },\n close,\n };\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { EventEmitter } from "eventemitter3";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { observable } from "./observable.js";
|
|
4
|
+
import { createScope } from "./scope.js";
|
|
5
|
+
describe("scope", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
it("runs cleanups newest first, once", async () => {
|
|
13
|
+
const scope = createScope();
|
|
14
|
+
const order = [];
|
|
15
|
+
scope.add(() => {
|
|
16
|
+
order.push("first");
|
|
17
|
+
});
|
|
18
|
+
scope.add(async () => {
|
|
19
|
+
order.push("second");
|
|
20
|
+
});
|
|
21
|
+
await Promise.all([scope.close(), scope.close()]);
|
|
22
|
+
await scope.close();
|
|
23
|
+
expect(order).toEqual(["second", "first"]);
|
|
24
|
+
});
|
|
25
|
+
it("runs every cleanup when one throws, then rejects", async () => {
|
|
26
|
+
const scope = createScope();
|
|
27
|
+
const order = [];
|
|
28
|
+
scope.add(() => {
|
|
29
|
+
order.push("first");
|
|
30
|
+
});
|
|
31
|
+
scope.add(() => {
|
|
32
|
+
throw new Error("boom");
|
|
33
|
+
});
|
|
34
|
+
scope.add(() => {
|
|
35
|
+
order.push("third");
|
|
36
|
+
});
|
|
37
|
+
await expect(scope.close()).rejects.toBeInstanceOf(AggregateError);
|
|
38
|
+
expect(order).toEqual(["third", "first"]);
|
|
39
|
+
});
|
|
40
|
+
it("runs a cleanup at once when it is added after close", async () => {
|
|
41
|
+
const scope = createScope();
|
|
42
|
+
const cleanup = vi.fn();
|
|
43
|
+
await scope.close();
|
|
44
|
+
scope.add(cleanup);
|
|
45
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
46
|
+
});
|
|
47
|
+
it("cancels a timeout on close", async () => {
|
|
48
|
+
const scope = createScope();
|
|
49
|
+
const fired = vi.fn();
|
|
50
|
+
scope.timeout(1000, fired);
|
|
51
|
+
await vi.advanceTimersByTimeAsync(999);
|
|
52
|
+
await scope.close();
|
|
53
|
+
await vi.advanceTimersByTimeAsync(10000);
|
|
54
|
+
expect(fired).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
it("fires a timeout once after its delay", async () => {
|
|
57
|
+
const scope = createScope();
|
|
58
|
+
const fired = vi.fn();
|
|
59
|
+
scope.timeout(1000, fired);
|
|
60
|
+
await vi.advanceTimersByTimeAsync(999);
|
|
61
|
+
expect(fired).not.toHaveBeenCalled();
|
|
62
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
63
|
+
await vi.advanceTimersByTimeAsync(10000);
|
|
64
|
+
expect(fired).toHaveBeenCalledOnce();
|
|
65
|
+
});
|
|
66
|
+
it("repeats from now, keeps going after a failed run, and stops on close", async () => {
|
|
67
|
+
const scope = createScope();
|
|
68
|
+
let runs = 0;
|
|
69
|
+
scope.repeat(2000, () => {
|
|
70
|
+
runs += 1;
|
|
71
|
+
if (runs === 2)
|
|
72
|
+
throw new Error("lost");
|
|
73
|
+
});
|
|
74
|
+
expect(runs).toBe(1);
|
|
75
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
76
|
+
expect(runs).toBe(4);
|
|
77
|
+
await scope.close();
|
|
78
|
+
await vi.advanceTimersByTimeAsync(10000);
|
|
79
|
+
expect(runs).toBe(4);
|
|
80
|
+
});
|
|
81
|
+
it("starts no timer on a closed scope", async () => {
|
|
82
|
+
const scope = createScope();
|
|
83
|
+
const run = vi.fn();
|
|
84
|
+
await scope.close();
|
|
85
|
+
scope.timeout(10, run);
|
|
86
|
+
scope.repeat(10, run);
|
|
87
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
88
|
+
expect(run).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
it("removes a listener on close", async () => {
|
|
91
|
+
const scope = createScope();
|
|
92
|
+
const emitter = new EventEmitter();
|
|
93
|
+
const listener = vi.fn();
|
|
94
|
+
scope.listen(emitter, "ping", listener);
|
|
95
|
+
emitter.emit("ping");
|
|
96
|
+
await scope.close();
|
|
97
|
+
emitter.emit("ping");
|
|
98
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
it("closes a child with its parent, and a closed child leaves its parent open", async () => {
|
|
101
|
+
const parent = createScope();
|
|
102
|
+
const first = parent.child();
|
|
103
|
+
const second = parent.child();
|
|
104
|
+
const parentTimer = vi.fn();
|
|
105
|
+
const secondTimer = vi.fn();
|
|
106
|
+
parent.timeout(1000, parentTimer);
|
|
107
|
+
second.timeout(1000, secondTimer);
|
|
108
|
+
await first.close();
|
|
109
|
+
await parent.close();
|
|
110
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
111
|
+
expect(parentTimer).not.toHaveBeenCalled();
|
|
112
|
+
expect(secondTimer).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
it("gives a closed child from a closed parent", async () => {
|
|
115
|
+
const parent = createScope();
|
|
116
|
+
const run = vi.fn();
|
|
117
|
+
await parent.close();
|
|
118
|
+
parent.child().timeout(10, run);
|
|
119
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
120
|
+
expect(run).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
it("closes when an observable reaches a value, the current one included", async () => {
|
|
123
|
+
const [state, setState] = observable("idle");
|
|
124
|
+
const later = createScope();
|
|
125
|
+
const now = createScope();
|
|
126
|
+
const run = vi.fn();
|
|
127
|
+
later.closeWhen(state, value => value === "done");
|
|
128
|
+
later.repeat(1000, run);
|
|
129
|
+
setState("busy");
|
|
130
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
131
|
+
setState("done");
|
|
132
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
133
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
134
|
+
now.closeWhen(state, value => value === "done");
|
|
135
|
+
now.timeout(10, run);
|
|
136
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
137
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
//# sourceMappingURL=scope.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.test.js","sourceRoot":"","sources":["../src/scope.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"eventemitter3\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { observable } from \"./observable.js\";\nimport { createScope } from \"./scope.js\";\n\ndescribe(\"scope\", () => {\n beforeEach(() => {\n vi.useFakeTimers();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"runs cleanups newest first, once\", async () => {\n const scope = createScope();\n const order: string[] = [];\n\n scope.add(() => {\n order.push(\"first\");\n });\n scope.add(async () => {\n order.push(\"second\");\n });\n\n await Promise.all([scope.close(), scope.close()]);\n await scope.close();\n\n expect(order).toEqual([\"second\", \"first\"]);\n });\n\n it(\"runs every cleanup when one throws, then rejects\", async () => {\n const scope = createScope();\n const order: string[] = [];\n\n scope.add(() => {\n order.push(\"first\");\n });\n scope.add(() => {\n throw new Error(\"boom\");\n });\n scope.add(() => {\n order.push(\"third\");\n });\n\n await expect(scope.close()).rejects.toBeInstanceOf(AggregateError);\n expect(order).toEqual([\"third\", \"first\"]);\n });\n\n it(\"runs a cleanup at once when it is added after close\", async () => {\n const scope = createScope();\n const cleanup = vi.fn();\n\n await scope.close();\n scope.add(cleanup);\n\n expect(cleanup).toHaveBeenCalledOnce();\n });\n\n it(\"cancels a timeout on close\", async () => {\n const scope = createScope();\n const fired = vi.fn();\n\n scope.timeout(1000, fired);\n await vi.advanceTimersByTimeAsync(999);\n await scope.close();\n await vi.advanceTimersByTimeAsync(10_000);\n\n expect(fired).not.toHaveBeenCalled();\n });\n\n it(\"fires a timeout once after its delay\", async () => {\n const scope = createScope();\n const fired = vi.fn();\n\n scope.timeout(1000, fired);\n await vi.advanceTimersByTimeAsync(999);\n expect(fired).not.toHaveBeenCalled();\n\n await vi.advanceTimersByTimeAsync(1);\n await vi.advanceTimersByTimeAsync(10_000);\n expect(fired).toHaveBeenCalledOnce();\n });\n\n it(\"repeats from now, keeps going after a failed run, and stops on close\", async () => {\n const scope = createScope();\n let runs = 0;\n\n scope.repeat(2000, () => {\n runs += 1;\n\n if (runs === 2) throw new Error(\"lost\");\n });\n expect(runs).toBe(1);\n\n await vi.advanceTimersByTimeAsync(6000);\n expect(runs).toBe(4);\n\n await scope.close();\n await vi.advanceTimersByTimeAsync(10_000);\n expect(runs).toBe(4);\n });\n\n it(\"starts no timer on a closed scope\", async () => {\n const scope = createScope();\n const run = vi.fn();\n\n await scope.close();\n scope.timeout(10, run);\n scope.repeat(10, run);\n await vi.advanceTimersByTimeAsync(1000);\n\n expect(run).not.toHaveBeenCalled();\n });\n\n it(\"removes a listener on close\", async () => {\n const scope = createScope();\n const emitter = new EventEmitter<{ ping: () => void; }>();\n const listener = vi.fn();\n\n scope.listen(emitter, \"ping\", listener);\n emitter.emit(\"ping\");\n await scope.close();\n emitter.emit(\"ping\");\n\n expect(listener).toHaveBeenCalledOnce();\n });\n\n it(\"closes a child with its parent, and a closed child leaves its parent open\", async () => {\n const parent = createScope();\n const first = parent.child();\n const second = parent.child();\n const parentTimer = vi.fn();\n const secondTimer = vi.fn();\n\n parent.timeout(1000, parentTimer);\n second.timeout(1000, secondTimer);\n await first.close();\n await parent.close();\n await vi.advanceTimersByTimeAsync(1000);\n\n expect(parentTimer).not.toHaveBeenCalled();\n expect(secondTimer).not.toHaveBeenCalled();\n });\n\n it(\"gives a closed child from a closed parent\", async () => {\n const parent = createScope();\n const run = vi.fn();\n\n await parent.close();\n parent.child().timeout(10, run);\n await vi.advanceTimersByTimeAsync(100);\n\n expect(run).not.toHaveBeenCalled();\n });\n\n it(\"closes when an observable reaches a value, the current one included\", async () => {\n const [state, setState] = observable(\"idle\");\n const later = createScope();\n const now = createScope();\n const run = vi.fn();\n\n later.closeWhen(state, value => value === \"done\");\n later.repeat(1000, run);\n setState(\"busy\");\n await vi.advanceTimersByTimeAsync(1000);\n setState(\"done\");\n await vi.advanceTimersByTimeAsync(5000);\n expect(run).toHaveBeenCalledTimes(2);\n\n now.closeWhen(state, value => value === \"done\");\n now.timeout(10, run);\n await vi.advanceTimersByTimeAsync(100);\n expect(run).toHaveBeenCalledTimes(2);\n });\n});\n"]}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { KeyPair, PublicKey } from "./encryption/asymmetric.js";
|
|
2
2
|
export type Hex<_L> = string;
|
|
3
3
|
/**
|
|
4
4
|
* The parameters from the connection url
|
|
@@ -11,14 +11,12 @@ export type SessionHandshakeParameters = {
|
|
|
11
11
|
p: string;
|
|
12
12
|
s: string;
|
|
13
13
|
};
|
|
14
|
+
export type SessionCreationParameters = Pick<SessionHandshakeParameters, "p" | "s">;
|
|
14
15
|
/**
|
|
15
|
-
* The parameters that describe a session
|
|
16
|
-
* these can be used to restart a session
|
|
16
|
+
* The parameters that describe a session these can be used to restart a session
|
|
17
17
|
*/
|
|
18
18
|
export type SessionConnectionParameters = SessionHandshakeParameters & {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
decryptionKey: DecryptionKey;
|
|
19
|
+
keyPair: KeyPair;
|
|
20
|
+
peerKey: PublicKey;
|
|
22
21
|
};
|
|
23
|
-
export type SessionCreationParameters = Pick<SessionHandshakeParameters, "p" | "s">;
|
|
24
22
|
export type SessionLinkParameters = SessionConnectionParameters | SessionHandshakeParameters | SessionCreationParameters;
|
package/dist/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"","sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"","sourcesContent":["import type { KeyPair, PublicKey } from \"./encryption/asymmetric.js\";\n\n// typescript type for 32 character hex string\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport type Hex<_L> = string;\n\n/**\n * The parameters from the connection url\n */\nexport type SessionHandshakeParameters = {\n // Version of the openlv protocol\n version: 1;\n // Unique session identifier\n sessionId: Hex<16>;\n // Hash of peerA's public key\n h: Hex<16>;\n // Shared key for symmetric encryption during handshake (32 hex chars = 16 bytes)\n k: Hex<32>;\n // Protocol for signaling\n p: string;\n // Signaling server URL\n s: string;\n};\n\nexport type SessionCreationParameters = Pick<\n SessionHandshakeParameters,\n \"p\" | \"s\"\n>;\n\n/**\n * The parameters that describe a session these can be used to restart a session\n */\nexport type SessionConnectionParameters = SessionHandshakeParameters & {\n keyPair: KeyPair;\n peerKey: PublicKey;\n};\n\nexport type SessionLinkParameters\n = | SessionConnectionParameters\n | SessionHandshakeParameters\n | SessionCreationParameters;\n"]}
|
package/dist/url/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export const decodeConnectionURL = (url) => {
|
|
|
28
28
|
const h = urlObject.searchParams.get("h") || "";
|
|
29
29
|
const k = urlObject.searchParams.get("k") || "";
|
|
30
30
|
const s = urlObject.searchParams.get("s") || undefined;
|
|
31
|
-
const p = urlObject.searchParams.get("p") || "
|
|
31
|
+
const p = urlObject.searchParams.get("p") || "";
|
|
32
32
|
if (!sessionId) {
|
|
33
33
|
throw new Error("Session ID is required in URL");
|
|
34
34
|
}
|
package/dist/url/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/url/index.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAmC,EAAE,EAAE;IACzE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IAEzC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/B,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAE/B,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;QACd,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;QACd,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IAED,OAAO,YAAY,OAAO,CAAC,SAAS,IAAI,uBAAuB,IAAI,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC7F,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,GAAW,EACiB,EAAE;IAC9B,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,MAAM,IAAI,SAAS,CAAC,0CAA0C,OAAO,GAAG,EAAE,CAAC,CAAC;IAEzG,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAEtE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,MAAM,IAAI,KAAK,CAC/C,yDAAyD,GAAG,EAAE,CAC/D,CAAC;IAEF,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC;QACrC,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,IAAI,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;QAElF,IAAI,OAAO,KAAK,uBAAuB,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CACb,sCAAsC,uBAAuB,SAAS,OAAO,EAAE,CAChF,CAAC;QACJ,CAAC;QAED,MAAM,CAAC,GAAG,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAChD,MAAM,CAAC,GAAG,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAChD,MAAM,CAAC,GAAG,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC;QAEvD,MAAM,CAAC,GAAG,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/url/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SessionHandshakeParameters } from \"../session.js\";\n\nexport const OPENLV_PROTOCOL_VERSION = 1;\n\nexport const encodeConnectionURL = (payload: SessionHandshakeParameters) => {\n const parameters = new URLSearchParams();\n\n parameters.set(\"h\", payload.h);\n parameters.set(\"k\", payload.k);\n\n if (payload.p) {\n parameters.set(\"p\", payload.p);\n }\n\n if (payload.s) {\n parameters.set(\"s\", payload.s);\n }\n\n return `openlv://${payload.sessionId}@${OPENLV_PROTOCOL_VERSION}?${parameters.toString()}`;\n};\n\nexport const decodeConnectionURL = (\n url: string,\n): SessionHandshakeParameters => {\n if (typeof url !== \"string\") throw new TypeError(`Invalid URL type: expected string, got ${typeof url}`);\n\n if (!url || url.trim() === \"\") throw new Error(\"URL cannot be empty\");\n\n if (!url.startsWith(\"openlv://\")) throw new Error(\n `Invalid URL format: must start with 'openlv://', got: ${url}`,\n );\n\n try {\n const urlObject = new URL(url);\n const sessionId = urlObject.username;\n const version = Number(urlObject.hostname || urlObject.pathname.replace(\"/\", \"\"));\n\n if (version !== OPENLV_PROTOCOL_VERSION) {\n throw new Error(\n `Invalid protocol version: expected ${OPENLV_PROTOCOL_VERSION}, got ${version}`,\n );\n }\n\n const h = urlObject.searchParams.get(\"h\") || \"\";\n const k = urlObject.searchParams.get(\"k\") || \"\";\n const s = urlObject.searchParams.get(\"s\") || undefined;\n\n const p = urlObject.searchParams.get(\"p\") || \"\";\n\n if (!sessionId) {\n throw new Error(\"Session ID is required in URL\");\n }\n\n // Validate session ID format (16 characters, URL-safe alphabet)\n if (!/^[A-Za-z0-9_-]{16}$/.test(sessionId)) {\n throw new Error(\n \"Invalid session ID format: must be 16 URL-safe characters\",\n );\n }\n\n if (!h) {\n throw new Error(\"Public key hash (h parameter) is required in URL\");\n }\n\n // Validate public key hash format (16 hex characters)\n if (!/^[0-9a-f]{16}$/.test(h)) {\n throw new Error(\n \"Invalid public key hash format: must be 16 hex characters\",\n );\n }\n\n if (!k) {\n throw new Error(\"Shared key (k parameter) is required in URL\");\n }\n\n // Validate shared key format (32 hex characters)\n if (!/^[0-9a-f]{32}$/.test(k)) {\n throw new Error(\"Invalid shared key format: must be 32 lowercase hex characters\");\n }\n\n return {\n version,\n sessionId,\n h,\n k,\n s: s ?? \"\",\n p,\n };\n }\n catch (error) {\n const errorMessage\n = error instanceof Error ? error.message : \"Unknown error\";\n\n // Strip query string so handshake secrets (k) are not exposed in error messages\n const safeUrl = url.includes(\"?\") ? url.slice(0, url.indexOf(\"?\")) + \"?[redacted]\" : url;\n\n throw new Error(`Failed to parse URL: ${safeUrl}. Original error: ${errorMessage}`);\n }\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { EncryptionKey } from "./asymmetric.js";
|
|
2
|
-
/**
|
|
3
|
-
* Hashes a public key using SHA-256 and returns the first 16 bytes as a hex string
|
|
4
|
-
*/
|
|
5
|
-
export declare const hashPublicKey: (publicKey: EncryptionKey) => Promise<string>;
|
|
6
|
-
export declare const validatePublicKeyHash: (publicKey: EncryptionKey, hash: string) => Promise<boolean>;
|
|
7
|
-
/**
|
|
8
|
-
* determines/or generates the hash and dictates wether is host or not
|
|
9
|
-
*/
|
|
10
|
-
export declare const initHash: (initialHash: string | undefined, encryptionKey: EncryptionKey) => Promise<{
|
|
11
|
-
hash: string;
|
|
12
|
-
isHost: boolean;
|
|
13
|
-
}>;
|
package/dist/encryption/hash.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { bytesToHex } from "@noble/ciphers/utils.js";
|
|
2
|
-
const HASH_LENGTH = 16;
|
|
3
|
-
/**
|
|
4
|
-
* Hashes a public key using SHA-256 and returns the first 16 bytes as a hex string
|
|
5
|
-
*/
|
|
6
|
-
export const hashPublicKey = async (publicKey) => {
|
|
7
|
-
const buffer = new TextEncoder().encode(publicKey.toString());
|
|
8
|
-
const hash = await crypto.subtle.digest("SHA-256", buffer);
|
|
9
|
-
return bytesToHex(new Uint8Array(hash)).slice(0, HASH_LENGTH);
|
|
10
|
-
};
|
|
11
|
-
export const validatePublicKeyHash = async (publicKey, hash) => {
|
|
12
|
-
const computedHash = await hashPublicKey(publicKey);
|
|
13
|
-
return computedHash === hash;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* determines/or generates the hash and dictates wether is host or not
|
|
17
|
-
*/
|
|
18
|
-
export const initHash = async (initialHash, encryptionKey) => {
|
|
19
|
-
const ourHash = await hashPublicKey(encryptionKey);
|
|
20
|
-
const hash = initialHash || ourHash;
|
|
21
|
-
const isHost = hash === ourHash;
|
|
22
|
-
return {
|
|
23
|
-
hash,
|
|
24
|
-
isHost,
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
//# sourceMappingURL=hash.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/encryption/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAIrD,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB;;GAEG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAChC,SAAwB,EACP,EAAE;IACnB,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9D,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAE3D,OAAO,UAAU,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EACxC,SAAwB,EACxB,IAAY,EACM,EAAE;IACpB,MAAM,YAAY,GAAG,MAAM,aAAa,CAAC,SAAS,CAAC,CAAC;IAEpD,OAAO,YAAY,KAAK,IAAI,CAAC;AAC/B,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAC3B,WAA+B,EAC/B,aAA4B,EAC5B,EAAE;IACF,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,aAAa,CAAC,CAAC;IACnD,MAAM,IAAI,GAAG,WAAW,IAAI,OAAO,CAAC;IACpC,MAAM,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC;IAEhC,OAAO;QACL,IAAI;QACJ,MAAM;KACP,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { bytesToHex } from \"@noble/ciphers/utils.js\";\n\nimport type { EncryptionKey } from \"./asymmetric.js\";\n\nconst HASH_LENGTH = 16;\n\n/**\n * Hashes a public key using SHA-256 and returns the first 16 bytes as a hex string\n */\nexport const hashPublicKey = async (\n publicKey: EncryptionKey,\n): Promise<string> => {\n const buffer = new TextEncoder().encode(publicKey.toString());\n const hash = await crypto.subtle.digest(\"SHA-256\", buffer);\n\n return bytesToHex(new Uint8Array(hash)).slice(0, HASH_LENGTH);\n};\n\nexport const validatePublicKeyHash = async (\n publicKey: EncryptionKey,\n hash: string,\n): Promise<boolean> => {\n const computedHash = await hashPublicKey(publicKey);\n\n return computedHash === hash;\n};\n\n/**\n * determines/or generates the hash and dictates wether is host or not\n */\nexport const initHash = async (\n initialHash: string | undefined,\n encryptionKey: EncryptionKey,\n) => {\n const ourHash = await hashPublicKey(encryptionKey);\n const hash = initialHash || ourHash;\n const isHost = hash === ourHash;\n\n return {\n hash,\n isHost,\n };\n};\n"]}
|