@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
- super(configuration);
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 an identity declaration instead of a JWT
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
- /** Returns the stored username, or null if no identity exists. */
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