@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.
- package/README.md +165 -2
- package/dist/api.cjs +528 -0
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.cts +453 -0
- package/dist/api.d.ts +453 -0
- package/dist/api.js +516 -0
- package/dist/api.js.map +1 -0
- package/dist/chunk-FEEJNQXV.js +64 -0
- package/dist/chunk-FEEJNQXV.js.map +1 -0
- package/dist/chunk-GTPMQKGX.js +277 -0
- package/dist/chunk-GTPMQKGX.js.map +1 -0
- package/dist/chunk-NPXO6SMZ.js +84 -0
- package/dist/chunk-NPXO6SMZ.js.map +1 -0
- package/dist/chunk-SDJFZPHJ.js +141 -0
- package/dist/chunk-SDJFZPHJ.js.map +1 -0
- package/dist/express.cjs +397 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +43 -0
- package/dist/express.d.ts +43 -0
- package/dist/express.js +87 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.cjs +363 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +62 -0
- package/dist/fastify.d.ts +62 -0
- package/dist/fastify.js +54 -0
- package/dist/fastify.js.map +1 -0
- package/dist/hono.cjs +421 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +21 -0
- package/dist/hono.d.ts +21 -0
- package/dist/hono.js +34 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +1481 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +196 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.js +920 -0
- package/dist/index.js.map +1 -0
- package/dist/lambda.cjs +405 -0
- package/dist/lambda.cjs.map +1 -0
- package/dist/lambda.d.cts +53 -0
- package/dist/lambda.d.ts +53 -0
- package/dist/lambda.js +86 -0
- package/dist/lambda.js.map +1 -0
- package/dist/nonces-scwljZwl.d.cts +92 -0
- package/dist/nonces-scwljZwl.d.ts +92 -0
- package/dist/provider-BqfTvF3g.d.ts +2016 -0
- package/dist/provider-Miaa9V7I.d.cts +2016 -0
- package/dist/stores.cjs +315 -0
- package/dist/stores.cjs.map +1 -0
- package/dist/stores.d.cts +52 -0
- package/dist/stores.d.ts +52 -0
- package/dist/stores.js +306 -0
- package/dist/stores.js.map +1 -0
- package/dist/testing.cjs +135 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +29 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +49 -0
- package/dist/testing.js.map +1 -0
- package/dist/vinaverse-v1.d-BVc4uyUd.d.cts +3460 -0
- package/dist/vinaverse-v1.d-BVc4uyUd.d.ts +3460 -0
- package/package.json +226 -4
package/README.md
CHANGED
|
@@ -1,3 +1,166 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vinaverse/server
|
|
2
2
|
|
|
3
|
-
|
|
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
|