@abraca/dabra 1.0.19 → 1.0.21
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/abracadabra-provider.cjs +302 -5
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +302 -6
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +104 -1
- package/package.json +1 -1
- package/src/AbracadabraClient.ts +27 -0
- package/src/AbracadabraWS.ts +2 -3
- package/src/BackgroundSyncManager.ts +19 -0
- package/src/types.ts +3 -0
- package/src/webrtc/AbracadabraWebRTC.ts +8 -4
- package/src/webrtc/DevicePairingChannel.ts +384 -0
- package/src/webrtc/index.ts +2 -0
|
@@ -1745,7 +1745,7 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1745
1745
|
}
|
|
1746
1746
|
attach(provider) {
|
|
1747
1747
|
const existing = this.configuration.providerMap.get(provider.configuration.name);
|
|
1748
|
-
if (existing && existing !== provider) console.
|
|
1748
|
+
if (existing && existing !== provider) console.debug(`[AbracadabraWS] attach: replacing provider for "${provider.configuration.name}".`);
|
|
1749
1749
|
this.configuration.providerMap.set(provider.configuration.name, provider);
|
|
1750
1750
|
if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) this.connect();
|
|
1751
1751
|
if (this.receivedOnOpenPayload && this.status === WebSocketStatus.Connected) provider.onOpen(this.receivedOnOpenPayload);
|
|
@@ -3228,10 +3228,30 @@ var AbracadabraClient = class {
|
|
|
3228
3228
|
async listKeys() {
|
|
3229
3229
|
return (await this.request("GET", "/auth/keys")).keys;
|
|
3230
3230
|
}
|
|
3231
|
+
/** Rename a registered device key. */
|
|
3232
|
+
async renameKey(keyId, deviceName) {
|
|
3233
|
+
await this.request("PATCH", `/auth/keys/${encodeURIComponent(keyId)}`, { body: { deviceName } });
|
|
3234
|
+
}
|
|
3231
3235
|
/** Revoke a public key by its ID. */
|
|
3232
3236
|
async revokeKey(keyId) {
|
|
3233
3237
|
await this.request("DELETE", `/auth/keys/${encodeURIComponent(keyId)}`);
|
|
3234
3238
|
}
|
|
3239
|
+
/** Create a single-use device invite code for pairing a new device to this account. */
|
|
3240
|
+
async createDeviceInvite(opts) {
|
|
3241
|
+
return this.request("POST", "/auth/device-invite", { body: opts?.expiresIn != null ? { expiresIn: opts.expiresIn } : {} });
|
|
3242
|
+
}
|
|
3243
|
+
/** Redeem a device invite code to register a new device key. Returns a JWT token. */
|
|
3244
|
+
async redeemDeviceInvite(opts) {
|
|
3245
|
+
return this.request("POST", "/auth/device-redeem", {
|
|
3246
|
+
body: {
|
|
3247
|
+
code: opts.code,
|
|
3248
|
+
publicKey: opts.publicKey,
|
|
3249
|
+
x25519Key: opts.x25519Key,
|
|
3250
|
+
deviceName: opts.deviceName
|
|
3251
|
+
},
|
|
3252
|
+
auth: false
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3235
3255
|
/** Get encryption info for a document. */
|
|
3236
3256
|
async getDocEncryption(docId) {
|
|
3237
3257
|
return this.request("GET", `/docs/${encodeURIComponent(docId)}/encryption`);
|
|
@@ -8629,6 +8649,14 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8629
8649
|
}
|
|
8630
8650
|
async _syncNonE2EDoc(docId) {
|
|
8631
8651
|
const alreadyCached = this.rootProvider.children.has(docId);
|
|
8652
|
+
if (!alreadyCached) {
|
|
8653
|
+
if ((this.rootProvider.configuration?.websocketProvider?.configuration?.providerMap)?.has(docId)) return {
|
|
8654
|
+
docId,
|
|
8655
|
+
status: "synced",
|
|
8656
|
+
lastSynced: Date.now(),
|
|
8657
|
+
isE2E: false
|
|
8658
|
+
};
|
|
8659
|
+
}
|
|
8632
8660
|
const childProvider = await this.rootProvider.loadChild(docId);
|
|
8633
8661
|
await childProvider.ready;
|
|
8634
8662
|
await this._waitForSynced(childProvider);
|
|
@@ -9941,19 +9969,23 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9941
9969
|
}
|
|
9942
9970
|
/**
|
|
9943
9971
|
* Send a custom string message to a specific peer via a data channel.
|
|
9972
|
+
* When E2EE is active, the message is encrypted through the router.
|
|
9944
9973
|
*/
|
|
9945
9974
|
sendCustomMessage(peerId, payload) {
|
|
9946
9975
|
const pc = this.peerConnections.get(peerId);
|
|
9947
9976
|
if (!pc) return;
|
|
9948
|
-
|
|
9977
|
+
const channelName = "custom";
|
|
9978
|
+
let channel = pc.router.getChannel(channelName);
|
|
9949
9979
|
if (!channel || channel.readyState !== "open") {
|
|
9950
|
-
channel = pc.router.createChannel(
|
|
9980
|
+
channel = pc.router.createChannel(channelName, { ordered: true });
|
|
9951
9981
|
channel.onopen = () => {
|
|
9952
|
-
|
|
9982
|
+
const data = new TextEncoder().encode(payload);
|
|
9983
|
+
pc.router.send(channelName, data);
|
|
9953
9984
|
};
|
|
9954
9985
|
return;
|
|
9955
9986
|
}
|
|
9956
|
-
|
|
9987
|
+
const data = new TextEncoder().encode(payload);
|
|
9988
|
+
pc.router.send(channelName, data);
|
|
9957
9989
|
}
|
|
9958
9990
|
/**
|
|
9959
9991
|
* Send a custom string message to all connected peers.
|
|
@@ -10300,6 +10332,270 @@ var ManualSignaling = class extends EventEmitter {
|
|
|
10300
10332
|
}
|
|
10301
10333
|
};
|
|
10302
10334
|
|
|
10335
|
+
//#endregion
|
|
10336
|
+
//#region packages/provider/src/webrtc/DevicePairingChannel.ts
|
|
10337
|
+
/**
|
|
10338
|
+
* DevicePairingChannel
|
|
10339
|
+
*
|
|
10340
|
+
* Enables cross-device identity pairing over an E2EE WebRTC data channel.
|
|
10341
|
+
* Device A (approver) creates a pairing session with a short code. Device B
|
|
10342
|
+
* (requester) joins with the code. After E2EE is established, Device B sends
|
|
10343
|
+
* its public key and Device A registers it via `addKey()`.
|
|
10344
|
+
*
|
|
10345
|
+
* Reuses the existing WebRTC stack: SignalingSocket, PeerConnection,
|
|
10346
|
+
* DataChannelRouter, and E2EEChannel (X25519 ECDH + AES-256-GCM).
|
|
10347
|
+
*/
|
|
10348
|
+
/** Ambiguity-free charset (no 0/O/1/I). */
|
|
10349
|
+
const CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
10350
|
+
const CODE_LENGTH = 6;
|
|
10351
|
+
const PAIRING_TIMEOUT_MS = 300 * 1e3;
|
|
10352
|
+
function generatePairingCode() {
|
|
10353
|
+
const bytes = crypto.getRandomValues(new Uint8Array(CODE_LENGTH));
|
|
10354
|
+
return Array.from(bytes).map((b) => CODE_CHARSET[b % 32]).join("");
|
|
10355
|
+
}
|
|
10356
|
+
function codeToRoomId(code) {
|
|
10357
|
+
const hash = sha256(new TextEncoder().encode(code.toUpperCase()));
|
|
10358
|
+
return `__pairing_${Array.from(hash.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
10359
|
+
}
|
|
10360
|
+
var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
10361
|
+
constructor(role, pairingCode, config) {
|
|
10362
|
+
super();
|
|
10363
|
+
this.config = config;
|
|
10364
|
+
this.webrtc = null;
|
|
10365
|
+
this.timeoutHandle = null;
|
|
10366
|
+
this._destroyed = false;
|
|
10367
|
+
this._pendingRequest = null;
|
|
10368
|
+
this._connectedPeerId = null;
|
|
10369
|
+
this.role = role;
|
|
10370
|
+
this.pairingCode = pairingCode;
|
|
10371
|
+
}
|
|
10372
|
+
/**
|
|
10373
|
+
* Create an approver session (Device A). Returns the channel and a
|
|
10374
|
+
* 6-character pairing code to share with Device B.
|
|
10375
|
+
*/
|
|
10376
|
+
static createApprover(config) {
|
|
10377
|
+
const code = generatePairingCode();
|
|
10378
|
+
const channel = new DevicePairingChannel("approver", code, config);
|
|
10379
|
+
channel.start();
|
|
10380
|
+
return {
|
|
10381
|
+
channel,
|
|
10382
|
+
pairingCode: code
|
|
10383
|
+
};
|
|
10384
|
+
}
|
|
10385
|
+
/**
|
|
10386
|
+
* Create a requester session (Device B). Joins with a pairing code
|
|
10387
|
+
* obtained from Device A.
|
|
10388
|
+
*/
|
|
10389
|
+
static createRequester(config, pairingCode) {
|
|
10390
|
+
const channel = new DevicePairingChannel("requester", pairingCode.toUpperCase().replace(/[^A-Z2-9]/g, ""), config);
|
|
10391
|
+
channel.start();
|
|
10392
|
+
return channel;
|
|
10393
|
+
}
|
|
10394
|
+
/**
|
|
10395
|
+
* Approve the pending pairing request. Calls `client.addKey()` to
|
|
10396
|
+
* register Device B's public key, then notifies Device B.
|
|
10397
|
+
*/
|
|
10398
|
+
async approve(client) {
|
|
10399
|
+
if (this.role !== "approver") return {
|
|
10400
|
+
success: false,
|
|
10401
|
+
error: "Only the approver can approve"
|
|
10402
|
+
};
|
|
10403
|
+
if (!this._pendingRequest) return {
|
|
10404
|
+
success: false,
|
|
10405
|
+
error: "No pending pairing request"
|
|
10406
|
+
};
|
|
10407
|
+
if (!this._connectedPeerId) return {
|
|
10408
|
+
success: false,
|
|
10409
|
+
error: "Peer not connected"
|
|
10410
|
+
};
|
|
10411
|
+
const req = this._pendingRequest;
|
|
10412
|
+
try {
|
|
10413
|
+
await client.addKey({
|
|
10414
|
+
publicKey: req.publicKey,
|
|
10415
|
+
deviceName: req.deviceName,
|
|
10416
|
+
x25519Key: req.x25519Key
|
|
10417
|
+
});
|
|
10418
|
+
this.sendMessage({ type: "pair-approved" });
|
|
10419
|
+
this._pendingRequest = null;
|
|
10420
|
+
this.emit("pairingComplete", { success: true });
|
|
10421
|
+
return { success: true };
|
|
10422
|
+
} catch (err) {
|
|
10423
|
+
const error = err?.message ?? "Failed to register device key";
|
|
10424
|
+
this.sendMessage({
|
|
10425
|
+
type: "pair-rejected",
|
|
10426
|
+
reason: error
|
|
10427
|
+
});
|
|
10428
|
+
this._pendingRequest = null;
|
|
10429
|
+
const result = {
|
|
10430
|
+
success: false,
|
|
10431
|
+
error
|
|
10432
|
+
};
|
|
10433
|
+
this.emit("pairingComplete", result);
|
|
10434
|
+
return result;
|
|
10435
|
+
}
|
|
10436
|
+
}
|
|
10437
|
+
/**
|
|
10438
|
+
* Approve via server-side device invite. Creates a single-use invite code
|
|
10439
|
+
* and sends it to Device B over the E2EE channel. Device B redeems it
|
|
10440
|
+
* independently via HTTP — Device A can go offline after this.
|
|
10441
|
+
*/
|
|
10442
|
+
async approveWithInvite(client) {
|
|
10443
|
+
if (this.role !== "approver") return {
|
|
10444
|
+
success: false,
|
|
10445
|
+
error: "Only the approver can approve"
|
|
10446
|
+
};
|
|
10447
|
+
if (!this._pendingRequest) return {
|
|
10448
|
+
success: false,
|
|
10449
|
+
error: "No pending pairing request"
|
|
10450
|
+
};
|
|
10451
|
+
if (!this._connectedPeerId) return {
|
|
10452
|
+
success: false,
|
|
10453
|
+
error: "Peer not connected"
|
|
10454
|
+
};
|
|
10455
|
+
try {
|
|
10456
|
+
const { code } = await client.createDeviceInvite();
|
|
10457
|
+
this.sendMessage({
|
|
10458
|
+
type: "pair-invite-code",
|
|
10459
|
+
code
|
|
10460
|
+
});
|
|
10461
|
+
this._pendingRequest = null;
|
|
10462
|
+
this.emit("pairingComplete", { success: true });
|
|
10463
|
+
return { success: true };
|
|
10464
|
+
} catch (err) {
|
|
10465
|
+
const error = err?.message ?? "Failed to create device invite";
|
|
10466
|
+
this.sendMessage({
|
|
10467
|
+
type: "pair-rejected",
|
|
10468
|
+
reason: error
|
|
10469
|
+
});
|
|
10470
|
+
this._pendingRequest = null;
|
|
10471
|
+
const result = {
|
|
10472
|
+
success: false,
|
|
10473
|
+
error
|
|
10474
|
+
};
|
|
10475
|
+
this.emit("pairingComplete", result);
|
|
10476
|
+
return result;
|
|
10477
|
+
}
|
|
10478
|
+
}
|
|
10479
|
+
/**
|
|
10480
|
+
* Reject the pending pairing request.
|
|
10481
|
+
*/
|
|
10482
|
+
reject(reason = "Rejected by user") {
|
|
10483
|
+
if (this.role !== "approver" || !this._pendingRequest) return;
|
|
10484
|
+
this.sendMessage({
|
|
10485
|
+
type: "pair-rejected",
|
|
10486
|
+
reason
|
|
10487
|
+
});
|
|
10488
|
+
this._pendingRequest = null;
|
|
10489
|
+
this.emit("pairingComplete", {
|
|
10490
|
+
success: false,
|
|
10491
|
+
error: reason
|
|
10492
|
+
});
|
|
10493
|
+
}
|
|
10494
|
+
/**
|
|
10495
|
+
* Send a pairing request to Device A. Call this after the "connected"
|
|
10496
|
+
* event fires.
|
|
10497
|
+
*/
|
|
10498
|
+
requestPairing(request) {
|
|
10499
|
+
if (this.role !== "requester") return;
|
|
10500
|
+
this.sendMessage({
|
|
10501
|
+
type: "pair-request",
|
|
10502
|
+
publicKey: request.publicKey,
|
|
10503
|
+
x25519Key: request.x25519Key,
|
|
10504
|
+
deviceName: request.deviceName
|
|
10505
|
+
});
|
|
10506
|
+
}
|
|
10507
|
+
get isDestroyed() {
|
|
10508
|
+
return this._destroyed;
|
|
10509
|
+
}
|
|
10510
|
+
destroy() {
|
|
10511
|
+
if (this._destroyed) return;
|
|
10512
|
+
this._destroyed = true;
|
|
10513
|
+
if (this.timeoutHandle) {
|
|
10514
|
+
clearTimeout(this.timeoutHandle);
|
|
10515
|
+
this.timeoutHandle = null;
|
|
10516
|
+
}
|
|
10517
|
+
if (this.webrtc) {
|
|
10518
|
+
this.webrtc.destroy();
|
|
10519
|
+
this.webrtc = null;
|
|
10520
|
+
}
|
|
10521
|
+
this.removeAllListeners();
|
|
10522
|
+
}
|
|
10523
|
+
start() {
|
|
10524
|
+
this.webrtc = new AbracadabraWebRTC({
|
|
10525
|
+
docId: codeToRoomId(this.pairingCode),
|
|
10526
|
+
url: this.config.serverUrl,
|
|
10527
|
+
token: this.config.token,
|
|
10528
|
+
iceServers: this.config.iceServers,
|
|
10529
|
+
e2ee: this.config.e2ee,
|
|
10530
|
+
enableDocSync: false,
|
|
10531
|
+
enableAwarenessSync: false,
|
|
10532
|
+
enableFileTransfer: false,
|
|
10533
|
+
autoConnect: false,
|
|
10534
|
+
WebSocketPolyfill: this.config.WebSocketPolyfill
|
|
10535
|
+
});
|
|
10536
|
+
this.webrtc.on("e2eeEstablished", ({ peerId }) => {
|
|
10537
|
+
this._connectedPeerId = peerId;
|
|
10538
|
+
this.emit("connected");
|
|
10539
|
+
});
|
|
10540
|
+
this.webrtc.on("customMessage", ({ peerId, payload }) => {
|
|
10541
|
+
this.handleMessage(peerId, payload);
|
|
10542
|
+
});
|
|
10543
|
+
this.webrtc.on("peerLeft", () => {
|
|
10544
|
+
if (!this._destroyed) this.emit("error", /* @__PURE__ */ new Error("Peer disconnected"));
|
|
10545
|
+
});
|
|
10546
|
+
this.webrtc.on("signalingError", (err) => {
|
|
10547
|
+
this.emit("error", /* @__PURE__ */ new Error(`Signaling: ${err.message}`));
|
|
10548
|
+
});
|
|
10549
|
+
this.timeoutHandle = setTimeout(() => {
|
|
10550
|
+
if (!this._destroyed) {
|
|
10551
|
+
this.emit("error", /* @__PURE__ */ new Error("Pairing timed out"));
|
|
10552
|
+
this.destroy();
|
|
10553
|
+
}
|
|
10554
|
+
}, PAIRING_TIMEOUT_MS);
|
|
10555
|
+
this.webrtc.connect();
|
|
10556
|
+
}
|
|
10557
|
+
sendMessage(msg) {
|
|
10558
|
+
if (!this.webrtc || !this._connectedPeerId) return;
|
|
10559
|
+
this.webrtc.sendCustomMessage(this._connectedPeerId, JSON.stringify(msg));
|
|
10560
|
+
}
|
|
10561
|
+
handleMessage(peerId, payload) {
|
|
10562
|
+
let msg;
|
|
10563
|
+
try {
|
|
10564
|
+
msg = JSON.parse(payload);
|
|
10565
|
+
} catch {
|
|
10566
|
+
return;
|
|
10567
|
+
}
|
|
10568
|
+
switch (msg.type) {
|
|
10569
|
+
case "pair-request":
|
|
10570
|
+
if (this.role !== "approver") return;
|
|
10571
|
+
this._pendingRequest = {
|
|
10572
|
+
publicKey: msg.publicKey,
|
|
10573
|
+
x25519Key: msg.x25519Key,
|
|
10574
|
+
deviceName: msg.deviceName
|
|
10575
|
+
};
|
|
10576
|
+
this.emit("pairingRequest", this._pendingRequest);
|
|
10577
|
+
break;
|
|
10578
|
+
case "pair-approved":
|
|
10579
|
+
if (this.role !== "requester") return;
|
|
10580
|
+
this.emit("approved");
|
|
10581
|
+
this.emit("pairingComplete", { success: true });
|
|
10582
|
+
break;
|
|
10583
|
+
case "pair-rejected":
|
|
10584
|
+
if (this.role !== "requester") return;
|
|
10585
|
+
this.emit("rejected", msg.reason);
|
|
10586
|
+
this.emit("pairingComplete", {
|
|
10587
|
+
success: false,
|
|
10588
|
+
error: msg.reason
|
|
10589
|
+
});
|
|
10590
|
+
break;
|
|
10591
|
+
case "pair-invite-code":
|
|
10592
|
+
if (this.role !== "requester") return;
|
|
10593
|
+
this.emit("inviteCode", msg.code);
|
|
10594
|
+
break;
|
|
10595
|
+
}
|
|
10596
|
+
}
|
|
10597
|
+
};
|
|
10598
|
+
|
|
10303
10599
|
//#endregion
|
|
10304
10600
|
//#region packages/provider/src/sync/BroadcastChannelSync.ts
|
|
10305
10601
|
/**
|
|
@@ -10468,6 +10764,7 @@ exports.CryptoIdentityKeystore = CryptoIdentityKeystore;
|
|
|
10468
10764
|
exports.DEFAULT_FILE_CHUNK_SIZE = DEFAULT_FILE_CHUNK_SIZE;
|
|
10469
10765
|
exports.DEFAULT_ICE_SERVERS = DEFAULT_ICE_SERVERS;
|
|
10470
10766
|
exports.DataChannelRouter = DataChannelRouter;
|
|
10767
|
+
exports.DevicePairingChannel = DevicePairingChannel;
|
|
10471
10768
|
exports.DocKeyManager = DocKeyManager;
|
|
10472
10769
|
exports.DocumentCache = DocumentCache;
|
|
10473
10770
|
exports.E2EAbracadabraProvider = E2EAbracadabraProvider;
|