@thisispamela/sdk 1.0.1 → 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 +104 -18
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +45 -2
- package/package.json +1 -1
- package/src/errors.ts +48 -0
- package/src/index.ts +54 -1
- package/tests/pamela.test.ts +160 -4
- package/tests/setup.ts +2 -2
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ API keys are created and managed through the Pamela Partner Portal or via the Pa
|
|
|
76
76
|
-d '{"project_id": "optional-project-id", "key_prefix": "pk_live_"}'
|
|
77
77
|
```
|
|
78
78
|
3. **Save your API key immediately** - the full key is only returned once during creation
|
|
79
|
-
4. **Use the key prefix** (`pk_live_`
|
|
79
|
+
4. **Use the key prefix** (`pk_live_`) to identify keys in your account
|
|
80
80
|
|
|
81
81
|
### Managing API Keys
|
|
82
82
|
|
|
@@ -86,8 +86,7 @@ API keys are created and managed through the Pamela Partner Portal or via the Pa
|
|
|
86
86
|
|
|
87
87
|
### API Key Format
|
|
88
88
|
|
|
89
|
-
- **Live keys**: Start with `pk_live_` (
|
|
90
|
-
- **Test keys**: Start with `pk_test_` (for development/testing)
|
|
89
|
+
- **Live keys**: Start with `pk_live_` (all API usage)
|
|
91
90
|
- **Security**: Keys are hashed in the database. Store them securely and never commit them to version control.
|
|
92
91
|
|
|
93
92
|
## Subscription Requirements
|
|
@@ -112,29 +111,116 @@ Check subscription status using the Enterprise Partner API:
|
|
|
112
111
|
- `POST /api/b2b/v1/partner/subscription/checkout` - Create checkout session
|
|
113
112
|
- `POST /api/b2b/v1/partner/subscription/portal` - Access Customer Portal
|
|
114
113
|
|
|
115
|
-
## Error
|
|
114
|
+
## Error Handling
|
|
116
115
|
|
|
117
|
-
|
|
116
|
+
The SDK provides typed exceptions for all API errors:
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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) |
|
|
123
200
|
|
|
124
|
-
### Validation Errors
|
|
201
|
+
### Validation Errors (400)
|
|
125
202
|
|
|
126
|
-
|
|
127
|
-
|
|
203
|
+
| Code | Description |
|
|
204
|
+
|------|-------------|
|
|
205
|
+
| 2001 | Validation error |
|
|
206
|
+
| 2002 | Invalid phone number format |
|
|
128
207
|
|
|
129
|
-
###
|
|
208
|
+
### Enterprise Errors (7xxx)
|
|
130
209
|
|
|
131
|
-
|
|
132
|
-
|
|
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 |
|
|
133
217
|
|
|
134
|
-
### Rate
|
|
218
|
+
### Rate Limiting (429)
|
|
135
219
|
|
|
136
|
-
|
|
137
|
-
|
|
220
|
+
| Code | Description |
|
|
221
|
+
|------|-------------|
|
|
222
|
+
| 6001 | Rate limit exceeded |
|
|
223
|
+
| 6002 | Quota exceeded |
|
|
138
224
|
|
|
139
225
|
## Usage Limits & Billing
|
|
140
226
|
|
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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Pamela Enterprise Voice API SDK for JavaScript/TypeScript
|
|
3
3
|
*/
|
|
4
4
|
import { AxiosInstance } from 'axios';
|
|
5
|
+
import { PamelaError, AuthenticationError, SubscriptionError, RateLimitError, ValidationError, CallError } from './errors';
|
|
5
6
|
export interface PamelaClientConfig {
|
|
6
7
|
apiKey: string;
|
|
7
8
|
baseUrl?: string;
|
|
@@ -150,3 +151,4 @@ export declare class PamelaClient {
|
|
|
150
151
|
}
|
|
151
152
|
export default PamelaClient;
|
|
152
153
|
export { PamelaClient as Pamela };
|
|
154
|
+
export { PamelaError, AuthenticationError, SubscriptionError, RateLimitError, ValidationError, CallError, };
|
package/dist/index.js
CHANGED
|
@@ -39,9 +39,52 @@ 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.Pamela = exports.PamelaClient = exports.UsageClient = 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
|
+
};
|
|
45
88
|
class UsageClient {
|
|
46
89
|
constructor(client) {
|
|
47
90
|
this.client = client;
|
|
@@ -81,7 +124,7 @@ class PamelaClient {
|
|
|
81
124
|
await new Promise(resolve => setTimeout(resolve, 1000 * config.retry));
|
|
82
125
|
return this.client.request(config);
|
|
83
126
|
}
|
|
84
|
-
return Promise.reject(error);
|
|
127
|
+
return Promise.reject(mapAxiosError(error, config?.url));
|
|
85
128
|
});
|
|
86
129
|
}
|
|
87
130
|
/**
|
package/package.json
CHANGED
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
|
@@ -4,6 +4,14 @@
|
|
|
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,6 +72,43 @@ 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
|
+
|
|
67
112
|
export class UsageClient {
|
|
68
113
|
private client: AxiosInstance;
|
|
69
114
|
|
|
@@ -127,7 +172,7 @@ export class PamelaClient {
|
|
|
127
172
|
return this.client.request(config);
|
|
128
173
|
}
|
|
129
174
|
|
|
130
|
-
return Promise.reject(error);
|
|
175
|
+
return Promise.reject(mapAxiosError(error, config?.url));
|
|
131
176
|
}
|
|
132
177
|
);
|
|
133
178
|
}
|
|
@@ -225,4 +270,12 @@ export class PamelaClient {
|
|
|
225
270
|
// Export as both default and named export for flexibility
|
|
226
271
|
export default PamelaClient;
|
|
227
272
|
export { PamelaClient as Pamela };
|
|
273
|
+
export {
|
|
274
|
+
PamelaError,
|
|
275
|
+
AuthenticationError,
|
|
276
|
+
SubscriptionError,
|
|
277
|
+
RateLimitError,
|
|
278
|
+
ValidationError,
|
|
279
|
+
CallError,
|
|
280
|
+
};
|
|
228
281
|
|
package/tests/pamela.test.ts
CHANGED
|
@@ -1,20 +1,171 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Tests for Pamela JavaScript/TypeScript SDK.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Includes unit tests (no network) and integration tests (require env vars).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import PamelaClient
|
|
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";
|
|
8
16
|
|
|
9
17
|
const TEST_API_URL =
|
|
10
18
|
process.env.PAMELA_API_URL || "https://pamela-dev.up.railway.app";
|
|
11
19
|
const TEST_API_KEY = process.env.PAMELA_TEST_API_KEY;
|
|
12
20
|
const SHOULD_RUN = Boolean(TEST_API_KEY);
|
|
13
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
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects invalid signature", () => {
|
|
43
|
+
const payload = { event: "call.completed" };
|
|
44
|
+
expect(
|
|
45
|
+
PamelaClient.verifyWebhookSignature(payload, "invalid_sig", secret)
|
|
46
|
+
).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("handles string payload", () => {
|
|
50
|
+
const payloadStr = '{"test":"value"}';
|
|
51
|
+
const signature = crypto
|
|
52
|
+
.createHmac("sha256", secret)
|
|
53
|
+
.update(payloadStr)
|
|
54
|
+
.digest("hex");
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
PamelaClient.verifyWebhookSignature(payloadStr, signature, secret)
|
|
58
|
+
).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
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,
|
|
68
|
+
});
|
|
69
|
+
|
|
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");
|
|
75
|
+
});
|
|
76
|
+
|
|
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
|
+
});
|
|
82
|
+
|
|
83
|
+
it("SubscriptionError has correct name", () => {
|
|
84
|
+
const error = new SubscriptionError("Subscription expired", {
|
|
85
|
+
errorCode: 7008,
|
|
86
|
+
});
|
|
87
|
+
expect(error.name).toBe("SubscriptionError");
|
|
88
|
+
expect(error.errorCode).toBe(7008);
|
|
89
|
+
});
|
|
90
|
+
|
|
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" },
|
|
100
|
+
});
|
|
101
|
+
expect(error.name).toBe("ValidationError");
|
|
102
|
+
expect(error.details).toEqual({ field: "to" });
|
|
103
|
+
});
|
|
104
|
+
|
|
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
|
+
});
|
|
110
|
+
|
|
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
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("Client Initialization", () => {
|
|
120
|
+
it("initializes with API key", () => {
|
|
121
|
+
const client = new PamelaClient({
|
|
122
|
+
apiKey: "pk_live_test",
|
|
123
|
+
});
|
|
124
|
+
expect(client).toBeDefined();
|
|
125
|
+
expect(client.usage).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("uses default base URL", () => {
|
|
129
|
+
const client = new PamelaClient({
|
|
130
|
+
apiKey: "pk_live_test",
|
|
131
|
+
});
|
|
132
|
+
// Client is created successfully with default URL
|
|
133
|
+
expect(client).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("accepts custom base URL", () => {
|
|
137
|
+
const client = new PamelaClient({
|
|
138
|
+
apiKey: "pk_live_test",
|
|
139
|
+
baseUrl: "https://custom.api.com",
|
|
140
|
+
});
|
|
141
|
+
expect(client).toBeDefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
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
|
+
});
|
|
153
|
+
|
|
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
|
+
// =============================================================================
|
|
164
|
+
|
|
14
165
|
describe("PamelaClient SDK", () => {
|
|
15
166
|
it("initializes with API key", () => {
|
|
16
167
|
const client = new PamelaClient({
|
|
17
|
-
apiKey: TEST_API_KEY || "
|
|
168
|
+
apiKey: TEST_API_KEY || "pk_live_placeholder",
|
|
18
169
|
baseUrl: TEST_API_URL,
|
|
19
170
|
});
|
|
20
171
|
expect(client).toBeDefined();
|
|
@@ -42,5 +193,10 @@ describe("PamelaClient SDK", () => {
|
|
|
42
193
|
expect(usage).toBeDefined();
|
|
43
194
|
expect(usage.call_count).toBeDefined();
|
|
44
195
|
});
|
|
196
|
+
|
|
197
|
+
it("lists tools", async () => {
|
|
198
|
+
const tools = await client.listTools();
|
|
199
|
+
expect(Array.isArray(tools)).toBe(true);
|
|
200
|
+
});
|
|
45
201
|
});
|
|
46
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 = {
|