@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 +238 -5
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +25 -1
- package/dist/index.js +70 -4
- package/package.json +3 -3
- package/src/errors.ts +48 -0
- package/src/index.ts +95 -3
- package/tests/pamela.test.ts +164 -98
- package/tests/setup.ts +2 -2
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 @
|
|
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 '@
|
|
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 '@
|
|
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
|
|
297
|
+
See the [Pamela Enterprise API Documentation](https://docs.thisispamela.com/enterprise) for full API reference.
|
|
65
298
|
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "Pamela
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
package/tests/pamela.test.ts
CHANGED
|
@@ -1,136 +1,202 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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
|
-
|
|
10
|
-
const
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
apiUrl: TEST_API_URL,
|
|
19
|
-
});
|
|
56
|
+
expect(
|
|
57
|
+
PamelaClient.verifyWebhookSignature(payloadStr, signature, secret)
|
|
58
|
+
).toBe(true);
|
|
20
59
|
});
|
|
60
|
+
});
|
|
21
61
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
11
|
-
export const TEST_API_KEY = process.env.TEST_API_KEY || "
|
|
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 = {
|