@withaevum/sdk 0.0.1

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.
@@ -0,0 +1,176 @@
1
+ export type WebhookEventType = 'booking.created' | 'booking.updated' | 'booking.cancelled' | 'booking.confirmed' | 'payment.succeeded' | 'payment.failed' | 'payment.refunded' | 'offering.created' | 'offering.updated' | 'offering.deleted' | 'availability.updated' | 'webhook.test';
2
+ export interface WebhookEvent {
3
+ type: WebhookEventType;
4
+ version: string;
5
+ id: string;
6
+ delivery_id: string;
7
+ timestamp: string;
8
+ data: Record<string, unknown>;
9
+ }
10
+ export interface BookingCreatedEvent extends WebhookEvent {
11
+ type: 'booking.created';
12
+ data: {
13
+ id: string;
14
+ organization_id: string;
15
+ customer_id: string | null;
16
+ status: string;
17
+ start_time: string;
18
+ end_time: string;
19
+ price_cents: number | null;
20
+ notes: string | null;
21
+ provider_ids: string[];
22
+ offering_ids: string[];
23
+ customer?: {
24
+ id: string;
25
+ name: string | null;
26
+ email: string | null;
27
+ phone: string | null;
28
+ };
29
+ };
30
+ }
31
+ export interface BookingUpdatedEvent extends WebhookEvent {
32
+ type: 'booking.updated';
33
+ data: {
34
+ id: string;
35
+ organization_id: string;
36
+ customer_id: string | null;
37
+ status: string;
38
+ start_time: string;
39
+ end_time: string;
40
+ price_cents: number | null;
41
+ notes: string | null;
42
+ provider_ids: string[];
43
+ offering_ids: string[];
44
+ previous_status?: string;
45
+ customer?: {
46
+ id: string;
47
+ name: string | null;
48
+ email: string | null;
49
+ phone: string | null;
50
+ };
51
+ };
52
+ }
53
+ export interface BookingCancelledEvent extends WebhookEvent {
54
+ type: 'booking.cancelled';
55
+ data: {
56
+ id: string;
57
+ organization_id: string;
58
+ cancelled_at: string;
59
+ cancellation_reason?: string;
60
+ [key: string]: unknown;
61
+ };
62
+ }
63
+ export interface BookingConfirmedEvent extends WebhookEvent {
64
+ type: 'booking.confirmed';
65
+ data: {
66
+ id: string;
67
+ organization_id: string;
68
+ confirmed_at: string;
69
+ [key: string]: unknown;
70
+ };
71
+ }
72
+ export interface PaymentSucceededEvent extends WebhookEvent {
73
+ type: 'payment.succeeded';
74
+ data: {
75
+ id: string;
76
+ booking_id: string;
77
+ organization_id: string;
78
+ amount_cents: number;
79
+ currency: string;
80
+ payment_intent_id: string;
81
+ charged_at: string;
82
+ customer_id: string | null;
83
+ };
84
+ }
85
+ export interface PaymentFailedEvent extends WebhookEvent {
86
+ type: 'payment.failed';
87
+ data: {
88
+ id: string;
89
+ booking_id: string;
90
+ organization_id: string;
91
+ amount_cents: number;
92
+ payment_intent_id: string;
93
+ failure_reason?: string;
94
+ customer_id: string | null;
95
+ };
96
+ }
97
+ export interface PaymentRefundedEvent extends WebhookEvent {
98
+ type: 'payment.refunded';
99
+ data: {
100
+ id: string;
101
+ booking_id: string;
102
+ organization_id: string;
103
+ refund_amount_cents: number;
104
+ refunded_at: string;
105
+ customer_id: string | null;
106
+ };
107
+ }
108
+ export interface WebhookTestEvent extends WebhookEvent {
109
+ type: 'webhook.test';
110
+ data: {
111
+ message: string;
112
+ test: boolean;
113
+ organization_id: string;
114
+ };
115
+ }
116
+ export declare class WebhookVerificationError extends Error {
117
+ constructor(message: string);
118
+ }
119
+ /**
120
+ * Extract webhook signature from request headers
121
+ * @param headers - Request headers (Headers object or Record<string, string>)
122
+ * @returns Signature string or null if not found
123
+ */
124
+ export declare function extractWebhookSignature(headers: Headers | Record<string, string | string[] | null | undefined>): string | null;
125
+ /**
126
+ * Verify webhook signature and parse event
127
+ * Supports secret rotation by checking previous secret if provided and not expired
128
+ * @param body - Raw request body as string
129
+ * @param signature - Signature from X-Aevum-Signature header
130
+ * @param secret - Webhook secret
131
+ * @param previousSecret - Previous secret (optional, for secret rotation grace period)
132
+ * @param previousSecretExpiresAt - When previous secret expires (optional, ISO 8601 string or Date)
133
+ * @returns Parsed webhook event
134
+ * @throws WebhookVerificationError if signature verification fails
135
+ */
136
+ export declare function verifyWebhook(body: string, signature: string, secret: string, previousSecret?: string | null, previousSecretExpiresAt?: string | Date | null): WebhookEvent;
137
+ /**
138
+ * Type guard: Check if event is a booking event
139
+ */
140
+ export declare function isBookingEvent(event: WebhookEvent): event is BookingCreatedEvent | BookingUpdatedEvent | BookingCancelledEvent | BookingConfirmedEvent;
141
+ /**
142
+ * Type guard: Check if event is a payment event
143
+ */
144
+ export declare function isPaymentEvent(event: WebhookEvent): event is PaymentSucceededEvent | PaymentFailedEvent | PaymentRefundedEvent;
145
+ /**
146
+ * Type guard: Check if event is booking.created
147
+ */
148
+ export declare function isBookingCreatedEvent(event: WebhookEvent): event is BookingCreatedEvent;
149
+ /**
150
+ * Type guard: Check if event is booking.updated
151
+ */
152
+ export declare function isBookingUpdatedEvent(event: WebhookEvent): event is BookingUpdatedEvent;
153
+ /**
154
+ * Type guard: Check if event is booking.cancelled
155
+ */
156
+ export declare function isBookingCancelledEvent(event: WebhookEvent): event is BookingCancelledEvent;
157
+ /**
158
+ * Type guard: Check if event is booking.confirmed
159
+ */
160
+ export declare function isBookingConfirmedEvent(event: WebhookEvent): event is BookingConfirmedEvent;
161
+ /**
162
+ * Type guard: Check if event is payment.succeeded
163
+ */
164
+ export declare function isPaymentSucceededEvent(event: WebhookEvent): event is PaymentSucceededEvent;
165
+ /**
166
+ * Type guard: Check if event is payment.failed
167
+ */
168
+ export declare function isPaymentFailedEvent(event: WebhookEvent): event is PaymentFailedEvent;
169
+ /**
170
+ * Type guard: Check if event is payment.refunded
171
+ */
172
+ export declare function isPaymentRefundedEvent(event: WebhookEvent): event is PaymentRefundedEvent;
173
+ /**
174
+ * Type guard: Check if event is webhook.test
175
+ */
176
+ export declare function isWebhookTestEvent(event: WebhookEvent): event is WebhookTestEvent;
@@ -0,0 +1,153 @@
1
+ // packages/sdk/src/webhooks.ts
2
+ // Webhook verification utilities
3
+ import crypto from 'crypto';
4
+ export class WebhookVerificationError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'WebhookVerificationError';
8
+ }
9
+ }
10
+ /**
11
+ * Extract webhook signature from request headers
12
+ * @param headers - Request headers (Headers object or Record<string, string>)
13
+ * @returns Signature string or null if not found
14
+ */
15
+ export function extractWebhookSignature(headers) {
16
+ if (headers instanceof Headers) {
17
+ return headers.get('x-aevum-signature');
18
+ }
19
+ const signature = headers['x-aevum-signature'];
20
+ if (typeof signature === 'string') {
21
+ return signature;
22
+ }
23
+ if (Array.isArray(signature) && signature.length > 0) {
24
+ return signature[0];
25
+ }
26
+ return null;
27
+ }
28
+ /**
29
+ * Verify webhook signature and parse event
30
+ * Supports secret rotation by checking previous secret if provided and not expired
31
+ * @param body - Raw request body as string
32
+ * @param signature - Signature from X-Aevum-Signature header
33
+ * @param secret - Webhook secret
34
+ * @param previousSecret - Previous secret (optional, for secret rotation grace period)
35
+ * @param previousSecretExpiresAt - When previous secret expires (optional, ISO 8601 string or Date)
36
+ * @returns Parsed webhook event
37
+ * @throws WebhookVerificationError if signature verification fails
38
+ */
39
+ export function verifyWebhook(body, signature, secret, previousSecret, previousSecretExpiresAt) {
40
+ if (!body || !signature || !secret) {
41
+ throw new WebhookVerificationError('Missing required parameters: body, signature, or secret');
42
+ }
43
+ // Extract signature from format: sha256=<hex>
44
+ const signatureMatch = signature.match(/^sha256=(.+)$/);
45
+ if (!signatureMatch) {
46
+ throw new WebhookVerificationError('Invalid signature format. Expected format: sha256=<hex>');
47
+ }
48
+ const providedSignature = signatureMatch[1];
49
+ // Compute signature with current secret
50
+ const computedSignature = crypto
51
+ .createHmac('sha256', secret)
52
+ .update(body)
53
+ .digest('hex');
54
+ // Compare signatures using constant-time comparison
55
+ let isValid = crypto.timingSafeEqual(Buffer.from(providedSignature, 'hex'), Buffer.from(computedSignature, 'hex'));
56
+ // If current secret doesn't match and previous secret is provided, check it
57
+ if (!isValid &&
58
+ previousSecret &&
59
+ previousSecretExpiresAt) {
60
+ const expiresAt = typeof previousSecretExpiresAt === 'string'
61
+ ? new Date(previousSecretExpiresAt)
62
+ : previousSecretExpiresAt;
63
+ // Check if previous secret is still valid (within grace period)
64
+ if (new Date() < expiresAt) {
65
+ const previousComputedSignature = crypto
66
+ .createHmac('sha256', previousSecret)
67
+ .update(body)
68
+ .digest('hex');
69
+ isValid = crypto.timingSafeEqual(Buffer.from(providedSignature, 'hex'), Buffer.from(previousComputedSignature, 'hex'));
70
+ }
71
+ }
72
+ if (!isValid) {
73
+ throw new WebhookVerificationError('Invalid webhook signature');
74
+ }
75
+ // Parse and validate event
76
+ let event;
77
+ try {
78
+ event = JSON.parse(body);
79
+ }
80
+ catch (error) {
81
+ throw new WebhookVerificationError(`Invalid JSON payload: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ // Validate event structure
84
+ if (!event.type || !event.version || !event.id || !event.delivery_id) {
85
+ throw new WebhookVerificationError('Invalid event structure: missing required fields');
86
+ }
87
+ return event;
88
+ }
89
+ /**
90
+ * Type guard: Check if event is a booking event
91
+ */
92
+ export function isBookingEvent(event) {
93
+ return (event.type === 'booking.created' ||
94
+ event.type === 'booking.updated' ||
95
+ event.type === 'booking.cancelled' ||
96
+ event.type === 'booking.confirmed');
97
+ }
98
+ /**
99
+ * Type guard: Check if event is a payment event
100
+ */
101
+ export function isPaymentEvent(event) {
102
+ return (event.type === 'payment.succeeded' ||
103
+ event.type === 'payment.failed' ||
104
+ event.type === 'payment.refunded');
105
+ }
106
+ /**
107
+ * Type guard: Check if event is booking.created
108
+ */
109
+ export function isBookingCreatedEvent(event) {
110
+ return event.type === 'booking.created';
111
+ }
112
+ /**
113
+ * Type guard: Check if event is booking.updated
114
+ */
115
+ export function isBookingUpdatedEvent(event) {
116
+ return event.type === 'booking.updated';
117
+ }
118
+ /**
119
+ * Type guard: Check if event is booking.cancelled
120
+ */
121
+ export function isBookingCancelledEvent(event) {
122
+ return event.type === 'booking.cancelled';
123
+ }
124
+ /**
125
+ * Type guard: Check if event is booking.confirmed
126
+ */
127
+ export function isBookingConfirmedEvent(event) {
128
+ return event.type === 'booking.confirmed';
129
+ }
130
+ /**
131
+ * Type guard: Check if event is payment.succeeded
132
+ */
133
+ export function isPaymentSucceededEvent(event) {
134
+ return event.type === 'payment.succeeded';
135
+ }
136
+ /**
137
+ * Type guard: Check if event is payment.failed
138
+ */
139
+ export function isPaymentFailedEvent(event) {
140
+ return event.type === 'payment.failed';
141
+ }
142
+ /**
143
+ * Type guard: Check if event is payment.refunded
144
+ */
145
+ export function isPaymentRefundedEvent(event) {
146
+ return event.type === 'payment.refunded';
147
+ }
148
+ /**
149
+ * Type guard: Check if event is webhook.test
150
+ */
151
+ export function isWebhookTestEvent(event) {
152
+ return event.type === 'webhook.test';
153
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@withaevum/sdk",
3
+ "version": "0.0.1",
4
+ "description": "TypeScript SDK for the Aevum API",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "module": "dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "prepublishOnly": "npm run build",
22
+ "test": "echo \"Error: no test specified\" && exit 1",
23
+ "lint": "echo \"No linting configured\""
24
+ },
25
+ "keywords": [
26
+ "aevum",
27
+ "scheduling",
28
+ "bookings",
29
+ "api",
30
+ "sdk",
31
+ "typescript",
32
+ "calendar",
33
+ "appointments"
34
+ ],
35
+ "author": "",
36
+ "license": "ISC",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/getaevum/aevum.git",
40
+ "directory": "packages/sdk"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^25.2.1",
50
+ "typescript": "^5.6.3"
51
+ }
52
+ }