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 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
+ });
@@ -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 };
@@ -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
+ }