@tuvl/client 0.0.1 → 2026.2.1-beta.2

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
@@ -1,5 +1,386 @@
1
- # tuvl
1
+ # @tuvl/client
2
2
 
3
- This package name has been reserved for the upcoming **TUVL Framework** – a lightweight, developer-friendly orchestration engine for CRUD APIs and LLM agents.
3
+ TypeScript client SDK for the [tuvl](https://tuvl.io) workflow orchestration engine.
4
4
 
5
- More information coming soon! Contact: sooraj@tuvl.io
5
+ Supports three transports plain REST, Server-Sent Events, and gRPC-Web — with automatic transport selection based on the workflow's streaming hints. Also ships first-class support for Human-in-the-Loop (HITL) resumption, explicit version pinning, and typed CRUD access to tuvl model endpoints.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @tuvl/client
13
+ # or
14
+ pnpm add @tuvl/client
15
+ # or
16
+ yarn add @tuvl/client
17
+ ```
18
+
19
+ **gRPC-Web** is optional. Install the peer dependencies only if you need `mode: "grpc"`:
20
+
21
+ ```bash
22
+ npm install @protobuf-ts/grpcweb-transport @protobuf-ts/runtime-rpc
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Quick start
28
+
29
+ ```ts
30
+ import { TuvlAuth, TuvlClient } from "@tuvl/client";
31
+
32
+ // 1. Login
33
+ const auth = new TuvlAuth({ baseUrl: "http://localhost:8000" });
34
+ const { access_token } = await auth.loginWithPassword("me@example.com", "secret");
35
+
36
+ // 2. Who am I? (roles + scopes without touching Biscuit internals)
37
+ const me = await auth.getMe(access_token);
38
+ console.log(me.groups); // ["hr_manager"]
39
+ console.log(me.scopes); // ["candidate:read", "requisition:write"]
40
+
41
+ // 3. Run a workflow
42
+ const client = new TuvlClient({ baseUrl: "http://localhost:8000", token: access_token });
43
+
44
+ // Plain REST call
45
+ const result = await client.execute("hello", {
46
+ payload: { message: "world" },
47
+ });
48
+
49
+ // SSE streaming — auto-detected when onProgress is provided
50
+ // and the workflow has slow steps (agent / mcp / api_call)
51
+ const result2 = await client.execute("screen-candidate", {
52
+ payload: { candidate_id: 42 },
53
+ onProgress: (ev) => console.log(`[${ev.step_id}] ${ev.duration_ms}ms`),
54
+ });
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Authentication
60
+
61
+ The `TuvlAuth` class wraps the most common `/auth/*` endpoints — `bootstrap`, `loginWithPassword`, `getOAuthLoginUrl`, `getMe`, `refresh`, `logout` — so you don't have to hand-craft form bodies or Biscuit decoding logic. Admin endpoints (user / role / scope CRUD, federation config) are intentionally not wrapped; call them directly with `fetch` or the lower-level `Transport` using the same Bearer token.
62
+
63
+ ### `new TuvlAuth(options)`
64
+
65
+ | Option | Type | Description |
66
+ |---|---|---|
67
+ | `baseUrl` | `string` | tuvl server URL |
68
+
69
+ ### Password login
70
+
71
+ ```ts
72
+ const auth = new TuvlAuth({ baseUrl: "http://localhost:8000" });
73
+ const { access_token } = await auth.loginWithPassword("me@example.com", "secret");
74
+ ```
75
+
76
+ ### Bootstrap (one-time IAM init)
77
+
78
+ ```ts
79
+ // First call ever: creates the superadmin user and returns a token.
80
+ // Every subsequent call rejects with HTTP 409.
81
+ const { access_token } = await auth.bootstrap({
82
+ email: "admin@example.com",
83
+ password: "change-me-now",
84
+ admin_scope: "iam:admin",
85
+ });
86
+ ```
87
+
88
+ ### OAuth2 login (browser)
89
+
90
+ ```ts
91
+ // Redirect the browser to the provider's login page
92
+ window.location.href = auth.getOAuthLoginUrl("google"); // or "github" / "microsoft"
93
+
94
+ // On the landing page (TUVL_OAUTH_UI_REDIRECT_URL), pick up the token:
95
+ const token = new URLSearchParams(window.location.search).get("token")!;
96
+ ```
97
+
98
+ ### Get current user identity and roles
99
+
100
+ Biscuit tokens are protobuf-encoded and cannot be decoded in pure JS. Use `getMe()` to ask the server to decode the token and return the user's identity, group memberships, and permission scopes:
101
+
102
+ ```ts
103
+ const me = await auth.getMe(token);
104
+ // me.user_id — user UUID
105
+ // me.groups — role names e.g. ["hr_manager", "member"]
106
+ // me.scopes — permission scopes e.g. ["candidate:read"]
107
+
108
+ if (me.groups.includes("hr_manager")) {
109
+ // show HR features
110
+ }
111
+
112
+ if (me.scopes.includes("iam:admin")) {
113
+ // show admin panel
114
+ }
115
+ ```
116
+
117
+ ### Token refresh
118
+
119
+ ```ts
120
+ const { access_token: newToken } = await auth.refresh(currentToken);
121
+ client.setToken(newToken); // update the workflow client in-place
122
+ ```
123
+
124
+ ### Logout
125
+
126
+ ```ts
127
+ await auth.logout(token);
128
+ // delete token from localStorage / state after this
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Transport selection
134
+
135
+ `execute()` picks the transport automatically — you never need to branch on it:
136
+
137
+ | Condition | Transport |
138
+ |---|---|
139
+ | `mode: "rest"` | Plain JSON POST |
140
+ | `mode: "sse"` | SSE stream |
141
+ | `mode: "grpc"` | gRPC-Web stream |
142
+ | `onProgress` provided **and** workflow `has_slow_steps` | SSE (auto) |
143
+ | Default | Plain JSON POST |
144
+
145
+ ---
146
+
147
+ ## API
148
+
149
+ ### `new TuvlClient(options)`
150
+
151
+ | Option | Type | Default | Description |
152
+ |---|---|---|---|
153
+ | `baseUrl` | `string` | — | tuvl server URL (trailing slash stripped) |
154
+ | `token` | `string` | — | Biscuit Bearer token for all requests |
155
+ | `manifestCacheTtl` | `number` | `60000` | Manifest cache TTL in ms. `0` disables caching |
156
+
157
+ ### `client.execute(workflowName, options?)`
158
+
159
+ Executes a workflow and returns `Promise<TOutput>` — **the unwrapped `data` payload, identical across REST / SSE / gRPC**.
160
+
161
+ | Option | Type | Description |
162
+ |---|---|---|
163
+ | `payload` | `Record<string, unknown>` | JSON body matching the workflow's `input_schema` |
164
+ | `onProgress` | `(event: StepEvent) => void` | Called for every step event (SSE / gRPC only) |
165
+ | `onSuspended` | `(event: SuspendedEvent) => void` | Called once when the workflow hits a HITL step. After this fires the promise rejects with `TuvlWorkflowSuspendedError` |
166
+ | `mode` | `"rest" \| "sse" \| "grpc"` | Force a specific transport |
167
+ | `token` | `string` | Per-call token override |
168
+ | `signal` | `AbortSignal` | Cancellation signal |
169
+
170
+ #### Return-value contract
171
+
172
+ | Outcome | `execute()` does |
173
+ |---|---|
174
+ | Workflow returns `success: true` | Resolves with `data` |
175
+ | Workflow returns `success: false` | Rejects with `TuvlWorkflowError` (carries `.data` and `.error`) |
176
+ | Workflow suspends at a HITL step | Rejects with `TuvlWorkflowSuspendedError` (carries `.suspended`) |
177
+ | Engine raises an unhandled exception | Rejects with a plain `Error` |
178
+ | HTTP / network failure | Rejects with a plain `Error` |
179
+
180
+ ```ts
181
+ import { TuvlClient, TuvlWorkflowError, TuvlWorkflowSuspendedError } from "@tuvl/client";
182
+
183
+ try {
184
+ const candidate = await client.execute<Candidate>("screen-candidate", {
185
+ payload: { id: 42 },
186
+ onProgress: (ev) => console.log(ev.step_id, ev.duration_ms),
187
+ onSuspended: (s) => renderHitlForm(s),
188
+ });
189
+ // candidate is the unwrapped data, fully typed
190
+ } catch (err) {
191
+ if (err instanceof TuvlWorkflowSuspendedError) {
192
+ // err.suspended.ui, err.suspended.schema, err.suspended.instance_id …
193
+ } else if (err instanceof TuvlWorkflowError) {
194
+ // err.data is the partial snapshot, err.error is the structured failure
195
+ } else {
196
+ // transport / engine failure
197
+ }
198
+ }
199
+ ```
200
+
201
+ ### `client.executeVersioned(workflowName, version, options?)`
202
+
203
+ Runs a workflow pinned to a specific schema version (`/{version}/run/{name}`) without relying on the server's active default trigger path. Accepts the same options as `execute()`.
204
+
205
+ ```ts
206
+ // Always run the v2 contract of "screen-candidate", even if v3 is the default
207
+ const result = await client.executeVersioned("screen-candidate", "v2", {
208
+ payload: { id: 42 },
209
+ onProgress: (ev) => console.log(ev.step_id),
210
+ });
211
+ ```
212
+
213
+ ### `client.resumeWorkflow(options)`
214
+
215
+ Resumes a workflow instance that was suspended at a Human-in-the-Loop (HITL) step.
216
+
217
+ | Option | Type | Description |
218
+ |---|---|---|
219
+ | `instanceId` | `string` | UUID from `SuspendedEvent.instance_id` |
220
+ | `humanInput` | `Record<string, unknown>` | Operator-supplied data to inject into the paused step |
221
+ | `onProgress` | `(event: StepEvent) => void` | Step events after the resume point (SSE mode) |
222
+ | `onSuspended` | `(event: SuspendedEvent) => void` | Called if the workflow suspends again at another HITL step |
223
+ | `mode` | `"rest" \| "sse"` | Force a specific transport (default: REST, SSE when `onProgress` is provided) |
224
+ | `token` | `string` | Per-call token override |
225
+ | `signal` | `AbortSignal` | Cancellation signal |
226
+
227
+ ```ts
228
+ import {
229
+ TuvlClient,
230
+ TuvlWorkflowSuspendedError,
231
+ type SuspendedEvent,
232
+ } from "@tuvl/client";
233
+
234
+ let suspendedEvent: SuspendedEvent | undefined;
235
+
236
+ // First pass — workflow suspends at an approval step
237
+ try {
238
+ await client.execute("hire-candidate", {
239
+ payload: { candidate_id: 99 },
240
+ onSuspended: (s) => { suspendedEvent = s; },
241
+ });
242
+ } catch (err) {
243
+ if (!(err instanceof TuvlWorkflowSuspendedError)) throw err;
244
+ }
245
+
246
+ // Human fills in the form …
247
+ const decision = { approved: true, note: "Strong fit" };
248
+
249
+ // Second pass — resume from where it paused
250
+ const finalResult = await client.resumeWorkflow({
251
+ instanceId: suspendedEvent!.instance_id,
252
+ humanInput: decision,
253
+ onProgress: (ev) => console.log(`[resumed] ${ev.step_id}`),
254
+ });
255
+ ```
256
+
257
+ ### `client.getManifest(workflowName)`
258
+
259
+ Fetches (and caches) the workflow manifest from `/api/_system/workflows/{name}`. Called automatically by `execute()` and `executeVersioned()`.
260
+
261
+ ### `client.listWorkflows()`
262
+
263
+ Returns all registered workflows with their trigger paths and streaming hints.
264
+
265
+ ### `client.setToken(token)`
266
+
267
+ Updates the default auth token without recreating the client.
268
+
269
+ ### `client.invalidateManifest(name?)`
270
+
271
+ Clears the manifest cache for one workflow, or all if no name is given.
272
+
273
+ ---
274
+
275
+ ## CRUD — model data access
276
+
277
+ `client.crud(modelName)` returns a `CrudClient` that targets the auto-generated REST endpoints at `/models/{modelname}/`. The token and base URL are shared with the parent `TuvlClient`.
278
+
279
+ ```ts
280
+ import { TuvlClient } from "@tuvl/client";
281
+
282
+ const client = new TuvlClient({ baseUrl: "http://localhost:8000", token });
283
+
284
+ // List all candidates (default limit: 100)
285
+ const all = await client.crud("candidate").list();
286
+
287
+ // Filter + embed relations
288
+ const screened = await client.crud("candidate").list({
289
+ filters: { stage: "screening" },
290
+ include: ["posting"],
291
+ limit: 50,
292
+ offset: 0,
293
+ });
294
+
295
+ // Get one record by UUID
296
+ const c = await client.crud("candidate").get("uuid-here");
297
+
298
+ // Get one with embedded relations
299
+ const cFull = await client.crud("candidate").get("uuid-here", {
300
+ include: ["posting", "assessments"],
301
+ });
302
+
303
+ // Create
304
+ const created = await client.crud<CandidateRead, CandidateCreate>("candidate")
305
+ .create({ name: "Alice", email: "alice@example.com", stage: "applied" });
306
+
307
+ // Partial update (PATCH — only the fields you pass are changed)
308
+ const updated = await client.crud("candidate").update("uuid", { stage: "interview" });
309
+
310
+ // Delete (resolves void on HTTP 204)
311
+ await client.crud("candidate").delete("uuid");
312
+ ```
313
+
314
+ ### `client.crud<TRead, TCreate, TUpdate>(modelName)`
315
+
316
+ Returns a `CrudClient<TRead, TCreate, TUpdate>` instance. All type parameters default to `unknown` / `Partial<TRead>`. The model name is lowercased automatically.
317
+
318
+ ### `CrudListOptions`
319
+
320
+ | Option | Type | Description |
321
+ |---|---|---|
322
+ | `limit` | `number` | Max records to return (server default: 100, max: 1000) |
323
+ | `offset` | `number` | Pagination offset |
324
+ | `filters` | `Record<string, string>` | Field filters: `{ stage: "screening" }` → `?filter[stage]=screening` |
325
+ | `include` | `string[]` | Relation names to embed: `["posting"]` → `?include=posting` |
326
+ | `token` | `string` | Per-call token override |
327
+ | `signal` | `AbortSignal` | Cancellation signal |
328
+
329
+ ### Auth scopes required
330
+
331
+ | Operation | Required scope |
332
+ |---|---|
333
+ | `list()` / `get()` | `{modelname}:read` |
334
+ | `create()` / `update()` | `{modelname}:write` |
335
+ | `delete()` | `{modelname}:delete` |
336
+
337
+ ---
338
+
339
+ ## TypeScript types
340
+
341
+ ```ts
342
+ import type {
343
+ // Auth
344
+ TokenResponse, // { access_token, token_type }
345
+ MeResponse, // { user_id, groups, scopes }
346
+ // Workflow execution
347
+ StepEvent, // single step update (event_type: "step")
348
+ DoneEvent, // terminal envelope { event_type:"done", success, data, error }
349
+ ErrorEvent, // engine-level error { event_type:"error", message, details }
350
+ SuspendedEvent, // HITL suspension { event_type:"suspended", instance_id, ui, schema, … }
351
+ RestEnvelope, // shape of the plain REST response body
352
+ WorkflowErrorPayload, // shape of the .error field on failures
353
+ WorkflowManifest, // shape of /api/_system/workflows/{name}
354
+ ExecuteOptions, // options for execute()
355
+ ExecuteVersionedOptions, // options for executeVersioned() — extends ExecuteOptions + version
356
+ ResumeOptions, // options for resumeWorkflow()
357
+ ResumeRequest, // raw body sent to POST /api/workflows/resume
358
+ TuvlClientOptions, // constructor options
359
+ CrudListOptions, // options for crud().list()
360
+ CrudGetOptions, // options for crud().get()
361
+ CrudMutateOptions, // options for crud().create() / update() / delete()
362
+ } from "@tuvl/client";
363
+
364
+ // Error classes are real classes — usable with `instanceof`
365
+ import { TuvlWorkflowError, TuvlWorkflowSuspendedError } from "@tuvl/client";
366
+ // CRUD client — also exported for use without TuvlClient
367
+ import { CrudClient } from "@tuvl/client";
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Lower-level exports
373
+
374
+ The package also exports its internal building blocks for advanced use:
375
+
376
+ ```ts
377
+ import { Transport } from "@tuvl/client"; // raw HTTP layer
378
+ import { parseSseStream } from "@tuvl/client"; // SSE frame parser (async generator)
379
+ import { openGrpcStream } from "@tuvl/client"; // gRPC-Web stream (async generator)
380
+ ```
381
+
382
+ ---
383
+
384
+ ## License
385
+
386
+ MIT