@vinaverse/server 0.0.0-stage → 0.1.0-alpha.1

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 (64) hide show
  1. package/README.md +165 -2
  2. package/dist/api.cjs +528 -0
  3. package/dist/api.cjs.map +1 -0
  4. package/dist/api.d.cts +453 -0
  5. package/dist/api.d.ts +453 -0
  6. package/dist/api.js +516 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/chunk-FEEJNQXV.js +64 -0
  9. package/dist/chunk-FEEJNQXV.js.map +1 -0
  10. package/dist/chunk-GTPMQKGX.js +277 -0
  11. package/dist/chunk-GTPMQKGX.js.map +1 -0
  12. package/dist/chunk-NPXO6SMZ.js +84 -0
  13. package/dist/chunk-NPXO6SMZ.js.map +1 -0
  14. package/dist/chunk-SDJFZPHJ.js +141 -0
  15. package/dist/chunk-SDJFZPHJ.js.map +1 -0
  16. package/dist/express.cjs +397 -0
  17. package/dist/express.cjs.map +1 -0
  18. package/dist/express.d.cts +43 -0
  19. package/dist/express.d.ts +43 -0
  20. package/dist/express.js +87 -0
  21. package/dist/express.js.map +1 -0
  22. package/dist/fastify.cjs +363 -0
  23. package/dist/fastify.cjs.map +1 -0
  24. package/dist/fastify.d.cts +62 -0
  25. package/dist/fastify.d.ts +62 -0
  26. package/dist/fastify.js +54 -0
  27. package/dist/fastify.js.map +1 -0
  28. package/dist/hono.cjs +421 -0
  29. package/dist/hono.cjs.map +1 -0
  30. package/dist/hono.d.cts +21 -0
  31. package/dist/hono.d.ts +21 -0
  32. package/dist/hono.js +34 -0
  33. package/dist/hono.js.map +1 -0
  34. package/dist/index.cjs +1481 -0
  35. package/dist/index.cjs.map +1 -0
  36. package/dist/index.d.cts +196 -0
  37. package/dist/index.d.ts +196 -0
  38. package/dist/index.js +920 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/lambda.cjs +405 -0
  41. package/dist/lambda.cjs.map +1 -0
  42. package/dist/lambda.d.cts +53 -0
  43. package/dist/lambda.d.ts +53 -0
  44. package/dist/lambda.js +86 -0
  45. package/dist/lambda.js.map +1 -0
  46. package/dist/nonces-scwljZwl.d.cts +92 -0
  47. package/dist/nonces-scwljZwl.d.ts +92 -0
  48. package/dist/provider-BqfTvF3g.d.ts +2016 -0
  49. package/dist/provider-Miaa9V7I.d.cts +2016 -0
  50. package/dist/stores.cjs +315 -0
  51. package/dist/stores.cjs.map +1 -0
  52. package/dist/stores.d.cts +52 -0
  53. package/dist/stores.d.ts +52 -0
  54. package/dist/stores.js +306 -0
  55. package/dist/stores.js.map +1 -0
  56. package/dist/testing.cjs +135 -0
  57. package/dist/testing.cjs.map +1 -0
  58. package/dist/testing.d.cts +29 -0
  59. package/dist/testing.d.ts +29 -0
  60. package/dist/testing.js +49 -0
  61. package/dist/testing.js.map +1 -0
  62. package/dist/vinaverse-v1.d-BVc4uyUd.d.cts +3460 -0
  63. package/dist/vinaverse-v1.d-BVc4uyUd.d.ts +3460 -0
  64. package/package.json +226 -4
package/README.md CHANGED
@@ -1,3 +1,166 @@
1
- # Temporary Holding Version
1
+ # @vinaverse/server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Vinaverse for Node backends: call the Vinaverse API typed with a service key, verify webhooks into typed events,
4
+ and serve the tools the agent calls with the contract enforced, with adapters for AWS Lambda, Express, Fastify, Hono
5
+ and any Fetch API runtime. Runs on Node 18+, AWS Lambda and edge runtimes: it uses Web APIs only (Web Crypto, never a
6
+ Node-only API).
7
+
8
+ Pre-release (`0.x`): it targets the contract named in `CONTRACT`. Its types need TypeScript 5.0 or newer (the gate
9
+ compiles a consumer against the published declarations with 5.0). Errors it throws carry a `name`
10
+ (`WebCryptoUnavailableError`, `RawBodyRequiredError`): check `error.name`, since each entry point and module format
11
+ has its own copy of the class.
12
+
13
+ ## Verify a delivery
14
+
15
+ ```ts
16
+ import { verifyWebhook } from '@vinaverse/server';
17
+
18
+ const result = await verifyWebhook({
19
+ rawBody, // the body exactly as received: string, Uint8Array/Buffer or ArrayBuffer
20
+ header: request.headers['vinaverse-signature'],
21
+ secret: process.env.VINAVERSE_WEBHOOK_SECRET, // the whole vv_whsec_… value; two while rotating
22
+ });
23
+
24
+ if (!result.ok) return reply(400); // result.error.code says why
25
+ if (result.kind === 'event') {
26
+ switch (result.event.type) {
27
+ case 'conversation.completed':
28
+ result.event.data.answers; // typed ConversationState
29
+ break;
30
+ }
31
+ }
32
+ return reply(200); // also for 'test' (a Studio test delivery) and 'unrecognized' (a newer event type)
33
+ ```
34
+
35
+ Deliveries are at least once: deduplicate on `event.id`. Answer within 5 seconds and do the work afterwards.
36
+
37
+ ## Adapters
38
+
39
+ Each answers 200 when handled, 400 when refused, 413 over `maxBodyBytes` (default 1 MiB), and lets your handler's
40
+ error through so Vinaverse retries.
41
+
42
+ ```ts
43
+ // AWS Lambda: API Gateway REST (v1), HTTP API (v2) or a function URL; base64 bodies handled
44
+ import { createLambdaWebhookHandler } from '@vinaverse/server/lambda';
45
+ export const handler = createLambdaWebhookHandler({ secret, onEvent: async (event) => { /* … */ } });
46
+
47
+ // Express: mount before express.json() (or behind express.raw({ type: 'application/json' }))
48
+ import { vinaverseWebhook } from '@vinaverse/server/express';
49
+ app.post('/webhooks/vinaverse', vinaverseWebhook({ secret, onEvent }));
50
+
51
+ // Fastify: the plugin reads its route's body raw; the rest of the app keeps its JSON parser
52
+ import { vinaverseWebhookPlugin } from '@vinaverse/server/fastify';
53
+ await app.register(vinaverseWebhookPlugin({ path: '/webhooks/vinaverse', secret, onEvent }));
54
+
55
+ // Hono
56
+ import { vinaverseWebhook } from '@vinaverse/server/hono';
57
+ app.post('/webhooks/vinaverse', vinaverseWebhook({ secret, onEvent }));
58
+
59
+ // Next.js route handlers, Cloudflare Workers, Deno, Bun: any Request → Response runtime
60
+ import { createFetchWebhookHandler } from '@vinaverse/server';
61
+ export const POST = createFetchWebhookHandler({ secret, onEvent });
62
+ ```
63
+
64
+ `onTest`, `onUnrecognized` and `onRejected` are optional.
65
+
66
+ ## Order a conversation's states
67
+
68
+ ```ts
69
+ import { applyConversationState, missingSequences } from '@vinaverse/server';
70
+
71
+ const { record, action } = applyConversationState(stored, event.data);
72
+ // action: 'apply' or 'stale' (an equal or lower sequence, even under a new envelope id)
73
+ // record: what to store: { state, seen, fullReadNeeded }. fullReadNeeded: the webhook left out the answers
74
+ // (over 64 KB); read GET /v1/conversations/{id}. missingSequences(record): the states still to come.
75
+ // Deduplicate deliveries on event.id first; ConversationReceiver does all of it in memory, for one process.
76
+ ```
77
+
78
+ ## Serve your tools
79
+
80
+ ```ts
81
+ import { createToolProvider, MemoryNonceStore, MemoryIdempotencyStore, ToolRefusal } from '@vinaverse/server';
82
+ import { redisNonceStore, redisIdempotencyStore } from '@vinaverse/server/stores'; // shared, for several instances
83
+
84
+ const tools = createToolProvider({
85
+ secret: process.env.VINAVERSE_TOOL_SECRET, // the whole tool signing secret; two while rotating
86
+ nonces: new MemoryNonceStore(), // or redisNonceStore({ command: (args) => redis.sendCommand(args) })
87
+ idempotency: new MemoryIdempotencyStore(), // or the matching shared store
88
+ basePath: '/api/v2/ai', // the path of the tool base URL registered in Studio
89
+ authenticate: async (token) => (await verifyMyToken(token)) ?? throwExpired(), // your principal
90
+ handlers: {
91
+ get_flow_definition: ({ params, query }) => myFlows.definition(params.flow, query.version),
92
+ submit_flow: async ({ params, body, principal }) => {
93
+ const problem = await myFlows.check(params.flow, body);
94
+ if (problem) throw ToolRefusal.flowRefused(problem.code, problem.questionId);
95
+ await myFlows.store(principal, body); // returning nothing accepts it
96
+ },
97
+ create_handoff: ({ body, principal }) => mySupport.openTicket(principal, body),
98
+ },
99
+ leaseSeconds: 60, // above the longest a handler can run
100
+ onRefused: (reason) => log.warn({ reason }, 'tool call signature refused'),
101
+ onError: (error, operation) => log.error({ error, operation }),
102
+ });
103
+ ```
104
+
105
+ A `submit_flow` handler refuses only with `ToolRefusal.flowRefused` (any other 422 is answered as a 500), and every
106
+ handler validates what it acts on. Idempotent answers are kept under a SHA-256 fingerprint of the request, never the
107
+ request itself; `scope(principal)` keeps them apart per tenant or environment.
108
+
109
+ Then mount it: `createLambdaToolHandler(tools)` (`/lambda`), `app.use('/api/v2/ai', vinaverseTools(tools))`
110
+ (`/express`), `vinaverseToolsPlugin({ provider: tools, basePath })` (`/fastify`), `vinaverseTools(tools)` (`/hono`)
111
+ or `createFetchToolHandler(tools)`. Signatures, nonces, token presence, sizes, refusals and idempotent submissions,
112
+ proposals and handoffs are handled; your handlers decide what an answer or a ticket means. Details:
113
+ [docs/architecture/tools.md](../../docs/architecture/tools.md).
114
+
115
+ ## Test your route
116
+
117
+ ```ts
118
+ import { signToolCall, signWebhook } from '@vinaverse/server/testing';
119
+ const header = await signWebhook({ body, secret }); // the Vinaverse-Signature value Vinaverse would send
120
+ const toolHeader = await signToolCall({ secret, method: 'POST', target, token, body }); // a tool call's
121
+ ```
122
+
123
+ ## The Vinaverse API (`@vinaverse/server/api`)
124
+
125
+ Typed calls to the Vinaverse API with a service key, generated from the pinned public contract.
126
+
127
+ ```ts
128
+ import { createApiClient, VinaverseApiError } from '@vinaverse/server/api';
129
+
130
+ const vinaverse = createApiClient({ apiKey: process.env.VINAVERSE_API_KEY }); // vv_sk_<mock|dev|live>_…
131
+
132
+ // Your app asks your backend for a session; give it the session's id, URLs and client_secret only.
133
+ const session = await vinaverse.sessions.create({ agent: 'intake', subject, tool_access }); // your agent's id
134
+
135
+ try {
136
+ await vinaverse.moderation.createJob({ policy: 'default', items });
137
+ } catch (error) {
138
+ if (error instanceof VinaverseApiError && error.code === 'invalid_request') console.log(error.errors);
139
+ }
140
+ ```
141
+
142
+ - **Idempotency keys** are added to the calls that take one (`sessions.create`, `moderation.createJob`,
143
+ `media.createJob`, `documentChecks.create`, `subjects.erase`); pass `{ idempotencyKey }` to use your own.
144
+ - **Retries are safe:** a `429` is retried after its `Retry-After`; a `5xx`, a retryable problem, a timeout or a
145
+ network error is retried only for a read or a call with an idempotency key. Default 2 retries, 30 s timeout per try
146
+ (reading the body included), and a `Retry-After` over 30 s is not waited for (`maxRetries`, `timeout`,
147
+ `maxRetryDelay`, checked when the client is made).
148
+ - **Trying again yourself:** when a keyed call still fails (`VinaverseTimeoutError`, `VinaverseNetworkError`, or a
149
+ retryable `VinaverseApiError`), its error carries `idempotencyKey`. Send the call again later with
150
+ `{ idempotencyKey: error.idempotencyKey }`: the API does it once, however many times it was sent.
151
+ - **Aborts:** pass `{ signal }`; aborting stops the call at once, during a try, a body read or a wait between tries,
152
+ and an already aborted signal sends nothing.
153
+ - **Refusals throw `VinaverseApiError`**, with the problem's `status`, `code`, `retryable`, `requestId`, `retryAfter`
154
+ and, for `invalid_request`, the field `errors`.
155
+ - **Every operation** is also on `vinaverse.api`, the typed openapi-fetch client (`api.GET('/v1/usage')`).
156
+ - **Backend only.** The client refuses a client secret, and refuses to start in a browser or in React Native: a
157
+ service key there ships to everyone who loads the app. Apps use `@vinaverse/client` with the session's client secret.
158
+ - The base URL follows the key: `https://api.vinaverse.com` for a live key, `https://api-dev.vinaverse.com` for mock
159
+ and dev keys. A `baseUrl` of your own must be `https:` (or `http:` on `localhost` for tests), and redirects are
160
+ refused: a service key never travels in clear or follows a redirect. Every request, the raw `api` client's included,
161
+ goes to that origin only. In the mock environment, the `Vinaverse-Mock-Scenario` header (`headers`
162
+ option) provokes a `429` or a `503` to test your handling.
163
+
164
+ Details: [docs/architecture/webhooks.md](../../docs/architecture/webhooks.md),
165
+ [docs/architecture/tools.md](../../docs/architecture/tools.md) and
166
+ [docs/architecture/api-client.md](../../docs/architecture/api-client.md). Licence: Apache-2.0.
package/dist/api.cjs ADDED
@@ -0,0 +1,528 @@
1
+ 'use strict';
2
+
3
+ var createClient = require('openapi-fetch');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var createClient__default = /*#__PURE__*/_interopDefault(createClient);
8
+
9
+ // src/api/client.ts
10
+
11
+ // src/api/errors.ts
12
+ var VinaverseApiError = class extends Error {
13
+ name = "VinaverseApiError";
14
+ status;
15
+ /** The problem's code (`invalid_request`, `rate_limited`, …), or `http_<status>` when the body was not a problem. */
16
+ code;
17
+ /** Whether the same call may succeed later: the problem's `retryable`, else true for 429 and 5xx. */
18
+ retryable;
19
+ /** `Vinaverse-Request-Id`, or the problem's `request_id`. */
20
+ requestId;
21
+ /** `Retry-After`, in seconds, when the API sent one. */
22
+ retryAfter;
23
+ /** For `invalid_request`: the fields refused, as JSON Pointers into the request. */
24
+ errors;
25
+ /** The problem as sent, or undefined when the body was not one. */
26
+ problem;
27
+ /** The `Idempotency-Key` the call carried: send the same call again with it to have it done once. */
28
+ idempotencyKey;
29
+ constructor(init) {
30
+ const { status, problem } = init;
31
+ super(
32
+ problem ? `${problem.title} (${problem.code})${problem.detail ? `: ${problem.detail}` : ""}` : `The Vinaverse API answered HTTP ${status}.`
33
+ );
34
+ this.idempotencyKey = init.idempotencyKey;
35
+ this.status = status;
36
+ this.problem = problem;
37
+ this.code = problem?.code ?? `http_${status}`;
38
+ this.retryable = problem?.retryable ?? (status === 429 || status >= 500);
39
+ this.requestId = init.requestId ?? problem?.request_id;
40
+ this.retryAfter = init.retryAfter;
41
+ this.errors = problem?.errors ?? [];
42
+ }
43
+ };
44
+ function apiError(response, body, idempotencyKey) {
45
+ return new VinaverseApiError({
46
+ status: response.status,
47
+ problem: isProblem(body) ? body : void 0,
48
+ requestId: response.headers.get("vinaverse-request-id") ?? void 0,
49
+ retryAfter: retryAfterSeconds(response),
50
+ idempotencyKey
51
+ });
52
+ }
53
+ function retryAfterSeconds(response, now = Date.now) {
54
+ const value = response.headers.get("retry-after");
55
+ if (value === null || value.trim() === "") return void 0;
56
+ if (/^\d+$/.test(value.trim())) return Number(value.trim());
57
+ const at = Date.parse(value);
58
+ return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - now()) / 1e3));
59
+ }
60
+ function isProblem(body) {
61
+ if (typeof body !== "object" || body === null) return false;
62
+ const b = body;
63
+ return typeof b.code === "string" && typeof b.status === "number";
64
+ }
65
+
66
+ // src/api/random.ts
67
+ var fromNode;
68
+ async function randomUUID() {
69
+ const global = globalThis.crypto;
70
+ if (global?.randomUUID) return global.randomUUID();
71
+ const specifier = ["node", "crypto"].join(":");
72
+ fromNode ??= import(
73
+ /* webpackIgnore: true */
74
+ /* @vite-ignore */
75
+ specifier
76
+ ).then((mod) => mod.randomUUID);
77
+ return (await fromNode)();
78
+ }
79
+
80
+ // src/api/retry.ts
81
+ var IDEMPOTENT = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
82
+ var NULL_BODY = /* @__PURE__ */ new Set([101, 204, 205, 304]);
83
+ var MAX_REFUSAL_BYTES = 64 * 1024;
84
+ var VinaverseTimeoutError = class extends Error {
85
+ name = "VinaverseTimeoutError";
86
+ idempotencyKey;
87
+ constructor(ms, idempotencyKey) {
88
+ super(`The Vinaverse API did not answer within ${ms} ms.`);
89
+ this.idempotencyKey = idempotencyKey;
90
+ }
91
+ };
92
+ var VinaverseNetworkError = class extends Error {
93
+ name = "VinaverseNetworkError";
94
+ idempotencyKey;
95
+ constructor(cause, idempotencyKey) {
96
+ super(
97
+ `The Vinaverse API could not be reached: ${cause instanceof Error ? cause.message : String(cause)}`,
98
+ { cause }
99
+ );
100
+ this.idempotencyKey = idempotencyKey;
101
+ }
102
+ };
103
+ var MAX_TIMER_MS = 2147483647;
104
+ function retryingFetch(fetchFn, options = {}, guard = () => {
105
+ }) {
106
+ const maxRetries = check(
107
+ "maxRetries",
108
+ options.maxRetries ?? 2,
109
+ (n) => Number.isSafeInteger(n) && n >= 0
110
+ );
111
+ const maxDelay = check(
112
+ "maxRetryDelay",
113
+ options.maxRetryDelay ?? 30,
114
+ (n) => Number.isFinite(n) && n >= 0 && n * 1e3 <= MAX_TIMER_MS
115
+ ) * 1e3;
116
+ const timeout = check(
117
+ "timeout",
118
+ options.timeout ?? 3e4,
119
+ (n) => Number.isFinite(n) && n > 0 && n <= MAX_TIMER_MS
120
+ );
121
+ const sleep = options.sleep;
122
+ const random = options.random ?? Math.random;
123
+ return async (request) => {
124
+ guard(request.url);
125
+ const signal = request.signal;
126
+ const key = request.headers.get("idempotency-key") ?? void 0;
127
+ const replayable = IDEMPOTENT.has(request.method.toUpperCase()) || key !== void 0;
128
+ for (let attempt = 0; ; attempt++) {
129
+ if (signal.aborted) throw aborted(signal);
130
+ const last = attempt >= maxRetries;
131
+ let response;
132
+ try {
133
+ response = await once(fetchFn, request.clone(), signal, timeout);
134
+ } catch (error) {
135
+ if (signal.aborted) throw aborted(signal);
136
+ if (last || !replayable) {
137
+ throw error instanceof VinaverseTimeoutError ? new VinaverseTimeoutError(timeout, key) : new VinaverseNetworkError(error, key);
138
+ }
139
+ await wait(sleep, Math.min(backoff(attempt, random), maxDelay), signal);
140
+ continue;
141
+ }
142
+ const retry = response.status === 429 || replayable && retryable(response, await problemOf(response));
143
+ if (!retry || last) return response;
144
+ const delay = retryAfter(response) ?? backoff(attempt, random);
145
+ if (delay > maxDelay) return response;
146
+ await wait(sleep, delay, signal);
147
+ }
148
+ };
149
+ }
150
+ async function once(fetchFn, request, signal, timeout) {
151
+ const controller = new AbortController();
152
+ const timer = setTimeout(() => controller.abort(), timeout);
153
+ const stop = () => controller.abort();
154
+ signal.addEventListener("abort", stop, { once: true });
155
+ if (signal.aborted) controller.abort();
156
+ try {
157
+ const pending = fetchFn(
158
+ new Request(request, { signal: controller.signal, redirect: "error" })
159
+ );
160
+ pending.catch(() => {
161
+ });
162
+ const response = await until(pending, controller.signal);
163
+ const empty = NULL_BODY.has(response.status);
164
+ const body = empty ? null : await readBody(
165
+ response,
166
+ response.ok ? Number.POSITIVE_INFINITY : MAX_REFUSAL_BYTES,
167
+ controller.signal
168
+ );
169
+ return new Response(body, {
170
+ status: response.status,
171
+ statusText: response.statusText,
172
+ headers: response.headers
173
+ });
174
+ } catch (error) {
175
+ if (controller.signal.aborted && !signal.aborted)
176
+ throw new VinaverseTimeoutError(timeout);
177
+ throw error;
178
+ } finally {
179
+ clearTimeout(timer);
180
+ signal.removeEventListener("abort", stop);
181
+ }
182
+ }
183
+ async function readBody(response, limit, signal) {
184
+ if (!response.body) return null;
185
+ const reader = response.body.getReader();
186
+ const chunks = [];
187
+ let size = 0;
188
+ for (; ; ) {
189
+ const read = reader.read();
190
+ read.catch(() => {
191
+ });
192
+ const { done, value } = await until(read, signal).catch(
193
+ async (error) => {
194
+ void reader.cancel().catch(() => {
195
+ });
196
+ throw error;
197
+ }
198
+ );
199
+ if (done) break;
200
+ chunks.push(value);
201
+ size += value.length;
202
+ if (size >= limit) {
203
+ void reader.cancel().catch(() => {
204
+ });
205
+ break;
206
+ }
207
+ }
208
+ const body = new Uint8Array(new ArrayBuffer(Math.min(size, limit)));
209
+ let at = 0;
210
+ for (const chunk of chunks) {
211
+ const part = chunk.subarray(0, body.length - at);
212
+ body.set(part, at);
213
+ at += part.length;
214
+ if (at >= body.length) break;
215
+ }
216
+ return body;
217
+ }
218
+ async function problemOf(response) {
219
+ if (response.ok) return void 0;
220
+ try {
221
+ return await response.clone().json();
222
+ } catch {
223
+ return void 0;
224
+ }
225
+ }
226
+ function retryable(response, problem) {
227
+ if (response.status >= 500) return true;
228
+ return response.status >= 400 && problem?.retryable === true;
229
+ }
230
+ async function until(promise, signal) {
231
+ if (signal.aborted) throw aborted(signal);
232
+ let stop = () => {
233
+ };
234
+ const abort = new Promise((_, reject) => {
235
+ stop = () => reject(aborted(signal));
236
+ signal.addEventListener("abort", stop, { once: true });
237
+ });
238
+ try {
239
+ return await Promise.race([promise, abort]);
240
+ } finally {
241
+ signal.removeEventListener("abort", stop);
242
+ }
243
+ }
244
+ function wait(sleep, ms, signal) {
245
+ if (sleep) return until(sleep(ms), signal);
246
+ if (signal.aborted) return Promise.reject(aborted(signal));
247
+ return new Promise((resolve, reject) => {
248
+ const stop = () => {
249
+ clearTimeout(timer);
250
+ reject(aborted(signal));
251
+ };
252
+ const timer = setTimeout(() => {
253
+ signal.removeEventListener("abort", stop);
254
+ resolve();
255
+ }, ms);
256
+ signal.addEventListener("abort", stop, { once: true });
257
+ });
258
+ }
259
+ function aborted(signal) {
260
+ return signal.reason ?? new DOMException("The call was aborted.", "AbortError");
261
+ }
262
+ function retryAfter(response) {
263
+ const seconds = retryAfterSeconds(response);
264
+ return seconds === void 0 ? void 0 : seconds * 1e3;
265
+ }
266
+ function backoff(attempt, random) {
267
+ const base = 500 * 2 ** attempt;
268
+ return Math.round(base / 2 + random() * (base / 2));
269
+ }
270
+ function check(name, value, valid) {
271
+ if (typeof value !== "number" || !valid(value)) {
272
+ throw new TypeError(`${name} is not valid: ${String(value)}`);
273
+ }
274
+ return value;
275
+ }
276
+
277
+ // src/api/client.ts
278
+ var SERVICE_KEY = /^vv_sk_(mock|dev|live)_[A-Za-z0-9_-]{8,}$/;
279
+ var ServiceKeyExposureError = class extends Error {
280
+ name = "ServiceKeyExposureError";
281
+ };
282
+ function createApiClient(options) {
283
+ refuseClientRuntime();
284
+ const key = options.apiKey;
285
+ if (typeof key !== "string" || !SERVICE_KEY.test(key)) {
286
+ throw new TypeError(
287
+ key?.startsWith?.("vv_cs_") ? "createApiClient takes a service key (vv_sk_\u2026); a client secret (vv_cs_\u2026) is for the app" : "createApiClient needs a service key: vv_sk_<mock|dev|live>_\u2026"
288
+ );
289
+ }
290
+ const live = key.startsWith("vv_sk_live_");
291
+ const baseUrl = safeBaseUrl(
292
+ options.baseUrl ?? (live ? "https://api.vinaverse.com" : "https://api-dev.vinaverse.com")
293
+ );
294
+ const transport = options.fetch ?? ((request) => globalThis.fetch(request));
295
+ const api = createClient__default.default({
296
+ baseUrl,
297
+ redirect: "error",
298
+ // the API never redirects; a service key never follows one
299
+ // Every request, from a helper or from `api` with a per-call baseUrl, goes to this origin only.
300
+ fetch: retryingFetch(transport, options, sameOrigin(baseUrl)),
301
+ headers: {
302
+ ...options.headers,
303
+ authorization: `Bearer ${key}`,
304
+ accept: "application/json"
305
+ }
306
+ });
307
+ async function run(call, idempotencyKey) {
308
+ const { data, error, response } = await call;
309
+ if (!response.ok) throw apiError(response, error, idempotencyKey);
310
+ return data;
311
+ }
312
+ async function keyed(o, call) {
313
+ const key2 = o?.idempotencyKey ?? await newKey();
314
+ if (typeof key2 !== "string" || key2.length < 1 || key2.length > 255) {
315
+ throw new TypeError("idempotencyKey must be 1 to 255 characters");
316
+ }
317
+ return run(call({ "Idempotency-Key": key2 }), key2);
318
+ }
319
+ const init = (o) => o?.signal ? { signal: o.signal } : {};
320
+ return {
321
+ /** Every operation of the contract, typed, for what the helpers below do not cover (openapi-fetch). */
322
+ api,
323
+ sessions: {
324
+ /** Mint a session for the person in your app; give the app its `id`, URLs and `client_secret` only. */
325
+ create: (body, o) => keyed(
326
+ o,
327
+ (header) => api.POST("/v1/sessions", {
328
+ body,
329
+ params: { header },
330
+ ...init(o)
331
+ })
332
+ ),
333
+ get: (sessionId, o) => run(
334
+ api.GET("/v1/sessions/{session_id}", {
335
+ params: { path: { session_id: sessionId } },
336
+ ...init(o)
337
+ })
338
+ ),
339
+ revoke: (sessionId, o) => run(
340
+ api.POST("/v1/sessions/{session_id}/revoke", {
341
+ params: { path: { session_id: sessionId } },
342
+ ...init(o)
343
+ })
344
+ ),
345
+ /** A fresh client secret for an open session: what an app's `refreshSecret(sessionId)` asks your backend for. */
346
+ refreshClientSecret: (sessionId, o) => run(
347
+ api.POST("/v1/sessions/{session_id}/client_secret", {
348
+ params: { path: { session_id: sessionId } },
349
+ ...init(o)
350
+ })
351
+ )
352
+ },
353
+ conversations: {
354
+ get: (conversationId, o) => run(
355
+ api.GET("/v1/conversations/{conversation_id}", {
356
+ params: { path: { conversation_id: conversationId } },
357
+ ...init(o)
358
+ })
359
+ )
360
+ },
361
+ suggestions: {
362
+ create: (body, o) => run(
363
+ api.POST("/v1/suggestions", { body, ...init(o) })
364
+ )
365
+ },
366
+ moderation: {
367
+ createJob: (body, o) => keyed(
368
+ o,
369
+ (header) => api.POST("/v1/moderation/jobs", {
370
+ body,
371
+ params: { header },
372
+ ...init(o)
373
+ })
374
+ ),
375
+ getJob: (jobId, o) => run(
376
+ api.GET("/v1/moderation/jobs/{job_id}", {
377
+ params: { path: { job_id: jobId } },
378
+ ...init(o)
379
+ })
380
+ ),
381
+ getItem: (itemId, o) => run(
382
+ api.GET("/v1/moderation/items/{item_id}", {
383
+ params: { path: { item_id: itemId } },
384
+ ...init(o)
385
+ })
386
+ )
387
+ },
388
+ media: {
389
+ createJob: (body, o) => keyed(
390
+ o,
391
+ (header) => api.POST("/v1/media/jobs", {
392
+ body,
393
+ params: { header },
394
+ ...init(o)
395
+ })
396
+ ),
397
+ listJobs: (query = {}, o) => run(
398
+ api.GET("/v1/media/jobs", { params: { query }, ...init(o) })
399
+ ),
400
+ getJob: (jobId, o) => run(
401
+ api.GET("/v1/media/jobs/{job_id}", {
402
+ params: { path: { job_id: jobId } },
403
+ ...init(o)
404
+ })
405
+ ),
406
+ cancelJob: (jobId, o) => run(
407
+ api.POST("/v1/media/jobs/{job_id}/cancel", {
408
+ params: { path: { job_id: jobId } },
409
+ ...init(o)
410
+ })
411
+ ),
412
+ listProfiles: (o) => run(
413
+ api.GET("/v1/media/profiles", { ...init(o) })
414
+ )
415
+ },
416
+ screening: {
417
+ text: (body, o) => run(
418
+ api.POST("/v1/screening/text", { body, ...init(o) })
419
+ )
420
+ },
421
+ documentChecks: {
422
+ create: (body, o) => keyed(
423
+ o,
424
+ (header) => api.POST("/v1/document-checks", {
425
+ body,
426
+ params: { header },
427
+ ...init(o)
428
+ })
429
+ ),
430
+ get: (checkId, o) => run(
431
+ api.GET("/v1/document-checks/{check_id}", {
432
+ params: { path: { check_id: checkId } },
433
+ ...init(o)
434
+ })
435
+ )
436
+ },
437
+ subjects: {
438
+ erase: (subjectId, o) => keyed(
439
+ o,
440
+ (header) => api.POST("/v1/subjects/{subject_id}/erase", {
441
+ params: { path: { subject_id: subjectId }, header },
442
+ ...init(o)
443
+ })
444
+ )
445
+ },
446
+ feedback: {
447
+ send: (body, o) => run(
448
+ api.POST("/v1/feedback", { body, ...init(o) })
449
+ )
450
+ },
451
+ events: {
452
+ send: (body, o) => run(
453
+ api.POST("/v1/events", { body, ...init(o) })
454
+ )
455
+ },
456
+ knowledge: {
457
+ sendDocument: (body, o) => run(
458
+ api.POST("/v1/knowledge/documents", { body, ...init(o) })
459
+ )
460
+ },
461
+ catalog: {
462
+ put: (type, id, body, o) => run(
463
+ api.PUT("/v1/catalog/{type}/{id}", {
464
+ params: { path: { type, id } },
465
+ body,
466
+ ...init(o)
467
+ })
468
+ )
469
+ },
470
+ usage: {
471
+ get: (query = {}, o) => run(
472
+ api.GET("/v1/usage", { params: { query }, ...init(o) })
473
+ )
474
+ }
475
+ };
476
+ }
477
+ function refuseClientRuntime() {
478
+ const scope = globalThis;
479
+ if (scope.window !== void 0 && scope.document !== void 0) {
480
+ throw new ServiceKeyExposureError(
481
+ "The Vinaverse API client holds a service key and runs on your backend only, never in a browser. Give the app a session client secret (sessions.create)."
482
+ );
483
+ }
484
+ if (scope.navigator?.product === "ReactNative") {
485
+ throw new ServiceKeyExposureError(
486
+ "The Vinaverse API client holds a service key and runs on your backend only, never in an app. Use @vinaverse/client with a session client secret."
487
+ );
488
+ }
489
+ }
490
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
491
+ function safeBaseUrl(value) {
492
+ let url;
493
+ try {
494
+ url = new URL(value);
495
+ } catch {
496
+ throw new TypeError(`baseUrl is not a URL: ${value}`);
497
+ }
498
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && LOOPBACK.has(url.hostname))) {
499
+ throw new TypeError(
500
+ `baseUrl must be https: (or http: on localhost); a service key never travels in clear: ${url.origin}`
501
+ );
502
+ }
503
+ return value.replace(/\/+$/, "");
504
+ }
505
+ function sameOrigin(baseUrl) {
506
+ const origin = new URL(baseUrl).origin;
507
+ return (url) => {
508
+ const target = new URL(url);
509
+ if (target.origin !== origin) {
510
+ throw new TypeError(
511
+ `The Vinaverse API client sends its service key to ${origin} only, not to ${target.origin}`
512
+ );
513
+ }
514
+ };
515
+ }
516
+ function newKey() {
517
+ return randomUUID();
518
+ }
519
+
520
+ exports.ServiceKeyExposureError = ServiceKeyExposureError;
521
+ exports.VinaverseApiError = VinaverseApiError;
522
+ exports.VinaverseNetworkError = VinaverseNetworkError;
523
+ exports.VinaverseTimeoutError = VinaverseTimeoutError;
524
+ exports.apiError = apiError;
525
+ exports.createApiClient = createApiClient;
526
+ exports.retryAfterSeconds = retryAfterSeconds;
527
+ //# sourceMappingURL=api.cjs.map
528
+ //# sourceMappingURL=api.cjs.map