@abraca/dabra 0.1.1 → 0.1.3
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 +253 -7
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +253 -8
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +189 -8
- package/package.json +1 -1
- package/src/AbracadabraClient.ts +381 -0
- package/src/AbracadabraProvider.ts +46 -6
- package/src/CryptoIdentityKeystore.ts +14 -2
- package/src/index.ts +1 -0
- package/src/types.ts +48 -3
|
@@ -2133,10 +2133,17 @@ var SubdocMessage = class extends OutgoingMessage {
|
|
|
2133
2133
|
*/
|
|
2134
2134
|
var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
2135
2135
|
constructor(configuration) {
|
|
2136
|
-
|
|
2136
|
+
const resolved = { ...configuration };
|
|
2137
|
+
const client = configuration.client ?? null;
|
|
2138
|
+
if (client) {
|
|
2139
|
+
if (!resolved.url && !resolved.websocketProvider) resolved.url = client.wsUrl;
|
|
2140
|
+
if (resolved.token === void 0 && !configuration.cryptoIdentity) resolved.token = () => client.token ?? "";
|
|
2141
|
+
}
|
|
2142
|
+
super(resolved);
|
|
2137
2143
|
this.effectiveRole = null;
|
|
2138
2144
|
this.childProviders = /* @__PURE__ */ new Map();
|
|
2139
2145
|
this.boundHandleYSubdocsChange = this.handleYSubdocsChange.bind(this);
|
|
2146
|
+
this._client = client;
|
|
2140
2147
|
this.abracadabraConfig = configuration;
|
|
2141
2148
|
this.subdocLoading = configuration.subdocLoading ?? "lazy";
|
|
2142
2149
|
this.offlineStore = configuration.disableOfflineStore ? null : new OfflineStore(configuration.name);
|
|
@@ -2152,8 +2159,12 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2152
2159
|
this.offlineStore?.savePermissionSnapshot(this.effectiveRole);
|
|
2153
2160
|
}
|
|
2154
2161
|
/**
|
|
2155
|
-
* Override sendToken to send
|
|
2156
|
-
* when cryptoIdentity is configured.
|
|
2162
|
+
* Override sendToken to send a pubkey-only identity declaration instead of a
|
|
2163
|
+
* JWT when cryptoIdentity is configured.
|
|
2164
|
+
*
|
|
2165
|
+
* The public key is the sole identifier in the crypto auth handshake.
|
|
2166
|
+
* Username is decoupled from auth; it lives on the server as an immutable
|
|
2167
|
+
* internal field and is never sent in the challenge-response frames.
|
|
2157
2168
|
*/
|
|
2158
2169
|
async sendToken() {
|
|
2159
2170
|
const { cryptoIdentity } = this.abracadabraConfig;
|
|
@@ -2161,7 +2172,6 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2161
2172
|
const id = typeof cryptoIdentity === "function" ? await cryptoIdentity() : cryptoIdentity;
|
|
2162
2173
|
const json = JSON.stringify({
|
|
2163
2174
|
type: "identity",
|
|
2164
|
-
username: id.username,
|
|
2165
2175
|
publicKey: id.publicKey
|
|
2166
2176
|
});
|
|
2167
2177
|
this.send(AuthenticationMessage, {
|
|
@@ -2185,7 +2195,6 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2185
2195
|
const signature = await signChallenge(challenge);
|
|
2186
2196
|
const proof = JSON.stringify({
|
|
2187
2197
|
type: "proof",
|
|
2188
|
-
username: id.username,
|
|
2189
2198
|
publicKey: id.publicKey,
|
|
2190
2199
|
signature,
|
|
2191
2200
|
challenge
|
|
@@ -2203,6 +2212,10 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2203
2212
|
get canWrite() {
|
|
2204
2213
|
return this.effectiveRole === "owner" || this.effectiveRole === "editor";
|
|
2205
2214
|
}
|
|
2215
|
+
/** The AbracadabraClient instance for REST API access, if configured. */
|
|
2216
|
+
get client() {
|
|
2217
|
+
return this._client;
|
|
2218
|
+
}
|
|
2206
2219
|
/**
|
|
2207
2220
|
* Called when a MSG_STATELESS frame arrives from the server.
|
|
2208
2221
|
* Abracadabra uses stateless frames to deliver subdoc confirmations
|
|
@@ -2272,7 +2285,10 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2272
2285
|
WebSocketPolyfill: parentWsp.configuration.WebSocketPolyfill,
|
|
2273
2286
|
token: this.configuration.token,
|
|
2274
2287
|
subdocLoading: this.subdocLoading,
|
|
2275
|
-
disableOfflineStore: this.abracadabraConfig.disableOfflineStore
|
|
2288
|
+
disableOfflineStore: this.abracadabraConfig.disableOfflineStore,
|
|
2289
|
+
client: this._client ?? void 0,
|
|
2290
|
+
cryptoIdentity: this.abracadabraConfig.cryptoIdentity,
|
|
2291
|
+
signChallenge: this.abracadabraConfig.signChallenge
|
|
2276
2292
|
});
|
|
2277
2293
|
this.childProviders.set(childId, childProvider);
|
|
2278
2294
|
this.emit("subdocLoaded", {
|
|
@@ -2334,6 +2350,229 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2334
2350
|
}
|
|
2335
2351
|
};
|
|
2336
2352
|
|
|
2353
|
+
//#endregion
|
|
2354
|
+
//#region packages/provider/src/AbracadabraClient.ts
|
|
2355
|
+
var AbracadabraClient = class {
|
|
2356
|
+
constructor(config) {
|
|
2357
|
+
this.baseUrl = config.url.replace(/\/+$/, "");
|
|
2358
|
+
this.persistAuth = config.persistAuth ?? typeof localStorage !== "undefined";
|
|
2359
|
+
this.storageKey = config.storageKey ?? "abracadabra:auth";
|
|
2360
|
+
this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2361
|
+
this._token = config.token ?? this.loadPersistedToken() ?? null;
|
|
2362
|
+
}
|
|
2363
|
+
get token() {
|
|
2364
|
+
return this._token;
|
|
2365
|
+
}
|
|
2366
|
+
set token(value) {
|
|
2367
|
+
this._token = value;
|
|
2368
|
+
if (this.persistAuth) if (value) this.persistToken(value);
|
|
2369
|
+
else this.clearPersistedToken();
|
|
2370
|
+
}
|
|
2371
|
+
get isAuthenticated() {
|
|
2372
|
+
return this._token !== null;
|
|
2373
|
+
}
|
|
2374
|
+
/** Derives ws:// or wss:// URL from the http(s) base URL. */
|
|
2375
|
+
get wsUrl() {
|
|
2376
|
+
return this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/ws";
|
|
2377
|
+
}
|
|
2378
|
+
/** Register a new user with password. */
|
|
2379
|
+
async register(opts) {
|
|
2380
|
+
return this.request("POST", "/auth/register", {
|
|
2381
|
+
body: opts,
|
|
2382
|
+
auth: false
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Register a new user with an Ed25519 public key (crypto auth).
|
|
2387
|
+
* Username is optional — if omitted, a short identifier is derived from the key.
|
|
2388
|
+
*/
|
|
2389
|
+
async registerWithKey(opts) {
|
|
2390
|
+
const username = opts.username ?? `user-${opts.publicKey.slice(0, 8)}`;
|
|
2391
|
+
return this.request("POST", "/auth/register", {
|
|
2392
|
+
body: {
|
|
2393
|
+
username,
|
|
2394
|
+
identityPublicKey: opts.publicKey,
|
|
2395
|
+
deviceName: opts.deviceName,
|
|
2396
|
+
displayName: opts.displayName,
|
|
2397
|
+
email: opts.email
|
|
2398
|
+
},
|
|
2399
|
+
auth: false
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
/** Login with username + password. Auto-persists returned token. */
|
|
2403
|
+
async login(opts) {
|
|
2404
|
+
const res = await this.request("POST", "/auth/login", {
|
|
2405
|
+
body: opts,
|
|
2406
|
+
auth: false
|
|
2407
|
+
});
|
|
2408
|
+
this.token = res.token;
|
|
2409
|
+
return res.token;
|
|
2410
|
+
}
|
|
2411
|
+
/** Request an Ed25519 crypto auth challenge for the given public key. */
|
|
2412
|
+
async challenge(publicKey) {
|
|
2413
|
+
return this.request("POST", "/auth/challenge", {
|
|
2414
|
+
body: { publicKey },
|
|
2415
|
+
auth: false
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
/** Verify an Ed25519 signature to complete crypto auth. Auto-persists token. */
|
|
2419
|
+
async verify(opts) {
|
|
2420
|
+
const res = await this.request("POST", "/auth/verify", {
|
|
2421
|
+
body: opts,
|
|
2422
|
+
auth: false
|
|
2423
|
+
});
|
|
2424
|
+
this.token = res.token;
|
|
2425
|
+
return res.token;
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Full crypto auth flow: challenge → sign → verify.
|
|
2429
|
+
* Convenience method combining challenge() + external signing + verify().
|
|
2430
|
+
*/
|
|
2431
|
+
async loginWithKey(publicKey, signChallenge) {
|
|
2432
|
+
const { challenge } = await this.challenge(publicKey);
|
|
2433
|
+
const signature = await signChallenge(challenge);
|
|
2434
|
+
return this.verify({
|
|
2435
|
+
publicKey,
|
|
2436
|
+
signature,
|
|
2437
|
+
challenge
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
/** Add a new Ed25519 public key to the current user (multi-device). */
|
|
2441
|
+
async addKey(opts) {
|
|
2442
|
+
await this.request("POST", "/auth/keys", { body: opts });
|
|
2443
|
+
}
|
|
2444
|
+
/** List all registered public keys for the current user. */
|
|
2445
|
+
async listKeys() {
|
|
2446
|
+
return (await this.request("GET", "/auth/keys")).keys;
|
|
2447
|
+
}
|
|
2448
|
+
/** Revoke a public key by its ID. */
|
|
2449
|
+
async revokeKey(keyId) {
|
|
2450
|
+
await this.request("DELETE", `/auth/keys/${encodeURIComponent(keyId)}`);
|
|
2451
|
+
}
|
|
2452
|
+
/** Clear token from memory and storage. */
|
|
2453
|
+
logout() {
|
|
2454
|
+
this.token = null;
|
|
2455
|
+
}
|
|
2456
|
+
/** Get the current user's profile. */
|
|
2457
|
+
async getMe() {
|
|
2458
|
+
return this.request("GET", "/users/me");
|
|
2459
|
+
}
|
|
2460
|
+
/** Update the current user's display name. */
|
|
2461
|
+
async updateMe(opts) {
|
|
2462
|
+
await this.request("PATCH", "/users/me", { body: opts });
|
|
2463
|
+
}
|
|
2464
|
+
/** Create a new root document. Returns its metadata. */
|
|
2465
|
+
async createDoc(opts) {
|
|
2466
|
+
return this.request("POST", "/docs", { body: opts ?? {} });
|
|
2467
|
+
}
|
|
2468
|
+
/** Get document metadata. */
|
|
2469
|
+
async getDoc(docId) {
|
|
2470
|
+
return this.request("GET", `/docs/${encodeURIComponent(docId)}`);
|
|
2471
|
+
}
|
|
2472
|
+
/** Delete a document (requires Owner role). Cascades to children and uploads. */
|
|
2473
|
+
async deleteDoc(docId) {
|
|
2474
|
+
await this.request("DELETE", `/docs/${encodeURIComponent(docId)}`);
|
|
2475
|
+
}
|
|
2476
|
+
/** List immediate child documents. */
|
|
2477
|
+
async listChildren(docId) {
|
|
2478
|
+
return (await this.request("GET", `/docs/${encodeURIComponent(docId)}/children`)).children;
|
|
2479
|
+
}
|
|
2480
|
+
/** Create a child document under a parent (requires write permission). */
|
|
2481
|
+
async createChild(docId, opts) {
|
|
2482
|
+
return this.request("POST", `/docs/${encodeURIComponent(docId)}/children`, { body: opts ?? {} });
|
|
2483
|
+
}
|
|
2484
|
+
/** Grant or change a user's role on a document (requires Owner). */
|
|
2485
|
+
async setPermission(docId, opts) {
|
|
2486
|
+
await this.request("POST", `/docs/${encodeURIComponent(docId)}/permissions`, { body: opts });
|
|
2487
|
+
}
|
|
2488
|
+
/** Revoke a user's permission on a document (requires Owner). */
|
|
2489
|
+
async removePermission(docId, opts) {
|
|
2490
|
+
await this.request("DELETE", `/docs/${encodeURIComponent(docId)}/permissions`, { body: opts });
|
|
2491
|
+
}
|
|
2492
|
+
/** Upload a file to a document (requires write permission). */
|
|
2493
|
+
async upload(docId, file, filename) {
|
|
2494
|
+
const formData = new FormData();
|
|
2495
|
+
formData.append("file", file, filename);
|
|
2496
|
+
const headers = {};
|
|
2497
|
+
if (this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
2498
|
+
const res = await this._fetch(`${this.baseUrl}/docs/${encodeURIComponent(docId)}/uploads`, {
|
|
2499
|
+
method: "POST",
|
|
2500
|
+
headers,
|
|
2501
|
+
body: formData
|
|
2502
|
+
});
|
|
2503
|
+
if (!res.ok) throw await this.toError(res);
|
|
2504
|
+
return res.json();
|
|
2505
|
+
}
|
|
2506
|
+
/** List all uploads for a document. */
|
|
2507
|
+
async listUploads(docId) {
|
|
2508
|
+
return (await this.request("GET", `/docs/${encodeURIComponent(docId)}/uploads`)).uploads;
|
|
2509
|
+
}
|
|
2510
|
+
/** Download an upload as a Blob. */
|
|
2511
|
+
async getUpload(docId, uploadId) {
|
|
2512
|
+
const headers = {};
|
|
2513
|
+
if (this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
2514
|
+
const res = await this._fetch(`${this.baseUrl}/docs/${encodeURIComponent(docId)}/uploads/${encodeURIComponent(uploadId)}`, {
|
|
2515
|
+
method: "GET",
|
|
2516
|
+
headers
|
|
2517
|
+
});
|
|
2518
|
+
if (!res.ok) throw await this.toError(res);
|
|
2519
|
+
return res.blob();
|
|
2520
|
+
}
|
|
2521
|
+
/** Delete an upload (requires uploader or document Owner). */
|
|
2522
|
+
async deleteUpload(docId, uploadId) {
|
|
2523
|
+
await this.request("DELETE", `/docs/${encodeURIComponent(docId)}/uploads/${encodeURIComponent(uploadId)}`);
|
|
2524
|
+
}
|
|
2525
|
+
/** Health check — no auth required. */
|
|
2526
|
+
async health() {
|
|
2527
|
+
return this.request("GET", "/health", { auth: false });
|
|
2528
|
+
}
|
|
2529
|
+
async request(method, path, opts) {
|
|
2530
|
+
const auth = opts?.auth ?? true;
|
|
2531
|
+
const headers = {};
|
|
2532
|
+
if (auth && this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
2533
|
+
const init = {
|
|
2534
|
+
method,
|
|
2535
|
+
headers
|
|
2536
|
+
};
|
|
2537
|
+
if (opts?.body !== void 0) {
|
|
2538
|
+
headers["Content-Type"] = "application/json";
|
|
2539
|
+
init.body = JSON.stringify(opts.body);
|
|
2540
|
+
}
|
|
2541
|
+
const res = await this._fetch(`${this.baseUrl}${path}`, init);
|
|
2542
|
+
if (!res.ok) throw await this.toError(res);
|
|
2543
|
+
if (res.status === 204) return;
|
|
2544
|
+
return res.json();
|
|
2545
|
+
}
|
|
2546
|
+
async toError(res) {
|
|
2547
|
+
let message;
|
|
2548
|
+
try {
|
|
2549
|
+
message = (await res.json()).error ?? res.statusText;
|
|
2550
|
+
} catch {
|
|
2551
|
+
message = res.statusText;
|
|
2552
|
+
}
|
|
2553
|
+
const err = new Error(message);
|
|
2554
|
+
err.status = res.status;
|
|
2555
|
+
return err;
|
|
2556
|
+
}
|
|
2557
|
+
loadPersistedToken() {
|
|
2558
|
+
try {
|
|
2559
|
+
return localStorage.getItem(this.storageKey);
|
|
2560
|
+
} catch {
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
persistToken(token) {
|
|
2565
|
+
try {
|
|
2566
|
+
localStorage.setItem(this.storageKey, token);
|
|
2567
|
+
} catch {}
|
|
2568
|
+
}
|
|
2569
|
+
clearPersistedToken() {
|
|
2570
|
+
try {
|
|
2571
|
+
localStorage.removeItem(this.storageKey);
|
|
2572
|
+
} catch {}
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
|
|
2337
2576
|
//#endregion
|
|
2338
2577
|
//#region node_modules/@noble/hashes/esm/utils.js
|
|
2339
2578
|
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
|
|
@@ -3151,7 +3390,13 @@ var CryptoIdentityKeystore = class {
|
|
|
3151
3390
|
db.close();
|
|
3152
3391
|
return stored?.publicKey ?? null;
|
|
3153
3392
|
}
|
|
3154
|
-
/**
|
|
3393
|
+
/**
|
|
3394
|
+
* Returns the locally-stored internal username label, or null if no identity exists.
|
|
3395
|
+
*
|
|
3396
|
+
* This is NOT the auth identifier (the public key is). It can be used as a
|
|
3397
|
+
* hint when calling POST /auth/register, or displayed before the user sets
|
|
3398
|
+
* a real display name via PATCH /users/me.
|
|
3399
|
+
*/
|
|
3155
3400
|
async getUsername() {
|
|
3156
3401
|
const db = await openDb();
|
|
3157
3402
|
const stored = await dbGet(db);
|
|
@@ -3174,6 +3419,7 @@ var CryptoIdentityKeystore = class {
|
|
|
3174
3419
|
};
|
|
3175
3420
|
|
|
3176
3421
|
//#endregion
|
|
3422
|
+
exports.AbracadabraClient = AbracadabraClient;
|
|
3177
3423
|
exports.AbracadabraProvider = AbracadabraProvider;
|
|
3178
3424
|
exports.AwarenessError = AwarenessError;
|
|
3179
3425
|
exports.CryptoIdentityKeystore = CryptoIdentityKeystore;
|