js-bao-wss-client 2.0.4 → 2.0.6

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.
Files changed (60) hide show
  1. package/README.md +242 -39
  2. package/dist/JsBaoClient.d.ts +293 -41
  3. package/dist/JsBaoClient.d.ts.map +1 -1
  4. package/dist/JsBaoClient.js +241 -5
  5. package/dist/JsBaoClient.js.map +1 -1
  6. package/dist/api/blobBucketsApi.d.ts +29 -1
  7. package/dist/api/blobBucketsApi.d.ts.map +1 -1
  8. package/dist/api/blobBucketsApi.js +5 -3
  9. package/dist/api/blobBucketsApi.js.map +1 -1
  10. package/dist/api/collectionsApi.d.ts +9 -1
  11. package/dist/api/collectionsApi.d.ts.map +1 -1
  12. package/dist/api/collectionsApi.js.map +1 -1
  13. package/dist/api/cronTriggersApi.d.ts +19 -6
  14. package/dist/api/cronTriggersApi.d.ts.map +1 -1
  15. package/dist/api/cronTriggersApi.js.map +1 -1
  16. package/dist/api/databaseTypeConfigsApi.d.ts +6 -4
  17. package/dist/api/databaseTypeConfigsApi.d.ts.map +1 -1
  18. package/dist/api/databaseTypeConfigsApi.js.map +1 -1
  19. package/dist/api/databasesApi.d.ts +83 -69
  20. package/dist/api/databasesApi.d.ts.map +1 -1
  21. package/dist/api/databasesApi.js +10 -0
  22. package/dist/api/databasesApi.js.map +1 -1
  23. package/dist/api/documentsApi.d.ts +71 -12
  24. package/dist/api/documentsApi.d.ts.map +1 -1
  25. package/dist/api/documentsApi.js +56 -20
  26. package/dist/api/documentsApi.js.map +1 -1
  27. package/dist/api/geminiApi.d.ts.map +1 -1
  28. package/dist/api/geminiApi.js +11 -14
  29. package/dist/api/geminiApi.js.map +1 -1
  30. package/dist/api/groupsApi.d.ts +20 -4
  31. package/dist/api/groupsApi.d.ts.map +1 -1
  32. package/dist/api/groupsApi.js +9 -1
  33. package/dist/api/groupsApi.js.map +1 -1
  34. package/dist/api/notificationsApi.d.ts +134 -0
  35. package/dist/api/notificationsApi.d.ts.map +1 -0
  36. package/dist/api/notificationsApi.js +52 -0
  37. package/dist/api/notificationsApi.js.map +1 -0
  38. package/dist/api/operationDefinition.d.ts +279 -0
  39. package/dist/api/operationDefinition.d.ts.map +1 -0
  40. package/dist/api/operationDefinition.js +47 -0
  41. package/dist/api/operationDefinition.js.map +1 -0
  42. package/dist/api/resourceMetadataApi.d.ts +182 -0
  43. package/dist/api/resourceMetadataApi.d.ts.map +1 -0
  44. package/dist/api/resourceMetadataApi.js +56 -0
  45. package/dist/api/resourceMetadataApi.js.map +1 -0
  46. package/dist/browser.umd.js +554 -59
  47. package/dist/browser.umd.js.map +1 -1
  48. package/dist/errors.d.ts +34 -1
  49. package/dist/errors.d.ts.map +1 -1
  50. package/dist/errors.js +56 -0
  51. package/dist/errors.js.map +1 -1
  52. package/dist/internal/authController.d.ts +10 -0
  53. package/dist/internal/authController.d.ts.map +1 -1
  54. package/dist/internal/authController.js +14 -2
  55. package/dist/internal/authController.js.map +1 -1
  56. package/dist/internal/httpClient.d.ts +8 -0
  57. package/dist/internal/httpClient.d.ts.map +1 -1
  58. package/dist/internal/httpClient.js +34 -5
  59. package/dist/internal/httpClient.js.map +1 -1
  60. package/package.json +6 -2
package/README.md CHANGED
@@ -345,6 +345,9 @@ async function testSync() {
345
345
  # Core (always required)
346
346
  npm install js-bao-wss-client yjs lib0 js-bao
347
347
 
348
+ # WebSocket transport (required in Node.js — the browser uses the global WebSocket)
349
+ npm install ws
350
+
348
351
  # For SQLite storage (optional, recommended for persistence)
349
352
  npm install better-sqlite3
350
353
 
@@ -352,7 +355,82 @@ npm install better-sqlite3
352
355
  npm install y-sqlite3
353
356
  ```
354
357
 
355
- Note: `better-sqlite3` and `y-sqlite3` are native modules requiring compilation. If you encounter build issues, ensure you have the appropriate build tools installed (Python, C++ compiler).
358
+ Note: `ws`, `better-sqlite3`, and `y-sqlite3` are declared as **optional peer
359
+ dependencies**. In Node.js the client connects over WebSocket via `ws` (it
360
+ dynamically `import("ws")` when there is no global `WebSocket`), so `ws` must be
361
+ installed for any test or script that opens a document or database — otherwise
362
+ the connection attempt fails with `Cannot find package 'ws'`. `better-sqlite3`
363
+ and `y-sqlite3` are native modules requiring compilation; if you encounter build
364
+ issues, ensure you have the appropriate build tools installed (Python, C++
365
+ compiler). None are needed in the browser.
366
+
367
+ ### Testing in Node.js (dual-context tests)
368
+
369
+ The same test can run in **two contexts** — interactively in an app's in-browser
370
+ test-harness panel, and headlessly in Node under a normal test runner (e.g.
371
+ [Vitest](https://vitest.dev)) to gate CI — because `JsBaoClient` runs the full
372
+ document / Yjs / database / blob lifecycle in Node with no browser. Author the
373
+ test once; run it in the panel while developing and under `vitest run` in CI.
374
+
375
+ **Authenticate from Node with no browser and no email delivery.** Use the
376
+ `+primitivetest` OTP bypass: `otpVerify(email, "000000")` authenticates and
377
+ connects a fresh test user, provided the email's base address is on the app's
378
+ `testAccountBaseEmails` whitelist (the app owner's per-app consent gate). No admin
379
+ credentials, no pre-existing user — the user is auto-provisioned on first verify.
380
+
381
+ Prerequisites:
382
+
383
+ - The app's `testAccountBaseEmails` includes the base of your sign-in email — set
384
+ it with `primitive apps update <appId> --test-account-bases <base@example.com>`.
385
+ The sign-in email is then any `<base>+primitivetest<suffix>@example.com`
386
+ derivative (e.g. `alice+primitivetest-ci@example.com`). Reuse a **stable
387
+ suffix** per CI project so the same test user is reused across runs (the
388
+ provisioner is find-or-create) rather than accumulating one user per run.
389
+ - `ws` is installed (see Required Packages above).
390
+ - Storage defaults to `auto` — SQLite via `better-sqlite3` when present, in-memory
391
+ otherwise. No native dependency is required to run ephemeral tests.
392
+
393
+ ```typescript
394
+ import { initializeClient } from "js-bao-wss-client";
395
+
396
+ // Author once — this same async body is what a browser harness test runs too.
397
+ const client = await initializeClient({
398
+ apiUrl: "https://your-api.example.com",
399
+ wsUrl: "wss://your-ws.example.com",
400
+ appId: "your-app-id",
401
+ models: [/* the models your tests need, as in the browser */],
402
+ // storageConfig defaults to { type: "auto" } — memory fallback in Node,
403
+ // no native deps required.
404
+ });
405
+
406
+ // #1413 OTP bypass: whitelisted +primitivetest email + code "000000".
407
+ // On success the client is authenticated AND connected — no browser.
408
+ await client.otpVerify("alice+primitivetest-ci@example.com", "000000");
409
+
410
+ // From here the client behaves exactly as in the browser.
411
+ const doc = await client.documents.create();
412
+ await client.documents.open(doc.id);
413
+ ```
414
+
415
+ Under `vitest run`, a failing assertion or a group that throws surfaces as a test
416
+ failure and yields a **non-zero exit** natively (all passing → exit `0`), so the
417
+ suite gates a merge with no bespoke runner. Emit JUnit for CI with
418
+ `vitest run --reporter=junit`.
419
+
420
+ **Failure mode.** If the sign-in email is **not** whitelisted (its base is absent
421
+ from `testAccountBaseEmails`, or the app's mode rejects the provisioning),
422
+ `otpVerify` rejects with a clear authentication error rather than hanging — catch
423
+ it and fail the test:
424
+
425
+ ```typescript
426
+ await expect(
427
+ client.otpVerify("not-whitelisted@example.com", "000000"),
428
+ ).rejects.toThrow(/otp|auth|verif/i);
429
+ ```
430
+
431
+ > The in-browser test-harness panel and this Node path run the **same registered
432
+ > tests**. Nothing in the test authoring contract requires a DOM — every browser
433
+ > API the client touches is guarded with `typeof window === "undefined"`.
356
434
 
357
435
  ## Quick Start
358
436
 
@@ -1092,14 +1170,12 @@ await client.documents.delete(documentId, { forceCloseIfOpen: true }); // auto-c
1092
1170
 
1093
1171
  ### Document Aliases
1094
1172
 
1095
- ```typescript
1096
- // Create or update an app-scoped alias
1097
- await client.documents.aliases.set({
1098
- scope: "app",
1099
- aliasKey: "home",
1100
- documentId,
1101
- });
1173
+ > **Note:** Only `scope: "user"` is supported. App-scoped aliases were removed
1174
+ > in #1168 they conferred no access (there is no app-wide document-sharing
1175
+ > primitive) and only named a document every caller had to already be permitted
1176
+ > to open.
1102
1177
 
1178
+ ```typescript
1103
1179
  // User-scoped alias (defaults userId to the current user)
1104
1180
  await client.documents.aliases.set({
1105
1181
  scope: "user",
@@ -1109,21 +1185,21 @@ await client.documents.aliases.set({
1109
1185
 
1110
1186
  // Resolve an alias
1111
1187
  const alias = await client.documents.aliases.resolve({
1112
- scope: "app",
1113
- aliasKey: "home",
1188
+ scope: "user",
1189
+ aliasKey: "current-draft",
1114
1190
  });
1115
1191
 
1116
1192
  // Open a document via alias (same return shape as documents.open)
1117
1193
  const { doc } = await client.documents.openAlias({
1118
- scope: "app",
1119
- aliasKey: "home",
1194
+ scope: "user",
1195
+ aliasKey: "current-draft",
1120
1196
  });
1121
1197
 
1122
1198
  // List aliases for a document (requires document access)
1123
1199
  const aliases = await client.documents.aliases.listForDocument(documentId);
1124
1200
 
1125
1201
  // Delete an alias (no error if already missing)
1126
- await client.documents.aliases.delete({ scope: "app", aliasKey: "home" });
1202
+ await client.documents.aliases.delete({ scope: "user", aliasKey: "current-draft" });
1127
1203
  ```
1128
1204
 
1129
1205
  #### Atomic Create with Alias
@@ -1131,21 +1207,8 @@ await client.documents.aliases.delete({ scope: "app", aliasKey: "home" });
1131
1207
  Create a document and alias in a single atomic operation. This is an **online-only** operation that only creates the document if the alias doesn't already exist:
1132
1208
 
1133
1209
  ```typescript
1134
- // Create document with app-scoped alias (requires admin/owner role)
1210
+ // Create document with a user-scoped alias
1135
1211
  const result = await client.documents.createWithAlias({
1136
- title: "Home Page",
1137
- alias: {
1138
- scope: "app",
1139
- aliasKey: "home",
1140
- },
1141
- });
1142
-
1143
- console.log(result.documentId); // The created document ID
1144
- console.log(result.alias.aliasKey); // "home"
1145
- console.log(result.alias.documentId); // Same as documentId
1146
-
1147
- // Create document with user-scoped alias
1148
- const userResult = await client.documents.createWithAlias({
1149
1212
  title: "My Draft",
1150
1213
  alias: {
1151
1214
  scope: "user",
@@ -1153,11 +1216,15 @@ const userResult = await client.documents.createWithAlias({
1153
1216
  },
1154
1217
  });
1155
1218
 
1156
- // Attempting to create with existing alias throws HTTP 409
1219
+ console.log(result.documentId); // The created document ID
1220
+ console.log(result.alias.aliasKey); // "current-draft"
1221
+ console.log(result.alias.documentId); // Same as documentId
1222
+
1223
+ // Attempting to create with an existing alias throws HTTP 409
1157
1224
  try {
1158
1225
  await client.documents.createWithAlias({
1159
- title: "Another Home",
1160
- alias: { scope: "app", aliasKey: "home" },
1226
+ title: "Another Draft",
1227
+ alias: { scope: "user", aliasKey: "current-draft" },
1161
1228
  });
1162
1229
  } catch (err) {
1163
1230
  console.log("Alias already exists");
@@ -1988,6 +2055,67 @@ const status = await client.workflows.getStatus({
1988
2055
 
1989
2056
  If `contextDocId` is omitted, the user's root document is used by default.
1990
2057
 
2058
+ ### Waiting for Workflow Completion
2059
+
2060
+ For an async run, `waitFor(runId)` resolves once the run reaches a terminal
2061
+ state — no manual subscribe/poll/timeout loop:
2062
+
2063
+ ```typescript
2064
+ const { runId } = await client.workflows.start({
2065
+ workflowKey: "process-document",
2066
+ input: { action: "analyze" },
2067
+ });
2068
+
2069
+ const result = await client.workflows.waitFor(runId);
2070
+ // result: { status, output?, error? }
2071
+
2072
+ if (result.status === "completed") {
2073
+ console.log("Output:", result.output);
2074
+ } else if (result.status === "failed") {
2075
+ console.log("Error:", result.error); // a failed run RESOLVES — it does not reject
2076
+ }
2077
+ ```
2078
+
2079
+ `waitFor` is event-driven: it subscribes to the same `workflowStatus`
2080
+ WebSocket frame (no polling) and does a single reconcile fetch right after
2081
+ subscribing to catch a run that already finished before you called it. On a
2082
+ WebSocket reconnect it re-runs that single reconcile — there is no interval
2083
+ timer.
2084
+
2085
+ - **Resolves on every terminal state**: `completed`, `failed`, `terminated`,
2086
+ and the apply-ready states `apply_pending` / `apply_claimed` (the run
2087
+ finished and its result is available for the client-side apply step). It also
2088
+ treats any run whose `endedAt` is set as terminal regardless of its reported
2089
+ `status`, so it never hangs on a stale status.
2090
+ - **Rejects only** on two conditions: the run is unknown or not readable by the
2091
+ caller (a `NOT_FOUND` `JsBaoError`, surfaced immediately — it does not wait
2092
+ for the timeout), or the timeout elapses (a `WORKFLOW_WAIT_TIMEOUT`
2093
+ `JsBaoError`).
2094
+
2095
+ ```typescript
2096
+ import { isJsBaoError } from "js-bao-wss-client";
2097
+
2098
+ try {
2099
+ const result = await client.workflows.waitFor(runId, { timeoutMs: 60000 });
2100
+ // ...
2101
+ } catch (err) {
2102
+ if (isJsBaoError(err) && err.code === "WORKFLOW_WAIT_TIMEOUT") {
2103
+ // still running after the deadline
2104
+ } else if (isJsBaoError(err) && err.code === "NOT_FOUND") {
2105
+ // unknown or not-owned runId
2106
+ }
2107
+ }
2108
+ ```
2109
+
2110
+ The default `timeoutMs` is 15 minutes. Set it to `0` or `Infinity` to disable
2111
+ the timeout — but then the returned promise and its listener live until the run
2112
+ terminates, so only do this for runs you know will end.
2113
+
2114
+ To receive the resolving `workflowStatus` frame promptly you need an active
2115
+ WebSocket connection (see [Listening for Workflow
2116
+ Events](#listening-for-workflow-events) below); without one, `waitFor` still
2117
+ resolves via its reconcile fetch, just less immediately.
2118
+
1991
2119
  ### Listening for Workflow Events
1992
2120
 
1993
2121
  Subscribe to real-time workflow events via WebSocket:
@@ -2054,7 +2182,7 @@ const runs = await client.workflows.listRuns();
2054
2182
  console.log("Total runs:", runs.items.length);
2055
2183
 
2056
2184
  runs.items.forEach((run) => {
2057
- console.log(run.runKey, run.status, run.createdAt);
2185
+ console.log(run.runKey, run.status, run.startedAt);
2058
2186
  });
2059
2187
 
2060
2188
  // Filter by workflow
@@ -2097,6 +2225,15 @@ const combined = await client.workflows.listRuns({
2097
2225
  #### Run Record Fields
2098
2226
 
2099
2227
  ```typescript
2228
+ type WorkflowRunStatus =
2229
+ | "queued"
2230
+ | "running"
2231
+ | "completed"
2232
+ | "failed"
2233
+ | "terminated"
2234
+ | "apply_pending"
2235
+ | "apply_claimed";
2236
+
2100
2237
  interface WorkflowRun {
2101
2238
  runId: string;
2102
2239
  runKey: string;
@@ -2105,9 +2242,11 @@ interface WorkflowRun {
2105
2242
  workflowKey: string;
2106
2243
  revisionId: string;
2107
2244
  contextDocId?: string;
2108
- status: string;
2109
- createdAt: string;
2245
+ status: WorkflowRunStatus;
2246
+ startedAt?: string; // ISO timestamp stamped when the run started
2110
2247
  endedAt?: string;
2248
+ errorMessage?: string | null; // set when status is "failed", null otherwise
2249
+ meta?: Record<string, any>; // custom metadata passed to workflows.start()
2111
2250
  }
2112
2251
  ```
2113
2252
 
@@ -2933,21 +3072,85 @@ console.log("Last activity:", session.lastActivity);
2933
3072
 
2934
3073
  ## Error Handling
2935
3074
 
3075
+ HTTP operations throw **typed errors** so you can branch on the kind of failure
3076
+ without parsing message strings:
3077
+
3078
+ - **`JsBaoApiError`** — the server responded with a non-2xx status. Carries:
3079
+ - `status: number` — the HTTP status code (always present).
3080
+ - `code?: string` — a stable, machine-readable code from the server body
3081
+ (e.g. `"alias_exists"` on a 409), or `undefined` when the server didn't
3082
+ supply one.
3083
+ - `body?: unknown` — the parsed JSON error body
3084
+ (`{ error, status, timestamp, code?, details? }`), the raw text for a
3085
+ non-JSON body, or `null` for an empty body.
3086
+ - `message` — keeps the legacy `HTTP <status>: <text>` prefix, so older code
3087
+ that regex-parses `.message` keeps working.
3088
+ - **`JsBaoNetworkError`** — the request never reached the server (unreachable
3089
+ host, refused connection, DNS failure, abort). A network failure is
3090
+ inherently retryable; decide retries with `instanceof JsBaoNetworkError`.
3091
+ - **`JsBaoError`** — client-side logical errors with a `code` (e.g. `"OFFLINE"`
3092
+ when the client is in offline mode). Unchanged.
3093
+
3094
+ Use the guards `isJsBaoApiError` / `isJsBaoNetworkError` / `isJsBaoError`, or
3095
+ `instanceof`. Neither `JsBaoApiError` nor `JsBaoNetworkError` exposes a
3096
+ `retryable` field — the class already tells you whether a retry makes sense.
3097
+
2936
3098
  ```typescript
3099
+ import {
3100
+ JsBaoApiError,
3101
+ JsBaoNetworkError,
3102
+ isJsBaoApiError,
3103
+ isJsBaoNetworkError,
3104
+ } from "js-bao-wss-client";
3105
+
2937
3106
  try {
2938
- // HTTP operations throw exceptions
2939
3107
  const res = await client.documents.create({ title: "Test" });
2940
3108
  } catch (error) {
2941
- if ((error as any).message?.includes("401")) {
2942
- console.error("Authentication failed");
2943
- } else if ((error as any).message?.includes("403")) {
2944
- console.error("Permission denied");
3109
+ if (isJsBaoNetworkError(error)) {
3110
+ console.error("Server unreachable — safe to retry");
3111
+ } else if (isJsBaoApiError(error)) {
3112
+ switch (error.status) {
3113
+ case 401:
3114
+ console.error("Authentication failed");
3115
+ break;
3116
+ case 403:
3117
+ console.error("Permission denied");
3118
+ break;
3119
+ case 404:
3120
+ console.error("Not found");
3121
+ break;
3122
+ default:
3123
+ console.error(`API error ${error.status} (${error.code ?? "no code"})`);
3124
+ }
2945
3125
  } else if ((error as any).code === "OFFLINE") {
2946
3126
  console.error("Client offline");
2947
3127
  } else {
2948
- console.error("API error:", (error as any).message);
3128
+ throw error;
3129
+ }
3130
+ }
3131
+ ```
3132
+
3133
+ ### Idempotent add/remove without string parsing
3134
+
3135
+ Branch on `status`/`code` to treat an expected conflict as success:
3136
+
3137
+ ```typescript
3138
+ // Adding an item that already exists (409) is success for an idempotent add.
3139
+ async function addOnce() {
3140
+ try {
3141
+ await client.makeRequest("PUT", `/document-aliases/app/${key}`, {
3142
+ documentId,
3143
+ mustNotExist: true,
3144
+ });
3145
+ return "created";
3146
+ } catch (error) {
3147
+ if (isJsBaoApiError(error) && error.status === 409) {
3148
+ return "already-exists"; // idempotent — not a real failure
3149
+ }
3150
+ throw error;
2949
3151
  }
2950
3152
  }
3153
+ ```
2951
3154
 
2952
3155
  // WebSocket errors are handled via events
2953
3156
  client.on("connection-error", (error) => {