@thisispamela/sdk 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Pamela SDK for JavaScript/TypeScript
2
2
 
3
- Official SDK for the Pamela Voice API.
3
+ Official SDK for the Pamela Enterprise Voice API.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @pamela/sdk
8
+ npm install @thisispamela/sdk
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -13,7 +13,7 @@ npm install @pamela/sdk
13
13
  ### Basic Example
14
14
 
15
15
  ```typescript
16
- import { PamelaClient } from '@pamela/sdk';
16
+ import { PamelaClient } from '@thisispamela/sdk';
17
17
 
18
18
  const client = new PamelaClient({
19
19
  apiKey: 'pk_live_your_api_key_here',
@@ -37,7 +37,7 @@ console.log('Call status:', status.status);
37
37
  ### Webhook Verification
38
38
 
39
39
  ```typescript
40
- import { PamelaClient } from '@pamela/b2b-sdk';
40
+ import { PamelaClient } from '@thisispamela/sdk';
41
41
  import express from 'express';
42
42
 
43
43
  const app = express();
@@ -59,7 +59,240 @@ app.post('/webhooks/pamela', express.json(), (req, res) => {
59
59
  });
60
60
  ```
61
61
 
62
+ ## Getting API Keys
63
+
64
+ ### Obtaining Your API Key
65
+
66
+ API keys are created and managed through the Pamela Partner Portal or via the Partner API:
67
+
68
+ 1. **Sign up for an Enterprise subscription** (see Subscription Requirements below)
69
+ 2. **Create an API key** via one of these methods:
70
+ - Partner Portal: Log in to your account and navigate to the Enterprise panel
71
+ - Partner API: `POST /api/b2b/v1/partner/api-keys`
72
+ ```bash
73
+ curl -X POST https://api.thisispamela.com/api/b2b/v1/partner/api-keys \
74
+ -H "Authorization: Bearer YOUR_B2C_USER_TOKEN" \
75
+ -H "Content-Type: application/json" \
76
+ -d '{"project_id": "optional-project-id", "key_prefix": "pk_live_"}'
77
+ ```
78
+ 3. **Save your API key immediately** - the full key is only returned once during creation
79
+ 4. **Use the key prefix** (`pk_live_`) to identify keys in your account
80
+
81
+ ### Managing API Keys
82
+
83
+ - **List API keys**: `GET /api/b2b/v1/partner/api-keys`
84
+ - **Revoke API key**: `POST /api/b2b/v1/partner/api-keys/{key_id}/revoke`
85
+ - **Associate with projects**: Optionally link API keys to specific projects for better organization
86
+
87
+ ### API Key Format
88
+
89
+ - **Live keys**: Start with `pk_live_` (all API usage)
90
+ - **Security**: Keys are hashed in the database. Store them securely and never commit them to version control.
91
+
92
+ ## Subscription Requirements
93
+
94
+ ### Enterprise Subscription Required
95
+
96
+ **All API access requires an active Enterprise subscription.** API calls will return `403 Forbidden` if:
97
+ - No Enterprise subscription is active
98
+ - Subscription status is `past_due` and grace period has expired
99
+ - Subscription status is `canceled`
100
+
101
+ ### Grace Period
102
+
103
+ Enterprise subscriptions have a **1-week grace period** when payment fails:
104
+ - During grace period: API access is allowed, but usage is still charged
105
+ - After grace period expires: API access is blocked until payment is updated
106
+
107
+ ### Subscription Status Endpoints
108
+
109
+ Check subscription status using the Enterprise Partner API:
110
+ - `GET /api/b2b/v1/partner/subscription` - Get subscription status
111
+ - `POST /api/b2b/v1/partner/subscription/checkout` - Create checkout session
112
+ - `POST /api/b2b/v1/partner/subscription/portal` - Access Customer Portal
113
+
114
+ ## Error Handling
115
+
116
+ The SDK provides typed exceptions for all API errors:
117
+
118
+ ```typescript
119
+ import {
120
+ PamelaClient,
121
+ PamelaError,
122
+ AuthenticationError,
123
+ SubscriptionError,
124
+ RateLimitError,
125
+ ValidationError,
126
+ CallError,
127
+ } from '@thisispamela/sdk';
128
+
129
+ const client = new PamelaClient({ apiKey: 'pk_live_your_key' });
130
+
131
+ try {
132
+ const call = await client.createCall({ to: '+1234567890', task: 'Test' });
133
+ } catch (e) {
134
+ if (e instanceof AuthenticationError) {
135
+ // 401: Invalid or missing API key
136
+ console.log('Auth failed:', e.message);
137
+ console.log('Error code:', e.errorCode);
138
+ } else if (e instanceof SubscriptionError) {
139
+ // 403: Subscription inactive or expired
140
+ if (e.errorCode === 7008) {
141
+ console.log('Grace period expired - update payment method');
142
+ } else {
143
+ console.log('Subscription issue:', e.message);
144
+ }
145
+ } else if (e instanceof RateLimitError) {
146
+ // 429: Rate limit exceeded
147
+ const retryAfter = e.details?.retry_after ?? 30;
148
+ console.log(`Rate limited, retry after ${retryAfter}s`);
149
+ } else if (e instanceof ValidationError) {
150
+ // 400/422: Invalid request parameters
151
+ console.log('Invalid request:', e.message);
152
+ console.log('Details:', e.details);
153
+ } else if (e instanceof CallError) {
154
+ // Call-specific errors
155
+ console.log('Call error:', e.message);
156
+ } else if (e instanceof PamelaError) {
157
+ // All other API errors
158
+ console.log(`API error ${e.errorCode}: ${e.message}`);
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### Exception Hierarchy
164
+
165
+ All exceptions extend `PamelaError`:
166
+
167
+ ```
168
+ PamelaError (base)
169
+ ├── AuthenticationError // 401 errors
170
+ ├── SubscriptionError // 403 errors (subscription issues)
171
+ ├── RateLimitError // 429 errors
172
+ ├── ValidationError // 400/422 errors
173
+ └── CallError // Call-specific errors
174
+ ```
175
+
176
+ ### Exception Properties
177
+
178
+ All exceptions have:
179
+ - `message`: Human-readable error message
180
+ - `errorCode?`: Numeric error code (e.g., 7008 for subscription expired)
181
+ - `details?`: Object with additional context
182
+ - `statusCode?`: HTTP status code
183
+
184
+ ## Error Codes Reference
185
+
186
+ ### Authentication Errors (401)
187
+
188
+ | Code | Description |
189
+ |------|-------------|
190
+ | 1001 | API key required |
191
+ | 1002 | Invalid API key |
192
+ | 1003 | API key expired |
193
+
194
+ ### Subscription Errors (403)
195
+
196
+ | Code | Description |
197
+ |------|-------------|
198
+ | 1005 | Enterprise subscription required |
199
+ | 7008 | Subscription expired (grace period ended) |
200
+
201
+ ### Validation Errors (400)
202
+
203
+ | Code | Description |
204
+ |------|-------------|
205
+ | 2001 | Validation error |
206
+ | 2002 | Invalid phone number format |
207
+
208
+ ### Enterprise Errors (7xxx)
209
+
210
+ | Code | Description |
211
+ |------|-------------|
212
+ | 7001 | Partner not found |
213
+ | 7002 | Project not found |
214
+ | 7003 | Call not found |
215
+ | 7004 | No phone number for country |
216
+ | 7005 | Unsupported country |
217
+
218
+ ### Rate Limiting (429)
219
+
220
+ | Code | Description |
221
+ |------|-------------|
222
+ | 6001 | Rate limit exceeded |
223
+ | 6002 | Quota exceeded |
224
+
225
+ ## Usage Limits & Billing
226
+
227
+ ### Enterprise API Usage
228
+
229
+ - **Unlimited API calls** (no call count limits)
230
+ - **All API usage billed at $0.10/minute** (10 cents per minute)
231
+ - **Minimum billing: 1 minute per call** (even if call duration < 60 seconds)
232
+ - **Billing calculation**: `billed_minutes = max(ceil(duration_seconds / 60), 1)`
233
+ - **Only calls that connect** (have `started_at`) are billed
234
+
235
+ ### Usage Tracking
236
+
237
+ - Usage is tracked in `b2b_usage` collection with `type: "api_usage"` (collection name stays `b2b_usage`)
238
+ - Usage is synced to Stripe hourly (at :00 minutes)
239
+ - Stripe meter name: `stripe_minutes`
240
+ - Failed syncs are retried with exponential backoff (1s, 2s, 4s, 8s, 16s), max 5 retries
241
+
242
+ ### Billing Period
243
+
244
+ - Billing is based on calendar months (UTC midnight on 1st of each month)
245
+ - Calls are billed in the month where `started_at` occurred
246
+ - Usage sync status: `pending`, `synced`, or `failed`
247
+
248
+ ## API Methods
249
+
250
+ ### Calls
251
+
252
+ - `createCall(request)` - Create a new call
253
+ - `getCall(callId)` - Get call status and details
254
+ - `listCalls(params?)` - List calls with optional filters
255
+ - `cancelCall(callId)` - Cancel an in-progress call
256
+
257
+ ### Tools
258
+
259
+ - `registerTool(tool)` - Register a tool
260
+ - `listTools()` - List all tools
261
+ - `deleteTool(toolId)` - Delete a tool
262
+
263
+ ### Usage
264
+
265
+ - `usage.get(period?)` - Get usage statistics
266
+
267
+ **Example:**
268
+ ```typescript
269
+ // Get current month usage
270
+ const usage = await client.usage.get();
271
+
272
+ // Get usage for specific period
273
+ const janUsage = await client.usage.get("2024-01");
274
+
275
+ console.log(`Usage: ${usage.call_count} calls, ${usage.api_minutes} minutes`);
276
+ console.log(`Quota: ${usage.quota?.partner_limit || 'Unlimited'}`);
277
+ ```
278
+
279
+ **Response:**
280
+ ```typescript
281
+ {
282
+ partner_id: "partner_123",
283
+ project_id?: "project_456",
284
+ period: "2024-01",
285
+ call_count: 150,
286
+ quota?: {
287
+ partner_limit?: number,
288
+ project_limit?: number
289
+ }
290
+ }
291
+ ```
292
+
293
+ **Note:** Enterprise subscriptions have no quota limits - all usage is billed per-minute.
294
+
62
295
  ## API Reference
63
296
 
64
- See the [Pamela B2B API Documentation](https://docs.thisispamela.com/b2b) for full API reference.
297
+ See the [Pamela Enterprise API Documentation](https://docs.thisispamela.com/enterprise) for full API reference.
65
298
 
@@ -0,0 +1,45 @@
1
+ export declare class PamelaError extends Error {
2
+ errorCode?: number;
3
+ details?: Record<string, any>;
4
+ statusCode?: number;
5
+ constructor(message: string, options?: {
6
+ errorCode?: number;
7
+ details?: Record<string, any>;
8
+ statusCode?: number;
9
+ });
10
+ }
11
+ export declare class AuthenticationError extends PamelaError {
12
+ constructor(message: string, options?: {
13
+ errorCode?: number;
14
+ details?: Record<string, any>;
15
+ statusCode?: number;
16
+ });
17
+ }
18
+ export declare class SubscriptionError extends PamelaError {
19
+ constructor(message: string, options?: {
20
+ errorCode?: number;
21
+ details?: Record<string, any>;
22
+ statusCode?: number;
23
+ });
24
+ }
25
+ export declare class RateLimitError extends PamelaError {
26
+ constructor(message: string, options?: {
27
+ errorCode?: number;
28
+ details?: Record<string, any>;
29
+ statusCode?: number;
30
+ });
31
+ }
32
+ export declare class ValidationError extends PamelaError {
33
+ constructor(message: string, options?: {
34
+ errorCode?: number;
35
+ details?: Record<string, any>;
36
+ statusCode?: number;
37
+ });
38
+ }
39
+ export declare class CallError extends PamelaError {
40
+ constructor(message: string, options?: {
41
+ errorCode?: number;
42
+ details?: Record<string, any>;
43
+ statusCode?: number;
44
+ });
45
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CallError = exports.ValidationError = exports.RateLimitError = exports.SubscriptionError = exports.AuthenticationError = exports.PamelaError = void 0;
4
+ class PamelaError extends Error {
5
+ constructor(message, options) {
6
+ super(message);
7
+ this.name = 'PamelaError';
8
+ this.errorCode = options?.errorCode;
9
+ this.details = options?.details;
10
+ this.statusCode = options?.statusCode;
11
+ }
12
+ }
13
+ exports.PamelaError = PamelaError;
14
+ class AuthenticationError extends PamelaError {
15
+ constructor(message, options) {
16
+ super(message, options);
17
+ this.name = 'AuthenticationError';
18
+ }
19
+ }
20
+ exports.AuthenticationError = AuthenticationError;
21
+ class SubscriptionError extends PamelaError {
22
+ constructor(message, options) {
23
+ super(message, options);
24
+ this.name = 'SubscriptionError';
25
+ }
26
+ }
27
+ exports.SubscriptionError = SubscriptionError;
28
+ class RateLimitError extends PamelaError {
29
+ constructor(message, options) {
30
+ super(message, options);
31
+ this.name = 'RateLimitError';
32
+ }
33
+ }
34
+ exports.RateLimitError = RateLimitError;
35
+ class ValidationError extends PamelaError {
36
+ constructor(message, options) {
37
+ super(message, options);
38
+ this.name = 'ValidationError';
39
+ }
40
+ }
41
+ exports.ValidationError = ValidationError;
42
+ class CallError extends PamelaError {
43
+ constructor(message, options) {
44
+ super(message, options);
45
+ this.name = 'CallError';
46
+ }
47
+ }
48
+ exports.CallError = CallError;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Pamela B2B Voice API SDK for JavaScript/TypeScript
2
+ * Pamela Enterprise Voice API SDK for JavaScript/TypeScript
3
3
  */
4
+ import { AxiosInstance } from 'axios';
5
+ import { PamelaError, AuthenticationError, SubscriptionError, RateLimitError, ValidationError, CallError } from './errors';
4
6
  export interface PamelaClientConfig {
5
7
  apiKey: string;
6
8
  baseUrl?: string;
@@ -54,9 +56,28 @@ export interface WebhookPayload {
54
56
  timestamp: string;
55
57
  data: Record<string, any>;
56
58
  }
59
+ export declare class UsageClient {
60
+ private client;
61
+ constructor(client: AxiosInstance);
62
+ /**
63
+ * Get usage statistics for partner/project.
64
+ */
65
+ get(period?: string): Promise<{
66
+ partner_id: string;
67
+ project_id?: string;
68
+ period: string;
69
+ call_count: number;
70
+ last_updated?: string;
71
+ quota?: {
72
+ partner_limit?: number;
73
+ project_limit?: number;
74
+ };
75
+ }>;
76
+ }
57
77
  export declare class PamelaClient {
58
78
  private client;
59
79
  private apiKey;
80
+ usage: UsageClient;
60
81
  constructor(config: PamelaClientConfig);
61
82
  /**
62
83
  * Create a new call.
@@ -71,6 +92,7 @@ export declare class PamelaClient {
71
92
  */
72
93
  listCalls(params?: {
73
94
  status?: string;
95
+ status_filter?: string;
74
96
  limit?: number;
75
97
  offset?: number;
76
98
  start_date?: string;
@@ -128,3 +150,5 @@ export declare class PamelaClient {
128
150
  static verifyWebhookSignature(payload: string | object, signature: string, secret: string): boolean;
129
151
  }
130
152
  export default PamelaClient;
153
+ export { PamelaClient as Pamela };
154
+ export { PamelaError, AuthenticationError, SubscriptionError, RateLimitError, ValidationError, CallError, };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Pamela B2B Voice API SDK for JavaScript/TypeScript
3
+ * Pamela Enterprise Voice API SDK for JavaScript/TypeScript
4
4
  */
5
5
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
6
  if (k2 === undefined) k2 = k;
@@ -39,9 +39,66 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.PamelaClient = void 0;
42
+ exports.CallError = exports.ValidationError = exports.RateLimitError = exports.SubscriptionError = exports.AuthenticationError = exports.PamelaError = exports.Pamela = exports.PamelaClient = exports.UsageClient = void 0;
43
43
  const axios_1 = __importDefault(require("axios"));
44
44
  const crypto = __importStar(require("crypto"));
45
+ const errors_1 = require("./errors");
46
+ Object.defineProperty(exports, "PamelaError", { enumerable: true, get: function () { return errors_1.PamelaError; } });
47
+ Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_1.AuthenticationError; } });
48
+ Object.defineProperty(exports, "SubscriptionError", { enumerable: true, get: function () { return errors_1.SubscriptionError; } });
49
+ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_1.RateLimitError; } });
50
+ Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return errors_1.ValidationError; } });
51
+ Object.defineProperty(exports, "CallError", { enumerable: true, get: function () { return errors_1.CallError; } });
52
+ const mapAxiosError = (error, endpoint) => {
53
+ const statusCode = error.response?.status;
54
+ const data = error.response?.data;
55
+ let errorCode;
56
+ let message;
57
+ let details;
58
+ if (data && typeof data === 'object') {
59
+ const detail = data.detail;
60
+ if (detail && typeof detail === 'object') {
61
+ errorCode = detail.error_code ?? detail.error?.code;
62
+ message = detail.message ?? detail.error?.message;
63
+ details = detail.details ?? detail.error?.details;
64
+ }
65
+ else {
66
+ errorCode = data.error_code ?? data.error?.code;
67
+ message = data.message ?? data.detail;
68
+ details = data.details ?? data.error?.details;
69
+ }
70
+ }
71
+ if (!message) {
72
+ message = error.message || 'Request failed';
73
+ }
74
+ const options = { errorCode, details, statusCode };
75
+ if (statusCode === 401)
76
+ return new errors_1.AuthenticationError(message, options);
77
+ if (statusCode === 403)
78
+ return new errors_1.SubscriptionError(message, options);
79
+ if (statusCode === 429)
80
+ return new errors_1.RateLimitError(message, options);
81
+ if (statusCode === 400 || statusCode === 422)
82
+ return new errors_1.ValidationError(message, options);
83
+ if (endpoint?.startsWith('/calls')) {
84
+ return new errors_1.CallError(message, options);
85
+ }
86
+ return new errors_1.PamelaError(message, options);
87
+ };
88
+ class UsageClient {
89
+ constructor(client) {
90
+ this.client = client;
91
+ }
92
+ /**
93
+ * Get usage statistics for partner/project.
94
+ */
95
+ async get(period) {
96
+ const params = period ? { period } : {};
97
+ const response = await this.client.get('/usage', { params });
98
+ return response.data;
99
+ }
100
+ }
101
+ exports.UsageClient = UsageClient;
45
102
  class PamelaClient {
46
103
  constructor(config) {
47
104
  this.apiKey = config.apiKey;
@@ -54,6 +111,8 @@ class PamelaClient {
54
111
  },
55
112
  timeout: 30000,
56
113
  });
114
+ // Initialize usage client
115
+ this.usage = new UsageClient(this.client);
57
116
  // Add retry logic
58
117
  this.client.interceptors.response.use((response) => response, async (error) => {
59
118
  const config = error.config;
@@ -65,7 +124,7 @@ class PamelaClient {
65
124
  await new Promise(resolve => setTimeout(resolve, 1000 * config.retry));
66
125
  return this.client.request(config);
67
126
  }
68
- return Promise.reject(error);
127
+ return Promise.reject(mapAxiosError(error, config?.url));
69
128
  });
70
129
  }
71
130
  /**
@@ -86,7 +145,12 @@ class PamelaClient {
86
145
  * List calls for the authenticated partner/project.
87
146
  */
88
147
  async listCalls(params) {
89
- const response = await this.client.get('/calls', { params });
148
+ const normalizedParams = { ...params };
149
+ if (params?.status_filter || params?.status) {
150
+ normalizedParams.status_filter = params?.status_filter ?? params?.status;
151
+ delete normalizedParams.status;
152
+ }
153
+ const response = await this.client.get('/calls', { params: normalizedParams });
90
154
  // Backend returns { items: [...], total, limit, offset }
91
155
  return response.data;
92
156
  }
@@ -131,4 +195,6 @@ class PamelaClient {
131
195
  }
132
196
  }
133
197
  exports.PamelaClient = PamelaClient;
198
+ exports.Pamela = PamelaClient;
199
+ // Export as both default and named export for flexibility
134
200
  exports.default = PamelaClient;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thisispamela/sdk",
3
- "version": "1.0.0",
4
- "description": "Pamela B2B Voice API SDK for JavaScript/TypeScript",
3
+ "version": "1.0.2",
4
+ "description": "Pamela Enterprise Voice API SDK for JavaScript/TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
@@ -14,7 +14,7 @@
14
14
  "pamela",
15
15
  "voice",
16
16
  "api",
17
- "b2b",
17
+ "enterprise",
18
18
  "phone",
19
19
  "calling"
20
20
  ],
package/src/errors.ts ADDED
@@ -0,0 +1,48 @@
1
+ export class PamelaError extends Error {
2
+ public errorCode?: number;
3
+ public details?: Record<string, any>;
4
+ public statusCode?: number;
5
+
6
+ constructor(message: string, options?: { errorCode?: number; details?: Record<string, any>; statusCode?: number }) {
7
+ super(message);
8
+ this.name = 'PamelaError';
9
+ this.errorCode = options?.errorCode;
10
+ this.details = options?.details;
11
+ this.statusCode = options?.statusCode;
12
+ }
13
+ }
14
+
15
+ export class AuthenticationError extends PamelaError {
16
+ constructor(message: string, options?: { errorCode?: number; details?: Record<string, any>; statusCode?: number }) {
17
+ super(message, options);
18
+ this.name = 'AuthenticationError';
19
+ }
20
+ }
21
+
22
+ export class SubscriptionError extends PamelaError {
23
+ constructor(message: string, options?: { errorCode?: number; details?: Record<string, any>; statusCode?: number }) {
24
+ super(message, options);
25
+ this.name = 'SubscriptionError';
26
+ }
27
+ }
28
+
29
+ export class RateLimitError extends PamelaError {
30
+ constructor(message: string, options?: { errorCode?: number; details?: Record<string, any>; statusCode?: number }) {
31
+ super(message, options);
32
+ this.name = 'RateLimitError';
33
+ }
34
+ }
35
+
36
+ export class ValidationError extends PamelaError {
37
+ constructor(message: string, options?: { errorCode?: number; details?: Record<string, any>; statusCode?: number }) {
38
+ super(message, options);
39
+ this.name = 'ValidationError';
40
+ }
41
+ }
42
+
43
+ export class CallError extends PamelaError {
44
+ constructor(message: string, options?: { errorCode?: number; details?: Record<string, any>; statusCode?: number }) {
45
+ super(message, options);
46
+ this.name = 'CallError';
47
+ }
48
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,17 @@
1
1
  /**
2
- * Pamela B2B Voice API SDK for JavaScript/TypeScript
2
+ * Pamela Enterprise Voice API SDK for JavaScript/TypeScript
3
3
  */
4
4
 
5
5
  import axios, { AxiosInstance, AxiosError } from 'axios';
6
6
  import * as crypto from 'crypto';
7
+ import {
8
+ PamelaError,
9
+ AuthenticationError,
10
+ SubscriptionError,
11
+ RateLimitError,
12
+ ValidationError,
13
+ CallError,
14
+ } from './errors';
7
15
 
8
16
  export interface PamelaClientConfig {
9
17
  apiKey: string;
@@ -64,9 +72,74 @@ export interface WebhookPayload {
64
72
  data: Record<string, any>;
65
73
  }
66
74
 
75
+ const mapAxiosError = (error: AxiosError, endpoint?: string): PamelaError => {
76
+ const statusCode = error.response?.status;
77
+ const data = error.response?.data as any;
78
+ let errorCode: number | undefined;
79
+ let message: string | undefined;
80
+ let details: Record<string, any> | undefined;
81
+
82
+ if (data && typeof data === 'object') {
83
+ const detail = data.detail;
84
+ if (detail && typeof detail === 'object') {
85
+ errorCode = detail.error_code ?? detail.error?.code;
86
+ message = detail.message ?? detail.error?.message;
87
+ details = detail.details ?? detail.error?.details;
88
+ } else {
89
+ errorCode = data.error_code ?? data.error?.code;
90
+ message = data.message ?? data.detail;
91
+ details = data.details ?? data.error?.details;
92
+ }
93
+ }
94
+
95
+ if (!message) {
96
+ message = error.message || 'Request failed';
97
+ }
98
+
99
+ const options = { errorCode, details, statusCode };
100
+ if (statusCode === 401) return new AuthenticationError(message, options);
101
+ if (statusCode === 403) return new SubscriptionError(message, options);
102
+ if (statusCode === 429) return new RateLimitError(message, options);
103
+ if (statusCode === 400 || statusCode === 422) return new ValidationError(message, options);
104
+
105
+ if (endpoint?.startsWith('/calls')) {
106
+ return new CallError(message, options);
107
+ }
108
+
109
+ return new PamelaError(message, options);
110
+ };
111
+
112
+ export class UsageClient {
113
+ private client: AxiosInstance;
114
+
115
+ constructor(client: AxiosInstance) {
116
+ this.client = client;
117
+ }
118
+
119
+ /**
120
+ * Get usage statistics for partner/project.
121
+ */
122
+ async get(period?: string): Promise<{
123
+ partner_id: string;
124
+ project_id?: string;
125
+ period: string;
126
+ call_count: number;
127
+ last_updated?: string;
128
+ quota?: {
129
+ partner_limit?: number;
130
+ project_limit?: number;
131
+ };
132
+ }> {
133
+ const params = period ? { period } : {};
134
+ const response = await this.client.get('/usage', { params });
135
+ return response.data;
136
+ }
137
+ }
138
+
67
139
  export class PamelaClient {
68
140
  private client: AxiosInstance;
69
141
  private apiKey: string;
142
+ public usage: UsageClient;
70
143
 
71
144
  constructor(config: PamelaClientConfig) {
72
145
  this.apiKey = config.apiKey;
@@ -81,6 +154,9 @@ export class PamelaClient {
81
154
  timeout: 30000,
82
155
  });
83
156
 
157
+ // Initialize usage client
158
+ this.usage = new UsageClient(this.client);
159
+
84
160
  // Add retry logic
85
161
  this.client.interceptors.response.use(
86
162
  (response) => response,
@@ -96,7 +172,7 @@ export class PamelaClient {
96
172
  return this.client.request(config);
97
173
  }
98
174
 
99
- return Promise.reject(error);
175
+ return Promise.reject(mapAxiosError(error, config?.url));
100
176
  }
101
177
  );
102
178
  }
@@ -122,12 +198,18 @@ export class PamelaClient {
122
198
  */
123
199
  async listCalls(params?: {
124
200
  status?: string;
201
+ status_filter?: string;
125
202
  limit?: number;
126
203
  offset?: number;
127
204
  start_date?: string;
128
205
  end_date?: string;
129
206
  }): Promise<{ items: CallStatus[]; total: number; limit: number; offset: number }> {
130
- const response = await this.client.get('/calls', { params });
207
+ const normalizedParams = { ...params };
208
+ if (params?.status_filter || params?.status) {
209
+ normalizedParams.status_filter = params?.status_filter ?? params?.status;
210
+ delete (normalizedParams as { status?: string }).status;
211
+ }
212
+ const response = await this.client.get('/calls', { params: normalizedParams });
131
213
  // Backend returns { items: [...], total, limit, offset }
132
214
  return response.data;
133
215
  }
@@ -185,5 +267,15 @@ export class PamelaClient {
185
267
  }
186
268
  }
187
269
 
270
+ // Export as both default and named export for flexibility
188
271
  export default PamelaClient;
272
+ export { PamelaClient as Pamela };
273
+ export {
274
+ PamelaError,
275
+ AuthenticationError,
276
+ SubscriptionError,
277
+ RateLimitError,
278
+ ValidationError,
279
+ CallError,
280
+ };
189
281
 
@@ -1,136 +1,202 @@
1
1
  /**
2
- * Integration tests for Pamela JavaScript SDK.
3
- *
4
- * Tests against staging API or mocked responses.
2
+ * Tests for Pamela JavaScript/TypeScript SDK.
3
+ *
4
+ * Includes unit tests (no network) and integration tests (require env vars).
5
5
  */
6
6
 
7
- import { Pamela } from "../src/index";
7
+ import PamelaClient, {
8
+ PamelaError,
9
+ AuthenticationError,
10
+ SubscriptionError,
11
+ RateLimitError,
12
+ ValidationError,
13
+ CallError,
14
+ } from "../src/index";
15
+ import * as crypto from "crypto";
16
+
17
+ const TEST_API_URL =
18
+ process.env.PAMELA_API_URL || "https://pamela-dev.up.railway.app";
19
+ const TEST_API_KEY = process.env.PAMELA_TEST_API_KEY;
20
+ const SHOULD_RUN = Boolean(TEST_API_KEY);
21
+
22
+ // =============================================================================
23
+ // Unit Tests (No Network Required)
24
+ // =============================================================================
25
+
26
+ describe("Webhook Signature Verification", () => {
27
+ const secret = "test_secret_123";
28
+
29
+ it("verifies valid signature", () => {
30
+ const payload = { event: "call.completed", call_id: "call_123" };
31
+ const payloadStr = JSON.stringify(payload);
32
+ const signature = crypto
33
+ .createHmac("sha256", secret)
34
+ .update(payloadStr)
35
+ .digest("hex");
36
+
37
+ expect(
38
+ PamelaClient.verifyWebhookSignature(payload, signature, secret)
39
+ ).toBe(true);
40
+ });
8
41
 
9
- const TEST_API_URL = process.env.TEST_API_URL || "https://pamela-dev.up.railway.app";
10
- const TEST_API_KEY = process.env.TEST_API_KEY || "pk_test_placeholder";
42
+ it("rejects invalid signature", () => {
43
+ const payload = { event: "call.completed" };
44
+ expect(
45
+ PamelaClient.verifyWebhookSignature(payload, "invalid_sig", secret)
46
+ ).toBe(false);
47
+ });
11
48
 
12
- describe("Pamela SDK", () => {
13
- let sdk: Pamela;
49
+ it("handles string payload", () => {
50
+ const payloadStr = '{"test":"value"}';
51
+ const signature = crypto
52
+ .createHmac("sha256", secret)
53
+ .update(payloadStr)
54
+ .digest("hex");
14
55
 
15
- beforeEach(() => {
16
- sdk = new Pamela({
17
- apiKey: TEST_API_KEY,
18
- apiUrl: TEST_API_URL,
19
- });
56
+ expect(
57
+ PamelaClient.verifyWebhookSignature(payloadStr, signature, secret)
58
+ ).toBe(true);
20
59
  });
60
+ });
21
61
 
22
- describe("Initialization", () => {
23
- it("should initialize with API key", () => {
24
- expect(sdk).toBeDefined();
62
+ describe("Exception Classes", () => {
63
+ it("PamelaError has correct properties", () => {
64
+ const error = new PamelaError("Test error", {
65
+ errorCode: 1001,
66
+ details: { key: "value" },
67
+ statusCode: 403,
25
68
  });
26
69
 
27
- it("should throw error without API key", () => {
28
- expect(() => {
29
- new Pamela({ apiKey: "" });
30
- }).toThrow();
31
- });
70
+ expect(error.message).toBe("Test error");
71
+ expect(error.errorCode).toBe(1001);
72
+ expect(error.details).toEqual({ key: "value" });
73
+ expect(error.statusCode).toBe(403);
74
+ expect(error.name).toBe("PamelaError");
32
75
  });
33
76
 
34
- describe("Call Creation", () => {
35
- it("should create a call with required parameters", async () => {
36
- // Mock or use staging API
37
- const call = await sdk.calls.create({
38
- to: "+1234567890",
39
- from_: "+1987654321",
40
- country: "US",
41
- });
77
+ it("AuthenticationError has correct name", () => {
78
+ const error = new AuthenticationError("Invalid API key");
79
+ expect(error.name).toBe("AuthenticationError");
80
+ expect(error instanceof PamelaError).toBe(true);
81
+ });
42
82
 
43
- expect(call).toBeDefined();
44
- expect(call.id).toBeDefined();
45
- expect(call.status).toBeDefined();
83
+ it("SubscriptionError has correct name", () => {
84
+ const error = new SubscriptionError("Subscription expired", {
85
+ errorCode: 7008,
46
86
  });
87
+ expect(error.name).toBe("SubscriptionError");
88
+ expect(error.errorCode).toBe(7008);
89
+ });
47
90
 
48
- it("should handle invalid phone numbers", async () => {
49
- await expect(
50
- sdk.calls.create({
51
- to: "invalid",
52
- from_: "+1987654321",
53
- country: "US",
54
- })
55
- ).rejects.toThrow();
91
+ it("RateLimitError has correct name", () => {
92
+ const error = new RateLimitError("Rate limit exceeded", { statusCode: 429 });
93
+ expect(error.name).toBe("RateLimitError");
94
+ expect(error.statusCode).toBe(429);
95
+ });
96
+
97
+ it("ValidationError has correct name", () => {
98
+ const error = new ValidationError("Invalid phone number", {
99
+ details: { field: "to" },
56
100
  });
101
+ expect(error.name).toBe("ValidationError");
102
+ expect(error.details).toEqual({ field: "to" });
57
103
  });
58
104
 
59
- describe("Call Status", () => {
60
- it("should get call status by ID", async () => {
61
- const callId = "test_call_id";
62
- const status = await sdk.calls.getStatus(callId);
105
+ it("CallError has correct name", () => {
106
+ const error = new CallError("Call failed");
107
+ expect(error.name).toBe("CallError");
108
+ expect(error instanceof PamelaError).toBe(true);
109
+ });
63
110
 
64
- expect(status).toBeDefined();
65
- expect(status.id).toBe(callId);
66
- expect(status.status).toBeDefined();
67
- });
111
+ it("exceptions can be created with minimal args", () => {
112
+ const error = new PamelaError("Simple error");
113
+ expect(error.message).toBe("Simple error");
114
+ expect(error.errorCode).toBeUndefined();
115
+ expect(error.details).toBeUndefined();
116
+ });
117
+ });
68
118
 
69
- it("should handle non-existent call ID", async () => {
70
- await expect(sdk.calls.getStatus("nonexistent")).rejects.toThrow();
119
+ describe("Client Initialization", () => {
120
+ it("initializes with API key", () => {
121
+ const client = new PamelaClient({
122
+ apiKey: "pk_live_test",
71
123
  });
124
+ expect(client).toBeDefined();
125
+ expect(client.usage).toBeDefined();
72
126
  });
73
127
 
74
- describe("Call Cancellation", () => {
75
- it("should cancel an in-progress call", async () => {
76
- const callId = "test_call_id";
77
- const result = await sdk.calls.cancel(callId);
78
-
79
- expect(result).toBeDefined();
80
- expect(result.success).toBe(true);
128
+ it("uses default base URL", () => {
129
+ const client = new PamelaClient({
130
+ apiKey: "pk_live_test",
81
131
  });
132
+ // Client is created successfully with default URL
133
+ expect(client).toBeDefined();
134
+ });
82
135
 
83
- it("should handle cancelling already completed call", async () => {
84
- await expect(sdk.calls.cancel("completed_call")).rejects.toThrow();
136
+ it("accepts custom base URL", () => {
137
+ const client = new PamelaClient({
138
+ apiKey: "pk_live_test",
139
+ baseUrl: "https://custom.api.com",
85
140
  });
141
+ expect(client).toBeDefined();
86
142
  });
143
+ });
87
144
 
88
- describe("Usage", () => {
89
- it("should get usage statistics", async () => {
90
- const usage = await sdk.usage.get("2024-01");
145
+ describe("Exception Hierarchy", () => {
146
+ it("all exceptions inherit from PamelaError", () => {
147
+ expect(new AuthenticationError("test") instanceof PamelaError).toBe(true);
148
+ expect(new SubscriptionError("test") instanceof PamelaError).toBe(true);
149
+ expect(new RateLimitError("test") instanceof PamelaError).toBe(true);
150
+ expect(new ValidationError("test") instanceof PamelaError).toBe(true);
151
+ expect(new CallError("test") instanceof PamelaError).toBe(true);
152
+ });
91
153
 
92
- expect(usage).toBeDefined();
93
- expect(usage.call_count).toBeDefined();
94
- expect(usage.quota).toBeDefined();
95
- });
154
+ it("all exceptions inherit from Error", () => {
155
+ expect(new PamelaError("test") instanceof Error).toBe(true);
156
+ expect(new AuthenticationError("test") instanceof Error).toBe(true);
157
+ expect(new SubscriptionError("test") instanceof Error).toBe(true);
158
+ });
159
+ });
160
+
161
+ // =============================================================================
162
+ // Integration Tests (Require PAMELA_TEST_API_KEY)
163
+ // =============================================================================
96
164
 
97
- it("should handle invalid period format", async () => {
98
- await expect(sdk.usage.get("invalid")).rejects.toThrow();
165
+ describe("PamelaClient SDK", () => {
166
+ it("initializes with API key", () => {
167
+ const client = new PamelaClient({
168
+ apiKey: TEST_API_KEY || "pk_live_placeholder",
169
+ baseUrl: TEST_API_URL,
99
170
  });
171
+ expect(client).toBeDefined();
100
172
  });
173
+ });
101
174
 
102
- describe("Error Handling", () => {
103
- it("should handle network errors", async () => {
104
- // Test with invalid API URL
105
- const badSdk = new Pamela({
106
- apiKey: TEST_API_KEY,
107
- apiUrl: "https://invalid-url.example.com",
108
- });
109
-
110
- await expect(
111
- badSdk.calls.create({
112
- to: "+1234567890",
113
- from_: "+1987654321",
114
- country: "US",
115
- })
116
- ).rejects.toThrow();
117
- });
175
+ (SHOULD_RUN ? describe : describe.skip)("PamelaClient integration", () => {
176
+ let client: PamelaClient;
118
177
 
119
- it("should handle API errors (400, 401, 403, 500)", async () => {
120
- // Test with invalid API key
121
- const badSdk = new Pamela({
122
- apiKey: "invalid_key",
123
- apiUrl: TEST_API_URL,
124
- });
125
-
126
- await expect(
127
- badSdk.calls.create({
128
- to: "+1234567890",
129
- from_: "+1987654321",
130
- country: "US",
131
- })
132
- ).rejects.toThrow();
178
+ beforeAll(() => {
179
+ client = new PamelaClient({
180
+ apiKey: TEST_API_KEY as string,
181
+ baseUrl: TEST_API_URL,
133
182
  });
134
183
  });
184
+
185
+ it("lists calls", async () => {
186
+ const result = await client.listCalls({ limit: 1 });
187
+ expect(result).toBeDefined();
188
+ expect(Array.isArray(result.items)).toBe(true);
189
+ });
190
+
191
+ it("gets usage", async () => {
192
+ const usage = await client.usage.get();
193
+ expect(usage).toBeDefined();
194
+ expect(usage.call_count).toBeDefined();
195
+ });
196
+
197
+ it("lists tools", async () => {
198
+ const tools = await client.listTools();
199
+ expect(Array.isArray(tools)).toBe(true);
200
+ });
135
201
  });
136
202
 
package/tests/setup.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  // Set test API URL (use staging or mock server)
8
8
  export const TEST_API_URL = process.env.TEST_API_URL || "https://pamela-dev.up.railway.app";
9
9
 
10
- // Test API key (use test keys from staging environment)
11
- export const TEST_API_KEY = process.env.TEST_API_KEY || "pk_test_placeholder";
10
+ // API key for tests (use a staging key)
11
+ export const TEST_API_KEY = process.env.TEST_API_KEY || "pk_live_placeholder";
12
12
 
13
13
  // Mock data helpers
14
14
  export const mockCallResponse = {