@abraca/dabra 0.9.0 → 1.0.1
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 +7081 -2848
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +7063 -2864
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +409 -114
- package/package.json +2 -1
- package/src/AbracadabraClient.ts +79 -2
- package/src/AbracadabraProvider.ts +15 -1
- package/src/BackgroundSyncManager.ts +400 -0
- package/src/BackgroundSyncPersistence.ts +107 -0
- package/src/CryptoIdentityKeystore.ts +65 -2
- package/src/DocKeyManager.ts +107 -0
- package/src/E2EAbracadabraProvider.ts +200 -0
- package/src/E2EOfflineStore.ts +55 -0
- package/src/EncryptedY.ts +145 -0
- package/src/FileBlobStore.ts +36 -0
- package/src/OfflineStore.ts +23 -0
- package/src/TreeTimestamps.ts +51 -0
- package/src/index.ts +10 -0
- package/src/types.ts +6 -0
package/dist/index.d.ts
CHANGED
|
@@ -131,6 +131,63 @@ declare class EventEmitter {
|
|
|
131
131
|
removeAllListeners(): void;
|
|
132
132
|
}
|
|
133
133
|
//#endregion
|
|
134
|
+
//#region packages/provider/src/OfflineStore.d.ts
|
|
135
|
+
/**
|
|
136
|
+
* IndexedDB-backed offline store for a single Abracadabra document.
|
|
137
|
+
*
|
|
138
|
+
* Responsibilities:
|
|
139
|
+
* - Persist CRDT updates locally so they survive page refreshes / network loss.
|
|
140
|
+
* - Store the last-known state vector for fast reconnect diffs.
|
|
141
|
+
* - Store a permission snapshot so the UI can gate writes without a network round-trip.
|
|
142
|
+
* - Queue subdoc registration events created while offline.
|
|
143
|
+
* - Store a full document snapshot so the app can load content without a server connection.
|
|
144
|
+
*
|
|
145
|
+
* Falls back to a silent no-op when IndexedDB is unavailable (e.g. SSR / Node.js).
|
|
146
|
+
*
|
|
147
|
+
* Database key is scoped by server origin to prevent cross-server data contamination
|
|
148
|
+
* when the same docId is used on multiple servers.
|
|
149
|
+
*/
|
|
150
|
+
interface PendingSubdoc {
|
|
151
|
+
childId: string;
|
|
152
|
+
parentId: string;
|
|
153
|
+
createdAt: number;
|
|
154
|
+
}
|
|
155
|
+
declare class OfflineStore {
|
|
156
|
+
private storeKey;
|
|
157
|
+
private db;
|
|
158
|
+
/**
|
|
159
|
+
* @param docId The document UUID.
|
|
160
|
+
* @param serverOrigin Hostname of the server (e.g. "abra.cou.sh").
|
|
161
|
+
* When provided the IndexedDB database is namespaced
|
|
162
|
+
* per-server, preventing cross-server data contamination.
|
|
163
|
+
*/
|
|
164
|
+
constructor(docId: string, serverOrigin?: string);
|
|
165
|
+
private dbPromise;
|
|
166
|
+
private getDb;
|
|
167
|
+
persistUpdate(update: Uint8Array): Promise<void>;
|
|
168
|
+
getPendingUpdates(): Promise<Uint8Array[]>;
|
|
169
|
+
clearPendingUpdates(): Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Persist a full Y.js state snapshot (Y.encodeStateAsUpdate output).
|
|
172
|
+
* Replaces any previously stored snapshot.
|
|
173
|
+
*/
|
|
174
|
+
saveDocSnapshot(snapshot: Uint8Array): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Retrieve the stored full document snapshot, or null if none exists.
|
|
177
|
+
*/
|
|
178
|
+
getDocSnapshot(): Promise<Uint8Array | null>;
|
|
179
|
+
getStateVector(): Promise<Uint8Array | null>;
|
|
180
|
+
saveStateVector(sv: Uint8Array): Promise<void>;
|
|
181
|
+
getPermissionSnapshot(): Promise<string | null>;
|
|
182
|
+
savePermissionSnapshot(role: string): Promise<void>;
|
|
183
|
+
queueSubdoc(entry: PendingSubdoc): Promise<void>;
|
|
184
|
+
getPendingSubdocs(): Promise<PendingSubdoc[]>;
|
|
185
|
+
removeSubdocFromQueue(childId: string): Promise<void>;
|
|
186
|
+
getMeta(key: string): Promise<string | null>;
|
|
187
|
+
setMeta(key: string, value: string): Promise<void>;
|
|
188
|
+
destroy(): void;
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
134
191
|
//#region packages/provider/src/DocumentCache.d.ts
|
|
135
192
|
interface DocumentCacheOptions {
|
|
136
193
|
/** How long cached entries remain valid. Default: 5 minutes. */
|
|
@@ -245,11 +302,42 @@ declare class AbracadabraClient {
|
|
|
245
302
|
addKey(opts: {
|
|
246
303
|
publicKey: string;
|
|
247
304
|
deviceName?: string;
|
|
305
|
+
x25519Key?: string;
|
|
248
306
|
}): Promise<void>;
|
|
249
307
|
/** List all registered public keys for the current user. */
|
|
250
308
|
listKeys(): Promise<PublicKeyInfo[]>;
|
|
251
309
|
/** Revoke a public key by its ID. */
|
|
252
310
|
revokeKey(keyId: string): Promise<void>;
|
|
311
|
+
/** Get encryption info for a document. */
|
|
312
|
+
getDocEncryption(docId: string): Promise<DocEncryptionInfo>;
|
|
313
|
+
/** Set the encryption mode for a document (no downgrade). */
|
|
314
|
+
setDocEncryption(docId: string, mode: "none" | "cse" | "e2e"): Promise<void>;
|
|
315
|
+
/** Get the caller's key envelope for a document (for decrypting the DocKey). */
|
|
316
|
+
getMyKeyEnvelope(docId: string): Promise<{
|
|
317
|
+
encrypted_key: string;
|
|
318
|
+
key_epoch: number;
|
|
319
|
+
} | null>;
|
|
320
|
+
/** Upload key envelopes for a document (Owner only). */
|
|
321
|
+
uploadKeyEnvelopes(docId: string, opts: {
|
|
322
|
+
key_epoch: number;
|
|
323
|
+
envelopes: {
|
|
324
|
+
recipient_key_id: string;
|
|
325
|
+
encrypted_key: string;
|
|
326
|
+
}[];
|
|
327
|
+
}): Promise<void>;
|
|
328
|
+
/** Get the X25519 public key for a user. */
|
|
329
|
+
getUserX25519Key(userId: string): Promise<string | null>;
|
|
330
|
+
/** List all non-revoked keys for a user (Owner/Admin or self). */
|
|
331
|
+
listUserKeys(userId: string): Promise<{
|
|
332
|
+
id: string;
|
|
333
|
+
publicKey: string;
|
|
334
|
+
x25519Key: string | null;
|
|
335
|
+
}[]>;
|
|
336
|
+
/** Fetch encrypted E2E update blobs since a given sequence number. */
|
|
337
|
+
getE2EUpdatesSince(docId: string, sinceSeq: number): Promise<{
|
|
338
|
+
seq: number;
|
|
339
|
+
data: Uint8Array;
|
|
340
|
+
}[]>;
|
|
253
341
|
/** Clear token from memory and storage. */
|
|
254
342
|
logout(): void;
|
|
255
343
|
/** Get the current user's profile. */
|
|
@@ -317,6 +405,98 @@ declare class AbracadabraClient {
|
|
|
317
405
|
private clearPersistedToken;
|
|
318
406
|
}
|
|
319
407
|
//#endregion
|
|
408
|
+
//#region packages/provider/src/CryptoIdentityKeystore.d.ts
|
|
409
|
+
/**
|
|
410
|
+
* CryptoIdentityKeystore
|
|
411
|
+
*
|
|
412
|
+
* Per-device Ed25519 keypair, private key protected by WebAuthn PRF + AES-256-GCM.
|
|
413
|
+
* Stored in IndexedDB under "abracadabra:identity" / "identity" / key "current".
|
|
414
|
+
*
|
|
415
|
+
* No private key is ever shared between devices. Each device generates its own
|
|
416
|
+
* keypair, encrypts the private key with the PRF output from its own WebAuthn
|
|
417
|
+
* credential, and stores the ciphertext in IndexedDB.
|
|
418
|
+
*
|
|
419
|
+
* Dependencies: @noble/ed25519, @noble/hashes (for HKDF)
|
|
420
|
+
*/
|
|
421
|
+
declare class CryptoIdentityKeystore {
|
|
422
|
+
/**
|
|
423
|
+
* One-time setup for a device: generates an Ed25519 keypair, creates a
|
|
424
|
+
* WebAuthn credential with PRF extension, encrypts the private key, and
|
|
425
|
+
* stores everything in IndexedDB.
|
|
426
|
+
*
|
|
427
|
+
* Returns the base64url-encoded public key. The caller must register this
|
|
428
|
+
* key with the server via POST /auth/register (first device) or
|
|
429
|
+
* POST /auth/keys (additional device).
|
|
430
|
+
*
|
|
431
|
+
* @param username - The user's account name.
|
|
432
|
+
* @param rpId - WebAuthn relying party ID (e.g. "example.com").
|
|
433
|
+
* @param rpName - Human-readable relying party name.
|
|
434
|
+
*/
|
|
435
|
+
register(username: string, rpId: string, rpName: string): Promise<{
|
|
436
|
+
publicKey: string;
|
|
437
|
+
x25519PublicKey: string;
|
|
438
|
+
}>;
|
|
439
|
+
/**
|
|
440
|
+
* Sign a base64url-encoded challenge using the stored Ed25519 private key.
|
|
441
|
+
*
|
|
442
|
+
* This triggers a WebAuthn assertion (biometric / PIN prompt) to unlock the
|
|
443
|
+
* private key via PRF → HKDF → AES-GCM decryption. The private key is
|
|
444
|
+
* wiped from memory after signing.
|
|
445
|
+
*
|
|
446
|
+
* @param challengeB64 - base64url-encoded challenge bytes from the server.
|
|
447
|
+
* @returns base64url-encoded Ed25519 signature (64 bytes).
|
|
448
|
+
*/
|
|
449
|
+
sign(challengeB64: string): Promise<string>;
|
|
450
|
+
/** Returns the stored base64url public key, or null if no identity exists. */
|
|
451
|
+
getPublicKey(): Promise<string | null>;
|
|
452
|
+
/**
|
|
453
|
+
* Returns the locally-stored internal username label, or null if no identity exists.
|
|
454
|
+
*
|
|
455
|
+
* This is NOT the auth identifier (the public key is). It can be used as a
|
|
456
|
+
* hint when calling POST /auth/register, or displayed before the user sets
|
|
457
|
+
* a real display name via PATCH /users/me.
|
|
458
|
+
*/
|
|
459
|
+
getUsername(): Promise<string | null>;
|
|
460
|
+
/** Returns true if an identity is stored in IndexedDB. */
|
|
461
|
+
hasIdentity(): Promise<boolean>;
|
|
462
|
+
/** Remove the stored identity from IndexedDB. */
|
|
463
|
+
clear(): Promise<void>;
|
|
464
|
+
/**
|
|
465
|
+
* Returns the X25519 public key derived from the stored Ed25519 private key.
|
|
466
|
+
* Does NOT require WebAuthn — computed from the stored encrypted key... actually
|
|
467
|
+
* we derive from the Ed25519 public key directly (Montgomery form), no decryption needed
|
|
468
|
+
* since nobleEd25519Curves.utils.toMontgomery only needs the public key.
|
|
469
|
+
* Returns null if no identity is stored.
|
|
470
|
+
*/
|
|
471
|
+
getX25519PublicKey(): Promise<Uint8Array | null>;
|
|
472
|
+
/**
|
|
473
|
+
* Returns the X25519 private key derived from the stored Ed25519 private key.
|
|
474
|
+
* Requires WebAuthn assertion to decrypt the private key.
|
|
475
|
+
* The caller MUST wipe the returned Uint8Array after use.
|
|
476
|
+
*/
|
|
477
|
+
getX25519PrivateKey(): Promise<Uint8Array>;
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region packages/provider/src/DocKeyManager.d.ts
|
|
481
|
+
declare class DocKeyManager {
|
|
482
|
+
private cache;
|
|
483
|
+
/** Generate a new random AES-256-GCM document key. */
|
|
484
|
+
static generateDocKey(): Promise<CryptoKey>;
|
|
485
|
+
/**
|
|
486
|
+
* Get (or fetch) the DocKey for a document.
|
|
487
|
+
* Returns null if no envelope exists (user not provisioned).
|
|
488
|
+
*/
|
|
489
|
+
getDocKey(docId: string, client: AbracadabraClient, keystore: CryptoIdentityKeystore): Promise<CryptoKey | null>;
|
|
490
|
+
/**
|
|
491
|
+
* Wrap a DocKey for a recipient.
|
|
492
|
+
* Output: [ephemeralPub(32) || nonce(12) || ciphertext(~48)] bytes
|
|
493
|
+
*/
|
|
494
|
+
wrapKeyForRecipient(docKey: CryptoKey, recipientX25519PubKey: Uint8Array, docId: string): Promise<Uint8Array>;
|
|
495
|
+
private _unwrapKey;
|
|
496
|
+
/** Clear the cached key for a document (or all if docId omitted). */
|
|
497
|
+
clearCache(docId?: string): void;
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
320
500
|
//#region packages/provider/src/AbracadabraProvider.d.ts
|
|
321
501
|
interface AbracadabraProviderConfiguration extends Omit<AbracadabraBaseProviderConfiguration, "url" | "websocketProvider"> {
|
|
322
502
|
/**
|
|
@@ -352,6 +532,10 @@ interface AbracadabraProviderConfiguration extends Omit<AbracadabraBaseProviderC
|
|
|
352
532
|
client?: AbracadabraClient;
|
|
353
533
|
/** WebSocket URL. Derived from client.wsUrl if client is provided. */
|
|
354
534
|
url?: string;
|
|
535
|
+
/** DocKeyManager for E2E/CSE encryption key management. */
|
|
536
|
+
docKeyManager?: DocKeyManager;
|
|
537
|
+
/** Keystore for X25519 key derivation. */
|
|
538
|
+
keystore?: CryptoIdentityKeystore;
|
|
355
539
|
/** Shared WebSocket connection (use when multiplexing multiple root documents). */
|
|
356
540
|
websocketProvider?: AbracadabraWS;
|
|
357
541
|
}
|
|
@@ -398,7 +582,7 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
398
582
|
* Used to namespace the IndexedDB key so docs from different servers
|
|
399
583
|
* never share the same database.
|
|
400
584
|
*/
|
|
401
|
-
|
|
585
|
+
protected static deriveServerOrigin(config: AbracadabraProviderConfiguration, client: AbracadabraClient | null): string | undefined;
|
|
402
586
|
/**
|
|
403
587
|
* Load the stored document snapshot (and any pending local edits) from
|
|
404
588
|
* IndexedDB and apply them to the Y.Doc so the UI can render without a
|
|
@@ -425,6 +609,8 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
425
609
|
get canWrite(): boolean;
|
|
426
610
|
/** The AbracadabraClient instance for REST API access, if configured. */
|
|
427
611
|
get client(): AbracadabraClient | null;
|
|
612
|
+
/** The OfflineStore instance, or null if offline storage is disabled. */
|
|
613
|
+
get store(): OfflineStore | null;
|
|
428
614
|
/**
|
|
429
615
|
* Called when a MSG_STATELESS frame arrives from the server.
|
|
430
616
|
* Abracadabra uses stateless frames to deliver subdoc confirmations
|
|
@@ -475,7 +661,7 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
475
661
|
destroy(): void;
|
|
476
662
|
}
|
|
477
663
|
//#endregion
|
|
478
|
-
//#region node_modules/lib0/encoding.d.ts
|
|
664
|
+
//#region packages/provider/node_modules/lib0/encoding.d.ts
|
|
479
665
|
/**
|
|
480
666
|
* A BinaryEncoder handles the encoding to an Uint8Array.
|
|
481
667
|
*/
|
|
@@ -519,7 +705,7 @@ declare const Forbidden: CloseEvent;
|
|
|
519
705
|
*/
|
|
520
706
|
declare const ConnectionTimeout: CloseEvent;
|
|
521
707
|
//#endregion
|
|
522
|
-
//#region node_modules/lib0/decoding.d.ts
|
|
708
|
+
//#region packages/provider/node_modules/lib0/decoding.d.ts
|
|
523
709
|
/**
|
|
524
710
|
* A Decoder handles the decoding of an Uint8Array.
|
|
525
711
|
* @template {ArrayBufferLike} [Buf=ArrayBufferLike]
|
|
@@ -801,6 +987,11 @@ interface UploadQueueEntry {
|
|
|
801
987
|
createdAt: number;
|
|
802
988
|
error?: string;
|
|
803
989
|
}
|
|
990
|
+
interface DocEncryptionInfo {
|
|
991
|
+
mode: "none" | "cse" | "e2e";
|
|
992
|
+
effective_mode: "none" | "cse" | "e2e";
|
|
993
|
+
inherited_from?: string;
|
|
994
|
+
}
|
|
804
995
|
//#endregion
|
|
805
996
|
//#region packages/provider/src/AbracadabraWS.d.ts
|
|
806
997
|
type AbracadabraWebSocketConn = WebSocket & {
|
|
@@ -1061,61 +1252,6 @@ declare const HocuspocusProvider: typeof AbracadabraBaseProvider;
|
|
|
1061
1252
|
/** @deprecated Use AbracadabraBaseProvider */
|
|
1062
1253
|
type HocuspocusProvider = AbracadabraBaseProvider;
|
|
1063
1254
|
//#endregion
|
|
1064
|
-
//#region packages/provider/src/OfflineStore.d.ts
|
|
1065
|
-
/**
|
|
1066
|
-
* IndexedDB-backed offline store for a single Abracadabra document.
|
|
1067
|
-
*
|
|
1068
|
-
* Responsibilities:
|
|
1069
|
-
* - Persist CRDT updates locally so they survive page refreshes / network loss.
|
|
1070
|
-
* - Store the last-known state vector for fast reconnect diffs.
|
|
1071
|
-
* - Store a permission snapshot so the UI can gate writes without a network round-trip.
|
|
1072
|
-
* - Queue subdoc registration events created while offline.
|
|
1073
|
-
* - Store a full document snapshot so the app can load content without a server connection.
|
|
1074
|
-
*
|
|
1075
|
-
* Falls back to a silent no-op when IndexedDB is unavailable (e.g. SSR / Node.js).
|
|
1076
|
-
*
|
|
1077
|
-
* Database key is scoped by server origin to prevent cross-server data contamination
|
|
1078
|
-
* when the same docId is used on multiple servers.
|
|
1079
|
-
*/
|
|
1080
|
-
interface PendingSubdoc {
|
|
1081
|
-
childId: string;
|
|
1082
|
-
parentId: string;
|
|
1083
|
-
createdAt: number;
|
|
1084
|
-
}
|
|
1085
|
-
declare class OfflineStore {
|
|
1086
|
-
private storeKey;
|
|
1087
|
-
private db;
|
|
1088
|
-
/**
|
|
1089
|
-
* @param docId The document UUID.
|
|
1090
|
-
* @param serverOrigin Hostname of the server (e.g. "abra.cou.sh").
|
|
1091
|
-
* When provided the IndexedDB database is namespaced
|
|
1092
|
-
* per-server, preventing cross-server data contamination.
|
|
1093
|
-
*/
|
|
1094
|
-
constructor(docId: string, serverOrigin?: string);
|
|
1095
|
-
private dbPromise;
|
|
1096
|
-
private getDb;
|
|
1097
|
-
persistUpdate(update: Uint8Array): Promise<void>;
|
|
1098
|
-
getPendingUpdates(): Promise<Uint8Array[]>;
|
|
1099
|
-
clearPendingUpdates(): Promise<void>;
|
|
1100
|
-
/**
|
|
1101
|
-
* Persist a full Y.js state snapshot (Y.encodeStateAsUpdate output).
|
|
1102
|
-
* Replaces any previously stored snapshot.
|
|
1103
|
-
*/
|
|
1104
|
-
saveDocSnapshot(snapshot: Uint8Array): Promise<void>;
|
|
1105
|
-
/**
|
|
1106
|
-
* Retrieve the stored full document snapshot, or null if none exists.
|
|
1107
|
-
*/
|
|
1108
|
-
getDocSnapshot(): Promise<Uint8Array | null>;
|
|
1109
|
-
getStateVector(): Promise<Uint8Array | null>;
|
|
1110
|
-
saveStateVector(sv: Uint8Array): Promise<void>;
|
|
1111
|
-
getPermissionSnapshot(): Promise<string | null>;
|
|
1112
|
-
savePermissionSnapshot(role: string): Promise<void>;
|
|
1113
|
-
queueSubdoc(entry: PendingSubdoc): Promise<void>;
|
|
1114
|
-
getPendingSubdocs(): Promise<PendingSubdoc[]>;
|
|
1115
|
-
removeSubdocFromQueue(childId: string): Promise<void>;
|
|
1116
|
-
destroy(): void;
|
|
1117
|
-
}
|
|
1118
|
-
//#endregion
|
|
1119
1255
|
//#region packages/provider/src/auth.d.ts
|
|
1120
1256
|
declare enum AuthMessageType {
|
|
1121
1257
|
Token = 0,
|
|
@@ -1150,61 +1286,6 @@ declare class SubdocMessage extends OutgoingMessage {
|
|
|
1150
1286
|
get(args: Partial<OutgoingMessageArguments>): Encoder;
|
|
1151
1287
|
}
|
|
1152
1288
|
//#endregion
|
|
1153
|
-
//#region packages/provider/src/CryptoIdentityKeystore.d.ts
|
|
1154
|
-
/**
|
|
1155
|
-
* CryptoIdentityKeystore
|
|
1156
|
-
*
|
|
1157
|
-
* Per-device Ed25519 keypair, private key protected by WebAuthn PRF + AES-256-GCM.
|
|
1158
|
-
* Stored in IndexedDB under "abracadabra:identity" / "identity" / key "current".
|
|
1159
|
-
*
|
|
1160
|
-
* No private key is ever shared between devices. Each device generates its own
|
|
1161
|
-
* keypair, encrypts the private key with the PRF output from its own WebAuthn
|
|
1162
|
-
* credential, and stores the ciphertext in IndexedDB.
|
|
1163
|
-
*
|
|
1164
|
-
* Dependencies: @noble/ed25519, @noble/hashes (for HKDF)
|
|
1165
|
-
*/
|
|
1166
|
-
declare class CryptoIdentityKeystore {
|
|
1167
|
-
/**
|
|
1168
|
-
* One-time setup for a device: generates an Ed25519 keypair, creates a
|
|
1169
|
-
* WebAuthn credential with PRF extension, encrypts the private key, and
|
|
1170
|
-
* stores everything in IndexedDB.
|
|
1171
|
-
*
|
|
1172
|
-
* Returns the base64url-encoded public key. The caller must register this
|
|
1173
|
-
* key with the server via POST /auth/register (first device) or
|
|
1174
|
-
* POST /auth/keys (additional device).
|
|
1175
|
-
*
|
|
1176
|
-
* @param username - The user's account name.
|
|
1177
|
-
* @param rpId - WebAuthn relying party ID (e.g. "example.com").
|
|
1178
|
-
* @param rpName - Human-readable relying party name.
|
|
1179
|
-
*/
|
|
1180
|
-
register(username: string, rpId: string, rpName: string): Promise<string>;
|
|
1181
|
-
/**
|
|
1182
|
-
* Sign a base64url-encoded challenge using the stored Ed25519 private key.
|
|
1183
|
-
*
|
|
1184
|
-
* This triggers a WebAuthn assertion (biometric / PIN prompt) to unlock the
|
|
1185
|
-
* private key via PRF → HKDF → AES-GCM decryption. The private key is
|
|
1186
|
-
* wiped from memory after signing.
|
|
1187
|
-
*
|
|
1188
|
-
* @param challengeB64 - base64url-encoded challenge bytes from the server.
|
|
1189
|
-
* @returns base64url-encoded Ed25519 signature (64 bytes).
|
|
1190
|
-
*/
|
|
1191
|
-
sign(challengeB64: string): Promise<string>;
|
|
1192
|
-
/** Returns the stored base64url public key, or null if no identity exists. */
|
|
1193
|
-
getPublicKey(): Promise<string | null>;
|
|
1194
|
-
/**
|
|
1195
|
-
* Returns the locally-stored internal username label, or null if no identity exists.
|
|
1196
|
-
*
|
|
1197
|
-
* This is NOT the auth identifier (the public key is). It can be used as a
|
|
1198
|
-
* hint when calling POST /auth/register, or displayed before the user sets
|
|
1199
|
-
* a real display name via PATCH /users/me.
|
|
1200
|
-
*/
|
|
1201
|
-
getUsername(): Promise<string | null>;
|
|
1202
|
-
/** Returns true if an identity is stored in IndexedDB. */
|
|
1203
|
-
hasIdentity(): Promise<boolean>;
|
|
1204
|
-
/** Remove the stored identity from IndexedDB. */
|
|
1205
|
-
clear(): Promise<void>;
|
|
1206
|
-
}
|
|
1207
|
-
//#endregion
|
|
1208
1289
|
//#region packages/provider/src/SearchIndex.d.ts
|
|
1209
1290
|
declare class SearchIndex {
|
|
1210
1291
|
private readonly origin;
|
|
@@ -1248,6 +1329,12 @@ declare class FileBlobStore extends EventEmitter {
|
|
|
1248
1329
|
* URL.createObjectURL is unavailable (e.g. Node.js / SSR).
|
|
1249
1330
|
*/
|
|
1250
1331
|
getBlobUrl(docId: string, uploadId: string): Promise<string | null>;
|
|
1332
|
+
/**
|
|
1333
|
+
* Store a blob directly into the cache under the given (docId, uploadId) key
|
|
1334
|
+
* and return its object URL. Use this to pre-populate the cache for files
|
|
1335
|
+
* that haven't been uploaded to the server yet (e.g. offline upload queue).
|
|
1336
|
+
*/
|
|
1337
|
+
putBlob(docId: string, uploadId: string, blob: Blob, filename: string): Promise<string>;
|
|
1251
1338
|
/** Revoke the object URL and remove the blob from cache. */
|
|
1252
1339
|
evictBlob(docId: string, uploadId: string): Promise<void>;
|
|
1253
1340
|
/**
|
|
@@ -1268,4 +1355,212 @@ declare class FileBlobStore extends EventEmitter {
|
|
|
1268
1355
|
destroy(): void;
|
|
1269
1356
|
}
|
|
1270
1357
|
//#endregion
|
|
1271
|
-
|
|
1358
|
+
//#region packages/provider/src/E2EAbracadabraProvider.d.ts
|
|
1359
|
+
interface E2EAbracadabraProviderConfiguration extends AbracadabraProviderConfiguration {
|
|
1360
|
+
docKeyManager: DocKeyManager;
|
|
1361
|
+
keystore: CryptoIdentityKeystore;
|
|
1362
|
+
client: AbracadabraClient;
|
|
1363
|
+
}
|
|
1364
|
+
declare class E2EAbracadabraProvider extends AbracadabraProvider {
|
|
1365
|
+
private readonly docKeyManager;
|
|
1366
|
+
private readonly keystore;
|
|
1367
|
+
private readonly e2eClient;
|
|
1368
|
+
private docKey;
|
|
1369
|
+
private lastSeq;
|
|
1370
|
+
/** E2E-encrypted offline store; created after the doc key is available. */
|
|
1371
|
+
private e2eStore;
|
|
1372
|
+
private readonly e2eServerOrigin;
|
|
1373
|
+
constructor(configuration: E2EAbracadabraProviderConfiguration);
|
|
1374
|
+
/** Fetch the doc key from the server (requires WebAuthn if not cached). */
|
|
1375
|
+
private ensureDocKey;
|
|
1376
|
+
/** Handle stateless messages including e2e_ready and e2e_update. */
|
|
1377
|
+
receiveStateless(payload: string): void;
|
|
1378
|
+
private _fetchAndApplyAllBlobs;
|
|
1379
|
+
private _decryptAndApply;
|
|
1380
|
+
/** Encrypt local updates before sending over WebSocket. */
|
|
1381
|
+
documentUpdateHandler(update: Uint8Array, origin: unknown): void;
|
|
1382
|
+
private _encryptAndSend;
|
|
1383
|
+
destroy(): void;
|
|
1384
|
+
}
|
|
1385
|
+
//#endregion
|
|
1386
|
+
//#region packages/provider/src/E2EOfflineStore.d.ts
|
|
1387
|
+
declare class E2EOfflineStore extends OfflineStore {
|
|
1388
|
+
private readonly docKey;
|
|
1389
|
+
/**
|
|
1390
|
+
* @param docId The document UUID.
|
|
1391
|
+
* @param serverOrigin Hostname of the server (namespaces the IDB key).
|
|
1392
|
+
* @param docKey AES-GCM CryptoKey used to encrypt/decrypt the snapshot.
|
|
1393
|
+
*/
|
|
1394
|
+
constructor(docId: string, serverOrigin: string | undefined, docKey: CryptoKey);
|
|
1395
|
+
/**
|
|
1396
|
+
* Encrypt the snapshot before storing it.
|
|
1397
|
+
*/
|
|
1398
|
+
saveDocSnapshot(snapshot: Uint8Array): Promise<void>;
|
|
1399
|
+
/**
|
|
1400
|
+
* Decrypt the snapshot after loading it.
|
|
1401
|
+
* Returns null if decryption fails (e.g. key mismatch or corrupt data).
|
|
1402
|
+
*/
|
|
1403
|
+
getDocSnapshot(): Promise<Uint8Array | null>;
|
|
1404
|
+
}
|
|
1405
|
+
//#endregion
|
|
1406
|
+
//#region packages/provider/src/EncryptedY.d.ts
|
|
1407
|
+
/** Encrypt a field value with AES-256-GCM. Returns [nonce(12) || ciphertext]. */
|
|
1408
|
+
declare function encryptField(value: Uint8Array, docKey: CryptoKey): Promise<Uint8Array>;
|
|
1409
|
+
/** Decrypt a field value from [nonce(12) || ciphertext] format. */
|
|
1410
|
+
declare function decryptField(ciphertext: Uint8Array, docKey: CryptoKey): Promise<Uint8Array>;
|
|
1411
|
+
/**
|
|
1412
|
+
* A proxy wrapper around a Y.Map<Uint8Array> that transparently encrypts
|
|
1413
|
+
* values on .set() and decrypts on .get().
|
|
1414
|
+
*
|
|
1415
|
+
* NOTE: Concurrent writes to the same key produce last-write-wins (LWW)
|
|
1416
|
+
* because encrypted values are opaque blobs from Yrs's perspective.
|
|
1417
|
+
*/
|
|
1418
|
+
declare class EncryptedYMap {
|
|
1419
|
+
private readonly ymap;
|
|
1420
|
+
private readonly docKey;
|
|
1421
|
+
constructor(ymap: Y.Map<Uint8Array>, docKey: CryptoKey);
|
|
1422
|
+
set(key: string, value: Uint8Array): Promise<void>;
|
|
1423
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
1424
|
+
has(key: string): boolean;
|
|
1425
|
+
delete(key: string): void;
|
|
1426
|
+
get size(): number;
|
|
1427
|
+
/** Get all keys (encrypted values remain opaque until .get() is called). */
|
|
1428
|
+
keys(): string[];
|
|
1429
|
+
/** Observe changes on the underlying Y.Map. */
|
|
1430
|
+
observe(f: (event: Y.YMapEvent<Uint8Array>) => void): void;
|
|
1431
|
+
unobserve(f: (event: Y.YMapEvent<Uint8Array>) => void): void;
|
|
1432
|
+
}
|
|
1433
|
+
/** Create an EncryptedYMap wrapping a Y.Map<Uint8Array>. */
|
|
1434
|
+
declare function makeEncryptedYMap(ymap: Y.Map<Uint8Array>, docKey: CryptoKey): EncryptedYMap;
|
|
1435
|
+
/**
|
|
1436
|
+
* Stores full text as a single encrypted blob in a Y.Map<Uint8Array> under a
|
|
1437
|
+
* fixed field name. This is last-write-wins for the entire text field.
|
|
1438
|
+
*
|
|
1439
|
+
* NOTE: This does NOT preserve character-level CRDT merge for concurrent edits.
|
|
1440
|
+
* It trades CRDT merge for confidentiality.
|
|
1441
|
+
*/
|
|
1442
|
+
declare class EncryptedYText {
|
|
1443
|
+
private readonly docKey;
|
|
1444
|
+
private readonly ymap;
|
|
1445
|
+
constructor(ydoc: Y.Doc, fieldName: string, docKey: CryptoKey);
|
|
1446
|
+
set(text: string): Promise<void>;
|
|
1447
|
+
get(): Promise<string | null>;
|
|
1448
|
+
observe(f: (event: Y.YMapEvent<Uint8Array>) => void): void;
|
|
1449
|
+
unobserve(f: (event: Y.YMapEvent<Uint8Array>) => void): void;
|
|
1450
|
+
}
|
|
1451
|
+
/** Create an EncryptedYText for a field in a Y.Doc. */
|
|
1452
|
+
declare function makeEncryptedYText(ydoc: Y.Doc, fieldName: string, docKey: CryptoKey): EncryptedYText;
|
|
1453
|
+
//#endregion
|
|
1454
|
+
//#region packages/provider/src/TreeTimestamps.d.ts
|
|
1455
|
+
/**
|
|
1456
|
+
* Attach an observer that writes `updatedAt: Date.now()` to the root
|
|
1457
|
+
* doc-tree entry for `childDocId` whenever the child doc receives a
|
|
1458
|
+
* non-offline update.
|
|
1459
|
+
*
|
|
1460
|
+
* @param treeMap The root doc's "doc-tree" Y.Map.
|
|
1461
|
+
* @param childDocId The child document's UUID (key in treeMap).
|
|
1462
|
+
* @param childDoc The child Y.Doc to observe.
|
|
1463
|
+
* @param offlineStore The child provider's OfflineStore (used to detect
|
|
1464
|
+
* offline-replay origins and skip them). Pass null when
|
|
1465
|
+
* the offline store is disabled.
|
|
1466
|
+
* @returns Cleanup function — call on provider destroy.
|
|
1467
|
+
*/
|
|
1468
|
+
declare function attachUpdatedAtObserver(treeMap: Y.Map<any>, childDocId: string, childDoc: Y.Doc, offlineStore: OfflineStore | null): () => void;
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region packages/provider/src/BackgroundSyncPersistence.d.ts
|
|
1471
|
+
/**
|
|
1472
|
+
* BackgroundSyncPersistence
|
|
1473
|
+
*
|
|
1474
|
+
* Minimal IndexedDB store (`abracadabra:bgsync:{serverOrigin}`) that persists
|
|
1475
|
+
* per-document sync state across page reloads. A single object store,
|
|
1476
|
+
* `sync_state`, is keyed by `docId`.
|
|
1477
|
+
*
|
|
1478
|
+
* Falls back to a no-op when IndexedDB is unavailable (SSR / Node.js).
|
|
1479
|
+
*/
|
|
1480
|
+
interface DocSyncState {
|
|
1481
|
+
docId: string;
|
|
1482
|
+
/** Current lifecycle status of this document's background sync. */
|
|
1483
|
+
status: "pending" | "syncing" | "synced" | "error" | "skipped";
|
|
1484
|
+
/** Unix ms of the last successful sync, or null if never synced. */
|
|
1485
|
+
lastSynced: number | null;
|
|
1486
|
+
/** Human-readable error message if status === "error". */
|
|
1487
|
+
error?: string;
|
|
1488
|
+
/** Whether the document uses E2E encryption. */
|
|
1489
|
+
isE2E: boolean;
|
|
1490
|
+
}
|
|
1491
|
+
declare class BackgroundSyncPersistence {
|
|
1492
|
+
private dbPromise;
|
|
1493
|
+
private readonly serverOrigin;
|
|
1494
|
+
constructor(serverOrigin: string);
|
|
1495
|
+
private getDb;
|
|
1496
|
+
getState(docId: string): Promise<DocSyncState | null>;
|
|
1497
|
+
getAllStates(): Promise<DocSyncState[]>;
|
|
1498
|
+
setState(state: DocSyncState): Promise<void>;
|
|
1499
|
+
deleteState(docId: string): Promise<void>;
|
|
1500
|
+
destroy(): void;
|
|
1501
|
+
}
|
|
1502
|
+
//#endregion
|
|
1503
|
+
//#region packages/provider/src/BackgroundSyncManager.d.ts
|
|
1504
|
+
interface BackgroundSyncManagerOptions {
|
|
1505
|
+
/** Max parallel WS connections for background sync. Default: 2. */
|
|
1506
|
+
concurrency?: number;
|
|
1507
|
+
/** Timeout (ms) waiting for a provider to sync. Default: 15 000. */
|
|
1508
|
+
syncTimeout?: number;
|
|
1509
|
+
/** Pre-cache file blobs after syncing a doc. Default: true. */
|
|
1510
|
+
prefetchFiles?: boolean;
|
|
1511
|
+
}
|
|
1512
|
+
declare class BackgroundSyncManager extends EventEmitter {
|
|
1513
|
+
private readonly rootProvider;
|
|
1514
|
+
private readonly client;
|
|
1515
|
+
private readonly fileBlobStore;
|
|
1516
|
+
private readonly opts;
|
|
1517
|
+
private readonly persistence;
|
|
1518
|
+
private readonly semaphore;
|
|
1519
|
+
private readonly syncStates;
|
|
1520
|
+
private _destroyed;
|
|
1521
|
+
constructor(rootProvider: AbracadabraProvider, client: AbracadabraClient, fileBlobStore?: FileBlobStore | null, opts?: BackgroundSyncManagerOptions);
|
|
1522
|
+
/** Sync all documents in the root tree. */
|
|
1523
|
+
syncAll(): Promise<void>;
|
|
1524
|
+
/** Sync a single document by ID. */
|
|
1525
|
+
syncDoc(docId: string): Promise<DocSyncState>;
|
|
1526
|
+
/** Return a snapshot of all known sync states. */
|
|
1527
|
+
getSyncStatus(): Map<string, DocSyncState>;
|
|
1528
|
+
/**
|
|
1529
|
+
* Start periodic background sync.
|
|
1530
|
+
* @param intervalMs Interval between sync runs. Default: 5 minutes.
|
|
1531
|
+
* @returns Cleanup function to stop the periodic sync.
|
|
1532
|
+
*/
|
|
1533
|
+
startPeriodicSync(intervalMs?: number): () => void;
|
|
1534
|
+
destroy(): void;
|
|
1535
|
+
/**
|
|
1536
|
+
* Build a priority-sorted list of doc IDs:
|
|
1537
|
+
* 1. Never-synced docs first (lastSynced === null, status !== "error")
|
|
1538
|
+
* 2. Synced docs sorted by updatedAt desc (most-recently-edited first)
|
|
1539
|
+
* 3. Errored docs last
|
|
1540
|
+
*/
|
|
1541
|
+
private _buildQueue;
|
|
1542
|
+
private _syncWithSemaphore;
|
|
1543
|
+
private _doSyncDoc;
|
|
1544
|
+
private _syncNonE2EDoc;
|
|
1545
|
+
private _syncE2EDoc;
|
|
1546
|
+
/**
|
|
1547
|
+
* Wait for a provider to emit the "synced" event (with state=true),
|
|
1548
|
+
* timing out after opts.syncTimeout ms.
|
|
1549
|
+
*/
|
|
1550
|
+
private _waitForSynced;
|
|
1551
|
+
/**
|
|
1552
|
+
* Pre-cache all cover images referenced in the root tree map.
|
|
1553
|
+
*/
|
|
1554
|
+
private _prefetchCovers;
|
|
1555
|
+
/**
|
|
1556
|
+
* Pre-cache file blobs referenced in a synced Y.Doc.
|
|
1557
|
+
* Walks:
|
|
1558
|
+
* - ydoc.getXmlFragment('default') for `fileBlock` nodes (TipTap)
|
|
1559
|
+
* - ydoc.getMap('doc-tree') for meta.coverUploadId entries
|
|
1560
|
+
*/
|
|
1561
|
+
private _prefetchDocFiles;
|
|
1562
|
+
private _extractFileRefs;
|
|
1563
|
+
private _walkXml;
|
|
1564
|
+
}
|
|
1565
|
+
//#endregion
|
|
1566
|
+
export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebSocketConn, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, CryptoIdentity, CryptoIdentityKeystore, type DocEncryptionInfo, DocKeyManager, type DocSyncState, DocumentCache, type DocumentCacheOptions, DocumentMeta, E2EAbracadabraProvider, E2EOfflineStore, EffectiveRole, EncryptedYMap, EncryptedYText, FileBlobStore, Forbidden, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, InviteRow, MessageTooBig, MessageType, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, PendingSubdoc, PermissionEntry, PublicKeyInfo, ResetConnection, SearchIndex, SearchResult, ServerInfo, StatesArray, SubdocMessage, SubdocRegisteredEvent, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserProfile, WebSocketStatus, WsReadyStates, attachUpdatedAtObserver, awarenessStatesToArray, decryptField, encryptField, makeEncryptedYMap, makeEncryptedYText, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, readAuthMessage, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abraca/dabra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "abracadabra provider",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abracadabra",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@lifeomic/attempt": "^3.1.0",
|
|
32
|
+
"@noble/curves": "^2.0.1",
|
|
32
33
|
"@noble/ed25519": "^2.3.0",
|
|
33
34
|
"@noble/hashes": "^1.8.0",
|
|
34
35
|
"lib0": "^0.2.117",
|