@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
|
@@ -1715,7 +1715,7 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1715
1715
|
}
|
|
1716
1716
|
attach(provider) {
|
|
1717
1717
|
const existing = this.configuration.providerMap.get(provider.configuration.name);
|
|
1718
|
-
if (existing && existing !== provider) console.
|
|
1718
|
+
if (existing && existing !== provider) console.debug(`[AbracadabraWS] attach: replacing provider for "${provider.configuration.name}".`);
|
|
1719
1719
|
this.configuration.providerMap.set(provider.configuration.name, provider);
|
|
1720
1720
|
if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) this.connect();
|
|
1721
1721
|
if (this.receivedOnOpenPayload && this.status === WebSocketStatus.Connected) provider.onOpen(this.receivedOnOpenPayload);
|
|
@@ -3198,10 +3198,30 @@ var AbracadabraClient = class {
|
|
|
3198
3198
|
async listKeys() {
|
|
3199
3199
|
return (await this.request("GET", "/auth/keys")).keys;
|
|
3200
3200
|
}
|
|
3201
|
+
/** Rename a registered device key. */
|
|
3202
|
+
async renameKey(keyId, deviceName) {
|
|
3203
|
+
await this.request("PATCH", `/auth/keys/${encodeURIComponent(keyId)}`, { body: { deviceName } });
|
|
3204
|
+
}
|
|
3201
3205
|
/** Revoke a public key by its ID. */
|
|
3202
3206
|
async revokeKey(keyId) {
|
|
3203
3207
|
await this.request("DELETE", `/auth/keys/${encodeURIComponent(keyId)}`);
|
|
3204
3208
|
}
|
|
3209
|
+
/** Create a single-use device invite code for pairing a new device to this account. */
|
|
3210
|
+
async createDeviceInvite(opts) {
|
|
3211
|
+
return this.request("POST", "/auth/device-invite", { body: opts?.expiresIn != null ? { expiresIn: opts.expiresIn } : {} });
|
|
3212
|
+
}
|
|
3213
|
+
/** Redeem a device invite code to register a new device key. Returns a JWT token. */
|
|
3214
|
+
async redeemDeviceInvite(opts) {
|
|
3215
|
+
return this.request("POST", "/auth/device-redeem", {
|
|
3216
|
+
body: {
|
|
3217
|
+
code: opts.code,
|
|
3218
|
+
publicKey: opts.publicKey,
|
|
3219
|
+
x25519Key: opts.x25519Key,
|
|
3220
|
+
deviceName: opts.deviceName
|
|
3221
|
+
},
|
|
3222
|
+
auth: false
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3205
3225
|
/** Get encryption info for a document. */
|
|
3206
3226
|
async getDocEncryption(docId) {
|
|
3207
3227
|
return this.request("GET", `/docs/${encodeURIComponent(docId)}/encryption`);
|
|
@@ -8577,6 +8597,14 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
8577
8597
|
}
|
|
8578
8598
|
async _syncNonE2EDoc(docId) {
|
|
8579
8599
|
const alreadyCached = this.rootProvider.children.has(docId);
|
|
8600
|
+
if (!alreadyCached) {
|
|
8601
|
+
if ((this.rootProvider.configuration?.websocketProvider?.configuration?.providerMap)?.has(docId)) return {
|
|
8602
|
+
docId,
|
|
8603
|
+
status: "synced",
|
|
8604
|
+
lastSynced: Date.now(),
|
|
8605
|
+
isE2E: false
|
|
8606
|
+
};
|
|
8607
|
+
}
|
|
8580
8608
|
const childProvider = await this.rootProvider.loadChild(docId);
|
|
8581
8609
|
await childProvider.ready;
|
|
8582
8610
|
await this._waitForSynced(childProvider);
|
|
@@ -9889,19 +9917,23 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
|
|
|
9889
9917
|
}
|
|
9890
9918
|
/**
|
|
9891
9919
|
* Send a custom string message to a specific peer via a data channel.
|
|
9920
|
+
* When E2EE is active, the message is encrypted through the router.
|
|
9892
9921
|
*/
|
|
9893
9922
|
sendCustomMessage(peerId, payload) {
|
|
9894
9923
|
const pc = this.peerConnections.get(peerId);
|
|
9895
9924
|
if (!pc) return;
|
|
9896
|
-
|
|
9925
|
+
const channelName = "custom";
|
|
9926
|
+
let channel = pc.router.getChannel(channelName);
|
|
9897
9927
|
if (!channel || channel.readyState !== "open") {
|
|
9898
|
-
channel = pc.router.createChannel(
|
|
9928
|
+
channel = pc.router.createChannel(channelName, { ordered: true });
|
|
9899
9929
|
channel.onopen = () => {
|
|
9900
|
-
|
|
9930
|
+
const data = new TextEncoder().encode(payload);
|
|
9931
|
+
pc.router.send(channelName, data);
|
|
9901
9932
|
};
|
|
9902
9933
|
return;
|
|
9903
9934
|
}
|
|
9904
|
-
|
|
9935
|
+
const data = new TextEncoder().encode(payload);
|
|
9936
|
+
pc.router.send(channelName, data);
|
|
9905
9937
|
}
|
|
9906
9938
|
/**
|
|
9907
9939
|
* Send a custom string message to all connected peers.
|
|
@@ -10248,6 +10280,270 @@ var ManualSignaling = class extends EventEmitter {
|
|
|
10248
10280
|
}
|
|
10249
10281
|
};
|
|
10250
10282
|
|
|
10283
|
+
//#endregion
|
|
10284
|
+
//#region packages/provider/src/webrtc/DevicePairingChannel.ts
|
|
10285
|
+
/**
|
|
10286
|
+
* DevicePairingChannel
|
|
10287
|
+
*
|
|
10288
|
+
* Enables cross-device identity pairing over an E2EE WebRTC data channel.
|
|
10289
|
+
* Device A (approver) creates a pairing session with a short code. Device B
|
|
10290
|
+
* (requester) joins with the code. After E2EE is established, Device B sends
|
|
10291
|
+
* its public key and Device A registers it via `addKey()`.
|
|
10292
|
+
*
|
|
10293
|
+
* Reuses the existing WebRTC stack: SignalingSocket, PeerConnection,
|
|
10294
|
+
* DataChannelRouter, and E2EEChannel (X25519 ECDH + AES-256-GCM).
|
|
10295
|
+
*/
|
|
10296
|
+
/** Ambiguity-free charset (no 0/O/1/I). */
|
|
10297
|
+
const CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
10298
|
+
const CODE_LENGTH = 6;
|
|
10299
|
+
const PAIRING_TIMEOUT_MS = 300 * 1e3;
|
|
10300
|
+
function generatePairingCode() {
|
|
10301
|
+
const bytes = crypto.getRandomValues(new Uint8Array(CODE_LENGTH));
|
|
10302
|
+
return Array.from(bytes).map((b) => CODE_CHARSET[b % 32]).join("");
|
|
10303
|
+
}
|
|
10304
|
+
function codeToRoomId(code) {
|
|
10305
|
+
const hash = sha256(new TextEncoder().encode(code.toUpperCase()));
|
|
10306
|
+
return `__pairing_${Array.from(hash.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
10307
|
+
}
|
|
10308
|
+
var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
10309
|
+
constructor(role, pairingCode, config) {
|
|
10310
|
+
super();
|
|
10311
|
+
this.config = config;
|
|
10312
|
+
this.webrtc = null;
|
|
10313
|
+
this.timeoutHandle = null;
|
|
10314
|
+
this._destroyed = false;
|
|
10315
|
+
this._pendingRequest = null;
|
|
10316
|
+
this._connectedPeerId = null;
|
|
10317
|
+
this.role = role;
|
|
10318
|
+
this.pairingCode = pairingCode;
|
|
10319
|
+
}
|
|
10320
|
+
/**
|
|
10321
|
+
* Create an approver session (Device A). Returns the channel and a
|
|
10322
|
+
* 6-character pairing code to share with Device B.
|
|
10323
|
+
*/
|
|
10324
|
+
static createApprover(config) {
|
|
10325
|
+
const code = generatePairingCode();
|
|
10326
|
+
const channel = new DevicePairingChannel("approver", code, config);
|
|
10327
|
+
channel.start();
|
|
10328
|
+
return {
|
|
10329
|
+
channel,
|
|
10330
|
+
pairingCode: code
|
|
10331
|
+
};
|
|
10332
|
+
}
|
|
10333
|
+
/**
|
|
10334
|
+
* Create a requester session (Device B). Joins with a pairing code
|
|
10335
|
+
* obtained from Device A.
|
|
10336
|
+
*/
|
|
10337
|
+
static createRequester(config, pairingCode) {
|
|
10338
|
+
const channel = new DevicePairingChannel("requester", pairingCode.toUpperCase().replace(/[^A-Z2-9]/g, ""), config);
|
|
10339
|
+
channel.start();
|
|
10340
|
+
return channel;
|
|
10341
|
+
}
|
|
10342
|
+
/**
|
|
10343
|
+
* Approve the pending pairing request. Calls `client.addKey()` to
|
|
10344
|
+
* register Device B's public key, then notifies Device B.
|
|
10345
|
+
*/
|
|
10346
|
+
async approve(client) {
|
|
10347
|
+
if (this.role !== "approver") return {
|
|
10348
|
+
success: false,
|
|
10349
|
+
error: "Only the approver can approve"
|
|
10350
|
+
};
|
|
10351
|
+
if (!this._pendingRequest) return {
|
|
10352
|
+
success: false,
|
|
10353
|
+
error: "No pending pairing request"
|
|
10354
|
+
};
|
|
10355
|
+
if (!this._connectedPeerId) return {
|
|
10356
|
+
success: false,
|
|
10357
|
+
error: "Peer not connected"
|
|
10358
|
+
};
|
|
10359
|
+
const req = this._pendingRequest;
|
|
10360
|
+
try {
|
|
10361
|
+
await client.addKey({
|
|
10362
|
+
publicKey: req.publicKey,
|
|
10363
|
+
deviceName: req.deviceName,
|
|
10364
|
+
x25519Key: req.x25519Key
|
|
10365
|
+
});
|
|
10366
|
+
this.sendMessage({ type: "pair-approved" });
|
|
10367
|
+
this._pendingRequest = null;
|
|
10368
|
+
this.emit("pairingComplete", { success: true });
|
|
10369
|
+
return { success: true };
|
|
10370
|
+
} catch (err) {
|
|
10371
|
+
const error = err?.message ?? "Failed to register device key";
|
|
10372
|
+
this.sendMessage({
|
|
10373
|
+
type: "pair-rejected",
|
|
10374
|
+
reason: error
|
|
10375
|
+
});
|
|
10376
|
+
this._pendingRequest = null;
|
|
10377
|
+
const result = {
|
|
10378
|
+
success: false,
|
|
10379
|
+
error
|
|
10380
|
+
};
|
|
10381
|
+
this.emit("pairingComplete", result);
|
|
10382
|
+
return result;
|
|
10383
|
+
}
|
|
10384
|
+
}
|
|
10385
|
+
/**
|
|
10386
|
+
* Approve via server-side device invite. Creates a single-use invite code
|
|
10387
|
+
* and sends it to Device B over the E2EE channel. Device B redeems it
|
|
10388
|
+
* independently via HTTP — Device A can go offline after this.
|
|
10389
|
+
*/
|
|
10390
|
+
async approveWithInvite(client) {
|
|
10391
|
+
if (this.role !== "approver") return {
|
|
10392
|
+
success: false,
|
|
10393
|
+
error: "Only the approver can approve"
|
|
10394
|
+
};
|
|
10395
|
+
if (!this._pendingRequest) return {
|
|
10396
|
+
success: false,
|
|
10397
|
+
error: "No pending pairing request"
|
|
10398
|
+
};
|
|
10399
|
+
if (!this._connectedPeerId) return {
|
|
10400
|
+
success: false,
|
|
10401
|
+
error: "Peer not connected"
|
|
10402
|
+
};
|
|
10403
|
+
try {
|
|
10404
|
+
const { code } = await client.createDeviceInvite();
|
|
10405
|
+
this.sendMessage({
|
|
10406
|
+
type: "pair-invite-code",
|
|
10407
|
+
code
|
|
10408
|
+
});
|
|
10409
|
+
this._pendingRequest = null;
|
|
10410
|
+
this.emit("pairingComplete", { success: true });
|
|
10411
|
+
return { success: true };
|
|
10412
|
+
} catch (err) {
|
|
10413
|
+
const error = err?.message ?? "Failed to create device invite";
|
|
10414
|
+
this.sendMessage({
|
|
10415
|
+
type: "pair-rejected",
|
|
10416
|
+
reason: error
|
|
10417
|
+
});
|
|
10418
|
+
this._pendingRequest = null;
|
|
10419
|
+
const result = {
|
|
10420
|
+
success: false,
|
|
10421
|
+
error
|
|
10422
|
+
};
|
|
10423
|
+
this.emit("pairingComplete", result);
|
|
10424
|
+
return result;
|
|
10425
|
+
}
|
|
10426
|
+
}
|
|
10427
|
+
/**
|
|
10428
|
+
* Reject the pending pairing request.
|
|
10429
|
+
*/
|
|
10430
|
+
reject(reason = "Rejected by user") {
|
|
10431
|
+
if (this.role !== "approver" || !this._pendingRequest) return;
|
|
10432
|
+
this.sendMessage({
|
|
10433
|
+
type: "pair-rejected",
|
|
10434
|
+
reason
|
|
10435
|
+
});
|
|
10436
|
+
this._pendingRequest = null;
|
|
10437
|
+
this.emit("pairingComplete", {
|
|
10438
|
+
success: false,
|
|
10439
|
+
error: reason
|
|
10440
|
+
});
|
|
10441
|
+
}
|
|
10442
|
+
/**
|
|
10443
|
+
* Send a pairing request to Device A. Call this after the "connected"
|
|
10444
|
+
* event fires.
|
|
10445
|
+
*/
|
|
10446
|
+
requestPairing(request) {
|
|
10447
|
+
if (this.role !== "requester") return;
|
|
10448
|
+
this.sendMessage({
|
|
10449
|
+
type: "pair-request",
|
|
10450
|
+
publicKey: request.publicKey,
|
|
10451
|
+
x25519Key: request.x25519Key,
|
|
10452
|
+
deviceName: request.deviceName
|
|
10453
|
+
});
|
|
10454
|
+
}
|
|
10455
|
+
get isDestroyed() {
|
|
10456
|
+
return this._destroyed;
|
|
10457
|
+
}
|
|
10458
|
+
destroy() {
|
|
10459
|
+
if (this._destroyed) return;
|
|
10460
|
+
this._destroyed = true;
|
|
10461
|
+
if (this.timeoutHandle) {
|
|
10462
|
+
clearTimeout(this.timeoutHandle);
|
|
10463
|
+
this.timeoutHandle = null;
|
|
10464
|
+
}
|
|
10465
|
+
if (this.webrtc) {
|
|
10466
|
+
this.webrtc.destroy();
|
|
10467
|
+
this.webrtc = null;
|
|
10468
|
+
}
|
|
10469
|
+
this.removeAllListeners();
|
|
10470
|
+
}
|
|
10471
|
+
start() {
|
|
10472
|
+
this.webrtc = new AbracadabraWebRTC({
|
|
10473
|
+
docId: codeToRoomId(this.pairingCode),
|
|
10474
|
+
url: this.config.serverUrl,
|
|
10475
|
+
token: this.config.token,
|
|
10476
|
+
iceServers: this.config.iceServers,
|
|
10477
|
+
e2ee: this.config.e2ee,
|
|
10478
|
+
enableDocSync: false,
|
|
10479
|
+
enableAwarenessSync: false,
|
|
10480
|
+
enableFileTransfer: false,
|
|
10481
|
+
autoConnect: false,
|
|
10482
|
+
WebSocketPolyfill: this.config.WebSocketPolyfill
|
|
10483
|
+
});
|
|
10484
|
+
this.webrtc.on("e2eeEstablished", ({ peerId }) => {
|
|
10485
|
+
this._connectedPeerId = peerId;
|
|
10486
|
+
this.emit("connected");
|
|
10487
|
+
});
|
|
10488
|
+
this.webrtc.on("customMessage", ({ peerId, payload }) => {
|
|
10489
|
+
this.handleMessage(peerId, payload);
|
|
10490
|
+
});
|
|
10491
|
+
this.webrtc.on("peerLeft", () => {
|
|
10492
|
+
if (!this._destroyed) this.emit("error", /* @__PURE__ */ new Error("Peer disconnected"));
|
|
10493
|
+
});
|
|
10494
|
+
this.webrtc.on("signalingError", (err) => {
|
|
10495
|
+
this.emit("error", /* @__PURE__ */ new Error(`Signaling: ${err.message}`));
|
|
10496
|
+
});
|
|
10497
|
+
this.timeoutHandle = setTimeout(() => {
|
|
10498
|
+
if (!this._destroyed) {
|
|
10499
|
+
this.emit("error", /* @__PURE__ */ new Error("Pairing timed out"));
|
|
10500
|
+
this.destroy();
|
|
10501
|
+
}
|
|
10502
|
+
}, PAIRING_TIMEOUT_MS);
|
|
10503
|
+
this.webrtc.connect();
|
|
10504
|
+
}
|
|
10505
|
+
sendMessage(msg) {
|
|
10506
|
+
if (!this.webrtc || !this._connectedPeerId) return;
|
|
10507
|
+
this.webrtc.sendCustomMessage(this._connectedPeerId, JSON.stringify(msg));
|
|
10508
|
+
}
|
|
10509
|
+
handleMessage(peerId, payload) {
|
|
10510
|
+
let msg;
|
|
10511
|
+
try {
|
|
10512
|
+
msg = JSON.parse(payload);
|
|
10513
|
+
} catch {
|
|
10514
|
+
return;
|
|
10515
|
+
}
|
|
10516
|
+
switch (msg.type) {
|
|
10517
|
+
case "pair-request":
|
|
10518
|
+
if (this.role !== "approver") return;
|
|
10519
|
+
this._pendingRequest = {
|
|
10520
|
+
publicKey: msg.publicKey,
|
|
10521
|
+
x25519Key: msg.x25519Key,
|
|
10522
|
+
deviceName: msg.deviceName
|
|
10523
|
+
};
|
|
10524
|
+
this.emit("pairingRequest", this._pendingRequest);
|
|
10525
|
+
break;
|
|
10526
|
+
case "pair-approved":
|
|
10527
|
+
if (this.role !== "requester") return;
|
|
10528
|
+
this.emit("approved");
|
|
10529
|
+
this.emit("pairingComplete", { success: true });
|
|
10530
|
+
break;
|
|
10531
|
+
case "pair-rejected":
|
|
10532
|
+
if (this.role !== "requester") return;
|
|
10533
|
+
this.emit("rejected", msg.reason);
|
|
10534
|
+
this.emit("pairingComplete", {
|
|
10535
|
+
success: false,
|
|
10536
|
+
error: msg.reason
|
|
10537
|
+
});
|
|
10538
|
+
break;
|
|
10539
|
+
case "pair-invite-code":
|
|
10540
|
+
if (this.role !== "requester") return;
|
|
10541
|
+
this.emit("inviteCode", msg.code);
|
|
10542
|
+
break;
|
|
10543
|
+
}
|
|
10544
|
+
}
|
|
10545
|
+
};
|
|
10546
|
+
|
|
10251
10547
|
//#endregion
|
|
10252
10548
|
//#region packages/provider/src/sync/BroadcastChannelSync.ts
|
|
10253
10549
|
/**
|
|
@@ -10400,5 +10696,5 @@ var BroadcastChannelSync = class BroadcastChannelSync extends EventEmitter {
|
|
|
10400
10696
|
};
|
|
10401
10697
|
|
|
10402
10698
|
//#endregion
|
|
10403
|
-
export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DocKeyManager, DocumentCache, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, HocuspocusProvider, HocuspocusProviderWebsocket, KEY_EXCHANGE_CHANNEL, ManualSignaling, MessageTooBig, MessageType, OfflineStore, PeerConnection, ResetConnection, SearchIndex, SignalingSocket, SubdocMessage, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, decryptField, encryptField, makeEncryptedYMap, makeEncryptedYText, readAuthMessage, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
|
|
10699
|
+
export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DocKeyManager, DocumentCache, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, HocuspocusProvider, HocuspocusProviderWebsocket, KEY_EXCHANGE_CHANNEL, ManualSignaling, MessageTooBig, MessageType, OfflineStore, PeerConnection, ResetConnection, SearchIndex, SignalingSocket, SubdocMessage, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, decryptField, encryptField, makeEncryptedYMap, makeEncryptedYText, readAuthMessage, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
|
|
10404
10700
|
//# sourceMappingURL=abracadabra-provider.esm.js.map
|