@skillrecordings/sdk 0.2.0 → 0.2.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,77 @@
1
+ /**
2
+ * User entity returned by app integration.
3
+ * Replaces Customer for consistency with SupportIntegration interface.
4
+ */
5
+ interface User {
6
+ id: string;
7
+ email: string;
8
+ name?: string;
9
+ createdAt: Date;
10
+ }
11
+ /**
12
+ * @deprecated Use User instead. Kept for backwards compatibility.
13
+ */
14
+ type Customer = User;
15
+ /**
16
+ * Purchase record with product and payment details.
17
+ * Used by agent tools to display purchase history.
18
+ */
19
+ interface Purchase {
20
+ id: string;
21
+ productId: string;
22
+ productName: string;
23
+ purchasedAt: Date;
24
+ amount: number;
25
+ currency: string;
26
+ stripeChargeId?: string;
27
+ status: 'active' | 'refunded' | 'transferred';
28
+ }
29
+ /**
30
+ * Subscription entity for recurring billing.
31
+ * Optional method - apps may not support subscriptions.
32
+ */
33
+ interface Subscription {
34
+ id: string;
35
+ productId: string;
36
+ productName: string;
37
+ status: 'active' | 'cancelled' | 'expired' | 'paused';
38
+ currentPeriodStart: Date;
39
+ currentPeriodEnd: Date;
40
+ cancelAtPeriodEnd: boolean;
41
+ }
42
+ /**
43
+ * Generic result type for mutations (refund, transfer, updates).
44
+ */
45
+ interface ActionResult {
46
+ success: boolean;
47
+ error?: string;
48
+ }
49
+ /**
50
+ * Claimed seat for team/bulk purchases.
51
+ * Used by getClaimedSeats optional method.
52
+ */
53
+ interface ClaimedSeat {
54
+ userId: string;
55
+ email: string;
56
+ claimedAt: Date;
57
+ }
58
+ /**
59
+ * Refund request payload.
60
+ * @deprecated Use revokeAccess via SupportIntegration instead.
61
+ */
62
+ interface RefundRequest {
63
+ purchaseId: string;
64
+ reason: string;
65
+ amount?: number;
66
+ }
67
+ /**
68
+ * Refund result.
69
+ * @deprecated Use ActionResult instead.
70
+ */
71
+ interface RefundResult {
72
+ success: boolean;
73
+ refundId?: string;
74
+ error?: string;
75
+ }
76
+
77
+ export type { ActionResult, ClaimedSeat, Customer, Purchase, RefundRequest, RefundResult, Subscription, User };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,22 +1,44 @@
1
1
  {
2
2
  "name": "@skillrecordings/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
+ "type": "module",
5
6
  "exports": {
6
- ".": "./src/index.ts",
7
- "./types": "./src/types.ts",
8
- "./integration": "./src/integration.ts",
9
- "./adapter": "./src/adapter.ts",
10
- "./client": "./src/client.ts",
11
- "./handler": "./src/handler.ts"
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./types": {
12
+ "types": "./dist/types.d.ts",
13
+ "import": "./dist/types.js"
14
+ },
15
+ "./integration": {
16
+ "types": "./dist/integration.d.ts",
17
+ "import": "./dist/integration.js"
18
+ },
19
+ "./adapter": {
20
+ "types": "./dist/adapter.d.ts",
21
+ "import": "./dist/adapter.js"
22
+ },
23
+ "./client": {
24
+ "types": "./dist/client.d.ts",
25
+ "import": "./dist/client.js"
26
+ },
27
+ "./handler": {
28
+ "types": "./dist/handler.d.ts",
29
+ "import": "./dist/handler.js"
30
+ }
12
31
  },
32
+ "files": ["dist"],
13
33
  "scripts": {
34
+ "build": "tsup",
14
35
  "check-types": "tsc --noEmit",
15
36
  "test": "vitest --run"
16
37
  },
17
38
  "devDependencies": {
18
39
  "@repo/typescript-config": "*",
19
40
  "@types/node": "^22.15.3",
41
+ "tsup": "^8.0.0",
20
42
  "typescript": "5.9.2"
21
43
  }
22
44
  }
package/CHANGELOG.md DELETED
@@ -1,12 +0,0 @@
1
- # @skillrecordings/sdk
2
-
3
- ## 0.2.0
4
-
5
- ### Minor Changes
6
-
7
- - 91c7136: Initial public release of the Skill Recordings Support SDK.
8
-
9
- Provides the integration contract for apps to connect to the support platform:
10
- - `IntegrationClient` for querying user data and purchases
11
- - Webhook handler utilities for SDK-to-platform communication
12
- - Type definitions for the integration interface
@@ -1,351 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import type { User, Purchase, ActionResult } from '../integration';
3
-
4
- // Import will fail until we create the client
5
- import { IntegrationClient } from '../client';
6
-
7
- describe('IntegrationClient', () => {
8
- let fetchMock: ReturnType<typeof vi.fn>;
9
- let client: IntegrationClient;
10
- const baseUrl = 'https://app.example.com';
11
- const webhookSecret = 'whsec_test123';
12
-
13
- beforeEach(() => {
14
- fetchMock = vi.fn();
15
- global.fetch = fetchMock;
16
-
17
- client = new IntegrationClient({
18
- baseUrl,
19
- webhookSecret,
20
- });
21
- });
22
-
23
- afterEach(() => {
24
- vi.restoreAllMocks();
25
- });
26
-
27
- describe('constructor', () => {
28
- it('creates client with baseUrl and secret', () => {
29
- expect(client).toBeInstanceOf(IntegrationClient);
30
- });
31
-
32
- it('strips trailing slash from baseUrl', () => {
33
- const clientWithSlash = new IntegrationClient({
34
- baseUrl: 'https://app.example.com/',
35
- webhookSecret: 'secret',
36
- });
37
- expect(clientWithSlash).toBeInstanceOf(IntegrationClient);
38
- });
39
- });
40
-
41
- describe('HMAC signature', () => {
42
- it('signs requests with X-Support-Signature header', async () => {
43
- const user: User = {
44
- id: 'usr_123',
45
- email: 'test@example.com',
46
- name: 'Test User',
47
- createdAt: new Date(),
48
- };
49
-
50
- fetchMock.mockResolvedValueOnce({
51
- ok: true,
52
- json: async () => user,
53
- });
54
-
55
- await client.lookupUser('test@example.com');
56
-
57
- expect(fetchMock).toHaveBeenCalledWith(
58
- `${baseUrl}/api/support/lookup-user`,
59
- expect.objectContaining({
60
- headers: expect.objectContaining({
61
- 'X-Support-Signature': expect.stringMatching(/^t=\d+,v1=[a-f0-9]+$/),
62
- }),
63
- }),
64
- );
65
- });
66
-
67
- it('includes timestamp in signature', async () => {
68
- fetchMock.mockResolvedValueOnce({
69
- ok: true,
70
- json: async () => null,
71
- });
72
-
73
- const now = Date.now();
74
- await client.lookupUser('test@example.com');
75
-
76
- const call = fetchMock.mock.calls[0];
77
- const signature = call?.[1]?.headers?.['X-Support-Signature'];
78
- expect(signature).toBeDefined();
79
- const timestampPart = (signature as string).split('t=')[1]?.split(',')[0];
80
- expect(timestampPart).toBeDefined();
81
- const timestamp = parseInt(timestampPart as string);
82
-
83
- // Timestamp should be within 1 second of now
84
- expect(Math.abs(timestamp - now / 1000)).toBeLessThan(1);
85
- });
86
- });
87
-
88
- describe('lookupUser', () => {
89
- it('calls /api/support/lookup-user endpoint', async () => {
90
- const user: User = {
91
- id: 'usr_123',
92
- email: 'test@example.com',
93
- name: 'Test User',
94
- createdAt: new Date(),
95
- };
96
-
97
- fetchMock.mockResolvedValueOnce({
98
- ok: true,
99
- json: async () => user,
100
- });
101
-
102
- const result = await client.lookupUser('test@example.com');
103
-
104
- expect(result).toEqual(user);
105
- expect(fetchMock).toHaveBeenCalledWith(
106
- `${baseUrl}/api/support/lookup-user`,
107
- expect.objectContaining({
108
- method: 'POST',
109
- body: JSON.stringify({ email: 'test@example.com' }),
110
- }),
111
- );
112
- });
113
-
114
- it('returns null when user not found', async () => {
115
- fetchMock.mockResolvedValueOnce({
116
- ok: true,
117
- json: async () => null,
118
- });
119
-
120
- const result = await client.lookupUser('notfound@example.com');
121
- expect(result).toBeNull();
122
- });
123
- });
124
-
125
- describe('getPurchases', () => {
126
- it('calls /api/support/get-purchases endpoint', async () => {
127
- const purchases: Purchase[] = [
128
- {
129
- id: 'pur_123',
130
- productId: 'prod_123',
131
- productName: 'Test Product',
132
- purchasedAt: new Date(),
133
- amount: 10000,
134
- currency: 'usd',
135
- status: 'active',
136
- },
137
- ];
138
-
139
- fetchMock.mockResolvedValueOnce({
140
- ok: true,
141
- json: async () => purchases,
142
- });
143
-
144
- const result = await client.getPurchases('usr_123');
145
-
146
- expect(result).toEqual(purchases);
147
- expect(fetchMock).toHaveBeenCalledWith(
148
- `${baseUrl}/api/support/get-purchases`,
149
- expect.objectContaining({
150
- method: 'POST',
151
- body: JSON.stringify({ userId: 'usr_123' }),
152
- }),
153
- );
154
- });
155
-
156
- it('returns empty array when no purchases', async () => {
157
- fetchMock.mockResolvedValueOnce({
158
- ok: true,
159
- json: async () => [],
160
- });
161
-
162
- const result = await client.getPurchases('usr_123');
163
- expect(result).toEqual([]);
164
- });
165
- });
166
-
167
- describe('revokeAccess', () => {
168
- it('calls /api/support/revoke-access endpoint', async () => {
169
- const actionResult: ActionResult = { success: true };
170
-
171
- fetchMock.mockResolvedValueOnce({
172
- ok: true,
173
- json: async () => actionResult,
174
- });
175
-
176
- const result = await client.revokeAccess({
177
- purchaseId: 'pur_123',
178
- reason: 'Customer requested refund',
179
- refundId: 're_123',
180
- });
181
-
182
- expect(result).toEqual(actionResult);
183
- expect(fetchMock).toHaveBeenCalledWith(
184
- `${baseUrl}/api/support/revoke-access`,
185
- expect.objectContaining({
186
- method: 'POST',
187
- body: JSON.stringify({
188
- purchaseId: 'pur_123',
189
- reason: 'Customer requested refund',
190
- refundId: 're_123',
191
- }),
192
- }),
193
- );
194
- });
195
- });
196
-
197
- describe('transferPurchase', () => {
198
- it('calls /api/support/transfer-purchase endpoint', async () => {
199
- const actionResult: ActionResult = { success: true };
200
-
201
- fetchMock.mockResolvedValueOnce({
202
- ok: true,
203
- json: async () => actionResult,
204
- });
205
-
206
- const result = await client.transferPurchase({
207
- purchaseId: 'pur_123',
208
- fromUserId: 'usr_123',
209
- toEmail: 'new@example.com',
210
- });
211
-
212
- expect(result).toEqual(actionResult);
213
- expect(fetchMock).toHaveBeenCalledWith(
214
- `${baseUrl}/api/support/transfer-purchase`,
215
- expect.objectContaining({
216
- method: 'POST',
217
- body: JSON.stringify({
218
- purchaseId: 'pur_123',
219
- fromUserId: 'usr_123',
220
- toEmail: 'new@example.com',
221
- }),
222
- }),
223
- );
224
- });
225
- });
226
-
227
- describe('generateMagicLink', () => {
228
- it('calls /api/support/generate-magic-link endpoint', async () => {
229
- const magicLink = { url: 'https://app.example.com/auth/magic?token=abc123' };
230
-
231
- fetchMock.mockResolvedValueOnce({
232
- ok: true,
233
- json: async () => magicLink,
234
- });
235
-
236
- const result = await client.generateMagicLink({
237
- email: 'test@example.com',
238
- expiresIn: 3600,
239
- });
240
-
241
- expect(result).toEqual(magicLink);
242
- expect(fetchMock).toHaveBeenCalledWith(
243
- `${baseUrl}/api/support/generate-magic-link`,
244
- expect.objectContaining({
245
- method: 'POST',
246
- body: JSON.stringify({
247
- email: 'test@example.com',
248
- expiresIn: 3600,
249
- }),
250
- }),
251
- );
252
- });
253
- });
254
-
255
- describe('error handling', () => {
256
- it('throws when response is not ok', async () => {
257
- fetchMock.mockResolvedValueOnce({
258
- ok: false,
259
- status: 500,
260
- statusText: 'Internal Server Error',
261
- });
262
-
263
- await expect(client.lookupUser('test@example.com')).rejects.toThrow(
264
- 'Integration request failed: 500 Internal Server Error',
265
- );
266
- });
267
-
268
- it('includes error message from response when available', async () => {
269
- fetchMock.mockResolvedValueOnce({
270
- ok: false,
271
- status: 400,
272
- statusText: 'Bad Request',
273
- json: async () => ({ error: 'Invalid email format' }),
274
- });
275
-
276
- await expect(client.lookupUser('invalid')).rejects.toThrow('Invalid email format');
277
- });
278
-
279
- it('handles network errors', async () => {
280
- fetchMock.mockRejectedValueOnce(new Error('Network error'));
281
-
282
- await expect(client.lookupUser('test@example.com')).rejects.toThrow('Network error');
283
- });
284
- });
285
-
286
- describe('optional methods', () => {
287
- it('calls getSubscriptions when implemented', async () => {
288
- fetchMock.mockResolvedValueOnce({
289
- ok: true,
290
- json: async () => [],
291
- });
292
-
293
- const result = await client.getSubscriptions?.('usr_123');
294
- expect(result).toEqual([]);
295
- expect(fetchMock).toHaveBeenCalledWith(
296
- `${baseUrl}/api/support/get-subscriptions`,
297
- expect.any(Object),
298
- );
299
- });
300
-
301
- it('calls updateEmail when implemented', async () => {
302
- fetchMock.mockResolvedValueOnce({
303
- ok: true,
304
- json: async () => ({ success: true }),
305
- });
306
-
307
- const result = await client.updateEmail?.({
308
- userId: 'usr_123',
309
- newEmail: 'new@example.com',
310
- });
311
-
312
- expect(result).toEqual({ success: true });
313
- expect(fetchMock).toHaveBeenCalledWith(
314
- `${baseUrl}/api/support/update-email`,
315
- expect.any(Object),
316
- );
317
- });
318
-
319
- it('calls updateName when implemented', async () => {
320
- fetchMock.mockResolvedValueOnce({
321
- ok: true,
322
- json: async () => ({ success: true }),
323
- });
324
-
325
- const result = await client.updateName?.({
326
- userId: 'usr_123',
327
- newName: 'New Name',
328
- });
329
-
330
- expect(result).toEqual({ success: true });
331
- expect(fetchMock).toHaveBeenCalledWith(
332
- `${baseUrl}/api/support/update-name`,
333
- expect.any(Object),
334
- );
335
- });
336
-
337
- it('calls getClaimedSeats when implemented', async () => {
338
- fetchMock.mockResolvedValueOnce({
339
- ok: true,
340
- json: async () => [],
341
- });
342
-
343
- const result = await client.getClaimedSeats?.('bulk_123');
344
- expect(result).toEqual([]);
345
- expect(fetchMock).toHaveBeenCalledWith(
346
- `${baseUrl}/api/support/get-claimed-seats`,
347
- expect.any(Object),
348
- );
349
- });
350
- });
351
- });