@interocitor/core 0.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/adapters/cloudflare.d.ts +72 -0
  4. package/dist/adapters/cloudflare.d.ts.map +1 -0
  5. package/dist/adapters/cloudflare.js +227 -0
  6. package/dist/adapters/cloudflare.js.map +1 -0
  7. package/dist/adapters/google-drive.d.ts +64 -0
  8. package/dist/adapters/google-drive.d.ts.map +1 -0
  9. package/dist/adapters/google-drive.js +340 -0
  10. package/dist/adapters/google-drive.js.map +1 -0
  11. package/dist/adapters/memory.d.ts +45 -0
  12. package/dist/adapters/memory.d.ts.map +1 -0
  13. package/dist/adapters/memory.js +129 -0
  14. package/dist/adapters/memory.js.map +1 -0
  15. package/dist/adapters/webdav.d.ts +59 -0
  16. package/dist/adapters/webdav.d.ts.map +1 -0
  17. package/dist/adapters/webdav.js +247 -0
  18. package/dist/adapters/webdav.js.map +1 -0
  19. package/dist/core/codec.d.ts +20 -0
  20. package/dist/core/codec.d.ts.map +1 -0
  21. package/dist/core/codec.js +66 -0
  22. package/dist/core/codec.js.map +1 -0
  23. package/dist/core/compaction.d.ts +37 -0
  24. package/dist/core/compaction.d.ts.map +1 -0
  25. package/dist/core/compaction.js +134 -0
  26. package/dist/core/compaction.js.map +1 -0
  27. package/dist/core/crdt.d.ts +33 -0
  28. package/dist/core/crdt.d.ts.map +1 -0
  29. package/dist/core/crdt.js +188 -0
  30. package/dist/core/crdt.js.map +1 -0
  31. package/dist/core/flush.d.ts +9 -0
  32. package/dist/core/flush.d.ts.map +1 -0
  33. package/dist/core/flush.js +41 -0
  34. package/dist/core/flush.js.map +1 -0
  35. package/dist/core/hlc.d.ts +25 -0
  36. package/dist/core/hlc.d.ts.map +1 -0
  37. package/dist/core/hlc.js +76 -0
  38. package/dist/core/hlc.js.map +1 -0
  39. package/dist/core/internals.d.ts +25 -0
  40. package/dist/core/internals.d.ts.map +1 -0
  41. package/dist/core/internals.js +54 -0
  42. package/dist/core/internals.js.map +1 -0
  43. package/dist/core/manifest.d.ts +31 -0
  44. package/dist/core/manifest.d.ts.map +1 -0
  45. package/dist/core/manifest.js +111 -0
  46. package/dist/core/manifest.js.map +1 -0
  47. package/dist/core/pull.d.ts +26 -0
  48. package/dist/core/pull.d.ts.map +1 -0
  49. package/dist/core/pull.js +98 -0
  50. package/dist/core/pull.js.map +1 -0
  51. package/dist/core/row-id.d.ts +12 -0
  52. package/dist/core/row-id.d.ts.map +1 -0
  53. package/dist/core/row-id.js +12 -0
  54. package/dist/core/row-id.js.map +1 -0
  55. package/dist/core/schema-types.d.ts +13 -0
  56. package/dist/core/schema-types.d.ts.map +1 -0
  57. package/dist/core/schema-types.js +18 -0
  58. package/dist/core/schema-types.js.map +1 -0
  59. package/dist/core/schema-types.type-test.d.ts +2 -0
  60. package/dist/core/schema-types.type-test.d.ts.map +1 -0
  61. package/dist/core/schema-types.type-test.js +149 -0
  62. package/dist/core/schema-types.type-test.js.map +1 -0
  63. package/dist/core/sync-engine.d.ts +158 -0
  64. package/dist/core/sync-engine.d.ts.map +1 -0
  65. package/dist/core/sync-engine.js +714 -0
  66. package/dist/core/sync-engine.js.map +1 -0
  67. package/dist/core/table.d.ts +60 -0
  68. package/dist/core/table.d.ts.map +1 -0
  69. package/dist/core/table.js +106 -0
  70. package/dist/core/table.js.map +1 -0
  71. package/dist/core/types.d.ts +478 -0
  72. package/dist/core/types.d.ts.map +1 -0
  73. package/dist/core/types.js +7 -0
  74. package/dist/core/types.js.map +1 -0
  75. package/dist/crypto/encryption.d.ts +57 -0
  76. package/dist/crypto/encryption.d.ts.map +1 -0
  77. package/dist/crypto/encryption.js +195 -0
  78. package/dist/crypto/encryption.js.map +1 -0
  79. package/dist/crypto/keys.d.ts +48 -0
  80. package/dist/crypto/keys.d.ts.map +1 -0
  81. package/dist/crypto/keys.js +55 -0
  82. package/dist/crypto/keys.js.map +1 -0
  83. package/dist/handshake/channel.d.ts +117 -0
  84. package/dist/handshake/channel.d.ts.map +1 -0
  85. package/dist/handshake/channel.js +246 -0
  86. package/dist/handshake/channel.js.map +1 -0
  87. package/dist/handshake/index.d.ts +213 -0
  88. package/dist/handshake/index.d.ts.map +1 -0
  89. package/dist/handshake/index.js +182 -0
  90. package/dist/handshake/index.js.map +1 -0
  91. package/dist/handshake/qr.d.ts +100 -0
  92. package/dist/handshake/qr.d.ts.map +1 -0
  93. package/dist/handshake/qr.js +103 -0
  94. package/dist/handshake/qr.js.map +1 -0
  95. package/dist/index.d.ts +46 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +46 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/storage/credential-store.d.ts +99 -0
  100. package/dist/storage/credential-store.d.ts.map +1 -0
  101. package/dist/storage/credential-store.js +309 -0
  102. package/dist/storage/credential-store.js.map +1 -0
  103. package/dist/storage/local-store.d.ts +56 -0
  104. package/dist/storage/local-store.d.ts.map +1 -0
  105. package/dist/storage/local-store.js +411 -0
  106. package/dist/storage/local-store.js.map +1 -0
  107. package/package.json +68 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * interocitor/handshake/channel
3
+ *
4
+ * Ephemeral ECDH-P256 key exchange over the shared cloud backend (relay).
5
+ * No extra server required — the backend the mesh already uses is the relay.
6
+ *
7
+ * ## Roles (determined by QR intent, not by which device is "bigger")
8
+ *
9
+ * Generator — device that created the QR (has the ECDH private key).
10
+ * Receives credentials from the scanner via the relay.
11
+ *
12
+ * Scanner — device that scanned the QR (has generatorPub from the QR).
13
+ * Pushes credentials to the generator via the relay.
14
+ *
15
+ * ## Protocol
16
+ *
17
+ * intent = "share" → Generator already has credentials.
18
+ * Scanner is joining → Scanner reads relay → gets credentials.
19
+ * (Scanner pushes scannerPub so Generator can encrypt for it.)
20
+ *
21
+ * intent = "join" → Generator wants credentials.
22
+ * Scanner has credentials → Scanner writes to relay.
23
+ * (Generator just waits and reads.)
24
+ *
25
+ * Both intents use identical wire mechanics; only which side writes
26
+ * credentials differs.
27
+ *
28
+ * ## Wire sequence (both intents)
29
+ *
30
+ * Scanner Relay Generator
31
+ * ─────── ───── ─────────
32
+ * [has generatorPub from QR]
33
+ * generate ephemeral keypair (Es, es)
34
+ * sharedSecret = ECDH(generatorPub, es)
35
+ * wrappingKey = HKDF(sharedSecret)
36
+ * write scanner-pub.json (Es) →
37
+ * read scanner-pub.json
38
+ * sharedSecret = ECDH(Es, eg)
39
+ * wrappingKey = HKDF(sharedSecret)
40
+ *
41
+ * ── if intent == "join": Generator has credentials, Scanner receives ──
42
+ * encrypt {remotePath, meshKey}
43
+ * ← write credentials.json
44
+ * read credentials.json
45
+ * decrypt → remotePath, meshKey
46
+ *
47
+ * ── if intent == "share": Scanner has credentials, Generator receives ──
48
+ * encrypt {remotePath, meshKey}
49
+ * write credentials.json →
50
+ * read credentials.json
51
+ * decrypt → remotePath, meshKey
52
+ *
53
+ * [whoever received credentials deletes relay files — best-effort]
54
+ *
55
+ * ## Relay files (scoped by handshakeId)
56
+ *
57
+ * {relayBase}/scanner-pub.json — scanner's ephemeral ECDH public key
58
+ * {relayBase}/credentials.json — encrypted {remotePath, meshKey?}
59
+ *
60
+ * ## Security
61
+ *
62
+ * wrappingKey is derived from ECDH(generatorPub, scannerPriv)
63
+ * = ECDH(scannerPub, generatorPriv) (commutativity)
64
+ *
65
+ * Anyone with cloud access sees scanner-pub.json and credentials.json.
66
+ * Without generatorPriv (which never leaves the generating device) they
67
+ * cannot derive wrappingKey and cannot decrypt credentials.json.
68
+ * generatorPriv is only accessible to someone who physically held the
69
+ * device that generated the QR.
70
+ */
71
+ // ─── ECDH / crypto helpers ───────────────────────────────────────────
72
+ const ECDH_PARAMS = { name: 'ECDH', namedCurve: 'P-256' };
73
+ const HKDF_PARAMS = { name: 'HKDF', hash: 'SHA-256' };
74
+ const WRAP_ALGO = { name: 'AES-GCM', length: 256 };
75
+ const IV_LEN = 12;
76
+ const HKDF_INFO = 'interocitor-handshake-v1';
77
+ function uint8ToB64url(b) {
78
+ let s = '';
79
+ for (let i = 0; i < b.length; i++)
80
+ s += String.fromCodePoint(b[i]);
81
+ return btoa(s).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
82
+ }
83
+ function b64urlToUint8(s) {
84
+ const p = s.replaceAll('-', '+').replaceAll('_', '/');
85
+ const pad = (4 - (p.length % 4)) % 4;
86
+ const bin = atob(p + '='.repeat(pad));
87
+ const b = new Uint8Array(bin.length);
88
+ for (let i = 0; i < bin.length; i++)
89
+ b[i] = bin.charCodeAt(i);
90
+ return b;
91
+ }
92
+ function toBuffer(b) {
93
+ return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
94
+ }
95
+ export async function generateECDHKeypair() {
96
+ return crypto.subtle.generateKey(ECDH_PARAMS, true, ['deriveKey', 'deriveBits']);
97
+ }
98
+ export async function exportECDHPublicKey(key) {
99
+ return uint8ToB64url(new Uint8Array(await crypto.subtle.exportKey('spki', key)));
100
+ }
101
+ export async function importECDHPublicKey(spki) {
102
+ return crypto.subtle.importKey('spki', toBuffer(b64urlToUint8(spki)), ECDH_PARAMS, true, []);
103
+ }
104
+ async function deriveWrappingKey(myPriv, peerPub) {
105
+ const bits = await crypto.subtle.deriveBits({ name: 'ECDH', public: peerPub }, myPriv, 256);
106
+ const ikm = await crypto.subtle.importKey('raw', bits, 'HKDF', false, ['deriveKey']);
107
+ const info = new TextEncoder().encode(HKDF_INFO);
108
+ return crypto.subtle.deriveKey({ ...HKDF_PARAMS, salt: new ArrayBuffer(32), info: toBuffer(info) }, ikm, WRAP_ALGO, false, ['encrypt', 'decrypt']);
109
+ }
110
+ async function encryptCredentials(wrappingKey, creds) {
111
+ const payload = { remotePath: creds.remotePath };
112
+ if (creds.passphrase !== null) {
113
+ payload.passphrase = creds.passphrase;
114
+ }
115
+ const pt = new TextEncoder().encode(JSON.stringify(payload));
116
+ const ivRaw = crypto.getRandomValues(new Uint8Array(IV_LEN));
117
+ const iv = toBuffer(ivRaw);
118
+ const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, wrappingKey, pt);
119
+ const env = { v: 1, iv: uint8ToB64url(ivRaw), ct: uint8ToB64url(new Uint8Array(ct)) };
120
+ return JSON.stringify(env);
121
+ }
122
+ async function decryptCredentials(wrappingKey, envelope) {
123
+ const { v, iv, ct } = JSON.parse(envelope);
124
+ if (v !== 1)
125
+ throw new Error(`Unknown handshake envelope version: ${v}`);
126
+ const ivBytes = b64urlToUint8(iv);
127
+ const ctBytes = b64urlToUint8(ct);
128
+ const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: toBuffer(ivBytes) }, wrappingKey, toBuffer(ctBytes));
129
+ const raw = JSON.parse(new TextDecoder().decode(pt));
130
+ return { remotePath: raw.remotePath, passphrase: raw.passphrase ?? null };
131
+ }
132
+ // ─── Relay paths ─────────────────────────────────────────────────────
133
+ function relayPaths(handshakeId, relayBase) {
134
+ const base = `${relayBase}/handshake/${handshakeId}`;
135
+ return {
136
+ scannerPub: `${base}/scanner-pub.json`,
137
+ credentials: `${base}/credentials.json`,
138
+ };
139
+ }
140
+ // ─── Polling ─────────────────────────────────────────────────────────
141
+ async function pollFor(fn, intervalMs, timeoutMs) {
142
+ const deadline = Date.now() + timeoutMs;
143
+ while (Date.now() < deadline) {
144
+ const r = await fn();
145
+ if (r !== null)
146
+ return r;
147
+ await new Promise((resolve) => {
148
+ setTimeout(resolve, intervalMs);
149
+ });
150
+ }
151
+ throw new Error(`Handshake timed out after ${timeoutMs}ms`);
152
+ }
153
+ // ─── Relay I/O ───────────────────────────────────────────────────────
154
+ async function relayRead(adapter, path) {
155
+ try {
156
+ const bytes = await adapter.readFile(path);
157
+ return new TextDecoder().decode(bytes);
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ async function relayWrite(adapter, path, data) {
164
+ await adapter.writeFile(path, data);
165
+ }
166
+ async function relayCleanup(adapter, paths) {
167
+ await Promise.allSettled([
168
+ adapter.deleteFile(paths.scannerPub),
169
+ adapter.deleteFile(paths.credentials),
170
+ ]);
171
+ }
172
+ export async function createGeneratorSession() {
173
+ const keypair = await generateECDHKeypair();
174
+ const generatorPub = await exportECDHPublicKey(keypair.publicKey);
175
+ return {
176
+ generatorPub,
177
+ async complete(adapter, handshakeId, relayBase, intent, ownCredentials, options = {}) {
178
+ const { pollIntervalMs = 2000, timeoutMs = 120000 } = options;
179
+ const paths = relayPaths(handshakeId, relayBase);
180
+ // Wait for the scanner to upload their ephemeral public key.
181
+ const scannerPubSpki = await pollFor(async () => {
182
+ const data = await relayRead(adapter, paths.scannerPub);
183
+ if (!data)
184
+ return null;
185
+ return JSON.parse(data).pub ?? null;
186
+ }, pollIntervalMs, timeoutMs);
187
+ const scannerPublicKey = await importECDHPublicKey(scannerPubSpki);
188
+ const wrappingKey = await deriveWrappingKey(keypair.privateKey, scannerPublicKey);
189
+ if (intent === 'share') {
190
+ // Generator has credentials → encrypt and push them for the scanner.
191
+ if (!ownCredentials)
192
+ throw new Error('intent=share requires ownCredentials');
193
+ const envelope = await encryptCredentials(wrappingKey, ownCredentials);
194
+ await relayWrite(adapter, paths.credentials, envelope);
195
+ // Generator does not clean up — scanner deletes after reading.
196
+ return null; // Generator already has credentials; nothing new to return.
197
+ }
198
+ // intent === 'join': scanner will push credentials to us.
199
+ const envelope = await pollFor(() => relayRead(adapter, paths.credentials), pollIntervalMs, timeoutMs);
200
+ const credentials = await decryptCredentials(wrappingKey, envelope);
201
+ // Clean up relay files after reading.
202
+ relayCleanup(adapter, paths).catch(() => { });
203
+ return credentials;
204
+ },
205
+ };
206
+ }
207
+ // ─── Scanner side ────────────────────────────────────────────────────
208
+ /**
209
+ * Run the scanner side of the handshake.
210
+ *
211
+ * intent = "share": generator has credentials and will push them →
212
+ * scanner uploads scannerPub, waits for credentials,
213
+ * decrypts and returns them.
214
+ *
215
+ * intent = "join": generator wants credentials → scanner uploads
216
+ * scannerPub, then writes encrypted credentials,
217
+ * resolves with null (scanner already has them).
218
+ */
219
+ export async function runScannerHandshake(adapter, payload, ownCredentials, relayBase, options = {}) {
220
+ const { intent, handshakeId, generatorPub } = payload;
221
+ const { pollIntervalMs = 2000, timeoutMs = 120000 } = options;
222
+ const paths = relayPaths(handshakeId, relayBase);
223
+ // Generate our ephemeral keypair.
224
+ const keypair = await generateECDHKeypair();
225
+ const scannerPub = await exportECDHPublicKey(keypair.publicKey);
226
+ // Upload our public key — this signals the generator we are here.
227
+ await relayWrite(adapter, paths.scannerPub, JSON.stringify({ pub: scannerPub }));
228
+ // Derive the shared wrapping key using the generator's public key from the QR.
229
+ const generatorPublicKey = await importECDHPublicKey(generatorPub);
230
+ const wrappingKey = await deriveWrappingKey(keypair.privateKey, generatorPublicKey);
231
+ if (intent === 'share') {
232
+ // Generator will push credentials → wait and decrypt.
233
+ const envelope = await pollFor(() => relayRead(adapter, paths.credentials), pollIntervalMs, timeoutMs);
234
+ const credentials = await decryptCredentials(wrappingKey, envelope);
235
+ relayCleanup(adapter, paths).catch(() => { });
236
+ return credentials;
237
+ }
238
+ // intent === 'join': we push credentials to the generator.
239
+ if (!ownCredentials)
240
+ throw new Error('intent=join requires scanner to have ownCredentials');
241
+ const envelope = await encryptCredentials(wrappingKey, ownCredentials);
242
+ await relayWrite(adapter, paths.credentials, envelope);
243
+ // Scanner already has credentials; nothing new to return.
244
+ return null;
245
+ }
246
+ //# sourceMappingURL=channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../../src/handshake/channel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * interocitor/handshake
3
+ *
4
+ * Secure device pairing via QR code.
5
+ *
6
+ * Both devices run an interocitor instance with a preconfigured backend
7
+ * adapter. Neither device needs to know the mesh `remotePath` or master
8
+ * key beforehand — everything is bootstrapped through the handshake.
9
+ *
10
+ * ## Two intents
11
+ *
12
+ * A QR code is generated with one of two intents, declared by the
13
+ * **generator** (the device showing the QR):
14
+ *
15
+ * "share" — I have mesh credentials. Scan me and I will push them to
16
+ * you through the relay.
17
+ *
18
+ * "join" — I want to join a mesh. Scan me and push your credentials
19
+ * to me through the relay.
20
+ *
21
+ * The **scanner** (the device that reads the QR) always does the
22
+ * opposite: it receives when the generator shares, and it pushes when
23
+ * the generator joins.
24
+ *
25
+ * ## Which device shows the QR?
26
+ *
27
+ * Desktop already in mesh, Mobile wants to join
28
+ * → Desktop generates "share" QR, Mobile scans → Mobile joins.
29
+ *
30
+ * Mobile wants to join, shows QR to Desktop that is already in mesh
31
+ * → Mobile generates "join" QR, Desktop scans → Mobile joins.
32
+ *
33
+ * ## QR payload — two pieces
34
+ *
35
+ * Cloud piece → handshakeId
36
+ * Scopes two relay files on the shared backend.
37
+ * Visible in the cloud but useless without the key.
38
+ *
39
+ * Eyes-only → generatorPub (ephemeral ECDH-P256 public key)
40
+ * The scanner derives a wrapping key from it via ECDH.
41
+ * Only someone who physically saw the QR can do this.
42
+ *
43
+ * remotePath and meshKey are NEVER in the QR. They travel through the
44
+ * relay, encrypted with the ECDH-derived wrapping key.
45
+ *
46
+ * ## Usage
47
+ *
48
+ * ### Generate a "share" QR (device already in mesh)
49
+ *
50
+ * ```ts
51
+ * import { generateShareQR } from 'interocitor';
52
+ *
53
+ * const { qrEncoded, pairUrl, complete } = await generateShareQR({
54
+ * adapter,
55
+ * relayBase: '/Interocitor', // any path on the shared backend
56
+ * remotePath: '/Interocitor/team-alpha',
57
+ * meshKey, // CryptoKey | null
58
+ * pairBaseUrl: 'https://app.example.com/pair',
59
+ * });
60
+ *
61
+ * renderQR(qrEncoded); // show QR on screen
62
+ * await complete(); // wait for scanner to pick up credentials
63
+ * ```
64
+ *
65
+ * ### Generate a "join" QR (device wanting to join)
66
+ *
67
+ * ```ts
68
+ * import { generateJoinQR } from 'interocitor';
69
+ *
70
+ * const { qrEncoded, pairUrl, credentials } = await generateJoinQR({
71
+ * adapter,
72
+ * relayBase: '/Interocitor',
73
+ * pairBaseUrl: 'https://app.example.com/pair',
74
+ * });
75
+ *
76
+ * renderQR(qrEncoded);
77
+ * const { remotePath, meshKey } = await credentials;
78
+ * // configure engine and connect
79
+ * ```
80
+ *
81
+ * ### Handle a scanned QR / opened pair URL
82
+ *
83
+ * ```ts
84
+ * import { handleScannedQR, parseQRFromUrl } from 'interocitor';
85
+ *
86
+ * const payload = parseQRFromUrl(window.location.hash);
87
+ * // or: const payload = decodeQRPayload(rawQRString);
88
+ *
89
+ * const result = await handleScannedQR({
90
+ * adapter,
91
+ * relayBase: '/Interocitor',
92
+ * payload,
93
+ * // required when payload.intent === 'join' (scanner must have credentials):
94
+ * ownCredentials: { remotePath, meshKey },
95
+ * });
96
+ *
97
+ * if (result) {
98
+ * // intent was 'share' — we received credentials
99
+ * const { remotePath, meshKey } = result;
100
+ * if (meshKey) engine.setEncryptionKey(meshKey);
101
+ * await engine.connect(remotePath);
102
+ * }
103
+ * // intent was 'join' — we pushed credentials, nothing to do on scanner side
104
+ * ```
105
+ */
106
+ import type { StorageAdapter } from '../core/types.ts';
107
+ import { type HandshakeQRPayload } from './qr.ts';
108
+ import { type HandshakeCredentials } from './channel.ts';
109
+ export { encodeQRPayload, decodeQRPayload, buildPairUrl, parseQRFromUrl, } from './qr.ts';
110
+ export type { HandshakeQRPayload, HandshakeIntent } from './qr.ts';
111
+ export { generateECDHKeypair, exportECDHPublicKey, importECDHPublicKey, createGeneratorSession, runScannerHandshake, } from './channel.ts';
112
+ export type { HandshakeCredentials, GeneratorSession } from './channel.ts';
113
+ export interface GenerateShareQROptions {
114
+ /** Storage adapter connected to the shared backend. */
115
+ adapter: StorageAdapter;
116
+ /**
117
+ * Base path on the backend used for relay files.
118
+ * Relay files are written under `{relayBase}/handshake/{handshakeId}/`.
119
+ * Typically the same root path your mesh uses.
120
+ */
121
+ relayBase: string;
122
+ /** The mesh cloud folder path to share with the joiner. */
123
+ remotePath: string;
124
+ /** Base58 passphrase for the mesh encryption key, or null for unencrypted. */
125
+ passphrase: string | null;
126
+ /** Base URL for the pair link embedded in the QR payload (optional). */
127
+ pairBaseUrl?: string;
128
+ /** Polling interval while waiting for the scanner (ms, default 2000). */
129
+ pollIntervalMs?: number;
130
+ /** Give up after this long (ms, default 120000). */
131
+ timeoutMs?: number;
132
+ }
133
+ export interface GenerateShareQRResult {
134
+ /** The raw QR payload object. */
135
+ qrPayload: HandshakeQRPayload;
136
+ /** Compact base64url string — pass to any QR library. */
137
+ qrEncoded: string;
138
+ /** Full pair URL with payload in fragment. null if pairBaseUrl not provided. */
139
+ pairUrl: string | null;
140
+ /**
141
+ * Wait for the scanner to pick up the credentials.
142
+ * Resolves when the scanner has read the relay and cleaned up.
143
+ */
144
+ complete(): Promise<void>;
145
+ }
146
+ /**
147
+ * Generate a "share" QR code. Call this on a device that already belongs
148
+ * to a mesh and wants to invite another device.
149
+ */
150
+ export declare function generateShareQR(options: GenerateShareQROptions): Promise<GenerateShareQRResult>;
151
+ export interface GenerateJoinQROptions {
152
+ /** Storage adapter connected to the shared backend. */
153
+ adapter: StorageAdapter;
154
+ /**
155
+ * Base path on the backend used for relay files.
156
+ * Must match the relayBase used by the scanning device.
157
+ */
158
+ relayBase: string;
159
+ /** Base URL for the pair link embedded in the QR payload (optional). */
160
+ pairBaseUrl?: string;
161
+ /** Polling interval while waiting for credentials (ms, default 2000). */
162
+ pollIntervalMs?: number;
163
+ /** Give up after this long (ms, default 120000). */
164
+ timeoutMs?: number;
165
+ }
166
+ export interface GenerateJoinQRResult {
167
+ /** The raw QR payload object. */
168
+ qrPayload: HandshakeQRPayload;
169
+ /** Compact base64url string — pass to any QR library. */
170
+ qrEncoded: string;
171
+ /** Full pair URL with payload in fragment. null if pairBaseUrl not provided. */
172
+ pairUrl: string | null;
173
+ /**
174
+ * Resolves with the received credentials once the scanner has pushed them.
175
+ * Configure your engine with these and connect.
176
+ */
177
+ credentials: Promise<HandshakeCredentials>;
178
+ }
179
+ /**
180
+ * Generate a "join" QR code. Call this on a device that wants to join a mesh
181
+ * but does not yet have credentials. Show this QR to a device that is already
182
+ * in the mesh; that device scans it and pushes credentials.
183
+ */
184
+ export declare function generateJoinQR(options: GenerateJoinQROptions): Promise<GenerateJoinQRResult>;
185
+ export interface HandleScannedQROptions {
186
+ /** Storage adapter connected to the shared backend. */
187
+ adapter: StorageAdapter;
188
+ /**
189
+ * Base path on the backend used for relay files.
190
+ * Must match the relayBase used by the generating device.
191
+ */
192
+ relayBase: string;
193
+ /** Payload decoded from the scanned QR or opened pair URL. */
194
+ payload: HandshakeQRPayload;
195
+ /**
196
+ * The scanner's own mesh credentials.
197
+ * Required when payload.intent === 'join' (you must push credentials).
198
+ * Ignored when payload.intent === 'share' (you will receive credentials).
199
+ */
200
+ ownCredentials?: HandshakeCredentials;
201
+ /** Polling interval (ms, default 2000). */
202
+ pollIntervalMs?: number;
203
+ /** Give up after this long (ms, default 120000). */
204
+ timeoutMs?: number;
205
+ }
206
+ /**
207
+ * Handle a scanned QR code or opened pair URL.
208
+ *
209
+ * Returns the received credentials when intent === 'share' (null otherwise,
210
+ * because the scanner already has credentials when intent === 'join').
211
+ */
212
+ export declare function handleScannedQR(options: HandleScannedQROptions): Promise<HandshakeCredentials | null>;
213
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handshake/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * interocitor/handshake
3
+ *
4
+ * Secure device pairing via QR code.
5
+ *
6
+ * Both devices run an interocitor instance with a preconfigured backend
7
+ * adapter. Neither device needs to know the mesh `remotePath` or master
8
+ * key beforehand — everything is bootstrapped through the handshake.
9
+ *
10
+ * ## Two intents
11
+ *
12
+ * A QR code is generated with one of two intents, declared by the
13
+ * **generator** (the device showing the QR):
14
+ *
15
+ * "share" — I have mesh credentials. Scan me and I will push them to
16
+ * you through the relay.
17
+ *
18
+ * "join" — I want to join a mesh. Scan me and push your credentials
19
+ * to me through the relay.
20
+ *
21
+ * The **scanner** (the device that reads the QR) always does the
22
+ * opposite: it receives when the generator shares, and it pushes when
23
+ * the generator joins.
24
+ *
25
+ * ## Which device shows the QR?
26
+ *
27
+ * Desktop already in mesh, Mobile wants to join
28
+ * → Desktop generates "share" QR, Mobile scans → Mobile joins.
29
+ *
30
+ * Mobile wants to join, shows QR to Desktop that is already in mesh
31
+ * → Mobile generates "join" QR, Desktop scans → Mobile joins.
32
+ *
33
+ * ## QR payload — two pieces
34
+ *
35
+ * Cloud piece → handshakeId
36
+ * Scopes two relay files on the shared backend.
37
+ * Visible in the cloud but useless without the key.
38
+ *
39
+ * Eyes-only → generatorPub (ephemeral ECDH-P256 public key)
40
+ * The scanner derives a wrapping key from it via ECDH.
41
+ * Only someone who physically saw the QR can do this.
42
+ *
43
+ * remotePath and meshKey are NEVER in the QR. They travel through the
44
+ * relay, encrypted with the ECDH-derived wrapping key.
45
+ *
46
+ * ## Usage
47
+ *
48
+ * ### Generate a "share" QR (device already in mesh)
49
+ *
50
+ * ```ts
51
+ * import { generateShareQR } from 'interocitor';
52
+ *
53
+ * const { qrEncoded, pairUrl, complete } = await generateShareQR({
54
+ * adapter,
55
+ * relayBase: '/Interocitor', // any path on the shared backend
56
+ * remotePath: '/Interocitor/team-alpha',
57
+ * meshKey, // CryptoKey | null
58
+ * pairBaseUrl: 'https://app.example.com/pair',
59
+ * });
60
+ *
61
+ * renderQR(qrEncoded); // show QR on screen
62
+ * await complete(); // wait for scanner to pick up credentials
63
+ * ```
64
+ *
65
+ * ### Generate a "join" QR (device wanting to join)
66
+ *
67
+ * ```ts
68
+ * import { generateJoinQR } from 'interocitor';
69
+ *
70
+ * const { qrEncoded, pairUrl, credentials } = await generateJoinQR({
71
+ * adapter,
72
+ * relayBase: '/Interocitor',
73
+ * pairBaseUrl: 'https://app.example.com/pair',
74
+ * });
75
+ *
76
+ * renderQR(qrEncoded);
77
+ * const { remotePath, meshKey } = await credentials;
78
+ * // configure engine and connect
79
+ * ```
80
+ *
81
+ * ### Handle a scanned QR / opened pair URL
82
+ *
83
+ * ```ts
84
+ * import { handleScannedQR, parseQRFromUrl } from 'interocitor';
85
+ *
86
+ * const payload = parseQRFromUrl(window.location.hash);
87
+ * // or: const payload = decodeQRPayload(rawQRString);
88
+ *
89
+ * const result = await handleScannedQR({
90
+ * adapter,
91
+ * relayBase: '/Interocitor',
92
+ * payload,
93
+ * // required when payload.intent === 'join' (scanner must have credentials):
94
+ * ownCredentials: { remotePath, meshKey },
95
+ * });
96
+ *
97
+ * if (result) {
98
+ * // intent was 'share' — we received credentials
99
+ * const { remotePath, meshKey } = result;
100
+ * if (meshKey) engine.setEncryptionKey(meshKey);
101
+ * await engine.connect(remotePath);
102
+ * }
103
+ * // intent was 'join' — we pushed credentials, nothing to do on scanner side
104
+ * ```
105
+ */
106
+ import { encodeQRPayload, buildPairUrl, } from "./qr.js";
107
+ import { createGeneratorSession, runScannerHandshake, } from "./channel.js";
108
+ export { encodeQRPayload, decodeQRPayload, buildPairUrl, parseQRFromUrl, } from "./qr.js";
109
+ export { generateECDHKeypair, exportECDHPublicKey, importECDHPublicKey, createGeneratorSession, runScannerHandshake, } from "./channel.js";
110
+ // ─── Helpers ─────────────────────────────────────────────────────────
111
+ function generateHandshakeId() {
112
+ const b = crypto.getRandomValues(new Uint8Array(12));
113
+ return Array.from(b, x => x.toString(16).padStart(2, '0')).join('');
114
+ }
115
+ /**
116
+ * Generate a "share" QR code. Call this on a device that already belongs
117
+ * to a mesh and wants to invite another device.
118
+ */
119
+ export async function generateShareQR(options) {
120
+ const { adapter, relayBase, remotePath, passphrase, pairBaseUrl, pollIntervalMs, timeoutMs } = options;
121
+ const session = await createGeneratorSession();
122
+ const handshakeId = generateHandshakeId();
123
+ const adapterConfig = adapter.getHandshakeConfig?.();
124
+ const qrPayload = {
125
+ intent: 'share',
126
+ handshakeId,
127
+ generatorPub: session.generatorPub,
128
+ ...(adapterConfig !== undefined && { adapterConfig }),
129
+ };
130
+ return {
131
+ qrPayload,
132
+ qrEncoded: encodeQRPayload(qrPayload),
133
+ pairUrl: pairBaseUrl ? buildPairUrl(pairBaseUrl, qrPayload) : null,
134
+ async complete() {
135
+ await session.complete(adapter, handshakeId, relayBase, 'share', { remotePath, passphrase }, { pollIntervalMs, timeoutMs });
136
+ },
137
+ };
138
+ }
139
+ /**
140
+ * Generate a "join" QR code. Call this on a device that wants to join a mesh
141
+ * but does not yet have credentials. Show this QR to a device that is already
142
+ * in the mesh; that device scans it and pushes credentials.
143
+ */
144
+ export async function generateJoinQR(options) {
145
+ const { adapter, relayBase, pairBaseUrl, pollIntervalMs, timeoutMs } = options;
146
+ const session = await createGeneratorSession();
147
+ const handshakeId = generateHandshakeId();
148
+ const adapterConfig = adapter.getHandshakeConfig?.();
149
+ const qrPayload = {
150
+ intent: 'join',
151
+ handshakeId,
152
+ generatorPub: session.generatorPub,
153
+ ...(adapterConfig !== undefined && { adapterConfig }),
154
+ };
155
+ const credentialsPromise = session.complete(adapter, handshakeId, relayBase, 'join', null, // generator doesn't have credentials — it wants them
156
+ { pollIntervalMs, timeoutMs }).then(result => {
157
+ if (!result)
158
+ throw new Error('join handshake produced no credentials');
159
+ return result;
160
+ });
161
+ return {
162
+ qrPayload,
163
+ qrEncoded: encodeQRPayload(qrPayload),
164
+ pairUrl: pairBaseUrl ? buildPairUrl(pairBaseUrl, qrPayload) : null,
165
+ credentials: credentialsPromise,
166
+ };
167
+ }
168
+ /**
169
+ * Handle a scanned QR code or opened pair URL.
170
+ *
171
+ * Returns the received credentials when intent === 'share' (null otherwise,
172
+ * because the scanner already has credentials when intent === 'join').
173
+ */
174
+ export async function handleScannedQR(options) {
175
+ const { adapter, relayBase, payload, ownCredentials, pollIntervalMs, timeoutMs } = options;
176
+ if (payload.intent === 'join' && !ownCredentials) {
177
+ throw new Error('handleScannedQR: ownCredentials required when scanning a "join" QR ' +
178
+ '(the scanner must push credentials to the generator)');
179
+ }
180
+ return runScannerHandshake(adapter, payload, ownCredentials ?? null, relayBase, { pollIntervalMs, timeoutMs });
181
+ }
182
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handshake/index.ts"],"names":[],"mappings":"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"}