@abraca/dabra 0.1.2 → 0.1.4
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.
|
@@ -2103,10 +2103,17 @@ var SubdocMessage = class extends OutgoingMessage {
|
|
|
2103
2103
|
*/
|
|
2104
2104
|
var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
2105
2105
|
constructor(configuration) {
|
|
2106
|
-
|
|
2106
|
+
const resolved = { ...configuration };
|
|
2107
|
+
const client = configuration.client ?? null;
|
|
2108
|
+
if (client) {
|
|
2109
|
+
if (!resolved.url && !resolved.websocketProvider) resolved.url = client.wsUrl;
|
|
2110
|
+
if (resolved.token === void 0 && !configuration.cryptoIdentity) resolved.token = () => client.token ?? "";
|
|
2111
|
+
}
|
|
2112
|
+
super(resolved);
|
|
2107
2113
|
this.effectiveRole = null;
|
|
2108
2114
|
this.childProviders = /* @__PURE__ */ new Map();
|
|
2109
2115
|
this.boundHandleYSubdocsChange = this.handleYSubdocsChange.bind(this);
|
|
2116
|
+
this._client = client;
|
|
2110
2117
|
this.abracadabraConfig = configuration;
|
|
2111
2118
|
this.subdocLoading = configuration.subdocLoading ?? "lazy";
|
|
2112
2119
|
this.offlineStore = configuration.disableOfflineStore ? null : new OfflineStore(configuration.name);
|
|
@@ -2122,8 +2129,12 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2122
2129
|
this.offlineStore?.savePermissionSnapshot(this.effectiveRole);
|
|
2123
2130
|
}
|
|
2124
2131
|
/**
|
|
2125
|
-
* Override sendToken to send
|
|
2126
|
-
* when cryptoIdentity is configured.
|
|
2132
|
+
* Override sendToken to send a pubkey-only identity declaration instead of a
|
|
2133
|
+
* JWT when cryptoIdentity is configured.
|
|
2134
|
+
*
|
|
2135
|
+
* The public key is the sole identifier in the crypto auth handshake.
|
|
2136
|
+
* Username is decoupled from auth; it lives on the server as an immutable
|
|
2137
|
+
* internal field and is never sent in the challenge-response frames.
|
|
2127
2138
|
*/
|
|
2128
2139
|
async sendToken() {
|
|
2129
2140
|
const { cryptoIdentity } = this.abracadabraConfig;
|
|
@@ -2131,7 +2142,6 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2131
2142
|
const id = typeof cryptoIdentity === "function" ? await cryptoIdentity() : cryptoIdentity;
|
|
2132
2143
|
const json = JSON.stringify({
|
|
2133
2144
|
type: "identity",
|
|
2134
|
-
username: id.username,
|
|
2135
2145
|
publicKey: id.publicKey
|
|
2136
2146
|
});
|
|
2137
2147
|
this.send(AuthenticationMessage, {
|
|
@@ -2155,7 +2165,6 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2155
2165
|
const signature = await signChallenge(challenge);
|
|
2156
2166
|
const proof = JSON.stringify({
|
|
2157
2167
|
type: "proof",
|
|
2158
|
-
username: id.username,
|
|
2159
2168
|
publicKey: id.publicKey,
|
|
2160
2169
|
signature,
|
|
2161
2170
|
challenge
|
|
@@ -2173,6 +2182,10 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2173
2182
|
get canWrite() {
|
|
2174
2183
|
return this.effectiveRole === "owner" || this.effectiveRole === "editor";
|
|
2175
2184
|
}
|
|
2185
|
+
/** The AbracadabraClient instance for REST API access, if configured. */
|
|
2186
|
+
get client() {
|
|
2187
|
+
return this._client;
|
|
2188
|
+
}
|
|
2176
2189
|
/**
|
|
2177
2190
|
* Called when a MSG_STATELESS frame arrives from the server.
|
|
2178
2191
|
* Abracadabra uses stateless frames to deliver subdoc confirmations
|
|
@@ -2242,7 +2255,10 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2242
2255
|
WebSocketPolyfill: parentWsp.configuration.WebSocketPolyfill,
|
|
2243
2256
|
token: this.configuration.token,
|
|
2244
2257
|
subdocLoading: this.subdocLoading,
|
|
2245
|
-
disableOfflineStore: this.abracadabraConfig.disableOfflineStore
|
|
2258
|
+
disableOfflineStore: this.abracadabraConfig.disableOfflineStore,
|
|
2259
|
+
client: this._client ?? void 0,
|
|
2260
|
+
cryptoIdentity: this.abracadabraConfig.cryptoIdentity,
|
|
2261
|
+
signChallenge: this.abracadabraConfig.signChallenge
|
|
2246
2262
|
});
|
|
2247
2263
|
this.childProviders.set(childId, childProvider);
|
|
2248
2264
|
this.emit("subdocLoaded", {
|
|
@@ -2304,6 +2320,229 @@ var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
|
|
|
2304
2320
|
}
|
|
2305
2321
|
};
|
|
2306
2322
|
|
|
2323
|
+
//#endregion
|
|
2324
|
+
//#region packages/provider/src/AbracadabraClient.ts
|
|
2325
|
+
var AbracadabraClient = class {
|
|
2326
|
+
constructor(config) {
|
|
2327
|
+
this.baseUrl = config.url.replace(/\/+$/, "");
|
|
2328
|
+
this.persistAuth = config.persistAuth ?? typeof localStorage !== "undefined";
|
|
2329
|
+
this.storageKey = config.storageKey ?? "abracadabra:auth";
|
|
2330
|
+
this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2331
|
+
this._token = config.token ?? this.loadPersistedToken() ?? null;
|
|
2332
|
+
}
|
|
2333
|
+
get token() {
|
|
2334
|
+
return this._token;
|
|
2335
|
+
}
|
|
2336
|
+
set token(value) {
|
|
2337
|
+
this._token = value;
|
|
2338
|
+
if (this.persistAuth) if (value) this.persistToken(value);
|
|
2339
|
+
else this.clearPersistedToken();
|
|
2340
|
+
}
|
|
2341
|
+
get isAuthenticated() {
|
|
2342
|
+
return this._token !== null;
|
|
2343
|
+
}
|
|
2344
|
+
/** Derives ws:// or wss:// URL from the http(s) base URL. */
|
|
2345
|
+
get wsUrl() {
|
|
2346
|
+
return this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/ws";
|
|
2347
|
+
}
|
|
2348
|
+
/** Register a new user with password. */
|
|
2349
|
+
async register(opts) {
|
|
2350
|
+
return this.request("POST", "/auth/register", {
|
|
2351
|
+
body: opts,
|
|
2352
|
+
auth: false
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Register a new user with an Ed25519 public key (crypto auth).
|
|
2357
|
+
* Username is optional — if omitted, a short identifier is derived from the key.
|
|
2358
|
+
*/
|
|
2359
|
+
async registerWithKey(opts) {
|
|
2360
|
+
const username = opts.username ?? `user-${opts.publicKey.slice(0, 8)}`;
|
|
2361
|
+
return this.request("POST", "/auth/register", {
|
|
2362
|
+
body: {
|
|
2363
|
+
username,
|
|
2364
|
+
identityPublicKey: opts.publicKey,
|
|
2365
|
+
deviceName: opts.deviceName,
|
|
2366
|
+
displayName: opts.displayName,
|
|
2367
|
+
email: opts.email
|
|
2368
|
+
},
|
|
2369
|
+
auth: false
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
/** Login with username + password. Auto-persists returned token. */
|
|
2373
|
+
async login(opts) {
|
|
2374
|
+
const res = await this.request("POST", "/auth/login", {
|
|
2375
|
+
body: opts,
|
|
2376
|
+
auth: false
|
|
2377
|
+
});
|
|
2378
|
+
this.token = res.token;
|
|
2379
|
+
return res.token;
|
|
2380
|
+
}
|
|
2381
|
+
/** Request an Ed25519 crypto auth challenge for the given public key. */
|
|
2382
|
+
async challenge(publicKey) {
|
|
2383
|
+
return this.request("POST", "/auth/challenge", {
|
|
2384
|
+
body: { publicKey },
|
|
2385
|
+
auth: false
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
/** Verify an Ed25519 signature to complete crypto auth. Auto-persists token. */
|
|
2389
|
+
async verify(opts) {
|
|
2390
|
+
const res = await this.request("POST", "/auth/verify", {
|
|
2391
|
+
body: opts,
|
|
2392
|
+
auth: false
|
|
2393
|
+
});
|
|
2394
|
+
this.token = res.token;
|
|
2395
|
+
return res.token;
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Full crypto auth flow: challenge → sign → verify.
|
|
2399
|
+
* Convenience method combining challenge() + external signing + verify().
|
|
2400
|
+
*/
|
|
2401
|
+
async loginWithKey(publicKey, signChallenge) {
|
|
2402
|
+
const { challenge } = await this.challenge(publicKey);
|
|
2403
|
+
const signature = await signChallenge(challenge);
|
|
2404
|
+
return this.verify({
|
|
2405
|
+
publicKey,
|
|
2406
|
+
signature,
|
|
2407
|
+
challenge
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
/** Add a new Ed25519 public key to the current user (multi-device). */
|
|
2411
|
+
async addKey(opts) {
|
|
2412
|
+
await this.request("POST", "/auth/keys", { body: opts });
|
|
2413
|
+
}
|
|
2414
|
+
/** List all registered public keys for the current user. */
|
|
2415
|
+
async listKeys() {
|
|
2416
|
+
return (await this.request("GET", "/auth/keys")).keys;
|
|
2417
|
+
}
|
|
2418
|
+
/** Revoke a public key by its ID. */
|
|
2419
|
+
async revokeKey(keyId) {
|
|
2420
|
+
await this.request("DELETE", `/auth/keys/${encodeURIComponent(keyId)}`);
|
|
2421
|
+
}
|
|
2422
|
+
/** Clear token from memory and storage. */
|
|
2423
|
+
logout() {
|
|
2424
|
+
this.token = null;
|
|
2425
|
+
}
|
|
2426
|
+
/** Get the current user's profile. */
|
|
2427
|
+
async getMe() {
|
|
2428
|
+
return this.request("GET", "/users/me");
|
|
2429
|
+
}
|
|
2430
|
+
/** Update the current user's display name. */
|
|
2431
|
+
async updateMe(opts) {
|
|
2432
|
+
await this.request("PATCH", "/users/me", { body: opts });
|
|
2433
|
+
}
|
|
2434
|
+
/** Create a new root document. Returns its metadata. */
|
|
2435
|
+
async createDoc(opts) {
|
|
2436
|
+
return this.request("POST", "/docs", { body: opts ?? {} });
|
|
2437
|
+
}
|
|
2438
|
+
/** Get document metadata. */
|
|
2439
|
+
async getDoc(docId) {
|
|
2440
|
+
return this.request("GET", `/docs/${encodeURIComponent(docId)}`);
|
|
2441
|
+
}
|
|
2442
|
+
/** Delete a document (requires Owner role). Cascades to children and uploads. */
|
|
2443
|
+
async deleteDoc(docId) {
|
|
2444
|
+
await this.request("DELETE", `/docs/${encodeURIComponent(docId)}`);
|
|
2445
|
+
}
|
|
2446
|
+
/** List immediate child documents. */
|
|
2447
|
+
async listChildren(docId) {
|
|
2448
|
+
return (await this.request("GET", `/docs/${encodeURIComponent(docId)}/children`)).children;
|
|
2449
|
+
}
|
|
2450
|
+
/** Create a child document under a parent (requires write permission). */
|
|
2451
|
+
async createChild(docId, opts) {
|
|
2452
|
+
return this.request("POST", `/docs/${encodeURIComponent(docId)}/children`, { body: opts ?? {} });
|
|
2453
|
+
}
|
|
2454
|
+
/** Grant or change a user's role on a document (requires Owner). */
|
|
2455
|
+
async setPermission(docId, opts) {
|
|
2456
|
+
await this.request("POST", `/docs/${encodeURIComponent(docId)}/permissions`, { body: opts });
|
|
2457
|
+
}
|
|
2458
|
+
/** Revoke a user's permission on a document (requires Owner). */
|
|
2459
|
+
async removePermission(docId, opts) {
|
|
2460
|
+
await this.request("DELETE", `/docs/${encodeURIComponent(docId)}/permissions`, { body: opts });
|
|
2461
|
+
}
|
|
2462
|
+
/** Upload a file to a document (requires write permission). */
|
|
2463
|
+
async upload(docId, file, filename) {
|
|
2464
|
+
const formData = new FormData();
|
|
2465
|
+
formData.append("file", file, filename);
|
|
2466
|
+
const headers = {};
|
|
2467
|
+
if (this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
2468
|
+
const res = await this._fetch(`${this.baseUrl}/docs/${encodeURIComponent(docId)}/uploads`, {
|
|
2469
|
+
method: "POST",
|
|
2470
|
+
headers,
|
|
2471
|
+
body: formData
|
|
2472
|
+
});
|
|
2473
|
+
if (!res.ok) throw await this.toError(res);
|
|
2474
|
+
return res.json();
|
|
2475
|
+
}
|
|
2476
|
+
/** List all uploads for a document. */
|
|
2477
|
+
async listUploads(docId) {
|
|
2478
|
+
return (await this.request("GET", `/docs/${encodeURIComponent(docId)}/uploads`)).uploads;
|
|
2479
|
+
}
|
|
2480
|
+
/** Download an upload as a Blob. */
|
|
2481
|
+
async getUpload(docId, uploadId) {
|
|
2482
|
+
const headers = {};
|
|
2483
|
+
if (this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
2484
|
+
const res = await this._fetch(`${this.baseUrl}/docs/${encodeURIComponent(docId)}/uploads/${encodeURIComponent(uploadId)}`, {
|
|
2485
|
+
method: "GET",
|
|
2486
|
+
headers
|
|
2487
|
+
});
|
|
2488
|
+
if (!res.ok) throw await this.toError(res);
|
|
2489
|
+
return res.blob();
|
|
2490
|
+
}
|
|
2491
|
+
/** Delete an upload (requires uploader or document Owner). */
|
|
2492
|
+
async deleteUpload(docId, uploadId) {
|
|
2493
|
+
await this.request("DELETE", `/docs/${encodeURIComponent(docId)}/uploads/${encodeURIComponent(uploadId)}`);
|
|
2494
|
+
}
|
|
2495
|
+
/** Health check — no auth required. */
|
|
2496
|
+
async health() {
|
|
2497
|
+
return this.request("GET", "/health", { auth: false });
|
|
2498
|
+
}
|
|
2499
|
+
async request(method, path, opts) {
|
|
2500
|
+
const auth = opts?.auth ?? true;
|
|
2501
|
+
const headers = {};
|
|
2502
|
+
if (auth && this._token) headers["Authorization"] = `Bearer ${this._token}`;
|
|
2503
|
+
const init = {
|
|
2504
|
+
method,
|
|
2505
|
+
headers
|
|
2506
|
+
};
|
|
2507
|
+
if (opts?.body !== void 0) {
|
|
2508
|
+
headers["Content-Type"] = "application/json";
|
|
2509
|
+
init.body = JSON.stringify(opts.body);
|
|
2510
|
+
}
|
|
2511
|
+
const res = await this._fetch(`${this.baseUrl}${path}`, init);
|
|
2512
|
+
if (!res.ok) throw await this.toError(res);
|
|
2513
|
+
if (res.status === 204) return;
|
|
2514
|
+
return res.json();
|
|
2515
|
+
}
|
|
2516
|
+
async toError(res) {
|
|
2517
|
+
let message;
|
|
2518
|
+
try {
|
|
2519
|
+
message = (await res.json()).error ?? res.statusText;
|
|
2520
|
+
} catch {
|
|
2521
|
+
message = res.statusText;
|
|
2522
|
+
}
|
|
2523
|
+
const err = new Error(message);
|
|
2524
|
+
err.status = res.status;
|
|
2525
|
+
return err;
|
|
2526
|
+
}
|
|
2527
|
+
loadPersistedToken() {
|
|
2528
|
+
try {
|
|
2529
|
+
return localStorage.getItem(this.storageKey);
|
|
2530
|
+
} catch {
|
|
2531
|
+
return null;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
persistToken(token) {
|
|
2535
|
+
try {
|
|
2536
|
+
localStorage.setItem(this.storageKey, token);
|
|
2537
|
+
} catch {}
|
|
2538
|
+
}
|
|
2539
|
+
clearPersistedToken() {
|
|
2540
|
+
try {
|
|
2541
|
+
localStorage.removeItem(this.storageKey);
|
|
2542
|
+
} catch {}
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
|
|
2307
2546
|
//#endregion
|
|
2308
2547
|
//#region node_modules/@noble/hashes/esm/utils.js
|
|
2309
2548
|
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
|
|
@@ -3121,7 +3360,13 @@ var CryptoIdentityKeystore = class {
|
|
|
3121
3360
|
db.close();
|
|
3122
3361
|
return stored?.publicKey ?? null;
|
|
3123
3362
|
}
|
|
3124
|
-
/**
|
|
3363
|
+
/**
|
|
3364
|
+
* Returns the locally-stored internal username label, or null if no identity exists.
|
|
3365
|
+
*
|
|
3366
|
+
* This is NOT the auth identifier (the public key is). It can be used as a
|
|
3367
|
+
* hint when calling POST /auth/register, or displayed before the user sets
|
|
3368
|
+
* a real display name via PATCH /users/me.
|
|
3369
|
+
*/
|
|
3125
3370
|
async getUsername() {
|
|
3126
3371
|
const db = await openDb();
|
|
3127
3372
|
const stored = await dbGet(db);
|
|
@@ -3144,5 +3389,5 @@ var CryptoIdentityKeystore = class {
|
|
|
3144
3389
|
};
|
|
3145
3390
|
|
|
3146
3391
|
//#endregion
|
|
3147
|
-
export { AbracadabraProvider, AwarenessError, CryptoIdentityKeystore, HocuspocusProvider, HocuspocusProviderWebsocket, MessageType, OfflineStore, SubdocMessage, WebSocketStatus };
|
|
3392
|
+
export { AbracadabraClient, AbracadabraProvider, AwarenessError, CryptoIdentityKeystore, HocuspocusProvider, HocuspocusProviderWebsocket, MessageType, OfflineStore, SubdocMessage, WebSocketStatus };
|
|
3148
3393
|
//# sourceMappingURL=abracadabra-provider.esm.js.map
|