byourside 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 By Your Side
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,125 @@
1
+ # By Your Side Node SDK
2
+
3
+ Zero-dependency Node 18+ SDK for the By Your Side Agent Outbound-Call API. Place objective-driven AI calls, poll for results, and verify webhooks.
4
+
5
+ ## Quickstart
6
+
7
+ ```js
8
+ import { Client, ByoursideError, verifyWebhook } from './src/index.js';
9
+
10
+ const client = new Client({ apiKey: 'bys_ak_...' });
11
+ ```
12
+
13
+ ## Place a call and wait for the result
14
+
15
+ ```js
16
+ try {
17
+ // Place the call
18
+ const { callId } = await client.placeCall({
19
+ to: '+14155550123',
20
+ objective: 'Confirm the appointment for tomorrow at 2 PM and ask if they need to reschedule.',
21
+ fields: [
22
+ { name: 'confirmed', type: 'boolean' },
23
+ { name: 'new_time', type: 'string' },
24
+ ],
25
+ });
26
+
27
+ // Poll until the call reaches a terminal status (default timeout: 3 minutes)
28
+ const call = await client.waitForCall(callId, { timeoutMs: 120_000, intervalMs: 5_000 });
29
+
30
+ console.log('Status:', call.status); // completed | no_answer | voicemail | declined | failed
31
+ console.log('Extracted fields:', call.extracted);
32
+ } catch (err) {
33
+ if (err instanceof ByoursideError) {
34
+ console.error(`[${err.code}] ${err.message}`);
35
+ } else {
36
+ throw err;
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## List recent calls
42
+
43
+ ```js
44
+ const calls = await client.listCalls({ limit: 20 });
45
+ calls.forEach((c) => console.log(c.callId, c.status));
46
+ ```
47
+
48
+ ## Verify a webhook (Express example)
49
+
50
+ Receive real-time call events by setting a `webhookUrl` when placing a call. The header `X-BYS-Signature` carries the signature.
51
+
52
+ ```js
53
+ import express from 'express';
54
+ import { verifyWebhook } from './src/index.js';
55
+
56
+ const app = express();
57
+
58
+ // IMPORTANT: parse the body as raw bytes so the signature can be verified.
59
+ app.post('/webhooks/bys', express.raw({ type: 'application/json' }), (req, res) => {
60
+ const sig = req.headers['x-bys-signature'];
61
+ const rawBody = req.body.toString('utf8');
62
+
63
+ if (!verifyWebhook(sig, rawBody, process.env.BYS_WEBHOOK_SECRET)) {
64
+ return res.status(400).send('Bad signature');
65
+ }
66
+
67
+ const event = JSON.parse(rawBody);
68
+ console.log('Webhook event:', event.callId, event.status);
69
+ res.status(200).send('OK');
70
+ });
71
+ ```
72
+
73
+ ## Call status values
74
+
75
+ | Status | Meaning |
76
+ |---|---|
77
+ | `queued` | Accepted, not yet dialing |
78
+ | `in_progress` | Call is active |
79
+ | `completed` | Call finished; summary + fields available |
80
+ | `no_answer` | Rang but nobody picked up |
81
+ | `voicemail` | Reached voicemail |
82
+ | `declined` | Call rejected by the recipient |
83
+ | `failed` | Carrier or trunk error |
84
+
85
+ Terminal statuses (waitForCall stops here): `completed`, `no_answer`, `voicemail`, `declined`, `failed`.
86
+
87
+ ## Error codes
88
+
89
+ `ByoursideError` instances carry `.code` (a token string) and `.status` (HTTP status, 0 for network errors).
90
+
91
+ | Code | Meaning |
92
+ |---|---|
93
+ | `destination_blocked` | That destination is not allowed (premium, IRSF, or unsupported country). |
94
+ | `invalid_number` | The destination number is invalid (use full E.164, e.g. +14155550123). |
95
+ | `to_required` | A destination number (to) is required. |
96
+ | `objective_required` | An objective for the call is required. |
97
+ | `caller_id_not_owned` | That caller ID is not a number on your account. |
98
+ | `rate_limited` | Rate limit reached. Try again shortly. |
99
+ | `over_minute_cap` | Outbound usage limit reached for now. |
100
+ | `unauthorized` | Invalid or missing API key. |
101
+ | `not_found` | No call found with that id (or it does not belong to your account). |
102
+ | `placement_failed` | The call could not be placed (carrier/trunk issue). Try again shortly. |
103
+ | `store_error` | Temporary service error. Please retry shortly. |
104
+ | `timeout` | waitForCall hit the timeout before reaching a terminal status. |
105
+ | `network_error` | Could not reach the API (network failure). |
106
+
107
+ ## Constructor options
108
+
109
+ ```js
110
+ new Client({
111
+ apiKey, // required
112
+ baseUrl, // default: 'https://api.byourside.ai'
113
+ fetchImpl, // override global fetch (useful for tests)
114
+ sleep, // override the poll sleep (useful for tests)
115
+ })
116
+ ```
117
+
118
+ ## Development
119
+
120
+ ```
121
+ node --check src/*.js # syntax check
122
+ node --test # run all tests (run from sdks/node/)
123
+ ```
124
+
125
+ No dependencies are required. Node 18+ is needed for built-in `fetch`.
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "byourside",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "By Your Side SDK: give an AI a phone (outbound objective-driven calls).",
6
+ "license": "MIT",
7
+ "author": "By Your Side",
8
+ "homepage": "https://byourside.ai/docs/agent-api",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/allexp1/voip-agent.git",
12
+ "directory": "sdks/node"
13
+ },
14
+ "keywords": ["voice", "ai", "phone", "outbound", "agent", "sdk", "voip", "calls"],
15
+ "files": ["src", "README.md", "LICENSE"],
16
+ "main": "src/index.js",
17
+ "scripts": { "test": "node --test" },
18
+ "engines": { "node": ">=18" }
19
+ }
package/src/client.js ADDED
@@ -0,0 +1,65 @@
1
+ // By Your Side SDK client. Zero deps (built-in fetch). Throws ByoursideError on any
2
+ // non-2xx or network failure. fetchImpl + sleep injectable for tests.
3
+ import { mapError, ByoursideError } from './errors.js';
4
+
5
+ const TERMINAL = new Set(['completed', 'no_answer', 'voicemail', 'declined', 'failed']);
6
+
7
+ export class Client {
8
+ constructor({ apiKey, baseUrl = 'https://api.byourside.ai', fetchImpl = globalThis.fetch, sleep } = {}) {
9
+ if (!apiKey) throw new ByoursideError('apiKey is required', 0, 'no_api_key');
10
+ this._apiKey = apiKey;
11
+ this._base = String(baseUrl).replace(/\/+$/, '');
12
+ this._fetch = fetchImpl;
13
+ this._sleep = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
14
+ }
15
+
16
+ async _request(method, path, body) {
17
+ const opts = { method, headers: { Authorization: `Bearer ${this._apiKey}` } };
18
+ if (body !== undefined && method !== 'GET') {
19
+ opts.headers['content-type'] = 'application/json';
20
+ opts.body = JSON.stringify(body);
21
+ }
22
+ let res;
23
+ try { res = await this._fetch(`${this._base}${path}`, opts); }
24
+ catch (e) { throw new ByoursideError(mapError(0, null), 0, 'network_error'); }
25
+ let data = null;
26
+ try { data = await res.json(); } catch { data = null; }
27
+ if (!res.ok) throw new ByoursideError(mapError(res.status, data), res.status, (data && data.error) || 'error');
28
+ return data;
29
+ }
30
+
31
+ placeCall({ to, objective, context, fields, webhookUrl, callerId } = {}) {
32
+ const body = { to, objective };
33
+ if (context !== undefined) body.context = context;
34
+ if (fields !== undefined) body.fields = fields;
35
+ if (webhookUrl !== undefined) body.webhookUrl = webhookUrl;
36
+ if (callerId !== undefined) body.callerId = callerId;
37
+ return this._request('POST', '/v1/agent/calls', body);
38
+ }
39
+
40
+ getCall(callId) {
41
+ return this._request('GET', `/v1/agent/calls/${encodeURIComponent(callId)}`);
42
+ }
43
+
44
+ async listCalls({ limit = 20 } = {}) {
45
+ const n = Math.min(Math.max(1, Number(limit) || 20), 100);
46
+ const data = await this._request('GET', `/v1/agent/calls?limit=${n}`);
47
+ return (data && data.calls) || [];
48
+ }
49
+
50
+ // Poll getCall until status is terminal or timeoutMs elapses. Throws ByoursideError
51
+ // code 'timeout' if still non-terminal. timeoutMs/intervalMs in milliseconds.
52
+ async waitForCall(callId, { timeoutMs = 180000, intervalMs = 3000 } = {}) {
53
+ const deadline = Date.now() + timeoutMs;
54
+ for (;;) {
55
+ const call = await this.getCall(callId);
56
+ if (call && TERMINAL.has(call.status)) return call;
57
+ if (Date.now() + intervalMs > deadline) {
58
+ throw new ByoursideError(`Call ${callId} did not finish within ${Math.round(timeoutMs / 1000)}s`, 0, 'timeout');
59
+ }
60
+ await this._sleep(intervalMs);
61
+ }
62
+ }
63
+ }
64
+
65
+ export { TERMINAL };
package/src/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ // Typed error + token->message mapping for the By Your Side SDK. Mirrors the table
2
+ // used by the MCP server. Never includes the API key.
3
+ const TOKEN_MESSAGES = {
4
+ destination_blocked: 'That destination is not allowed (premium, IRSF, or unsupported country).',
5
+ invalid_number: 'The destination number is invalid (use full E.164, e.g. +14155550123).',
6
+ to_required: 'A destination number (to) is required.',
7
+ objective_required: 'An objective for the call is required.',
8
+ caller_id_not_owned: 'That caller ID is not a number on your account.',
9
+ rate_limited: 'Rate limit reached. Try again shortly.',
10
+ over_minute_cap: 'Outbound usage limit reached for now.',
11
+ unauthorized: 'Invalid or missing API key.',
12
+ not_found: 'No call found with that id (or it does not belong to your account).',
13
+ placement_failed: 'The call could not be placed (carrier/trunk issue). Try again shortly.',
14
+ store_error: 'Temporary service error. Please retry shortly.',
15
+ };
16
+
17
+ export function mapError(status, body) {
18
+ const token = body && typeof body === 'object' ? String(body.error || '') : '';
19
+ if (token && TOKEN_MESSAGES[token]) return TOKEN_MESSAGES[token];
20
+ if (!status || status >= 500) return 'Temporary service error. Please retry shortly.';
21
+ if (token) return `Request failed (${token}).`;
22
+ return `Request failed with status ${status}.`;
23
+ }
24
+
25
+ export class ByoursideError extends Error {
26
+ constructor(message, status, code) {
27
+ super(message);
28
+ this.name = 'ByoursideError';
29
+ this.status = status;
30
+ this.code = code;
31
+ }
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { Client } from './client.js';
2
+ export { ByoursideError } from './errors.js';
3
+ export { verifyWebhook } from './webhooks.js';
@@ -0,0 +1,18 @@
1
+ // Verify a By Your Side webhook signature header. Never throws; returns boolean.
2
+ // Header: "t=<unix_seconds>,v1=<hex>"; sig = HMAC-SHA256(secret, `${t}.${rawBody}`).
3
+ import crypto from 'node:crypto';
4
+
5
+ export function verifyWebhook(signatureHeader, rawBody, secret, { toleranceSec = 300 } = {}) {
6
+ try {
7
+ if (!signatureHeader || !secret) return false;
8
+ const parts = Object.fromEntries(String(signatureHeader).split(',').map((kv) => {
9
+ const i = kv.indexOf('='); return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
10
+ }));
11
+ const t = Number(parts.t); const v1 = parts.v1;
12
+ if (!Number.isFinite(t) || !v1) return false;
13
+ if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;
14
+ const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
15
+ const a = Buffer.from(expected); const b = Buffer.from(String(v1));
16
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
17
+ } catch { return false; }
18
+ }