twelveai 0.1.0
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/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/index.cjs +246 -0
- package/dist/index.d.cts +230 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +216 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TwelveAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# twelveai
|
|
2
|
+
|
|
3
|
+
Official Node.js SDK for [TwelveAI](https://twelveai.app) - AI infrastructure for conversational banking.
|
|
4
|
+
|
|
5
|
+
Your customers chat in plain language; TwelveAI routes intent, calls your typed tools, grounds every answer in real data (never an invented figure), and gates money moves behind confirmation. This SDK runs the whole protocol in **one call**.
|
|
6
|
+
|
|
7
|
+
- **Zero runtime dependencies** - Node 18+, ESM and CommonJS, full TypeScript types.
|
|
8
|
+
- **One-call conversations** - continuations, tool hand-offs, and resume handled for you.
|
|
9
|
+
- **Zero-credential integration** - with client-fetch tools, TwelveAI never holds a key to your API; the SDK completes handed-off requests with *your* auth, on *your* server.
|
|
10
|
+
- **Money is never auto-executed** - transfers pause for your PIN/OTP flow, always.
|
|
11
|
+
- **Request verification included** - prove a webhook or tool call really came from TwelveAI (HMAC or public-key JWKS).
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install twelveai
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { TwelveAI } from 'twelveai'
|
|
21
|
+
|
|
22
|
+
const twelve = new TwelveAI({
|
|
23
|
+
apiKey: process.env.TWELVE_API_KEY!, // from https://console.twelveai.app
|
|
24
|
+
// Only needed when your tools use "My app calls it" (client-fetch): these are
|
|
25
|
+
// the headers YOUR server uses to call YOUR OWN API. TwelveAI never sees them.
|
|
26
|
+
clientAuth: { authorization: `Bearer ${process.env.MY_INTERNAL_TOKEN}` },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const res = await twelve.chat({
|
|
30
|
+
message: "what's my balance?",
|
|
31
|
+
customerId: 'cus_123', // your id for this end user
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
console.log(res.message) // "Your current balance is NGN 307.05."
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Behind that one call the SDK also completed any **client-fetch hand-offs**: when a tool is configured as *"my app calls it"*, the engine returns the resolved request (method, URL, body - no credentials attached) instead of calling your API. The SDK performs it with your `clientAuth`, resumes the turn, and hands you the final grounded answer. Inspect what ran via `res.executedHandoffs`.
|
|
38
|
+
|
|
39
|
+
## Multi-turn conversations
|
|
40
|
+
|
|
41
|
+
Pass the previous turn's `continuation` to keep context:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
let res = await twelve.chat({ message: "what's my balance?", customerId: 'cus_123' })
|
|
45
|
+
|
|
46
|
+
res = await twelve.chat({
|
|
47
|
+
message: 'and my last 3 transactions?',
|
|
48
|
+
customerId: 'cus_123',
|
|
49
|
+
continuation: res.continuation!,
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Money moves (confirmation + PIN)
|
|
54
|
+
|
|
55
|
+
Money is **never** auto-executed. A transfer pauses and comes back to you:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const res = await twelve.chat({
|
|
59
|
+
message: 'send 5,000 to 0123456789 GTBank',
|
|
60
|
+
customerId: 'cus_123',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// res.message -> "You're about to send NGN 5,000 to JOHN DOE. Confirm?"
|
|
64
|
+
const pending = res.pendingToolCalls?.[0]
|
|
65
|
+
if (pending) {
|
|
66
|
+
// 1. Show res.message and collect your PIN/OTP from the user.
|
|
67
|
+
// 2. Execute the transfer on YOUR rails (KYC, limits, PIN all enforced by you).
|
|
68
|
+
const receipt = await runTransferOnMyRails(pending.arguments)
|
|
69
|
+
// 3. Resume the conversation with the result - the reply is a grounded receipt.
|
|
70
|
+
const done = await twelve.resume(res.continuation!, [
|
|
71
|
+
{ id: pending.id, result: { ok: true, data: receipt } },
|
|
72
|
+
])
|
|
73
|
+
console.log(done.message)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Hosted writes pause with pendingConfirmation instead - approve with one call:
|
|
77
|
+
if (res.pendingConfirmation) {
|
|
78
|
+
const done = await twelve.confirm(res.continuation!, { customerId: 'cus_123' })
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Sandbox
|
|
83
|
+
|
|
84
|
+
Add `sandbox: true` and every tool returns realistic sample data - nothing real is called or moved. Perfect for CI and demos.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await twelve.chat({ message: 'balance?', customerId: 'test-1', sandbox: true })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Verifying requests FROM TwelveAI
|
|
91
|
+
|
|
92
|
+
When TwelveAI calls your endpoints (hosted tools) or delivers webhooks, verify the origin. With JWKS, **no shared secret exists at all**.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { verifySignedRequest, createJwksVerifier, verifyWebhook } from 'twelveai'
|
|
96
|
+
|
|
97
|
+
// Tool auth "signed" (HMAC with your signing secret from Settings -> Request signing):
|
|
98
|
+
app.post('/ai/*', (req, res, next) => {
|
|
99
|
+
const ok = verifySignedRequest(process.env.TWELVE_SIGNING_SECRET!, {
|
|
100
|
+
method: req.method,
|
|
101
|
+
path: req.originalUrl, // path + query
|
|
102
|
+
rawBody: req.rawBody, // capture the raw body bytes in your body parser
|
|
103
|
+
headers: req.headers,
|
|
104
|
+
})
|
|
105
|
+
if (!ok) return res.status(401).end()
|
|
106
|
+
next()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Tool auth "jwks" (public key, verified against the platform's published keys;
|
|
110
|
+
// each token is bound to that one request - method, URL, body hash):
|
|
111
|
+
const verify = createJwksVerifier() // defaults to https://ai.twelveai.app/.well-known/jwks.json
|
|
112
|
+
const v = await verify({
|
|
113
|
+
authorization: req.get('authorization')!,
|
|
114
|
+
method: req.method,
|
|
115
|
+
rawBody: req.rawBody,
|
|
116
|
+
})
|
|
117
|
+
if (!v.ok) return res.status(401).end()
|
|
118
|
+
|
|
119
|
+
// Webhook deliveries:
|
|
120
|
+
if (!verifyWebhook(process.env.TWELVE_WEBHOOK_SECRET!, req.headers)) {
|
|
121
|
+
return res.status(401).end()
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Your security team can also allowlist TwelveAI's static egress IPs: `https://ai.twelveai.app/.well-known/egress-ips`.
|
|
126
|
+
|
|
127
|
+
## API reference
|
|
128
|
+
|
|
129
|
+
### `new TwelveAI(options)`
|
|
130
|
+
|
|
131
|
+
| Option | Type | Description |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `apiKey` | `string` | **Required.** Your workspace API key (`sk_live_...` / `sk_test_...`). |
|
|
134
|
+
| `baseUrl` | `string` | Engine URL. Default `https://ai.twelveai.app`. |
|
|
135
|
+
| `clientAuth` | `object \| () => object` | Headers your server adds when executing client-fetch hand-offs against your own API. |
|
|
136
|
+
| `maxHandoffRounds` | `number` | Max auto-resume rounds per `chat()`. Default `3`. |
|
|
137
|
+
| `fetch` | `typeof fetch` | Custom fetch (tests, polyfills). |
|
|
138
|
+
|
|
139
|
+
### `twelve.chat(input, opts?)`
|
|
140
|
+
|
|
141
|
+
Sends one turn and returns the completed `ChatResponse`.
|
|
142
|
+
|
|
143
|
+
Input fields: `message`, `customerId`, `continuation`, `confirmed`, `sandbox`, `channel`, `intent`, `tier`, `customerToken` (per-turn end-user token for `customer_token` tool auth), `attachments` (images / voice notes as `{ url | data, mediaType }`), `metadata`.
|
|
144
|
+
|
|
145
|
+
Options: `autoExecute: false` returns the raw paused response without executing hand-offs; `onHandoff: async (call) => result` replaces the default executor.
|
|
146
|
+
|
|
147
|
+
### `twelve.resume(continuation, toolResults, input?)`
|
|
148
|
+
|
|
149
|
+
Resume a paused turn with results your system produced: `toolResults: [{ id, result }]`.
|
|
150
|
+
|
|
151
|
+
### `twelve.confirm(continuation, input?)`
|
|
152
|
+
|
|
153
|
+
Approve a `pendingConfirmation` (resends the turn with `confirmed: true`).
|
|
154
|
+
|
|
155
|
+
### `ChatResponse` (the important fields)
|
|
156
|
+
|
|
157
|
+
| Field | Meaning |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `message` | The assistant's reply to show your user. |
|
|
160
|
+
| `continuation` | Pass back next turn to keep context. |
|
|
161
|
+
| `pendingConfirmation` | A hosted write awaiting approval - call `confirm()`. |
|
|
162
|
+
| `pendingToolCalls` | Calls YOUR system must run (money moves). Each may carry `request` (a resolved HTTP request) for client-fetch tools. |
|
|
163
|
+
| `toolCalls` | Tools already executed this turn, with their grounded results. |
|
|
164
|
+
| `executedHandoffs` | Hand-offs the SDK auto-completed (observability). |
|
|
165
|
+
| `escalated` / `policy` | The turn was routed for human review, and by which rule. |
|
|
166
|
+
| `usage` / `billing` | Token usage and billing for the turn. |
|
|
167
|
+
|
|
168
|
+
## Docs
|
|
169
|
+
|
|
170
|
+
Full platform documentation - agents, policies, customer tiers, WhatsApp channel, endpoint auth options, streaming: **https://console.twelveai.app/docs**
|
|
171
|
+
|
|
172
|
+
AI coding agents: a machine-readable integration guide lives at **https://console.twelveai.app/llms.txt**.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
TwelveAI: () => TwelveAI,
|
|
24
|
+
createJwksVerifier: () => createJwksVerifier,
|
|
25
|
+
verifySignedRequest: () => verifySignedRequest,
|
|
26
|
+
verifyWebhook: () => verifyWebhook
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/client.ts
|
|
31
|
+
var DEFAULT_BASE_URL = "https://ai.twelveai.app";
|
|
32
|
+
var TwelveAI = class {
|
|
33
|
+
apiKey;
|
|
34
|
+
baseUrl;
|
|
35
|
+
fetchImpl;
|
|
36
|
+
clientAuth;
|
|
37
|
+
maxHandoffRounds;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
if (!options?.apiKey) throw new Error("TwelveAI: apiKey is required.");
|
|
40
|
+
this.apiKey = options.apiKey;
|
|
41
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
42
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
43
|
+
this.clientAuth = options.clientAuth;
|
|
44
|
+
this.maxHandoffRounds = options.maxHandoffRounds ?? 3;
|
|
45
|
+
if (typeof this.fetchImpl !== "function") {
|
|
46
|
+
throw new Error("TwelveAI: global fetch is unavailable - pass options.fetch (Node 18+ has fetch built in).");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Send one chat turn and return the completed result. Read hand-offs are
|
|
51
|
+
* auto-executed (see class docs); pass `autoExecute: false` to get the raw
|
|
52
|
+
* paused response instead, or `onHandoff` to execute them yourself.
|
|
53
|
+
*/
|
|
54
|
+
async chat(input, opts = {}) {
|
|
55
|
+
let res = await this.post("/v1/chat", this.chatBody(input));
|
|
56
|
+
if (opts.autoExecute === false) return res;
|
|
57
|
+
return this.completeHandoffs(res, input, opts.onHandoff);
|
|
58
|
+
}
|
|
59
|
+
/** Resume a paused turn with tool results your system produced. */
|
|
60
|
+
async resume(continuation, toolResults, input = {}) {
|
|
61
|
+
return this.post("/v1/chat", { ...this.chatBody(input), continuation, toolResults });
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Approve a pending action (after your PIN/OTP step) - resends the turn with
|
|
65
|
+
* `confirmed: true` so the engine proceeds.
|
|
66
|
+
*/
|
|
67
|
+
async confirm(continuation, input = {}) {
|
|
68
|
+
return this.chat({ message: "yes", ...input, continuation, confirmed: true });
|
|
69
|
+
}
|
|
70
|
+
/* ------------------------------ internals ------------------------------ */
|
|
71
|
+
chatBody(input) {
|
|
72
|
+
const body = {};
|
|
73
|
+
if (input.message !== void 0) body.message = input.message;
|
|
74
|
+
if (input.customerId !== void 0) body.customerId = input.customerId;
|
|
75
|
+
if (input.continuation) body.continuation = input.continuation;
|
|
76
|
+
if (input.confirmed !== void 0) body.confirmed = input.confirmed;
|
|
77
|
+
if (input.sandbox !== void 0) body.sandbox = input.sandbox;
|
|
78
|
+
if (input.channel) body.channel = input.channel;
|
|
79
|
+
if (input.intent) body.intent = input.intent;
|
|
80
|
+
if (input.tier) body.tier = input.tier;
|
|
81
|
+
if (input.customerToken) body.customerToken = input.customerToken;
|
|
82
|
+
if (input.attachments?.length) body.attachments = input.attachments;
|
|
83
|
+
if (input.metadata) body.metadata = input.metadata;
|
|
84
|
+
return body;
|
|
85
|
+
}
|
|
86
|
+
async post(path, body) {
|
|
87
|
+
try {
|
|
88
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "content-type": "application/json", "x-api-key": this.apiKey },
|
|
91
|
+
body: JSON.stringify(body)
|
|
92
|
+
});
|
|
93
|
+
const data = await res.json().catch(() => ({}));
|
|
94
|
+
return {
|
|
95
|
+
ok: res.ok && data.ok !== false,
|
|
96
|
+
message: data.message ?? null,
|
|
97
|
+
intent: data.intent ?? null,
|
|
98
|
+
toolCalls: data.toolCalls ?? [],
|
|
99
|
+
pendingConfirmation: data.pendingConfirmation ?? null,
|
|
100
|
+
pendingToolCalls: data.pendingToolCalls ?? null,
|
|
101
|
+
continuation: data.continuation ?? null,
|
|
102
|
+
...data,
|
|
103
|
+
status: res.status,
|
|
104
|
+
...res.ok ? {} : { error: data.error ?? `Engine returned ${res.status}` }
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
status: 0,
|
|
110
|
+
message: null,
|
|
111
|
+
intent: null,
|
|
112
|
+
toolCalls: [],
|
|
113
|
+
pendingConfirmation: null,
|
|
114
|
+
pendingToolCalls: null,
|
|
115
|
+
continuation: null,
|
|
116
|
+
error: error?.message || "Could not reach the engine."
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Execute client-fetch hand-offs and resume until the turn completes. Only
|
|
122
|
+
* calls that carry a resolved `request` are auto-executed - a hand-off
|
|
123
|
+
* without one (e.g. a money move awaiting your PIN flow) stops the loop and
|
|
124
|
+
* is returned to you untouched.
|
|
125
|
+
*/
|
|
126
|
+
async completeHandoffs(res, input, onHandoff) {
|
|
127
|
+
const executed = [];
|
|
128
|
+
for (let round = 0; round < this.maxHandoffRounds; round++) {
|
|
129
|
+
const calls = res.pendingToolCalls ?? [];
|
|
130
|
+
if (!calls.length || !res.continuation) break;
|
|
131
|
+
if (!calls.every((c) => c.request?.url)) break;
|
|
132
|
+
const results = [];
|
|
133
|
+
for (const call of calls) {
|
|
134
|
+
const result = onHandoff ? await onHandoff(call) : await this.performHandoff(call);
|
|
135
|
+
executed.push({
|
|
136
|
+
name: call.name,
|
|
137
|
+
method: call.request.method,
|
|
138
|
+
url: call.request.url,
|
|
139
|
+
ok: result?.ok !== false
|
|
140
|
+
});
|
|
141
|
+
results.push({ id: call.id, result });
|
|
142
|
+
}
|
|
143
|
+
const next = await this.resume(res.continuation, results, input);
|
|
144
|
+
if (next.error) break;
|
|
145
|
+
res = next;
|
|
146
|
+
}
|
|
147
|
+
if (executed.length) res.executedHandoffs = executed;
|
|
148
|
+
return res;
|
|
149
|
+
}
|
|
150
|
+
/** Default hand-off executor: perform the request with your clientAuth headers. */
|
|
151
|
+
async performHandoff(call) {
|
|
152
|
+
const req = call.request;
|
|
153
|
+
try {
|
|
154
|
+
const extra = typeof this.clientAuth === "function" ? await this.clientAuth() : this.clientAuth ?? {};
|
|
155
|
+
const res = await this.fetchImpl(req.url, {
|
|
156
|
+
method: req.method,
|
|
157
|
+
headers: { "content-type": "application/json", ...req.headers ?? {}, ...extra },
|
|
158
|
+
...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
|
|
159
|
+
});
|
|
160
|
+
const body = await res.json().catch(() => ({}));
|
|
161
|
+
if (!res.ok) return { ok: false, error: `Endpoint returned ${res.status}` };
|
|
162
|
+
return { ok: true, data: body.data ?? body };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return { ok: false, error: error?.message || "Request failed." };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// src/verify.ts
|
|
170
|
+
var import_node_crypto = require("crypto");
|
|
171
|
+
function safeEqual(a, b) {
|
|
172
|
+
const ba = Buffer.from(a);
|
|
173
|
+
const bb = Buffer.from(b);
|
|
174
|
+
return ba.length === bb.length && (0, import_node_crypto.timingSafeEqual)(ba, bb);
|
|
175
|
+
}
|
|
176
|
+
function verifyWebhook(secret, headers) {
|
|
177
|
+
const got = headers["x-webhook-secret"];
|
|
178
|
+
return typeof got === "string" && !!secret && safeEqual(got, secret);
|
|
179
|
+
}
|
|
180
|
+
function verifySignedRequest(secret, input) {
|
|
181
|
+
const ts = header(input.headers, "x-engine-timestamp");
|
|
182
|
+
const sig = header(input.headers, "x-engine-signature");
|
|
183
|
+
if (!secret || !ts || !sig) return false;
|
|
184
|
+
const tolerance = input.toleranceSeconds ?? 300;
|
|
185
|
+
const age = Math.abs(Date.now() / 1e3 - Number(ts));
|
|
186
|
+
if (!Number.isFinite(age) || age > tolerance) return false;
|
|
187
|
+
const payload = `${ts}.${input.method.toUpperCase()}.${input.path}.${input.rawBody ?? ""}`;
|
|
188
|
+
const expected = "sha256=" + (0, import_node_crypto.createHmac)("sha256", secret).update(payload, "utf8").digest("hex");
|
|
189
|
+
return safeEqual(expected, sig);
|
|
190
|
+
}
|
|
191
|
+
function createJwksVerifier(options = {}) {
|
|
192
|
+
const jwksUrl = options.jwksUrl ?? "https://ai.twelveai.app/.well-known/jwks.json";
|
|
193
|
+
const cacheMs = (options.cacheSeconds ?? 300) * 1e3;
|
|
194
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
195
|
+
let cache = null;
|
|
196
|
+
async function keys() {
|
|
197
|
+
if (cache && Date.now() - cache.fetchedAt < cacheMs) return cache.keys;
|
|
198
|
+
const res = await fetchImpl(jwksUrl);
|
|
199
|
+
if (!res.ok) throw new Error(`JWKS fetch failed (${res.status})`);
|
|
200
|
+
const doc = await res.json();
|
|
201
|
+
cache = { keys: doc.keys ?? [], fetchedAt: Date.now() };
|
|
202
|
+
return cache.keys;
|
|
203
|
+
}
|
|
204
|
+
return async function verify(input) {
|
|
205
|
+
try {
|
|
206
|
+
const jwt = (input.authorization ?? "").replace(/^Bearer\s+/i, "").trim();
|
|
207
|
+
const parts = jwt.split(".");
|
|
208
|
+
if (parts.length !== 3) return { ok: false, reason: "malformed token" };
|
|
209
|
+
const [h, p, s] = parts;
|
|
210
|
+
const head = JSON.parse(Buffer.from(h, "base64url").toString());
|
|
211
|
+
if (head.alg !== "ES256") return { ok: false, reason: `unsupported alg ${head.alg}` };
|
|
212
|
+
const jwk = (await keys()).find((k) => k.kid === head.kid);
|
|
213
|
+
if (!jwk) return { ok: false, reason: "unknown key id" };
|
|
214
|
+
const pub = (0, import_node_crypto.createPublicKey)({ key: jwk, format: "jwk" });
|
|
215
|
+
const v = (0, import_node_crypto.createVerify)("SHA256");
|
|
216
|
+
v.update(`${h}.${p}`);
|
|
217
|
+
if (!v.verify({ key: pub, dsaEncoding: "ieee-p1363" }, Buffer.from(s, "base64url"))) {
|
|
218
|
+
return { ok: false, reason: "bad signature" };
|
|
219
|
+
}
|
|
220
|
+
const claims = JSON.parse(Buffer.from(p, "base64url").toString());
|
|
221
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
222
|
+
if (typeof claims.exp !== "number" || claims.exp < now) return { ok: false, reason: "expired" };
|
|
223
|
+
if (typeof claims.htm === "string" && claims.htm !== input.method.toUpperCase()) return { ok: false, reason: "method mismatch" };
|
|
224
|
+
if (input.url && typeof claims.htu === "string" && claims.htu !== input.url) return { ok: false, reason: "url mismatch" };
|
|
225
|
+
if (input.rawBody !== void 0 && typeof claims.bh === "string") {
|
|
226
|
+
const bh = (0, import_node_crypto.createHash)("sha256").update(input.rawBody ?? "", "utf8").digest("hex");
|
|
227
|
+
if (bh !== claims.bh) return { ok: false, reason: "body hash mismatch" };
|
|
228
|
+
}
|
|
229
|
+
return { ok: true, claims };
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return { ok: false, reason: error?.message || "verification failed" };
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function header(headers, name) {
|
|
236
|
+
const v = headers[name] ?? headers[name.toLowerCase()];
|
|
237
|
+
if (Array.isArray(v)) return v[0] ?? null;
|
|
238
|
+
return typeof v === "string" ? v : null;
|
|
239
|
+
}
|
|
240
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
241
|
+
0 && (module.exports = {
|
|
242
|
+
TwelveAI,
|
|
243
|
+
createJwksVerifier,
|
|
244
|
+
verifySignedRequest,
|
|
245
|
+
verifyWebhook
|
|
246
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/** Shared request/response types for the TwelveAI chat API. */
|
|
2
|
+
interface TwelveAIOptions {
|
|
3
|
+
/** Your workspace API key (sk_live_... / sk_test_...). */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** Engine base URL. Defaults to the hosted platform. */
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Auth your server adds when the SDK performs client-fetch hand-offs against
|
|
9
|
+
* YOUR OWN API (the engine hands the resolved request back without any
|
|
10
|
+
* credentials). Static headers, or a function returning them per request.
|
|
11
|
+
*/
|
|
12
|
+
clientAuth?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
13
|
+
/** Custom fetch implementation (tests, polyfills). Defaults to global fetch. */
|
|
14
|
+
fetch?: typeof globalThis.fetch;
|
|
15
|
+
/** Max auto-resume rounds for client-fetch hand-offs per chat() call. Default 3. */
|
|
16
|
+
maxHandoffRounds?: number;
|
|
17
|
+
}
|
|
18
|
+
interface Attachment {
|
|
19
|
+
/** Public URL, or a data: URL. */
|
|
20
|
+
url?: string;
|
|
21
|
+
/** Base64 content (alternative to url). */
|
|
22
|
+
data?: string;
|
|
23
|
+
mediaType?: string;
|
|
24
|
+
/** A WhatsApp media id, if the engine should fetch it via your connected WhatsApp. */
|
|
25
|
+
whatsappMediaId?: string;
|
|
26
|
+
}
|
|
27
|
+
interface ChatInput {
|
|
28
|
+
/** The end user's message. Optional when resuming or sending only attachments. */
|
|
29
|
+
message?: string;
|
|
30
|
+
/** Your id for this end user (alias: userId). Required for customer-scoped agents. */
|
|
31
|
+
customerId?: string;
|
|
32
|
+
/** Continue an existing conversation with the token from the previous turn. */
|
|
33
|
+
continuation?: string;
|
|
34
|
+
/** The user approved a pending action (PIN/OTP collected on your side). */
|
|
35
|
+
confirmed?: boolean;
|
|
36
|
+
/** Sandbox: tools return sample data; nothing real is called or moved. */
|
|
37
|
+
sandbox?: boolean;
|
|
38
|
+
channel?: string;
|
|
39
|
+
/** Force a specific agent instead of routing. */
|
|
40
|
+
intent?: string;
|
|
41
|
+
/** Customer tier to sync for this turn's caps. */
|
|
42
|
+
tier?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Short-lived end-user session token, forwarded verbatim on tool bindings
|
|
45
|
+
* with auth type 'customer_token'. Never stored by the engine.
|
|
46
|
+
*/
|
|
47
|
+
customerToken?: string;
|
|
48
|
+
/** Images / voice notes attached to this turn. */
|
|
49
|
+
attachments?: Attachment[];
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
interface ToolCall {
|
|
53
|
+
name: string;
|
|
54
|
+
arguments: Record<string, unknown>;
|
|
55
|
+
result: unknown;
|
|
56
|
+
}
|
|
57
|
+
interface HandoffRequest {
|
|
58
|
+
method: string;
|
|
59
|
+
url: string;
|
|
60
|
+
headers?: Record<string, string>;
|
|
61
|
+
body?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
interface PendingToolCall {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
arguments: Record<string, unknown>;
|
|
67
|
+
/** Client-fetch hand-off: the resolved request your server should perform. */
|
|
68
|
+
request?: HandoffRequest;
|
|
69
|
+
}
|
|
70
|
+
interface ToolResult {
|
|
71
|
+
id: string;
|
|
72
|
+
result: unknown;
|
|
73
|
+
}
|
|
74
|
+
interface ChatResponse {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
sandbox?: boolean;
|
|
77
|
+
/** The assistant's reply to show the user. */
|
|
78
|
+
message: string | null;
|
|
79
|
+
intent: string | null;
|
|
80
|
+
toolCalls: ToolCall[];
|
|
81
|
+
/** A write awaiting the user's confirmation (resend with confirmed: true). */
|
|
82
|
+
pendingConfirmation: {
|
|
83
|
+
tool: string;
|
|
84
|
+
arguments: Record<string, unknown>;
|
|
85
|
+
} | null;
|
|
86
|
+
/**
|
|
87
|
+
* Client-executed tool calls YOUR system must run. With auto-execution on
|
|
88
|
+
* (the default), read hand-offs are already completed by the SDK; anything
|
|
89
|
+
* left here is yours to handle (typically a money move awaiting PIN).
|
|
90
|
+
*/
|
|
91
|
+
pendingToolCalls: PendingToolCall[] | null;
|
|
92
|
+
/** Pass this back on the next turn to continue the conversation. */
|
|
93
|
+
continuation: string | null;
|
|
94
|
+
escalated?: boolean;
|
|
95
|
+
policy?: string | null;
|
|
96
|
+
autonomy?: string | null;
|
|
97
|
+
fee?: {
|
|
98
|
+
feeNgn: number;
|
|
99
|
+
amountNgn: number;
|
|
100
|
+
totalNgn: number;
|
|
101
|
+
} | null;
|
|
102
|
+
usage?: {
|
|
103
|
+
inputTokens: number;
|
|
104
|
+
outputTokens: number;
|
|
105
|
+
};
|
|
106
|
+
billing?: Record<string, unknown>;
|
|
107
|
+
latencyMs?: number;
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
error?: string;
|
|
110
|
+
/** HTTP status of the underlying call. */
|
|
111
|
+
status: number;
|
|
112
|
+
/** Hand-offs the SDK auto-executed to complete this turn (for observability). */
|
|
113
|
+
executedHandoffs?: Array<{
|
|
114
|
+
name: string;
|
|
115
|
+
method: string;
|
|
116
|
+
url: string;
|
|
117
|
+
ok: boolean;
|
|
118
|
+
}>;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Executor for one client-fetch hand-off. Return the tool result the engine
|
|
122
|
+
* resumes with. The default executor performs `call.request` with your
|
|
123
|
+
* `clientAuth` headers and returns `{ ok, data }`.
|
|
124
|
+
*/
|
|
125
|
+
type HandoffExecutor = (call: PendingToolCall) => Promise<unknown>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The TwelveAI client. One call does the whole conversation protocol:
|
|
129
|
+
*
|
|
130
|
+
* const twelve = new TwelveAI({ apiKey: process.env.TWELVE_API_KEY! })
|
|
131
|
+
* const res = await twelve.chat({ message: "what's my balance?", customerId: 'cus_123' })
|
|
132
|
+
* console.log(res.message)
|
|
133
|
+
*
|
|
134
|
+
* Client-fetch hand-offs (tools configured as "my app calls it") are executed
|
|
135
|
+
* automatically against your own API - the engine never holds your credentials;
|
|
136
|
+
* you supply them once via `clientAuth` and the SDK completes the loop. Money
|
|
137
|
+
* moves are never auto-executed: they surface in `pendingToolCalls` /
|
|
138
|
+
* `pendingConfirmation` for your PIN flow, then you call `confirm()` or
|
|
139
|
+
* `resume()`.
|
|
140
|
+
*/
|
|
141
|
+
declare class TwelveAI {
|
|
142
|
+
private readonly apiKey;
|
|
143
|
+
private readonly baseUrl;
|
|
144
|
+
private readonly fetchImpl;
|
|
145
|
+
private readonly clientAuth;
|
|
146
|
+
private readonly maxHandoffRounds;
|
|
147
|
+
constructor(options: TwelveAIOptions);
|
|
148
|
+
/**
|
|
149
|
+
* Send one chat turn and return the completed result. Read hand-offs are
|
|
150
|
+
* auto-executed (see class docs); pass `autoExecute: false` to get the raw
|
|
151
|
+
* paused response instead, or `onHandoff` to execute them yourself.
|
|
152
|
+
*/
|
|
153
|
+
chat(input: ChatInput, opts?: {
|
|
154
|
+
autoExecute?: boolean;
|
|
155
|
+
onHandoff?: HandoffExecutor;
|
|
156
|
+
}): Promise<ChatResponse>;
|
|
157
|
+
/** Resume a paused turn with tool results your system produced. */
|
|
158
|
+
resume(continuation: string, toolResults: ToolResult[], input?: Pick<ChatInput, 'customerId' | 'channel' | 'sandbox' | 'customerToken'>): Promise<ChatResponse>;
|
|
159
|
+
/**
|
|
160
|
+
* Approve a pending action (after your PIN/OTP step) - resends the turn with
|
|
161
|
+
* `confirmed: true` so the engine proceeds.
|
|
162
|
+
*/
|
|
163
|
+
confirm(continuation: string, input?: Omit<ChatInput, 'continuation' | 'confirmed'>): Promise<ChatResponse>;
|
|
164
|
+
private chatBody;
|
|
165
|
+
private post;
|
|
166
|
+
/**
|
|
167
|
+
* Execute client-fetch hand-offs and resume until the turn completes. Only
|
|
168
|
+
* calls that carry a resolved `request` are auto-executed - a hand-off
|
|
169
|
+
* without one (e.g. a money move awaiting your PIN flow) stops the loop and
|
|
170
|
+
* is returned to you untouched.
|
|
171
|
+
*/
|
|
172
|
+
private completeHandoffs;
|
|
173
|
+
/** Default hand-off executor: perform the request with your clientAuth headers. */
|
|
174
|
+
private performHandoff;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Verify a TwelveAI webhook delivery: the x-webhook-secret header must match your secret. */
|
|
178
|
+
declare function verifyWebhook(secret: string, headers: Record<string, string | string[] | undefined>): boolean;
|
|
179
|
+
interface SignedRequestInput {
|
|
180
|
+
/** HTTP method of the incoming request. */
|
|
181
|
+
method: string;
|
|
182
|
+
/** Path + query as received, e.g. '/ai/balance?x=1' (Express: req.originalUrl). */
|
|
183
|
+
path: string;
|
|
184
|
+
/** The RAW request body string ('' for none). Must be the exact bytes received. */
|
|
185
|
+
rawBody: string;
|
|
186
|
+
/** Incoming headers (x-engine-timestamp / x-engine-signature). */
|
|
187
|
+
headers: Record<string, string | string[] | undefined>;
|
|
188
|
+
/** Allowed clock skew in seconds. Default 300. */
|
|
189
|
+
toleranceSeconds?: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Verify an HMAC-signed tool call (auth type 'signed'): recomputes
|
|
193
|
+
* HMAC-SHA256(secret, `${ts}.${METHOD}.${path}.${rawBody}`) and compares it to
|
|
194
|
+
* the X-Engine-Signature header inside the timestamp tolerance window.
|
|
195
|
+
*/
|
|
196
|
+
declare function verifySignedRequest(secret: string, input: SignedRequestInput): boolean;
|
|
197
|
+
interface JwksVerifierOptions {
|
|
198
|
+
/** JWKS document URL. Defaults to the hosted platform's. */
|
|
199
|
+
jwksUrl?: string;
|
|
200
|
+
/** How long to cache the JWKS, in seconds. Default 300. */
|
|
201
|
+
cacheSeconds?: number;
|
|
202
|
+
fetch?: typeof globalThis.fetch;
|
|
203
|
+
}
|
|
204
|
+
interface JwtRequestInput {
|
|
205
|
+
/** The Authorization header value ('Bearer <jwt>') or the raw JWT. */
|
|
206
|
+
authorization: string;
|
|
207
|
+
/** HTTP method of the incoming request (checked against the htm claim). */
|
|
208
|
+
method: string;
|
|
209
|
+
/** Full URL as the engine called it (checked against the htu claim). Optional. */
|
|
210
|
+
url?: string;
|
|
211
|
+
/** The RAW request body ('' for none) - checked against the bh (body hash) claim. */
|
|
212
|
+
rawBody?: string;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Build a verifier for JWKS-signed tool calls (auth type 'jwks'). Fetches and
|
|
216
|
+
* caches the platform's public keys; each call verifies the ES256 signature,
|
|
217
|
+
* the expiry, and that the token is bound to THIS request (method, URL, body
|
|
218
|
+
* hash) - so a captured token cannot be replayed against another request.
|
|
219
|
+
*
|
|
220
|
+
* const verify = createJwksVerifier()
|
|
221
|
+
* const v = await verify({ authorization: req.get('authorization'), method: req.method, rawBody })
|
|
222
|
+
* if (!v.ok) return res.status(401).end()
|
|
223
|
+
*/
|
|
224
|
+
declare function createJwksVerifier(options?: JwksVerifierOptions): (input: JwtRequestInput) => Promise<{
|
|
225
|
+
ok: boolean;
|
|
226
|
+
reason?: string;
|
|
227
|
+
claims?: Record<string, unknown>;
|
|
228
|
+
}>;
|
|
229
|
+
|
|
230
|
+
export { type Attachment, type ChatInput, type ChatResponse, type HandoffExecutor, type HandoffRequest, type JwksVerifierOptions, type JwtRequestInput, type PendingToolCall, type SignedRequestInput, type ToolCall, type ToolResult, TwelveAI, type TwelveAIOptions, createJwksVerifier, verifySignedRequest, verifyWebhook };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/** Shared request/response types for the TwelveAI chat API. */
|
|
2
|
+
interface TwelveAIOptions {
|
|
3
|
+
/** Your workspace API key (sk_live_... / sk_test_...). */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** Engine base URL. Defaults to the hosted platform. */
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Auth your server adds when the SDK performs client-fetch hand-offs against
|
|
9
|
+
* YOUR OWN API (the engine hands the resolved request back without any
|
|
10
|
+
* credentials). Static headers, or a function returning them per request.
|
|
11
|
+
*/
|
|
12
|
+
clientAuth?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
13
|
+
/** Custom fetch implementation (tests, polyfills). Defaults to global fetch. */
|
|
14
|
+
fetch?: typeof globalThis.fetch;
|
|
15
|
+
/** Max auto-resume rounds for client-fetch hand-offs per chat() call. Default 3. */
|
|
16
|
+
maxHandoffRounds?: number;
|
|
17
|
+
}
|
|
18
|
+
interface Attachment {
|
|
19
|
+
/** Public URL, or a data: URL. */
|
|
20
|
+
url?: string;
|
|
21
|
+
/** Base64 content (alternative to url). */
|
|
22
|
+
data?: string;
|
|
23
|
+
mediaType?: string;
|
|
24
|
+
/** A WhatsApp media id, if the engine should fetch it via your connected WhatsApp. */
|
|
25
|
+
whatsappMediaId?: string;
|
|
26
|
+
}
|
|
27
|
+
interface ChatInput {
|
|
28
|
+
/** The end user's message. Optional when resuming or sending only attachments. */
|
|
29
|
+
message?: string;
|
|
30
|
+
/** Your id for this end user (alias: userId). Required for customer-scoped agents. */
|
|
31
|
+
customerId?: string;
|
|
32
|
+
/** Continue an existing conversation with the token from the previous turn. */
|
|
33
|
+
continuation?: string;
|
|
34
|
+
/** The user approved a pending action (PIN/OTP collected on your side). */
|
|
35
|
+
confirmed?: boolean;
|
|
36
|
+
/** Sandbox: tools return sample data; nothing real is called or moved. */
|
|
37
|
+
sandbox?: boolean;
|
|
38
|
+
channel?: string;
|
|
39
|
+
/** Force a specific agent instead of routing. */
|
|
40
|
+
intent?: string;
|
|
41
|
+
/** Customer tier to sync for this turn's caps. */
|
|
42
|
+
tier?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Short-lived end-user session token, forwarded verbatim on tool bindings
|
|
45
|
+
* with auth type 'customer_token'. Never stored by the engine.
|
|
46
|
+
*/
|
|
47
|
+
customerToken?: string;
|
|
48
|
+
/** Images / voice notes attached to this turn. */
|
|
49
|
+
attachments?: Attachment[];
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
interface ToolCall {
|
|
53
|
+
name: string;
|
|
54
|
+
arguments: Record<string, unknown>;
|
|
55
|
+
result: unknown;
|
|
56
|
+
}
|
|
57
|
+
interface HandoffRequest {
|
|
58
|
+
method: string;
|
|
59
|
+
url: string;
|
|
60
|
+
headers?: Record<string, string>;
|
|
61
|
+
body?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
interface PendingToolCall {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
arguments: Record<string, unknown>;
|
|
67
|
+
/** Client-fetch hand-off: the resolved request your server should perform. */
|
|
68
|
+
request?: HandoffRequest;
|
|
69
|
+
}
|
|
70
|
+
interface ToolResult {
|
|
71
|
+
id: string;
|
|
72
|
+
result: unknown;
|
|
73
|
+
}
|
|
74
|
+
interface ChatResponse {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
sandbox?: boolean;
|
|
77
|
+
/** The assistant's reply to show the user. */
|
|
78
|
+
message: string | null;
|
|
79
|
+
intent: string | null;
|
|
80
|
+
toolCalls: ToolCall[];
|
|
81
|
+
/** A write awaiting the user's confirmation (resend with confirmed: true). */
|
|
82
|
+
pendingConfirmation: {
|
|
83
|
+
tool: string;
|
|
84
|
+
arguments: Record<string, unknown>;
|
|
85
|
+
} | null;
|
|
86
|
+
/**
|
|
87
|
+
* Client-executed tool calls YOUR system must run. With auto-execution on
|
|
88
|
+
* (the default), read hand-offs are already completed by the SDK; anything
|
|
89
|
+
* left here is yours to handle (typically a money move awaiting PIN).
|
|
90
|
+
*/
|
|
91
|
+
pendingToolCalls: PendingToolCall[] | null;
|
|
92
|
+
/** Pass this back on the next turn to continue the conversation. */
|
|
93
|
+
continuation: string | null;
|
|
94
|
+
escalated?: boolean;
|
|
95
|
+
policy?: string | null;
|
|
96
|
+
autonomy?: string | null;
|
|
97
|
+
fee?: {
|
|
98
|
+
feeNgn: number;
|
|
99
|
+
amountNgn: number;
|
|
100
|
+
totalNgn: number;
|
|
101
|
+
} | null;
|
|
102
|
+
usage?: {
|
|
103
|
+
inputTokens: number;
|
|
104
|
+
outputTokens: number;
|
|
105
|
+
};
|
|
106
|
+
billing?: Record<string, unknown>;
|
|
107
|
+
latencyMs?: number;
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
error?: string;
|
|
110
|
+
/** HTTP status of the underlying call. */
|
|
111
|
+
status: number;
|
|
112
|
+
/** Hand-offs the SDK auto-executed to complete this turn (for observability). */
|
|
113
|
+
executedHandoffs?: Array<{
|
|
114
|
+
name: string;
|
|
115
|
+
method: string;
|
|
116
|
+
url: string;
|
|
117
|
+
ok: boolean;
|
|
118
|
+
}>;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Executor for one client-fetch hand-off. Return the tool result the engine
|
|
122
|
+
* resumes with. The default executor performs `call.request` with your
|
|
123
|
+
* `clientAuth` headers and returns `{ ok, data }`.
|
|
124
|
+
*/
|
|
125
|
+
type HandoffExecutor = (call: PendingToolCall) => Promise<unknown>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The TwelveAI client. One call does the whole conversation protocol:
|
|
129
|
+
*
|
|
130
|
+
* const twelve = new TwelveAI({ apiKey: process.env.TWELVE_API_KEY! })
|
|
131
|
+
* const res = await twelve.chat({ message: "what's my balance?", customerId: 'cus_123' })
|
|
132
|
+
* console.log(res.message)
|
|
133
|
+
*
|
|
134
|
+
* Client-fetch hand-offs (tools configured as "my app calls it") are executed
|
|
135
|
+
* automatically against your own API - the engine never holds your credentials;
|
|
136
|
+
* you supply them once via `clientAuth` and the SDK completes the loop. Money
|
|
137
|
+
* moves are never auto-executed: they surface in `pendingToolCalls` /
|
|
138
|
+
* `pendingConfirmation` for your PIN flow, then you call `confirm()` or
|
|
139
|
+
* `resume()`.
|
|
140
|
+
*/
|
|
141
|
+
declare class TwelveAI {
|
|
142
|
+
private readonly apiKey;
|
|
143
|
+
private readonly baseUrl;
|
|
144
|
+
private readonly fetchImpl;
|
|
145
|
+
private readonly clientAuth;
|
|
146
|
+
private readonly maxHandoffRounds;
|
|
147
|
+
constructor(options: TwelveAIOptions);
|
|
148
|
+
/**
|
|
149
|
+
* Send one chat turn and return the completed result. Read hand-offs are
|
|
150
|
+
* auto-executed (see class docs); pass `autoExecute: false` to get the raw
|
|
151
|
+
* paused response instead, or `onHandoff` to execute them yourself.
|
|
152
|
+
*/
|
|
153
|
+
chat(input: ChatInput, opts?: {
|
|
154
|
+
autoExecute?: boolean;
|
|
155
|
+
onHandoff?: HandoffExecutor;
|
|
156
|
+
}): Promise<ChatResponse>;
|
|
157
|
+
/** Resume a paused turn with tool results your system produced. */
|
|
158
|
+
resume(continuation: string, toolResults: ToolResult[], input?: Pick<ChatInput, 'customerId' | 'channel' | 'sandbox' | 'customerToken'>): Promise<ChatResponse>;
|
|
159
|
+
/**
|
|
160
|
+
* Approve a pending action (after your PIN/OTP step) - resends the turn with
|
|
161
|
+
* `confirmed: true` so the engine proceeds.
|
|
162
|
+
*/
|
|
163
|
+
confirm(continuation: string, input?: Omit<ChatInput, 'continuation' | 'confirmed'>): Promise<ChatResponse>;
|
|
164
|
+
private chatBody;
|
|
165
|
+
private post;
|
|
166
|
+
/**
|
|
167
|
+
* Execute client-fetch hand-offs and resume until the turn completes. Only
|
|
168
|
+
* calls that carry a resolved `request` are auto-executed - a hand-off
|
|
169
|
+
* without one (e.g. a money move awaiting your PIN flow) stops the loop and
|
|
170
|
+
* is returned to you untouched.
|
|
171
|
+
*/
|
|
172
|
+
private completeHandoffs;
|
|
173
|
+
/** Default hand-off executor: perform the request with your clientAuth headers. */
|
|
174
|
+
private performHandoff;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Verify a TwelveAI webhook delivery: the x-webhook-secret header must match your secret. */
|
|
178
|
+
declare function verifyWebhook(secret: string, headers: Record<string, string | string[] | undefined>): boolean;
|
|
179
|
+
interface SignedRequestInput {
|
|
180
|
+
/** HTTP method of the incoming request. */
|
|
181
|
+
method: string;
|
|
182
|
+
/** Path + query as received, e.g. '/ai/balance?x=1' (Express: req.originalUrl). */
|
|
183
|
+
path: string;
|
|
184
|
+
/** The RAW request body string ('' for none). Must be the exact bytes received. */
|
|
185
|
+
rawBody: string;
|
|
186
|
+
/** Incoming headers (x-engine-timestamp / x-engine-signature). */
|
|
187
|
+
headers: Record<string, string | string[] | undefined>;
|
|
188
|
+
/** Allowed clock skew in seconds. Default 300. */
|
|
189
|
+
toleranceSeconds?: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Verify an HMAC-signed tool call (auth type 'signed'): recomputes
|
|
193
|
+
* HMAC-SHA256(secret, `${ts}.${METHOD}.${path}.${rawBody}`) and compares it to
|
|
194
|
+
* the X-Engine-Signature header inside the timestamp tolerance window.
|
|
195
|
+
*/
|
|
196
|
+
declare function verifySignedRequest(secret: string, input: SignedRequestInput): boolean;
|
|
197
|
+
interface JwksVerifierOptions {
|
|
198
|
+
/** JWKS document URL. Defaults to the hosted platform's. */
|
|
199
|
+
jwksUrl?: string;
|
|
200
|
+
/** How long to cache the JWKS, in seconds. Default 300. */
|
|
201
|
+
cacheSeconds?: number;
|
|
202
|
+
fetch?: typeof globalThis.fetch;
|
|
203
|
+
}
|
|
204
|
+
interface JwtRequestInput {
|
|
205
|
+
/** The Authorization header value ('Bearer <jwt>') or the raw JWT. */
|
|
206
|
+
authorization: string;
|
|
207
|
+
/** HTTP method of the incoming request (checked against the htm claim). */
|
|
208
|
+
method: string;
|
|
209
|
+
/** Full URL as the engine called it (checked against the htu claim). Optional. */
|
|
210
|
+
url?: string;
|
|
211
|
+
/** The RAW request body ('' for none) - checked against the bh (body hash) claim. */
|
|
212
|
+
rawBody?: string;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Build a verifier for JWKS-signed tool calls (auth type 'jwks'). Fetches and
|
|
216
|
+
* caches the platform's public keys; each call verifies the ES256 signature,
|
|
217
|
+
* the expiry, and that the token is bound to THIS request (method, URL, body
|
|
218
|
+
* hash) - so a captured token cannot be replayed against another request.
|
|
219
|
+
*
|
|
220
|
+
* const verify = createJwksVerifier()
|
|
221
|
+
* const v = await verify({ authorization: req.get('authorization'), method: req.method, rawBody })
|
|
222
|
+
* if (!v.ok) return res.status(401).end()
|
|
223
|
+
*/
|
|
224
|
+
declare function createJwksVerifier(options?: JwksVerifierOptions): (input: JwtRequestInput) => Promise<{
|
|
225
|
+
ok: boolean;
|
|
226
|
+
reason?: string;
|
|
227
|
+
claims?: Record<string, unknown>;
|
|
228
|
+
}>;
|
|
229
|
+
|
|
230
|
+
export { type Attachment, type ChatInput, type ChatResponse, type HandoffExecutor, type HandoffRequest, type JwksVerifierOptions, type JwtRequestInput, type PendingToolCall, type SignedRequestInput, type ToolCall, type ToolResult, TwelveAI, type TwelveAIOptions, createJwksVerifier, verifySignedRequest, verifyWebhook };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://ai.twelveai.app";
|
|
3
|
+
var TwelveAI = class {
|
|
4
|
+
apiKey;
|
|
5
|
+
baseUrl;
|
|
6
|
+
fetchImpl;
|
|
7
|
+
clientAuth;
|
|
8
|
+
maxHandoffRounds;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
if (!options?.apiKey) throw new Error("TwelveAI: apiKey is required.");
|
|
11
|
+
this.apiKey = options.apiKey;
|
|
12
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
13
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
14
|
+
this.clientAuth = options.clientAuth;
|
|
15
|
+
this.maxHandoffRounds = options.maxHandoffRounds ?? 3;
|
|
16
|
+
if (typeof this.fetchImpl !== "function") {
|
|
17
|
+
throw new Error("TwelveAI: global fetch is unavailable - pass options.fetch (Node 18+ has fetch built in).");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Send one chat turn and return the completed result. Read hand-offs are
|
|
22
|
+
* auto-executed (see class docs); pass `autoExecute: false` to get the raw
|
|
23
|
+
* paused response instead, or `onHandoff` to execute them yourself.
|
|
24
|
+
*/
|
|
25
|
+
async chat(input, opts = {}) {
|
|
26
|
+
let res = await this.post("/v1/chat", this.chatBody(input));
|
|
27
|
+
if (opts.autoExecute === false) return res;
|
|
28
|
+
return this.completeHandoffs(res, input, opts.onHandoff);
|
|
29
|
+
}
|
|
30
|
+
/** Resume a paused turn with tool results your system produced. */
|
|
31
|
+
async resume(continuation, toolResults, input = {}) {
|
|
32
|
+
return this.post("/v1/chat", { ...this.chatBody(input), continuation, toolResults });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Approve a pending action (after your PIN/OTP step) - resends the turn with
|
|
36
|
+
* `confirmed: true` so the engine proceeds.
|
|
37
|
+
*/
|
|
38
|
+
async confirm(continuation, input = {}) {
|
|
39
|
+
return this.chat({ message: "yes", ...input, continuation, confirmed: true });
|
|
40
|
+
}
|
|
41
|
+
/* ------------------------------ internals ------------------------------ */
|
|
42
|
+
chatBody(input) {
|
|
43
|
+
const body = {};
|
|
44
|
+
if (input.message !== void 0) body.message = input.message;
|
|
45
|
+
if (input.customerId !== void 0) body.customerId = input.customerId;
|
|
46
|
+
if (input.continuation) body.continuation = input.continuation;
|
|
47
|
+
if (input.confirmed !== void 0) body.confirmed = input.confirmed;
|
|
48
|
+
if (input.sandbox !== void 0) body.sandbox = input.sandbox;
|
|
49
|
+
if (input.channel) body.channel = input.channel;
|
|
50
|
+
if (input.intent) body.intent = input.intent;
|
|
51
|
+
if (input.tier) body.tier = input.tier;
|
|
52
|
+
if (input.customerToken) body.customerToken = input.customerToken;
|
|
53
|
+
if (input.attachments?.length) body.attachments = input.attachments;
|
|
54
|
+
if (input.metadata) body.metadata = input.metadata;
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
async post(path, body) {
|
|
58
|
+
try {
|
|
59
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "content-type": "application/json", "x-api-key": this.apiKey },
|
|
62
|
+
body: JSON.stringify(body)
|
|
63
|
+
});
|
|
64
|
+
const data = await res.json().catch(() => ({}));
|
|
65
|
+
return {
|
|
66
|
+
ok: res.ok && data.ok !== false,
|
|
67
|
+
message: data.message ?? null,
|
|
68
|
+
intent: data.intent ?? null,
|
|
69
|
+
toolCalls: data.toolCalls ?? [],
|
|
70
|
+
pendingConfirmation: data.pendingConfirmation ?? null,
|
|
71
|
+
pendingToolCalls: data.pendingToolCalls ?? null,
|
|
72
|
+
continuation: data.continuation ?? null,
|
|
73
|
+
...data,
|
|
74
|
+
status: res.status,
|
|
75
|
+
...res.ok ? {} : { error: data.error ?? `Engine returned ${res.status}` }
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
status: 0,
|
|
81
|
+
message: null,
|
|
82
|
+
intent: null,
|
|
83
|
+
toolCalls: [],
|
|
84
|
+
pendingConfirmation: null,
|
|
85
|
+
pendingToolCalls: null,
|
|
86
|
+
continuation: null,
|
|
87
|
+
error: error?.message || "Could not reach the engine."
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Execute client-fetch hand-offs and resume until the turn completes. Only
|
|
93
|
+
* calls that carry a resolved `request` are auto-executed - a hand-off
|
|
94
|
+
* without one (e.g. a money move awaiting your PIN flow) stops the loop and
|
|
95
|
+
* is returned to you untouched.
|
|
96
|
+
*/
|
|
97
|
+
async completeHandoffs(res, input, onHandoff) {
|
|
98
|
+
const executed = [];
|
|
99
|
+
for (let round = 0; round < this.maxHandoffRounds; round++) {
|
|
100
|
+
const calls = res.pendingToolCalls ?? [];
|
|
101
|
+
if (!calls.length || !res.continuation) break;
|
|
102
|
+
if (!calls.every((c) => c.request?.url)) break;
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const call of calls) {
|
|
105
|
+
const result = onHandoff ? await onHandoff(call) : await this.performHandoff(call);
|
|
106
|
+
executed.push({
|
|
107
|
+
name: call.name,
|
|
108
|
+
method: call.request.method,
|
|
109
|
+
url: call.request.url,
|
|
110
|
+
ok: result?.ok !== false
|
|
111
|
+
});
|
|
112
|
+
results.push({ id: call.id, result });
|
|
113
|
+
}
|
|
114
|
+
const next = await this.resume(res.continuation, results, input);
|
|
115
|
+
if (next.error) break;
|
|
116
|
+
res = next;
|
|
117
|
+
}
|
|
118
|
+
if (executed.length) res.executedHandoffs = executed;
|
|
119
|
+
return res;
|
|
120
|
+
}
|
|
121
|
+
/** Default hand-off executor: perform the request with your clientAuth headers. */
|
|
122
|
+
async performHandoff(call) {
|
|
123
|
+
const req = call.request;
|
|
124
|
+
try {
|
|
125
|
+
const extra = typeof this.clientAuth === "function" ? await this.clientAuth() : this.clientAuth ?? {};
|
|
126
|
+
const res = await this.fetchImpl(req.url, {
|
|
127
|
+
method: req.method,
|
|
128
|
+
headers: { "content-type": "application/json", ...req.headers ?? {}, ...extra },
|
|
129
|
+
...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
|
|
130
|
+
});
|
|
131
|
+
const body = await res.json().catch(() => ({}));
|
|
132
|
+
if (!res.ok) return { ok: false, error: `Endpoint returned ${res.status}` };
|
|
133
|
+
return { ok: true, data: body.data ?? body };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return { ok: false, error: error?.message || "Request failed." };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/verify.ts
|
|
141
|
+
import { createHash, createHmac, createPublicKey, createVerify, timingSafeEqual } from "crypto";
|
|
142
|
+
function safeEqual(a, b) {
|
|
143
|
+
const ba = Buffer.from(a);
|
|
144
|
+
const bb = Buffer.from(b);
|
|
145
|
+
return ba.length === bb.length && timingSafeEqual(ba, bb);
|
|
146
|
+
}
|
|
147
|
+
function verifyWebhook(secret, headers) {
|
|
148
|
+
const got = headers["x-webhook-secret"];
|
|
149
|
+
return typeof got === "string" && !!secret && safeEqual(got, secret);
|
|
150
|
+
}
|
|
151
|
+
function verifySignedRequest(secret, input) {
|
|
152
|
+
const ts = header(input.headers, "x-engine-timestamp");
|
|
153
|
+
const sig = header(input.headers, "x-engine-signature");
|
|
154
|
+
if (!secret || !ts || !sig) return false;
|
|
155
|
+
const tolerance = input.toleranceSeconds ?? 300;
|
|
156
|
+
const age = Math.abs(Date.now() / 1e3 - Number(ts));
|
|
157
|
+
if (!Number.isFinite(age) || age > tolerance) return false;
|
|
158
|
+
const payload = `${ts}.${input.method.toUpperCase()}.${input.path}.${input.rawBody ?? ""}`;
|
|
159
|
+
const expected = "sha256=" + createHmac("sha256", secret).update(payload, "utf8").digest("hex");
|
|
160
|
+
return safeEqual(expected, sig);
|
|
161
|
+
}
|
|
162
|
+
function createJwksVerifier(options = {}) {
|
|
163
|
+
const jwksUrl = options.jwksUrl ?? "https://ai.twelveai.app/.well-known/jwks.json";
|
|
164
|
+
const cacheMs = (options.cacheSeconds ?? 300) * 1e3;
|
|
165
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
166
|
+
let cache = null;
|
|
167
|
+
async function keys() {
|
|
168
|
+
if (cache && Date.now() - cache.fetchedAt < cacheMs) return cache.keys;
|
|
169
|
+
const res = await fetchImpl(jwksUrl);
|
|
170
|
+
if (!res.ok) throw new Error(`JWKS fetch failed (${res.status})`);
|
|
171
|
+
const doc = await res.json();
|
|
172
|
+
cache = { keys: doc.keys ?? [], fetchedAt: Date.now() };
|
|
173
|
+
return cache.keys;
|
|
174
|
+
}
|
|
175
|
+
return async function verify(input) {
|
|
176
|
+
try {
|
|
177
|
+
const jwt = (input.authorization ?? "").replace(/^Bearer\s+/i, "").trim();
|
|
178
|
+
const parts = jwt.split(".");
|
|
179
|
+
if (parts.length !== 3) return { ok: false, reason: "malformed token" };
|
|
180
|
+
const [h, p, s] = parts;
|
|
181
|
+
const head = JSON.parse(Buffer.from(h, "base64url").toString());
|
|
182
|
+
if (head.alg !== "ES256") return { ok: false, reason: `unsupported alg ${head.alg}` };
|
|
183
|
+
const jwk = (await keys()).find((k) => k.kid === head.kid);
|
|
184
|
+
if (!jwk) return { ok: false, reason: "unknown key id" };
|
|
185
|
+
const pub = createPublicKey({ key: jwk, format: "jwk" });
|
|
186
|
+
const v = createVerify("SHA256");
|
|
187
|
+
v.update(`${h}.${p}`);
|
|
188
|
+
if (!v.verify({ key: pub, dsaEncoding: "ieee-p1363" }, Buffer.from(s, "base64url"))) {
|
|
189
|
+
return { ok: false, reason: "bad signature" };
|
|
190
|
+
}
|
|
191
|
+
const claims = JSON.parse(Buffer.from(p, "base64url").toString());
|
|
192
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
193
|
+
if (typeof claims.exp !== "number" || claims.exp < now) return { ok: false, reason: "expired" };
|
|
194
|
+
if (typeof claims.htm === "string" && claims.htm !== input.method.toUpperCase()) return { ok: false, reason: "method mismatch" };
|
|
195
|
+
if (input.url && typeof claims.htu === "string" && claims.htu !== input.url) return { ok: false, reason: "url mismatch" };
|
|
196
|
+
if (input.rawBody !== void 0 && typeof claims.bh === "string") {
|
|
197
|
+
const bh = createHash("sha256").update(input.rawBody ?? "", "utf8").digest("hex");
|
|
198
|
+
if (bh !== claims.bh) return { ok: false, reason: "body hash mismatch" };
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, claims };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return { ok: false, reason: error?.message || "verification failed" };
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function header(headers, name) {
|
|
207
|
+
const v = headers[name] ?? headers[name.toLowerCase()];
|
|
208
|
+
if (Array.isArray(v)) return v[0] ?? null;
|
|
209
|
+
return typeof v === "string" ? v : null;
|
|
210
|
+
}
|
|
211
|
+
export {
|
|
212
|
+
TwelveAI,
|
|
213
|
+
createJwksVerifier,
|
|
214
|
+
verifySignedRequest,
|
|
215
|
+
verifyWebhook
|
|
216
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "twelveai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official SDK for TwelveAI - AI infrastructure for conversational banking. One call runs the whole chat protocol: routing, grounded tool calls, client-fetch hand-offs, confirmations, and request verification.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "TwelveAI",
|
|
7
|
+
"homepage": "https://github.com/Laozofficial/twelveai-sdk#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Laozofficial/twelveai-sdk.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"twelveai",
|
|
14
|
+
"conversational-banking",
|
|
15
|
+
"fintech",
|
|
16
|
+
"ai",
|
|
17
|
+
"agent",
|
|
18
|
+
"banking",
|
|
19
|
+
"chatbot"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^26.4.0",
|
|
48
|
+
"tsup": "^8.0.0",
|
|
49
|
+
"typescript": "^5.4.0",
|
|
50
|
+
"vitest": "^2.0.0"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/Laozofficial/twelveai-sdk/issues"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|