@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.
@@ -2133,10 +2133,17 @@ var SubdocMessage = class extends OutgoingMessage {
2133
2133
  */
2134
2134
  var AbracadabraProvider = class AbracadabraProvider extends HocuspocusProvider {
2135
2135
  constructor(configuration) {
2136
- super(configuration);
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 an identity declaration instead of a JWT
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
- /** Returns the stored username, or null if no identity exists. */
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;