@webhooks-cc/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/dist/index.d.mts +89 -7
- package/dist/index.d.ts +89 -7
- package/dist/index.js +138 -26
- package/dist/index.mjs +128 -25
- package/package.json +4 -3
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# @webhooks-cc/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [webhooks.cc](https://webhooks.cc). Create temporary webhook endpoints, capture requests, and assert on their contents in your test suite.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @webhooks-cc/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { WebhooksCC } from "@webhooks-cc/sdk";
|
|
15
|
+
|
|
16
|
+
const client = new WebhooksCC({ apiKey: "whcc_..." });
|
|
17
|
+
|
|
18
|
+
// Create a temporary endpoint
|
|
19
|
+
const endpoint = await client.endpoints.create({ name: "My Test" });
|
|
20
|
+
console.log(endpoint.url); // https://go.webhooks.cc/w/abc123
|
|
21
|
+
|
|
22
|
+
// Point your service at endpoint.url, then wait for the webhook
|
|
23
|
+
const request = await client.requests.waitFor(endpoint.slug, {
|
|
24
|
+
timeout: 10000,
|
|
25
|
+
match: (r) => r.method === "POST",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
console.log(request.body); // '{"event":"order.created"}'
|
|
29
|
+
console.log(request.headers); // { 'content-type': 'application/json', ... }
|
|
30
|
+
|
|
31
|
+
// Clean up
|
|
32
|
+
await client.endpoints.delete(endpoint.slug);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `new WebhooksCC(options)`
|
|
38
|
+
|
|
39
|
+
| Option | Type | Default | Description |
|
|
40
|
+
| --------- | -------- | --------------------- | -------------------- |
|
|
41
|
+
| `apiKey` | `string` | _required_ | API key (`whcc_...`) |
|
|
42
|
+
| `baseUrl` | `string` | `https://webhooks.cc` | API base URL |
|
|
43
|
+
| `timeout` | `number` | `30000` | Request timeout (ms) |
|
|
44
|
+
|
|
45
|
+
### Endpoints
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Create
|
|
49
|
+
const endpoint = await client.endpoints.create({ name: "optional name" });
|
|
50
|
+
|
|
51
|
+
// List all
|
|
52
|
+
const endpoints = await client.endpoints.list();
|
|
53
|
+
|
|
54
|
+
// Get by slug
|
|
55
|
+
const endpoint = await client.endpoints.get("abc123");
|
|
56
|
+
|
|
57
|
+
// Delete
|
|
58
|
+
await client.endpoints.delete("abc123");
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Requests
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// List captured requests for an endpoint
|
|
65
|
+
const requests = await client.requests.list("endpoint-slug", {
|
|
66
|
+
limit: 50, // default: 50, max: 1000
|
|
67
|
+
since: Date.now() - 60000, // only after this timestamp (ms)
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Get a single request by ID
|
|
71
|
+
const request = await client.requests.get("request-id");
|
|
72
|
+
|
|
73
|
+
// Poll until a matching request arrives
|
|
74
|
+
const request = await client.requests.waitFor("endpoint-slug", {
|
|
75
|
+
timeout: 30000, // max wait (ms), default: 30000
|
|
76
|
+
pollInterval: 500, // poll interval (ms), default: 500
|
|
77
|
+
match: (r) => r.method === "POST" && r.body?.includes("order"),
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Errors
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { WebhooksCC, ApiError } from "@webhooks-cc/sdk";
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await client.endpoints.get("nonexistent");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof ApiError) {
|
|
90
|
+
console.log(error.statusCode); // 404
|
|
91
|
+
console.log(error.message); // "API error (404): ..."
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## GitHub Actions
|
|
97
|
+
|
|
98
|
+
Add your API key as a repository secret named `WHK_API_KEY`:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
- name: Run webhook tests
|
|
102
|
+
env:
|
|
103
|
+
WHK_API_KEY: ${{ secrets.WHK_API_KEY }}
|
|
104
|
+
run: npx vitest run
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// webhook.test.ts
|
|
109
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
110
|
+
import { WebhooksCC } from "@webhooks-cc/sdk";
|
|
111
|
+
|
|
112
|
+
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
|
|
113
|
+
|
|
114
|
+
describe("webhook integration", () => {
|
|
115
|
+
let slug: string;
|
|
116
|
+
|
|
117
|
+
it("receives order webhook", async () => {
|
|
118
|
+
const endpoint = await client.endpoints.create({ name: "CI Test" });
|
|
119
|
+
slug = endpoint.slug;
|
|
120
|
+
|
|
121
|
+
// Trigger your service to send a webhook to endpoint.url
|
|
122
|
+
await yourService.registerWebhook(endpoint.url!);
|
|
123
|
+
await yourService.createOrder();
|
|
124
|
+
|
|
125
|
+
const req = await client.requests.waitFor(slug, {
|
|
126
|
+
timeout: 15000,
|
|
127
|
+
match: (r) => r.body?.includes("order.created"),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const body = JSON.parse(req.body!);
|
|
131
|
+
expect(body.event).toBe("order.created");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterAll(async () => {
|
|
135
|
+
if (slug) await client.endpoints.delete(slug);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Types
|
|
141
|
+
|
|
142
|
+
All types are exported:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import type {
|
|
146
|
+
ClientOptions,
|
|
147
|
+
Endpoint,
|
|
148
|
+
Request,
|
|
149
|
+
CreateEndpointOptions,
|
|
150
|
+
ListRequestsOptions,
|
|
151
|
+
WaitForOptions,
|
|
152
|
+
} from "@webhooks-cc/sdk";
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -69,6 +69,34 @@ interface WaitForOptions {
|
|
|
69
69
|
/** Filter function to match specific requests */
|
|
70
70
|
match?: (request: Request) => boolean;
|
|
71
71
|
}
|
|
72
|
+
/** Info passed to the onRequest hook before a request is sent. */
|
|
73
|
+
interface RequestHookInfo {
|
|
74
|
+
method: string;
|
|
75
|
+
url: string;
|
|
76
|
+
}
|
|
77
|
+
/** Info passed to the onResponse hook after a successful response. */
|
|
78
|
+
interface ResponseHookInfo {
|
|
79
|
+
method: string;
|
|
80
|
+
url: string;
|
|
81
|
+
status: number;
|
|
82
|
+
durationMs: number;
|
|
83
|
+
}
|
|
84
|
+
/** Info passed to the onError hook when a request fails. */
|
|
85
|
+
interface ErrorHookInfo {
|
|
86
|
+
method: string;
|
|
87
|
+
url: string;
|
|
88
|
+
error: Error;
|
|
89
|
+
durationMs: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Lifecycle hooks for observability and telemetry integration.
|
|
93
|
+
* All hooks are optional and are called synchronously (fire-and-forget).
|
|
94
|
+
*/
|
|
95
|
+
interface ClientHooks {
|
|
96
|
+
onRequest?: (info: RequestHookInfo) => void;
|
|
97
|
+
onResponse?: (info: ResponseHookInfo) => void;
|
|
98
|
+
onError?: (info: ErrorHookInfo) => void;
|
|
99
|
+
}
|
|
72
100
|
/**
|
|
73
101
|
* Configuration options for the WebhooksCC client.
|
|
74
102
|
*/
|
|
@@ -79,6 +107,35 @@ interface ClientOptions {
|
|
|
79
107
|
baseUrl?: string;
|
|
80
108
|
/** Request timeout in milliseconds (default: 30000) */
|
|
81
109
|
timeout?: number;
|
|
110
|
+
/** Lifecycle hooks for observability */
|
|
111
|
+
hooks?: ClientHooks;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Base error class for all webhooks.cc SDK errors.
|
|
116
|
+
* Extends the standard Error with an HTTP status code.
|
|
117
|
+
*/
|
|
118
|
+
declare class WebhooksCCError extends Error {
|
|
119
|
+
readonly statusCode: number;
|
|
120
|
+
constructor(statusCode: number, message: string);
|
|
121
|
+
}
|
|
122
|
+
/** Thrown when the API key is invalid or missing (401). */
|
|
123
|
+
declare class UnauthorizedError extends WebhooksCCError {
|
|
124
|
+
constructor(message?: string);
|
|
125
|
+
}
|
|
126
|
+
/** Thrown when the requested resource does not exist (404). */
|
|
127
|
+
declare class NotFoundError extends WebhooksCCError {
|
|
128
|
+
constructor(message?: string);
|
|
129
|
+
}
|
|
130
|
+
/** Thrown when the request times out. */
|
|
131
|
+
declare class TimeoutError extends WebhooksCCError {
|
|
132
|
+
constructor(timeoutMs: number);
|
|
133
|
+
}
|
|
134
|
+
/** Thrown when the API returns 429 Too Many Requests. */
|
|
135
|
+
declare class RateLimitError extends WebhooksCCError {
|
|
136
|
+
/** Seconds until the rate limit resets, if provided by the server. */
|
|
137
|
+
readonly retryAfter?: number;
|
|
138
|
+
constructor(retryAfter?: number);
|
|
82
139
|
}
|
|
83
140
|
|
|
84
141
|
/**
|
|
@@ -96,13 +153,9 @@ interface ClientOptions {
|
|
|
96
153
|
*/
|
|
97
154
|
|
|
98
155
|
/**
|
|
99
|
-
*
|
|
100
|
-
* Allows callers to distinguish between different error types.
|
|
156
|
+
* @deprecated Use {@link WebhooksCCError} instead. Kept for backward compatibility.
|
|
101
157
|
*/
|
|
102
|
-
declare
|
|
103
|
-
readonly statusCode: number;
|
|
104
|
-
constructor(statusCode: number, message: string);
|
|
105
|
-
}
|
|
158
|
+
declare const ApiError: typeof WebhooksCCError;
|
|
106
159
|
/**
|
|
107
160
|
* Client for the webhooks.cc API.
|
|
108
161
|
*
|
|
@@ -114,6 +167,7 @@ declare class WebhooksCC {
|
|
|
114
167
|
private readonly apiKey;
|
|
115
168
|
private readonly baseUrl;
|
|
116
169
|
private readonly timeout;
|
|
170
|
+
private readonly hooks;
|
|
117
171
|
constructor(options: ClientOptions);
|
|
118
172
|
private request;
|
|
119
173
|
endpoints: {
|
|
@@ -141,4 +195,32 @@ declare class WebhooksCC {
|
|
|
141
195
|
};
|
|
142
196
|
}
|
|
143
197
|
|
|
144
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Safely parse a JSON request body.
|
|
200
|
+
* Returns undefined if the body is empty or not valid JSON.
|
|
201
|
+
*/
|
|
202
|
+
declare function parseJsonBody(request: Request): unknown | undefined;
|
|
203
|
+
/**
|
|
204
|
+
* Check if a request looks like a Stripe webhook.
|
|
205
|
+
* Matches on the `stripe-signature` header being present.
|
|
206
|
+
*/
|
|
207
|
+
declare function isStripeWebhook(request: Request): boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Check if a request looks like a GitHub webhook.
|
|
210
|
+
* Matches on the `x-github-event` header being present.
|
|
211
|
+
*/
|
|
212
|
+
declare function isGitHubWebhook(request: Request): boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Returns a match function that checks whether a JSON field in the
|
|
215
|
+
* request body equals the expected value.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* const req = await client.requests.waitFor(slug, {
|
|
220
|
+
* match: matchJsonField("type", "checkout.session.completed"),
|
|
221
|
+
* });
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
declare function matchJsonField(field: string, value: unknown): (request: Request) => boolean;
|
|
225
|
+
|
|
226
|
+
export { ApiError, type ClientHooks, type ClientOptions, type CreateEndpointOptions, type Endpoint, type ErrorHookInfo, type ListRequestsOptions, NotFoundError, RateLimitError, type Request, type RequestHookInfo, type ResponseHookInfo, TimeoutError, UnauthorizedError, type WaitForOptions, WebhooksCC, WebhooksCCError, isGitHubWebhook, isStripeWebhook, matchJsonField, parseJsonBody };
|
package/dist/index.d.ts
CHANGED
|
@@ -69,6 +69,34 @@ interface WaitForOptions {
|
|
|
69
69
|
/** Filter function to match specific requests */
|
|
70
70
|
match?: (request: Request) => boolean;
|
|
71
71
|
}
|
|
72
|
+
/** Info passed to the onRequest hook before a request is sent. */
|
|
73
|
+
interface RequestHookInfo {
|
|
74
|
+
method: string;
|
|
75
|
+
url: string;
|
|
76
|
+
}
|
|
77
|
+
/** Info passed to the onResponse hook after a successful response. */
|
|
78
|
+
interface ResponseHookInfo {
|
|
79
|
+
method: string;
|
|
80
|
+
url: string;
|
|
81
|
+
status: number;
|
|
82
|
+
durationMs: number;
|
|
83
|
+
}
|
|
84
|
+
/** Info passed to the onError hook when a request fails. */
|
|
85
|
+
interface ErrorHookInfo {
|
|
86
|
+
method: string;
|
|
87
|
+
url: string;
|
|
88
|
+
error: Error;
|
|
89
|
+
durationMs: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Lifecycle hooks for observability and telemetry integration.
|
|
93
|
+
* All hooks are optional and are called synchronously (fire-and-forget).
|
|
94
|
+
*/
|
|
95
|
+
interface ClientHooks {
|
|
96
|
+
onRequest?: (info: RequestHookInfo) => void;
|
|
97
|
+
onResponse?: (info: ResponseHookInfo) => void;
|
|
98
|
+
onError?: (info: ErrorHookInfo) => void;
|
|
99
|
+
}
|
|
72
100
|
/**
|
|
73
101
|
* Configuration options for the WebhooksCC client.
|
|
74
102
|
*/
|
|
@@ -79,6 +107,35 @@ interface ClientOptions {
|
|
|
79
107
|
baseUrl?: string;
|
|
80
108
|
/** Request timeout in milliseconds (default: 30000) */
|
|
81
109
|
timeout?: number;
|
|
110
|
+
/** Lifecycle hooks for observability */
|
|
111
|
+
hooks?: ClientHooks;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Base error class for all webhooks.cc SDK errors.
|
|
116
|
+
* Extends the standard Error with an HTTP status code.
|
|
117
|
+
*/
|
|
118
|
+
declare class WebhooksCCError extends Error {
|
|
119
|
+
readonly statusCode: number;
|
|
120
|
+
constructor(statusCode: number, message: string);
|
|
121
|
+
}
|
|
122
|
+
/** Thrown when the API key is invalid or missing (401). */
|
|
123
|
+
declare class UnauthorizedError extends WebhooksCCError {
|
|
124
|
+
constructor(message?: string);
|
|
125
|
+
}
|
|
126
|
+
/** Thrown when the requested resource does not exist (404). */
|
|
127
|
+
declare class NotFoundError extends WebhooksCCError {
|
|
128
|
+
constructor(message?: string);
|
|
129
|
+
}
|
|
130
|
+
/** Thrown when the request times out. */
|
|
131
|
+
declare class TimeoutError extends WebhooksCCError {
|
|
132
|
+
constructor(timeoutMs: number);
|
|
133
|
+
}
|
|
134
|
+
/** Thrown when the API returns 429 Too Many Requests. */
|
|
135
|
+
declare class RateLimitError extends WebhooksCCError {
|
|
136
|
+
/** Seconds until the rate limit resets, if provided by the server. */
|
|
137
|
+
readonly retryAfter?: number;
|
|
138
|
+
constructor(retryAfter?: number);
|
|
82
139
|
}
|
|
83
140
|
|
|
84
141
|
/**
|
|
@@ -96,13 +153,9 @@ interface ClientOptions {
|
|
|
96
153
|
*/
|
|
97
154
|
|
|
98
155
|
/**
|
|
99
|
-
*
|
|
100
|
-
* Allows callers to distinguish between different error types.
|
|
156
|
+
* @deprecated Use {@link WebhooksCCError} instead. Kept for backward compatibility.
|
|
101
157
|
*/
|
|
102
|
-
declare
|
|
103
|
-
readonly statusCode: number;
|
|
104
|
-
constructor(statusCode: number, message: string);
|
|
105
|
-
}
|
|
158
|
+
declare const ApiError: typeof WebhooksCCError;
|
|
106
159
|
/**
|
|
107
160
|
* Client for the webhooks.cc API.
|
|
108
161
|
*
|
|
@@ -114,6 +167,7 @@ declare class WebhooksCC {
|
|
|
114
167
|
private readonly apiKey;
|
|
115
168
|
private readonly baseUrl;
|
|
116
169
|
private readonly timeout;
|
|
170
|
+
private readonly hooks;
|
|
117
171
|
constructor(options: ClientOptions);
|
|
118
172
|
private request;
|
|
119
173
|
endpoints: {
|
|
@@ -141,4 +195,32 @@ declare class WebhooksCC {
|
|
|
141
195
|
};
|
|
142
196
|
}
|
|
143
197
|
|
|
144
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Safely parse a JSON request body.
|
|
200
|
+
* Returns undefined if the body is empty or not valid JSON.
|
|
201
|
+
*/
|
|
202
|
+
declare function parseJsonBody(request: Request): unknown | undefined;
|
|
203
|
+
/**
|
|
204
|
+
* Check if a request looks like a Stripe webhook.
|
|
205
|
+
* Matches on the `stripe-signature` header being present.
|
|
206
|
+
*/
|
|
207
|
+
declare function isStripeWebhook(request: Request): boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Check if a request looks like a GitHub webhook.
|
|
210
|
+
* Matches on the `x-github-event` header being present.
|
|
211
|
+
*/
|
|
212
|
+
declare function isGitHubWebhook(request: Request): boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Returns a match function that checks whether a JSON field in the
|
|
215
|
+
* request body equals the expected value.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* const req = await client.requests.waitFor(slug, {
|
|
220
|
+
* match: matchJsonField("type", "checkout.session.completed"),
|
|
221
|
+
* });
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
declare function matchJsonField(field: string, value: unknown): (request: Request) => boolean;
|
|
225
|
+
|
|
226
|
+
export { ApiError, type ClientHooks, type ClientOptions, type CreateEndpointOptions, type Endpoint, type ErrorHookInfo, type ListRequestsOptions, NotFoundError, RateLimitError, type Request, type RequestHookInfo, type ResponseHookInfo, TimeoutError, UnauthorizedError, type WaitForOptions, WebhooksCC, WebhooksCCError, isGitHubWebhook, isStripeWebhook, matchJsonField, parseJsonBody };
|
package/dist/index.js
CHANGED
|
@@ -21,22 +21,80 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ApiError: () => ApiError,
|
|
24
|
-
|
|
24
|
+
NotFoundError: () => NotFoundError,
|
|
25
|
+
RateLimitError: () => RateLimitError,
|
|
26
|
+
TimeoutError: () => TimeoutError,
|
|
27
|
+
UnauthorizedError: () => UnauthorizedError,
|
|
28
|
+
WebhooksCC: () => WebhooksCC,
|
|
29
|
+
WebhooksCCError: () => WebhooksCCError,
|
|
30
|
+
isGitHubWebhook: () => isGitHubWebhook,
|
|
31
|
+
isStripeWebhook: () => isStripeWebhook,
|
|
32
|
+
matchJsonField: () => matchJsonField,
|
|
33
|
+
parseJsonBody: () => parseJsonBody
|
|
25
34
|
});
|
|
26
35
|
module.exports = __toCommonJS(index_exports);
|
|
27
36
|
|
|
37
|
+
// src/errors.ts
|
|
38
|
+
var WebhooksCCError = class extends Error {
|
|
39
|
+
constructor(statusCode, message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.statusCode = statusCode;
|
|
42
|
+
this.name = "WebhooksCCError";
|
|
43
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var UnauthorizedError = class extends WebhooksCCError {
|
|
47
|
+
constructor(message = "Invalid or missing API key") {
|
|
48
|
+
super(401, message);
|
|
49
|
+
this.name = "UnauthorizedError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var NotFoundError = class extends WebhooksCCError {
|
|
53
|
+
constructor(message = "Resource not found") {
|
|
54
|
+
super(404, message);
|
|
55
|
+
this.name = "NotFoundError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var TimeoutError = class extends WebhooksCCError {
|
|
59
|
+
constructor(timeoutMs) {
|
|
60
|
+
super(0, `Request timed out after ${timeoutMs}ms`);
|
|
61
|
+
this.name = "TimeoutError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var RateLimitError = class extends WebhooksCCError {
|
|
65
|
+
constructor(retryAfter) {
|
|
66
|
+
const message = retryAfter ? `Rate limited, retry after ${retryAfter}s` : "Rate limited";
|
|
67
|
+
super(429, message);
|
|
68
|
+
this.name = "RateLimitError";
|
|
69
|
+
this.retryAfter = retryAfter;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
28
73
|
// src/client.ts
|
|
29
74
|
var DEFAULT_BASE_URL = "https://webhooks.cc";
|
|
30
75
|
var DEFAULT_TIMEOUT = 3e4;
|
|
31
76
|
var MIN_POLL_INTERVAL = 10;
|
|
32
77
|
var MAX_POLL_INTERVAL = 6e4;
|
|
33
|
-
var ApiError =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
var ApiError = WebhooksCCError;
|
|
79
|
+
function mapStatusToError(status, message, response) {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case 401:
|
|
82
|
+
return new UnauthorizedError(message);
|
|
83
|
+
case 404:
|
|
84
|
+
return new NotFoundError(message);
|
|
85
|
+
case 429: {
|
|
86
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
87
|
+
let retryAfter;
|
|
88
|
+
if (retryAfterHeader) {
|
|
89
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
90
|
+
retryAfter = Number.isNaN(parsed) ? void 0 : parsed;
|
|
91
|
+
}
|
|
92
|
+
return new RateLimitError(retryAfter);
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
return new WebhooksCCError(status, message);
|
|
38
96
|
}
|
|
39
|
-
}
|
|
97
|
+
}
|
|
40
98
|
var SAFE_PATH_SEGMENT_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
41
99
|
function validatePathSegment(segment, name) {
|
|
42
100
|
if (!SAFE_PATH_SEGMENT_REGEX.test(segment)) {
|
|
@@ -116,38 +174,39 @@ var WebhooksCC = class {
|
|
|
116
174
|
return matched;
|
|
117
175
|
}
|
|
118
176
|
} catch (error) {
|
|
119
|
-
if (error instanceof
|
|
120
|
-
if (error
|
|
121
|
-
throw
|
|
122
|
-
}
|
|
123
|
-
if (error.statusCode === 403) {
|
|
124
|
-
throw new Error("Access denied: insufficient permissions for this endpoint");
|
|
177
|
+
if (error instanceof WebhooksCCError) {
|
|
178
|
+
if (error instanceof UnauthorizedError) {
|
|
179
|
+
throw error;
|
|
125
180
|
}
|
|
126
|
-
if (error
|
|
127
|
-
throw
|
|
181
|
+
if (error instanceof NotFoundError) {
|
|
182
|
+
throw error;
|
|
128
183
|
}
|
|
129
|
-
if (error.statusCode < 500) {
|
|
184
|
+
if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
|
|
130
185
|
throw error;
|
|
131
186
|
}
|
|
132
187
|
}
|
|
133
188
|
}
|
|
134
189
|
await sleep(safePollInterval);
|
|
135
190
|
}
|
|
136
|
-
|
|
137
|
-
throw new Error(`Max iterations (${MAX_ITERATIONS}) reached while waiting for request`);
|
|
138
|
-
}
|
|
139
|
-
throw new Error(`Timeout waiting for request after ${timeout}ms`);
|
|
191
|
+
throw new TimeoutError(timeout);
|
|
140
192
|
}
|
|
141
193
|
};
|
|
142
194
|
this.apiKey = options.apiKey;
|
|
143
195
|
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
144
196
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
197
|
+
this.hooks = options.hooks ?? {};
|
|
145
198
|
}
|
|
146
199
|
async request(method, path, body) {
|
|
200
|
+
const url = `${this.baseUrl}/api${path}`;
|
|
147
201
|
const controller = new AbortController();
|
|
148
202
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
203
|
+
const start = Date.now();
|
|
204
|
+
try {
|
|
205
|
+
this.hooks.onRequest?.({ method, url });
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
149
208
|
try {
|
|
150
|
-
const response = await fetch(
|
|
209
|
+
const response = await fetch(url, {
|
|
151
210
|
method,
|
|
152
211
|
headers: {
|
|
153
212
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -156,10 +215,20 @@ var WebhooksCC = class {
|
|
|
156
215
|
body: body ? JSON.stringify(body) : void 0,
|
|
157
216
|
signal: controller.signal
|
|
158
217
|
});
|
|
218
|
+
const durationMs = Date.now() - start;
|
|
159
219
|
if (!response.ok) {
|
|
160
|
-
const
|
|
161
|
-
const sanitizedError =
|
|
162
|
-
|
|
220
|
+
const errorText = await response.text();
|
|
221
|
+
const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
|
|
222
|
+
const error = mapStatusToError(response.status, sanitizedError, response);
|
|
223
|
+
try {
|
|
224
|
+
this.hooks.onError?.({ method, url, error, durationMs });
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
|
|
231
|
+
} catch {
|
|
163
232
|
}
|
|
164
233
|
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
165
234
|
return void 0;
|
|
@@ -171,7 +240,17 @@ var WebhooksCC = class {
|
|
|
171
240
|
return response.json();
|
|
172
241
|
} catch (error) {
|
|
173
242
|
if (error instanceof Error && error.name === "AbortError") {
|
|
174
|
-
|
|
243
|
+
const timeoutError = new TimeoutError(this.timeout);
|
|
244
|
+
try {
|
|
245
|
+
this.hooks.onError?.({
|
|
246
|
+
method,
|
|
247
|
+
url,
|
|
248
|
+
error: timeoutError,
|
|
249
|
+
durationMs: Date.now() - start
|
|
250
|
+
});
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
throw timeoutError;
|
|
175
254
|
}
|
|
176
255
|
throw error;
|
|
177
256
|
} finally {
|
|
@@ -182,8 +261,41 @@ var WebhooksCC = class {
|
|
|
182
261
|
function sleep(ms) {
|
|
183
262
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
184
263
|
}
|
|
264
|
+
|
|
265
|
+
// src/helpers.ts
|
|
266
|
+
function parseJsonBody(request) {
|
|
267
|
+
if (!request.body) return void 0;
|
|
268
|
+
try {
|
|
269
|
+
return JSON.parse(request.body);
|
|
270
|
+
} catch {
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function isStripeWebhook(request) {
|
|
275
|
+
return Object.keys(request.headers).some((k) => k.toLowerCase() === "stripe-signature");
|
|
276
|
+
}
|
|
277
|
+
function isGitHubWebhook(request) {
|
|
278
|
+
return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-github-event");
|
|
279
|
+
}
|
|
280
|
+
function matchJsonField(field, value) {
|
|
281
|
+
return (request) => {
|
|
282
|
+
const body = parseJsonBody(request);
|
|
283
|
+
if (typeof body !== "object" || body === null) return false;
|
|
284
|
+
if (!Object.prototype.hasOwnProperty.call(body, field)) return false;
|
|
285
|
+
return body[field] === value;
|
|
286
|
+
};
|
|
287
|
+
}
|
|
185
288
|
// Annotate the CommonJS export names for ESM import in node:
|
|
186
289
|
0 && (module.exports = {
|
|
187
290
|
ApiError,
|
|
188
|
-
|
|
291
|
+
NotFoundError,
|
|
292
|
+
RateLimitError,
|
|
293
|
+
TimeoutError,
|
|
294
|
+
UnauthorizedError,
|
|
295
|
+
WebhooksCC,
|
|
296
|
+
WebhooksCCError,
|
|
297
|
+
isGitHubWebhook,
|
|
298
|
+
isStripeWebhook,
|
|
299
|
+
matchJsonField,
|
|
300
|
+
parseJsonBody
|
|
189
301
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,64 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var WebhooksCCError = class extends Error {
|
|
3
|
+
constructor(statusCode, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
this.name = "WebhooksCCError";
|
|
7
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var UnauthorizedError = class extends WebhooksCCError {
|
|
11
|
+
constructor(message = "Invalid or missing API key") {
|
|
12
|
+
super(401, message);
|
|
13
|
+
this.name = "UnauthorizedError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var NotFoundError = class extends WebhooksCCError {
|
|
17
|
+
constructor(message = "Resource not found") {
|
|
18
|
+
super(404, message);
|
|
19
|
+
this.name = "NotFoundError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var TimeoutError = class extends WebhooksCCError {
|
|
23
|
+
constructor(timeoutMs) {
|
|
24
|
+
super(0, `Request timed out after ${timeoutMs}ms`);
|
|
25
|
+
this.name = "TimeoutError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var RateLimitError = class extends WebhooksCCError {
|
|
29
|
+
constructor(retryAfter) {
|
|
30
|
+
const message = retryAfter ? `Rate limited, retry after ${retryAfter}s` : "Rate limited";
|
|
31
|
+
super(429, message);
|
|
32
|
+
this.name = "RateLimitError";
|
|
33
|
+
this.retryAfter = retryAfter;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
1
37
|
// src/client.ts
|
|
2
38
|
var DEFAULT_BASE_URL = "https://webhooks.cc";
|
|
3
39
|
var DEFAULT_TIMEOUT = 3e4;
|
|
4
40
|
var MIN_POLL_INTERVAL = 10;
|
|
5
41
|
var MAX_POLL_INTERVAL = 6e4;
|
|
6
|
-
var ApiError =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
42
|
+
var ApiError = WebhooksCCError;
|
|
43
|
+
function mapStatusToError(status, message, response) {
|
|
44
|
+
switch (status) {
|
|
45
|
+
case 401:
|
|
46
|
+
return new UnauthorizedError(message);
|
|
47
|
+
case 404:
|
|
48
|
+
return new NotFoundError(message);
|
|
49
|
+
case 429: {
|
|
50
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
51
|
+
let retryAfter;
|
|
52
|
+
if (retryAfterHeader) {
|
|
53
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
54
|
+
retryAfter = Number.isNaN(parsed) ? void 0 : parsed;
|
|
55
|
+
}
|
|
56
|
+
return new RateLimitError(retryAfter);
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
return new WebhooksCCError(status, message);
|
|
11
60
|
}
|
|
12
|
-
}
|
|
61
|
+
}
|
|
13
62
|
var SAFE_PATH_SEGMENT_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
14
63
|
function validatePathSegment(segment, name) {
|
|
15
64
|
if (!SAFE_PATH_SEGMENT_REGEX.test(segment)) {
|
|
@@ -89,38 +138,39 @@ var WebhooksCC = class {
|
|
|
89
138
|
return matched;
|
|
90
139
|
}
|
|
91
140
|
} catch (error) {
|
|
92
|
-
if (error instanceof
|
|
93
|
-
if (error
|
|
94
|
-
throw
|
|
95
|
-
}
|
|
96
|
-
if (error.statusCode === 403) {
|
|
97
|
-
throw new Error("Access denied: insufficient permissions for this endpoint");
|
|
141
|
+
if (error instanceof WebhooksCCError) {
|
|
142
|
+
if (error instanceof UnauthorizedError) {
|
|
143
|
+
throw error;
|
|
98
144
|
}
|
|
99
|
-
if (error
|
|
100
|
-
throw
|
|
145
|
+
if (error instanceof NotFoundError) {
|
|
146
|
+
throw error;
|
|
101
147
|
}
|
|
102
|
-
if (error.statusCode < 500) {
|
|
148
|
+
if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
|
|
103
149
|
throw error;
|
|
104
150
|
}
|
|
105
151
|
}
|
|
106
152
|
}
|
|
107
153
|
await sleep(safePollInterval);
|
|
108
154
|
}
|
|
109
|
-
|
|
110
|
-
throw new Error(`Max iterations (${MAX_ITERATIONS}) reached while waiting for request`);
|
|
111
|
-
}
|
|
112
|
-
throw new Error(`Timeout waiting for request after ${timeout}ms`);
|
|
155
|
+
throw new TimeoutError(timeout);
|
|
113
156
|
}
|
|
114
157
|
};
|
|
115
158
|
this.apiKey = options.apiKey;
|
|
116
159
|
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
117
160
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
161
|
+
this.hooks = options.hooks ?? {};
|
|
118
162
|
}
|
|
119
163
|
async request(method, path, body) {
|
|
164
|
+
const url = `${this.baseUrl}/api${path}`;
|
|
120
165
|
const controller = new AbortController();
|
|
121
166
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
167
|
+
const start = Date.now();
|
|
168
|
+
try {
|
|
169
|
+
this.hooks.onRequest?.({ method, url });
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
122
172
|
try {
|
|
123
|
-
const response = await fetch(
|
|
173
|
+
const response = await fetch(url, {
|
|
124
174
|
method,
|
|
125
175
|
headers: {
|
|
126
176
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -129,10 +179,20 @@ var WebhooksCC = class {
|
|
|
129
179
|
body: body ? JSON.stringify(body) : void 0,
|
|
130
180
|
signal: controller.signal
|
|
131
181
|
});
|
|
182
|
+
const durationMs = Date.now() - start;
|
|
132
183
|
if (!response.ok) {
|
|
133
|
-
const
|
|
134
|
-
const sanitizedError =
|
|
135
|
-
|
|
184
|
+
const errorText = await response.text();
|
|
185
|
+
const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
|
|
186
|
+
const error = mapStatusToError(response.status, sanitizedError, response);
|
|
187
|
+
try {
|
|
188
|
+
this.hooks.onError?.({ method, url, error, durationMs });
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
|
|
195
|
+
} catch {
|
|
136
196
|
}
|
|
137
197
|
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
138
198
|
return void 0;
|
|
@@ -144,7 +204,17 @@ var WebhooksCC = class {
|
|
|
144
204
|
return response.json();
|
|
145
205
|
} catch (error) {
|
|
146
206
|
if (error instanceof Error && error.name === "AbortError") {
|
|
147
|
-
|
|
207
|
+
const timeoutError = new TimeoutError(this.timeout);
|
|
208
|
+
try {
|
|
209
|
+
this.hooks.onError?.({
|
|
210
|
+
method,
|
|
211
|
+
url,
|
|
212
|
+
error: timeoutError,
|
|
213
|
+
durationMs: Date.now() - start
|
|
214
|
+
});
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
throw timeoutError;
|
|
148
218
|
}
|
|
149
219
|
throw error;
|
|
150
220
|
} finally {
|
|
@@ -155,7 +225,40 @@ var WebhooksCC = class {
|
|
|
155
225
|
function sleep(ms) {
|
|
156
226
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
157
227
|
}
|
|
228
|
+
|
|
229
|
+
// src/helpers.ts
|
|
230
|
+
function parseJsonBody(request) {
|
|
231
|
+
if (!request.body) return void 0;
|
|
232
|
+
try {
|
|
233
|
+
return JSON.parse(request.body);
|
|
234
|
+
} catch {
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function isStripeWebhook(request) {
|
|
239
|
+
return Object.keys(request.headers).some((k) => k.toLowerCase() === "stripe-signature");
|
|
240
|
+
}
|
|
241
|
+
function isGitHubWebhook(request) {
|
|
242
|
+
return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-github-event");
|
|
243
|
+
}
|
|
244
|
+
function matchJsonField(field, value) {
|
|
245
|
+
return (request) => {
|
|
246
|
+
const body = parseJsonBody(request);
|
|
247
|
+
if (typeof body !== "object" || body === null) return false;
|
|
248
|
+
if (!Object.prototype.hasOwnProperty.call(body, field)) return false;
|
|
249
|
+
return body[field] === value;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
158
252
|
export {
|
|
159
253
|
ApiError,
|
|
160
|
-
|
|
254
|
+
NotFoundError,
|
|
255
|
+
RateLimitError,
|
|
256
|
+
TimeoutError,
|
|
257
|
+
UnauthorizedError,
|
|
258
|
+
WebhooksCC,
|
|
259
|
+
WebhooksCCError,
|
|
260
|
+
isGitHubWebhook,
|
|
261
|
+
isStripeWebhook,
|
|
262
|
+
matchJsonField,
|
|
263
|
+
parseJsonBody
|
|
161
264
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webhooks-cc/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "TypeScript SDK for webhooks.cc — create endpoints, capture requests, assert in tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"keywords": [
|
|
19
20
|
"webhook",
|