js-bao-wss-client 1.0.15 → 1.0.18

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/README.md CHANGED
@@ -10,11 +10,13 @@ A TypeScript/JavaScript client library for js-bao-wss that provides HTTP APIs an
10
10
  - **Realtime Collaboration**: Y.Doc sync over multi-tenant WebSocket
11
11
  - **Awareness**: Presence/cursor broadcast and server-triggered refresh
12
12
  - **Auth/OAuth**: Client-orchestrated OAuth and cookie refresh
13
+ - **Passkey Authentication**: WebAuthn/passkey support for passwordless sign-in
13
14
  - **Automatic Reconnect**: Backoff + re-auth on 401
14
15
  - **Token Management**: Proactive refresh in HTTP calls
15
16
  - **Analytics**: Buffered event logging API with optional automatic lifecycle events
16
17
  - **Blob Storage**: Upload/list/get/downloadUrl/delete per document with offline cache
17
18
  - **LLM**: Chat API and model listing
19
+ - **Workflows**: Server-side multi-step processes with LLM, delays, and transformations
18
20
  - **Offline-first Open**: Non-blocking open with IndexedDB-backed cache
19
21
  - **Offline Blob Cache**: Cache API + IndexedDB backed uploads/reads with eviction and retry
20
22
  - **Network Controls**: Online/offline modes, reachability, connection control
@@ -576,6 +578,161 @@ if (client.isAuthenticated()) {
576
578
  client.setToken("new-jwt-token");
577
579
  ```
578
580
 
581
+ ## Passkey Authentication
582
+
583
+ The client supports WebAuthn/passkey authentication for passwordless sign-in. Passkeys must be enabled in the admin console for your app.
584
+
585
+ ### Check Passkey Availability
586
+
587
+ ```typescript
588
+ // Check if passkeys are enabled for the app
589
+ const config = await client.getOAuthConfig();
590
+ if (config.passkeyEnabled) {
591
+ console.log("Passkeys are available");
592
+ }
593
+ ```
594
+
595
+ ### Sign Up with Passkey (New Account)
596
+
597
+ ```typescript
598
+ import {
599
+ startRegistration,
600
+ browserSupportsWebAuthn,
601
+ } from "@simplewebauthn/browser";
602
+
603
+ if (!browserSupportsWebAuthn()) {
604
+ console.error("WebAuthn not supported");
605
+ return;
606
+ }
607
+
608
+ // 1. Start signup
609
+ const startResponse = await fetch(
610
+ `${apiUrl}/app/${appId}/api/passkey/signup/start`,
611
+ {
612
+ method: "POST",
613
+ headers: { "Content-Type": "application/json" },
614
+ body: JSON.stringify({
615
+ email: "user@example.com",
616
+ name: "User Name", // optional
617
+ }),
618
+ }
619
+ );
620
+ const { options, token: signupToken } = await startResponse.json();
621
+
622
+ // 2. Create passkey with browser
623
+ const credential = await startRegistration({ optionsJSON: options });
624
+
625
+ // 3. Complete signup
626
+ const finishResponse = await fetch(
627
+ `${apiUrl}/app/${appId}/api/passkey/signup/finish`,
628
+ {
629
+ method: "POST",
630
+ headers: { "Content-Type": "application/json" },
631
+ body: JSON.stringify({
632
+ token: signupToken,
633
+ credential,
634
+ deviceName: "MacBook Pro",
635
+ }),
636
+ }
637
+ );
638
+ const { token: authToken, user } = await finishResponse.json();
639
+
640
+ // 4. Initialize client with token
641
+ const client = await initializeClient({
642
+ apiUrl,
643
+ wsUrl,
644
+ appId,
645
+ token: authToken,
646
+ });
647
+ ```
648
+
649
+ ### Sign In with Passkey
650
+
651
+ ```typescript
652
+ import { startAuthentication } from "@simplewebauthn/browser";
653
+
654
+ // 1. Start authentication
655
+ const startResponse = await fetch(
656
+ `${apiUrl}/app/${appId}/api/passkey/auth/start`,
657
+ { method: "POST" }
658
+ );
659
+ const { options, token: authToken } = await startResponse.json();
660
+
661
+ // 2. Authenticate with browser
662
+ const credential = await startAuthentication({ optionsJSON: options });
663
+
664
+ // 3. Complete authentication
665
+ const finishResponse = await fetch(
666
+ `${apiUrl}/app/${appId}/api/passkey/auth/finish`,
667
+ {
668
+ method: "POST",
669
+ headers: { "Content-Type": "application/json" },
670
+ body: JSON.stringify({ token: authToken, credential }),
671
+ }
672
+ );
673
+ const { token, user } = await finishResponse.json();
674
+
675
+ // 4. Initialize client with token
676
+ const client = await initializeClient({
677
+ apiUrl,
678
+ wsUrl,
679
+ appId,
680
+ token,
681
+ });
682
+ ```
683
+
684
+ ### Add Passkey to Existing Account
685
+
686
+ ```typescript
687
+ import { startRegistration } from "@simplewebauthn/browser";
688
+
689
+ // User must be authenticated
690
+ const jwt = client.getToken();
691
+
692
+ // 1. Start registration
693
+ const startResponse = await fetch(
694
+ `${apiUrl}/app/${appId}/api/passkey/register/start`,
695
+ {
696
+ method: "POST",
697
+ headers: { Authorization: `Bearer ${jwt}` },
698
+ }
699
+ );
700
+ const { options, token: regToken } = await startResponse.json();
701
+
702
+ // 2. Create passkey
703
+ const credential = await startRegistration({ optionsJSON: options });
704
+
705
+ // 3. Complete registration
706
+ await fetch(`${apiUrl}/app/${appId}/api/passkey/register/finish`, {
707
+ method: "POST",
708
+ headers: {
709
+ "Content-Type": "application/json",
710
+ Authorization: `Bearer ${jwt}`,
711
+ },
712
+ body: JSON.stringify({
713
+ token: regToken,
714
+ credential,
715
+ deviceName: "iPhone",
716
+ }),
717
+ });
718
+ ```
719
+
720
+ ### List and Delete Passkeys
721
+
722
+ ```typescript
723
+ // List user's passkeys
724
+ const listResponse = await fetch(`${apiUrl}/app/${appId}/api/passkey/list`, {
725
+ headers: { Authorization: `Bearer ${jwt}` },
726
+ });
727
+ const { passkeys } = await listResponse.json();
728
+
729
+ // Delete a passkey
730
+ await fetch(`${apiUrl}/app/${appId}/api/passkey/${passkeyId}`, {
731
+ method: "DELETE",
732
+ headers: { Authorization: `Bearer ${jwt}` },
733
+ });
734
+ ```
735
+
579
736
  ## Document Management
580
737
 
581
738
  ### Create and List Documents
@@ -1320,6 +1477,388 @@ console.log(tokens.totalTokens);
1320
1477
  - Error handling surfaces `JsBaoError` with `code: "GEMINI_ERROR"`; the `details` property contains the raw upstream payload so you can log or render troubleshooting info.
1321
1478
  - See `.dev.local.example` for sample environment values and `docs/gemini-direct-plan.md` for architectural details.
1322
1479
 
1480
+ ## Workflows
1481
+
1482
+ Workflows allow you to execute server-side, multi-step processes that can include LLM calls, delays, transformations, and more. The client provides APIs to start workflows, monitor their status, and receive real-time completion events.
1483
+
1484
+ ### Starting a Workflow
1485
+
1486
+ ```typescript
1487
+ // Start a workflow with input data
1488
+ const result = await client.workflows.start("my-workflow-key", {
1489
+ message: "Hello world",
1490
+ value: 42,
1491
+ });
1492
+
1493
+ console.log("Run started:", result.runKey);
1494
+ console.log("Run ID:", result.runId);
1495
+ console.log("Status:", result.status);
1496
+ ```
1497
+
1498
+ #### Start Options
1499
+
1500
+ ```typescript
1501
+ const result = await client.workflows.start(
1502
+ "my-workflow-key",
1503
+ { message: "Hello" },
1504
+ {
1505
+ // Provide a custom runKey for idempotency (auto-generated if omitted)
1506
+ runKey: "unique-run-identifier",
1507
+ // Associate the run with a document
1508
+ contextDocId: "doc-123",
1509
+ // Pass additional metadata
1510
+ meta: { source: "user-action", priority: "high" },
1511
+ }
1512
+ );
1513
+ ```
1514
+
1515
+ ### Duplicate Workflow Protection (Idempotency)
1516
+
1517
+ When you provide a `runKey`, the server ensures only one workflow run exists for that key. If you call `start()` again with the same `runKey`, the existing run is returned instead of creating a new one:
1518
+
1519
+ ```typescript
1520
+ // First call creates the workflow
1521
+ const first = await client.workflows.start(
1522
+ "process-document",
1523
+ { docId: "abc" },
1524
+ { runKey: "process-abc-v1" }
1525
+ );
1526
+ console.log(first.existing); // false - new run created
1527
+
1528
+ // Second call with same runKey returns existing run
1529
+ const second = await client.workflows.start(
1530
+ "process-document",
1531
+ { docId: "abc" },
1532
+ { runKey: "process-abc-v1" }
1533
+ );
1534
+ console.log(second.existing); // true - existing run returned
1535
+ console.log(second.runId === first.runId); // true - same run
1536
+ ```
1537
+
1538
+ This is useful for:
1539
+ - Preventing duplicate processing when users click a button multiple times
1540
+ - Safely retrying failed requests without creating duplicate work
1541
+ - Implementing exactly-once semantics for critical operations
1542
+
1543
+ ### Checking Workflow Status
1544
+
1545
+ Poll the status of a running workflow:
1546
+
1547
+ ```typescript
1548
+ const status = await client.workflows.getStatus("my-workflow-key", runKey);
1549
+
1550
+ console.log("Status:", status.status); // "running" | "complete" | "failed" | "terminated"
1551
+
1552
+ if (status.status === "complete") {
1553
+ console.log("Output:", status.output);
1554
+ }
1555
+
1556
+ if (status.status === "failed") {
1557
+ console.log("Error:", status.error);
1558
+ }
1559
+ ```
1560
+
1561
+ ### Listening for Workflow Events
1562
+
1563
+ Subscribe to real-time workflow completion events via WebSocket:
1564
+
1565
+ ```typescript
1566
+ // Listen for workflow status changes
1567
+ client.on("workflowStatus", (event) => {
1568
+ console.log("Workflow event:", event.workflowKey, event.runKey);
1569
+ console.log("Status:", event.status); // "completed" | "failed" | "terminated"
1570
+
1571
+ if (event.status === "completed") {
1572
+ console.log("Output:", event.output);
1573
+ }
1574
+
1575
+ if (event.status === "failed") {
1576
+ console.log("Error:", event.error);
1577
+ }
1578
+ });
1579
+ ```
1580
+
1581
+ **Note**: To receive workflow events, you must have an active WebSocket connection. Opening a document establishes this connection:
1582
+
1583
+ ```typescript
1584
+ // Open a document to establish WebSocket for receiving notifications
1585
+ await client.documents.open(documentId);
1586
+
1587
+ // Now workflow events will be delivered
1588
+ const result = await client.workflows.start("my-workflow", { data: "..." });
1589
+ ```
1590
+
1591
+ #### Event Payload
1592
+
1593
+ ```typescript
1594
+ interface WorkflowStatusEvent {
1595
+ type: "workflowStatus";
1596
+ workflowKey: string;
1597
+ workflowId: string;
1598
+ runKey: string;
1599
+ runId: string;
1600
+ status: "completed" | "failed" | "terminated";
1601
+ output?: any;
1602
+ error?: string;
1603
+ contextDocId?: string;
1604
+ }
1605
+ ```
1606
+
1607
+ ### Listing Workflow Runs
1608
+
1609
+ View all workflow runs for the current user:
1610
+
1611
+ ```typescript
1612
+ // List all runs
1613
+ const runs = await client.workflows.listRuns();
1614
+ console.log("Total runs:", runs.items.length);
1615
+
1616
+ runs.items.forEach((run) => {
1617
+ console.log(run.runKey, run.status, run.createdAt);
1618
+ });
1619
+
1620
+ // Filter by workflow
1621
+ const filtered = await client.workflows.listRuns({
1622
+ workflowKey: "my-workflow",
1623
+ });
1624
+
1625
+ // Filter by status
1626
+ const running = await client.workflows.listRuns({
1627
+ status: "running",
1628
+ });
1629
+
1630
+ // Pagination
1631
+ const page1 = await client.workflows.listRuns({ limit: 10 });
1632
+ if (page1.cursor) {
1633
+ const page2 = await client.workflows.listRuns({
1634
+ limit: 10,
1635
+ cursor: page1.cursor,
1636
+ });
1637
+ }
1638
+ ```
1639
+
1640
+ #### Run Record Fields
1641
+
1642
+ ```typescript
1643
+ interface WorkflowRun {
1644
+ runId: string;
1645
+ runKey: string;
1646
+ instanceId: string;
1647
+ workflowId: string;
1648
+ workflowKey: string;
1649
+ revisionId: string;
1650
+ contextDocId?: string;
1651
+ status: string;
1652
+ createdAt: string;
1653
+ endedAt?: string;
1654
+ }
1655
+ ```
1656
+
1657
+ ### Terminating a Workflow
1658
+
1659
+ Cancel a running workflow:
1660
+
1661
+ ```typescript
1662
+ const result = await client.workflows.terminate("my-workflow-key", runKey);
1663
+ console.log("Terminated, final status:", result.status);
1664
+ ```
1665
+
1666
+ ### Sending File Attachments (PDFs, Images)
1667
+
1668
+ Workflows can process files like PDFs and images. Files must be base64-encoded before sending:
1669
+
1670
+ ```typescript
1671
+ /**
1672
+ * Load a file and convert to base64.
1673
+ * Works in browsers with fetch + FileReader or ArrayBuffer.
1674
+ */
1675
+ async function loadFileAsBase64(url: string): Promise<string> {
1676
+ const response = await fetch(url);
1677
+ const arrayBuffer = await response.arrayBuffer();
1678
+ const bytes = new Uint8Array(arrayBuffer);
1679
+ let binary = "";
1680
+ for (let i = 0; i < bytes.length; i++) {
1681
+ binary += String.fromCharCode(bytes[i]);
1682
+ }
1683
+ return btoa(binary);
1684
+ }
1685
+
1686
+ // Load a PDF and send to workflow
1687
+ const pdfBase64 = await loadFileAsBase64("/path/to/document.pdf");
1688
+
1689
+ const result = await client.workflows.start("extract-pdf-data", {
1690
+ attachments: [
1691
+ {
1692
+ data: pdfBase64,
1693
+ type: "application/pdf",
1694
+ },
1695
+ ],
1696
+ });
1697
+ ```
1698
+
1699
+ #### Loading from File Input (Browser)
1700
+
1701
+ ```typescript
1702
+ async function fileToBase64(file: File): Promise<string> {
1703
+ return new Promise((resolve, reject) => {
1704
+ const reader = new FileReader();
1705
+ reader.onload = () => {
1706
+ const dataUrl = reader.result as string;
1707
+ // Remove the data URL prefix (e.g., "data:application/pdf;base64,")
1708
+ const base64 = dataUrl.split(",")[1];
1709
+ resolve(base64);
1710
+ };
1711
+ reader.onerror = reject;
1712
+ reader.readAsDataURL(file);
1713
+ });
1714
+ }
1715
+
1716
+ // Handle file input
1717
+ const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
1718
+ fileInput.addEventListener("change", async () => {
1719
+ const file = fileInput.files?.[0];
1720
+ if (!file) return;
1721
+
1722
+ const base64Data = await fileToBase64(file);
1723
+
1724
+ const result = await client.workflows.start("process-upload", {
1725
+ attachments: [
1726
+ {
1727
+ data: base64Data,
1728
+ type: file.type, // e.g., "image/png", "application/pdf"
1729
+ filename: file.name,
1730
+ },
1731
+ ],
1732
+ });
1733
+ });
1734
+ ```
1735
+
1736
+ #### Loading from URL (Node.js)
1737
+
1738
+ ```typescript
1739
+ import * as fs from "fs";
1740
+ import * as path from "path";
1741
+
1742
+ function loadFileAsBase64Sync(filePath: string): string {
1743
+ const buffer = fs.readFileSync(filePath);
1744
+ return buffer.toString("base64");
1745
+ }
1746
+
1747
+ const pdfPath = path.join(__dirname, "document.pdf");
1748
+ const pdfBase64 = loadFileAsBase64Sync(pdfPath);
1749
+
1750
+ const result = await client.workflows.start("analyze-document", {
1751
+ attachments: [
1752
+ {
1753
+ data: pdfBase64,
1754
+ type: "application/pdf",
1755
+ },
1756
+ ],
1757
+ });
1758
+ ```
1759
+
1760
+ ### Complete Example: PDF Processing Workflow
1761
+
1762
+ ```typescript
1763
+ import { initializeClient } from "js-bao-wss-client";
1764
+
1765
+ async function processPDF(pdfUrl: string) {
1766
+ const client = await initializeClient({
1767
+ apiUrl: "https://api.example.com",
1768
+ wsUrl: "wss://ws.example.com",
1769
+ appId: "my-app",
1770
+ token: "jwt-token",
1771
+ databaseConfig: { type: "sqljs" },
1772
+ });
1773
+
1774
+ // Set up event listener for completion
1775
+ const completionPromise = new Promise<any>((resolve) => {
1776
+ client.on("workflowStatus", (event) => {
1777
+ if (event.status === "completed") {
1778
+ resolve(event.output);
1779
+ }
1780
+ });
1781
+ });
1782
+
1783
+ // Open a document to establish WebSocket connection
1784
+ const { metadata } = await client.documents.create({ title: "temp" });
1785
+ await client.documents.open(metadata.documentId);
1786
+
1787
+ // Load and encode the PDF
1788
+ const response = await fetch(pdfUrl);
1789
+ const arrayBuffer = await response.arrayBuffer();
1790
+ const bytes = new Uint8Array(arrayBuffer);
1791
+ let binary = "";
1792
+ for (let i = 0; i < bytes.length; i++) {
1793
+ binary += String.fromCharCode(bytes[i]);
1794
+ }
1795
+ const pdfBase64 = btoa(binary);
1796
+
1797
+ // Start the workflow
1798
+ const result = await client.workflows.start("extract-pdf-data", {
1799
+ attachments: [
1800
+ {
1801
+ data: pdfBase64,
1802
+ type: "application/pdf",
1803
+ },
1804
+ ],
1805
+ });
1806
+
1807
+ console.log("Workflow started:", result.runKey);
1808
+
1809
+ // Wait for completion (or poll with getStatus)
1810
+ const output = await completionPromise;
1811
+ console.log("Extracted data:", output);
1812
+
1813
+ // Cleanup
1814
+ await client.documents.delete(metadata.documentId);
1815
+ await client.destroy();
1816
+
1817
+ return output;
1818
+ }
1819
+ ```
1820
+
1821
+ ### Polling for Completion
1822
+
1823
+ If you prefer polling over WebSocket events:
1824
+
1825
+ ```typescript
1826
+ async function waitForCompletion(
1827
+ client: JsBaoClient,
1828
+ workflowKey: string,
1829
+ runKey: string,
1830
+ timeoutMs = 60000,
1831
+ intervalMs = 2000
1832
+ ): Promise<any> {
1833
+ const startTime = Date.now();
1834
+
1835
+ while (Date.now() - startTime < timeoutMs) {
1836
+ const status = await client.workflows.getStatus(workflowKey, runKey);
1837
+
1838
+ if (status.status === "complete") {
1839
+ return status.output;
1840
+ }
1841
+
1842
+ if (status.status === "failed") {
1843
+ throw new Error(`Workflow failed: ${status.error}`);
1844
+ }
1845
+
1846
+ if (status.status === "terminated") {
1847
+ throw new Error("Workflow was terminated");
1848
+ }
1849
+
1850
+ // Still running, wait and retry
1851
+ await new Promise((r) => setTimeout(r, intervalMs));
1852
+ }
1853
+
1854
+ throw new Error("Workflow timed out");
1855
+ }
1856
+
1857
+ // Usage
1858
+ const result = await client.workflows.start("my-workflow", { data: "..." });
1859
+ const output = await waitForCompletion(client, "my-workflow", result.runKey);
1860
+ ```
1861
+
1323
1862
  ## Integrations API
1324
1863
 
1325
1864
  Proxy HTTP calls through the tenant-specific integrations defined in the admin UI:
@@ -2054,7 +2593,7 @@ See [LOCAL_TESTING.md](./LOCAL_TESTING.md) for a comprehensive guide on testing
2054
2593
  **Quick test:**
2055
2594
 
2056
2595
  ```bash
2057
- npm run build && npm pack
2596
+ pnpm run build && pnpm pack
2058
2597
  cd ../../../ && mkdir test-package && cd test-package
2059
2598
  npm init -y && npm install ../js-bao-wss/src/client/js-bao-wss-client-1.0.0.tgz
2060
2599
  echo 'import {JsBaoClient} from "js-bao-wss-client"; console.log("✅ Works!")' > test.js
@@ -2065,16 +2604,16 @@ sed -i '' 's/"type": "commonjs"/"type": "module"/' package.json && node test.js
2065
2604
 
2066
2605
  ```bash
2067
2606
  cd tests
2068
- npm install
2069
- npm run test:esm # Test ESM imports
2070
- npm run test:umd # Instructions for UMD testing
2607
+ pnpm install
2608
+ pnpm run test:esm # Test ESM imports
2609
+ pnpm run test:umd # Instructions for UMD testing
2071
2610
  ```
2072
2611
 
2073
2612
  ### Build Commands
2074
2613
 
2075
2614
  ```bash
2076
- npm run build # Build both ESM and UMD
2077
- npm run build:esm # Build ESM only
2078
- npm run build:umd # Build UMD only
2079
- npm pack # Create publishable package
2615
+ pnpm run build # Build both ESM and UMD
2616
+ pnpm run build:esm # Build ESM only
2617
+ pnpm run build:umd # Build UMD only
2618
+ pnpm pack # Create publishable package
2080
2619
  ```