js-bao-wss-client 1.0.15 → 1.0.17
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 +539 -0
- package/dist/JsBaoClient.d.ts +81 -0
- package/dist/JsBaoClient.d.ts.map +1 -1
- package/dist/JsBaoClient.js +108 -0
- package/dist/JsBaoClient.js.map +1 -1
- package/dist/browser.umd.js +108 -0
- package/dist/browser.umd.js.map +1 -1
- package/package.json +1 -1
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:
|
package/dist/JsBaoClient.d.ts
CHANGED
|
@@ -78,6 +78,16 @@ export interface IntegrationCallResponse<T = any> {
|
|
|
78
78
|
export interface IntegrationsAPI {
|
|
79
79
|
call<T = any>(request: IntegrationCallRequest): Promise<IntegrationCallResponse<T>>;
|
|
80
80
|
}
|
|
81
|
+
export interface WorkflowsAPI {
|
|
82
|
+
/** Start a workflow and return the run information */
|
|
83
|
+
start(workflowKey: string, input: Record<string, any>, options?: StartWorkflowOptions): Promise<StartWorkflowResult>;
|
|
84
|
+
/** Get the status of a workflow run */
|
|
85
|
+
getStatus(workflowKey: string, runKey: string): Promise<WorkflowStatusResult>;
|
|
86
|
+
/** Terminate a running workflow */
|
|
87
|
+
terminate(workflowKey: string, runKey: string): Promise<WorkflowStatusResult>;
|
|
88
|
+
/** List workflow runs for the current user */
|
|
89
|
+
listRuns(options?: ListWorkflowRunsOptions): Promise<ListWorkflowRunsResult>;
|
|
90
|
+
}
|
|
81
91
|
export interface JsBaoClientOptions {
|
|
82
92
|
apiUrl: string;
|
|
83
93
|
wsUrl: string;
|
|
@@ -273,6 +283,70 @@ export interface InvitationEvent {
|
|
|
273
283
|
createdBy?: string;
|
|
274
284
|
};
|
|
275
285
|
}
|
|
286
|
+
export interface WorkflowStatusEvent {
|
|
287
|
+
type: "workflowStatus";
|
|
288
|
+
workflowKey: string;
|
|
289
|
+
workflowId: string;
|
|
290
|
+
runKey: string;
|
|
291
|
+
runId: string;
|
|
292
|
+
status: "completed" | "failed" | "terminated";
|
|
293
|
+
output?: any;
|
|
294
|
+
error?: string;
|
|
295
|
+
contextDocId?: string;
|
|
296
|
+
}
|
|
297
|
+
/** Options for starting a workflow */
|
|
298
|
+
export interface StartWorkflowOptions {
|
|
299
|
+
/** Key to identify this run (for idempotency). Auto-generated if not provided. */
|
|
300
|
+
runKey?: string;
|
|
301
|
+
/** Document ID to associate the run with */
|
|
302
|
+
contextDocId?: string;
|
|
303
|
+
/** Additional metadata to pass to the workflow */
|
|
304
|
+
meta?: Record<string, any>;
|
|
305
|
+
}
|
|
306
|
+
/** Result from starting a workflow */
|
|
307
|
+
export interface StartWorkflowResult {
|
|
308
|
+
runId: string;
|
|
309
|
+
runKey: string;
|
|
310
|
+
instanceId: string;
|
|
311
|
+
status: string;
|
|
312
|
+
/** True if this runKey already existed and the existing run was returned */
|
|
313
|
+
existing?: boolean;
|
|
314
|
+
}
|
|
315
|
+
/** Options for listing workflow runs */
|
|
316
|
+
export interface ListWorkflowRunsOptions {
|
|
317
|
+
/** Filter by workflow key */
|
|
318
|
+
workflowKey?: string;
|
|
319
|
+
/** Filter by status */
|
|
320
|
+
status?: string;
|
|
321
|
+
/** Pagination limit */
|
|
322
|
+
limit?: number;
|
|
323
|
+
/** Pagination cursor */
|
|
324
|
+
cursor?: string;
|
|
325
|
+
}
|
|
326
|
+
/** A workflow run record */
|
|
327
|
+
export interface WorkflowRun {
|
|
328
|
+
runId: string;
|
|
329
|
+
runKey: string;
|
|
330
|
+
instanceId: string;
|
|
331
|
+
workflowId: string;
|
|
332
|
+
workflowKey: string;
|
|
333
|
+
revisionId: string;
|
|
334
|
+
contextDocId?: string;
|
|
335
|
+
status: string;
|
|
336
|
+
createdAt: string;
|
|
337
|
+
endedAt?: string;
|
|
338
|
+
}
|
|
339
|
+
/** Result from listing workflow runs */
|
|
340
|
+
export interface ListWorkflowRunsResult {
|
|
341
|
+
items: WorkflowRun[];
|
|
342
|
+
cursor?: string;
|
|
343
|
+
}
|
|
344
|
+
/** Workflow status response */
|
|
345
|
+
export interface WorkflowStatusResult {
|
|
346
|
+
status: string;
|
|
347
|
+
output?: any;
|
|
348
|
+
error?: string;
|
|
349
|
+
}
|
|
276
350
|
export interface JsBaoEvents {
|
|
277
351
|
error: {
|
|
278
352
|
error: any;
|
|
@@ -318,6 +392,7 @@ export interface JsBaoEvents {
|
|
|
318
392
|
meUpdated: MeUpdatedEvent;
|
|
319
393
|
meUpdateFailed: MeUpdateFailedEvent;
|
|
320
394
|
invitation: InvitationEvent;
|
|
395
|
+
workflowStatus: WorkflowStatusEvent;
|
|
321
396
|
}
|
|
322
397
|
export interface AnalyticsClient {
|
|
323
398
|
logEvent(event: AnalyticsEventInput): void;
|
|
@@ -432,6 +507,7 @@ export declare class JsBaoClient extends Observable<any> {
|
|
|
432
507
|
gemini: GeminiAPI;
|
|
433
508
|
users: UsersAPI;
|
|
434
509
|
integrations: IntegrationsAPI;
|
|
510
|
+
workflows: WorkflowsAPI;
|
|
435
511
|
private httpClient;
|
|
436
512
|
private applyingTokenFromController;
|
|
437
513
|
private controllerPreviousToken;
|
|
@@ -521,6 +597,10 @@ export declare class JsBaoClient extends Observable<any> {
|
|
|
521
597
|
private callIntegration;
|
|
522
598
|
private normalizeIntegrationHeaders;
|
|
523
599
|
private createIntegrationError;
|
|
600
|
+
private startWorkflow;
|
|
601
|
+
private getWorkflowStatus;
|
|
602
|
+
private terminateWorkflow;
|
|
603
|
+
private listWorkflowRuns;
|
|
524
604
|
isDocumentReadOnly(documentId: string): boolean;
|
|
525
605
|
getDocumentPermission(documentId: string): DocumentAccessLevel | null;
|
|
526
606
|
getRootDocId(): string | null;
|
|
@@ -637,6 +717,7 @@ export declare class JsBaoClient extends Observable<any> {
|
|
|
637
717
|
private handlePendingCreateFailed;
|
|
638
718
|
private handleDocumentMetadataUpdate;
|
|
639
719
|
private handleInvitationMessage;
|
|
720
|
+
private handleWorkflowStatusMessage;
|
|
640
721
|
private handleOfflineAuthMessage;
|
|
641
722
|
private handleUploadUrlResponse;
|
|
642
723
|
private handleLlmMessage;
|