@skillrecordings/sdk 0.2.2 → 0.3.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 +26 -9
- package/dist/client.d.ts +2 -1
- package/dist/client.js +3 -0
- package/dist/client.js.map +1 -1
- package/dist/handler.js +11 -0
- package/dist/handler.js.map +1 -1
- package/dist/integration.d.ts +29 -2
- package/dist/types.d.ts +66 -1
- package/package.json +1 -6
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# packages/sdk
|
|
2
2
|
|
|
3
|
-
Integration contract
|
|
3
|
+
Integration contract for Skill Recordings products.
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
|
-
- Define the SDK surface for app integrations
|
|
7
|
-
- Provide adapters for supported platforms
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
- `packages/sdk/src/`
|
|
7
|
+
Apps (Total TypeScript, Pro Tailwind, etc.) implement `SupportIntegration` interface. The support platform calls these methods via HTTP with HMAC auth.
|
|
11
8
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
## Interface
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
interface SupportIntegration {
|
|
13
|
+
lookupUser(email: string): Promise<User | null>
|
|
14
|
+
getPurchases(userId: string): Promise<Purchase[]>
|
|
15
|
+
getSubscriptions?(userId: string): Promise<Subscription[]>
|
|
16
|
+
revokeAccess(params: { purchaseId, reason, refundId }): Promise<ActionResult>
|
|
17
|
+
transferPurchase(params: { purchaseId, fromUserId, toEmail }): Promise<ActionResult>
|
|
18
|
+
generateMagicLink(params: { email, expiresIn }): Promise<{ url: string }>
|
|
19
|
+
updateEmail?(params: { userId, newEmail }): Promise<ActionResult>
|
|
20
|
+
updateName?(params: { userId, newName }): Promise<ActionResult>
|
|
21
|
+
getClaimedSeats?(bulkCouponId: string): Promise<ClaimedSeat[]>
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Exports
|
|
26
|
+
|
|
27
|
+
- `integration.ts` - SupportIntegration interface + types
|
|
28
|
+
- `handler.ts` - Webhook handler factory
|
|
29
|
+
- `client.ts` - IntegrationClient for calling apps
|
|
30
|
+
- `adapter.ts` - Adapter utilities
|
|
31
|
+
- `types.ts` - Shared types (User, Purchase, etc.)
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SupportIntegration } from './integration.js';
|
|
2
|
-
import { User, Purchase, Subscription, ActionResult, ClaimedSeat } from './types.js';
|
|
2
|
+
import { User, Purchase, Subscription, ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse } from './types.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Client for calling app integration endpoints with HMAC-signed requests.
|
|
@@ -65,6 +65,7 @@ declare class IntegrationClient implements SupportIntegration {
|
|
|
65
65
|
newName: string;
|
|
66
66
|
}): Promise<ActionResult>;
|
|
67
67
|
getClaimedSeats(bulkCouponId: string): Promise<ClaimedSeat[]>;
|
|
68
|
+
searchContent(request: ContentSearchRequest): Promise<ContentSearchResponse>;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
export { IntegrationClient };
|
package/dist/client.js
CHANGED
|
@@ -78,6 +78,9 @@ var IntegrationClient = class {
|
|
|
78
78
|
async getClaimedSeats(bulkCouponId) {
|
|
79
79
|
return this.request("/api/support/get-claimed-seats", { bulkCouponId });
|
|
80
80
|
}
|
|
81
|
+
async searchContent(request) {
|
|
82
|
+
return this.request("/api/support/search-content", request);
|
|
83
|
+
}
|
|
81
84
|
};
|
|
82
85
|
export {
|
|
83
86
|
IntegrationClient
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { createHmac } from 'node:crypto'\nimport type {\n ActionResult,\n ClaimedSeat,\n Purchase,\n Subscription,\n SupportIntegration,\n User,\n} from './integration'\n\n/**\n * Client for calling app integration endpoints with HMAC-signed requests.\n *\n * Used by core to call app-specific support actions (lookupUser, getPurchases, etc.)\n * with Stripe-style HMAC-SHA256 signature verification.\n *\n * @example\n * ```typescript\n * import { IntegrationClient } from '@skillrecordings/sdk/client'\n *\n * const client = new IntegrationClient({\n * baseUrl: 'https://totaltypescript.com',\n * webhookSecret: 'whsec_abc123',\n * })\n *\n * const user = await client.lookupUser('test@example.com')\n * ```\n */\nexport class IntegrationClient implements SupportIntegration {\n private readonly baseUrl: string\n private readonly webhookSecret: string\n\n constructor(config: { baseUrl: string; webhookSecret: string }) {\n // Strip trailing slash for consistent URL construction\n this.baseUrl = config.baseUrl.replace(/\\/$/, '')\n this.webhookSecret = config.webhookSecret\n }\n\n /**\n * Generate HMAC-SHA256 signature for request body.\n * Format: `t=<timestamp>,v1=<signature>`\n *\n * Signature is computed as: HMAC-SHA256(timestamp + \".\" + body, secret)\n */\n private generateSignature(body: string): string {\n const timestamp = Math.floor(Date.now() / 1000)\n const signedPayload = `${timestamp}.${body}`\n const signature = createHmac('sha256', this.webhookSecret)\n .update(signedPayload)\n .digest('hex')\n\n return `t=${timestamp},v1=${signature}`\n }\n\n /**\n * Make signed POST request to app integration endpoint.\n */\n private async request<T>(endpoint: string, payload: unknown): Promise<T> {\n const body = JSON.stringify(payload)\n const signature = this.generateSignature(body)\n\n const response = await fetch(`${this.baseUrl}${endpoint}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Support-Signature': signature,\n },\n body,\n })\n\n if (!response.ok) {\n // Try to extract error message from response body\n let errorMessage: string | undefined\n try {\n const errorBody = (await response.json()) as { error?: string }\n if (errorBody?.error) {\n errorMessage = errorBody.error\n }\n } catch {\n // If JSON parsing fails, ignore and use status text\n }\n\n if (errorMessage) {\n throw new Error(errorMessage)\n }\n throw new Error(\n `Integration request failed: ${response.status} ${response.statusText}`\n )\n }\n\n return (await response.json()) as T\n }\n\n async lookupUser(email: string): Promise<User | null> {\n return this.request('/api/support/lookup-user', { email })\n }\n\n async getPurchases(userId: string): Promise<Purchase[]> {\n return this.request('/api/support/get-purchases', { userId })\n }\n\n async getSubscriptions(userId: string): Promise<Subscription[]> {\n return this.request('/api/support/get-subscriptions', { userId })\n }\n\n async revokeAccess(params: {\n purchaseId: string\n reason: string\n refundId: string\n }): Promise<ActionResult> {\n return this.request('/api/support/revoke-access', params)\n }\n\n async transferPurchase(params: {\n purchaseId: string\n fromUserId: string\n toEmail: string\n }): Promise<ActionResult> {\n return this.request('/api/support/transfer-purchase', params)\n }\n\n async generateMagicLink(params: {\n email: string\n expiresIn: number\n }): Promise<{ url: string }> {\n return this.request('/api/support/generate-magic-link', params)\n }\n\n async updateEmail(params: {\n userId: string\n newEmail: string\n }): Promise<ActionResult> {\n return this.request('/api/support/update-email', params)\n }\n\n async updateName(params: {\n userId: string\n newName: string\n }): Promise<ActionResult> {\n return this.request('/api/support/update-name', params)\n }\n\n async getClaimedSeats(bulkCouponId: string): Promise<ClaimedSeat[]> {\n return this.request('/api/support/get-claimed-seats', { bulkCouponId })\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { createHmac } from 'node:crypto'\nimport type {\n ActionResult,\n ClaimedSeat,\n ContentSearchRequest,\n ContentSearchResponse,\n Purchase,\n Subscription,\n SupportIntegration,\n User,\n} from './integration'\n\n/**\n * Client for calling app integration endpoints with HMAC-signed requests.\n *\n * Used by core to call app-specific support actions (lookupUser, getPurchases, etc.)\n * with Stripe-style HMAC-SHA256 signature verification.\n *\n * @example\n * ```typescript\n * import { IntegrationClient } from '@skillrecordings/sdk/client'\n *\n * const client = new IntegrationClient({\n * baseUrl: 'https://totaltypescript.com',\n * webhookSecret: 'whsec_abc123',\n * })\n *\n * const user = await client.lookupUser('test@example.com')\n * ```\n */\nexport class IntegrationClient implements SupportIntegration {\n private readonly baseUrl: string\n private readonly webhookSecret: string\n\n constructor(config: { baseUrl: string; webhookSecret: string }) {\n // Strip trailing slash for consistent URL construction\n this.baseUrl = config.baseUrl.replace(/\\/$/, '')\n this.webhookSecret = config.webhookSecret\n }\n\n /**\n * Generate HMAC-SHA256 signature for request body.\n * Format: `t=<timestamp>,v1=<signature>`\n *\n * Signature is computed as: HMAC-SHA256(timestamp + \".\" + body, secret)\n */\n private generateSignature(body: string): string {\n const timestamp = Math.floor(Date.now() / 1000)\n const signedPayload = `${timestamp}.${body}`\n const signature = createHmac('sha256', this.webhookSecret)\n .update(signedPayload)\n .digest('hex')\n\n return `t=${timestamp},v1=${signature}`\n }\n\n /**\n * Make signed POST request to app integration endpoint.\n */\n private async request<T>(endpoint: string, payload: unknown): Promise<T> {\n const body = JSON.stringify(payload)\n const signature = this.generateSignature(body)\n\n const response = await fetch(`${this.baseUrl}${endpoint}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Support-Signature': signature,\n },\n body,\n })\n\n if (!response.ok) {\n // Try to extract error message from response body\n let errorMessage: string | undefined\n try {\n const errorBody = (await response.json()) as { error?: string }\n if (errorBody?.error) {\n errorMessage = errorBody.error\n }\n } catch {\n // If JSON parsing fails, ignore and use status text\n }\n\n if (errorMessage) {\n throw new Error(errorMessage)\n }\n throw new Error(\n `Integration request failed: ${response.status} ${response.statusText}`\n )\n }\n\n return (await response.json()) as T\n }\n\n async lookupUser(email: string): Promise<User | null> {\n return this.request('/api/support/lookup-user', { email })\n }\n\n async getPurchases(userId: string): Promise<Purchase[]> {\n return this.request('/api/support/get-purchases', { userId })\n }\n\n async getSubscriptions(userId: string): Promise<Subscription[]> {\n return this.request('/api/support/get-subscriptions', { userId })\n }\n\n async revokeAccess(params: {\n purchaseId: string\n reason: string\n refundId: string\n }): Promise<ActionResult> {\n return this.request('/api/support/revoke-access', params)\n }\n\n async transferPurchase(params: {\n purchaseId: string\n fromUserId: string\n toEmail: string\n }): Promise<ActionResult> {\n return this.request('/api/support/transfer-purchase', params)\n }\n\n async generateMagicLink(params: {\n email: string\n expiresIn: number\n }): Promise<{ url: string }> {\n return this.request('/api/support/generate-magic-link', params)\n }\n\n async updateEmail(params: {\n userId: string\n newEmail: string\n }): Promise<ActionResult> {\n return this.request('/api/support/update-email', params)\n }\n\n async updateName(params: {\n userId: string\n newName: string\n }): Promise<ActionResult> {\n return this.request('/api/support/update-name', params)\n }\n\n async getClaimedSeats(bulkCouponId: string): Promise<ClaimedSeat[]> {\n return this.request('/api/support/get-claimed-seats', { bulkCouponId })\n }\n\n async searchContent(\n request: ContentSearchRequest\n ): Promise<ContentSearchResponse> {\n return this.request('/api/support/search-content', request)\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;AA8BpB,IAAM,oBAAN,MAAsD;AAAA,EAC1C;AAAA,EACA;AAAA,EAEjB,YAAY,QAAoD;AAE9D,SAAK,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAC/C,SAAK,gBAAgB,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,kBAAkB,MAAsB;AAC9C,UAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC9C,UAAM,gBAAgB,GAAG,SAAS,IAAI,IAAI;AAC1C,UAAM,YAAY,WAAW,UAAU,KAAK,aAAa,EACtD,OAAO,aAAa,EACpB,OAAO,KAAK;AAEf,WAAO,KAAK,SAAS,OAAO,SAAS;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,QAAW,UAAkB,SAA8B;AACvE,UAAM,OAAO,KAAK,UAAU,OAAO;AACnC,UAAM,YAAY,KAAK,kBAAkB,IAAI;AAE7C,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,GAAG,QAAQ,IAAI;AAAA,MACzD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,uBAAuB;AAAA,MACzB;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAEhB,UAAI;AACJ,UAAI;AACF,cAAM,YAAa,MAAM,SAAS,KAAK;AACvC,YAAI,WAAW,OAAO;AACpB,yBAAe,UAAU;AAAA,QAC3B;AAAA,MACF,QAAQ;AAAA,MAER;AAEA,UAAI,cAAc;AAChB,cAAM,IAAI,MAAM,YAAY;AAAA,MAC9B;AACA,YAAM,IAAI;AAAA,QACR,+BAA+B,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,MACvE;AAAA,IACF;AAEA,WAAQ,MAAM,SAAS,KAAK;AAAA,EAC9B;AAAA,EAEA,MAAM,WAAW,OAAqC;AACpD,WAAO,KAAK,QAAQ,4BAA4B,EAAE,MAAM,CAAC;AAAA,EAC3D;AAAA,EAEA,MAAM,aAAa,QAAqC;AACtD,WAAO,KAAK,QAAQ,8BAA8B,EAAE,OAAO,CAAC;AAAA,EAC9D;AAAA,EAEA,MAAM,iBAAiB,QAAyC;AAC9D,WAAO,KAAK,QAAQ,kCAAkC,EAAE,OAAO,CAAC;AAAA,EAClE;AAAA,EAEA,MAAM,aAAa,QAIO;AACxB,WAAO,KAAK,QAAQ,8BAA8B,MAAM;AAAA,EAC1D;AAAA,EAEA,MAAM,iBAAiB,QAIG;AACxB,WAAO,KAAK,QAAQ,kCAAkC,MAAM;AAAA,EAC9D;AAAA,EAEA,MAAM,kBAAkB,QAGK;AAC3B,WAAO,KAAK,QAAQ,oCAAoC,MAAM;AAAA,EAChE;AAAA,EAEA,MAAM,YAAY,QAGQ;AACxB,WAAO,KAAK,QAAQ,6BAA6B,MAAM;AAAA,EACzD;AAAA,EAEA,MAAM,WAAW,QAGS;AACxB,WAAO,KAAK,QAAQ,4BAA4B,MAAM;AAAA,EACxD;AAAA,EAEA,MAAM,gBAAgB,cAA8C;AAClE,WAAO,KAAK,QAAQ,kCAAkC,EAAE,aAAa,CAAC;AAAA,EACxE;AAAA,EAEA,MAAM,cACJ,SACgC;AAChC,WAAO,KAAK,QAAQ,+BAA+B,OAAO;AAAA,EAC5D;AACF;","names":[]}
|
package/dist/handler.js
CHANGED
|
@@ -144,6 +144,17 @@ async function routeAction(integration, action, body) {
|
|
|
144
144
|
const result = await integration.getClaimedSeats(bulkCouponId);
|
|
145
145
|
return { data: result, status: 200 };
|
|
146
146
|
}
|
|
147
|
+
case "searchContent": {
|
|
148
|
+
if (!integration.searchContent) {
|
|
149
|
+
return {
|
|
150
|
+
data: { error: "Method not implemented: searchContent" },
|
|
151
|
+
status: 501
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const params = body;
|
|
155
|
+
const result = await integration.searchContent(params);
|
|
156
|
+
return { data: result, status: 200 };
|
|
157
|
+
}
|
|
147
158
|
default:
|
|
148
159
|
return {
|
|
149
160
|
data: { error: `Unknown action: ${action}` },
|
package/dist/handler.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handler.ts"],"sourcesContent":["import { timingSafeEqual } from 'crypto';\nimport type { SupportIntegration } from './integration';\n\n/**\n * Configuration for createSupportHandler\n */\nexport interface SupportHandlerConfig {\n integration: SupportIntegration;\n webhookSecret: string;\n}\n\n/**\n * Request body for webhook actions\n */\ninterface WebhookRequest {\n action: string;\n [key: string]: unknown;\n}\n\n/**\n * Creates a Next.js API route handler for SupportIntegration.\n * Verifies HMAC-SHA256 signature and routes actions to integration methods.\n *\n * Signature format: timestamp=1234567890,v1=hex_signature\n * Payload to sign: timestamp.JSON.stringify(body)\n * Replay protection: 5 minute window\n *\n * @example\n * ```typescript\n * import { createSupportHandler } from '@skillrecordings/sdk/handler'\n * import { integration } from './integration'\n *\n * export const POST = createSupportHandler({\n * integration,\n * webhookSecret: process.env.SUPPORT_WEBHOOK_SECRET!,\n * })\n * ```\n */\nexport function createSupportHandler(\n config: SupportHandlerConfig,\n): (request: Request) => Promise<Response> {\n const { integration, webhookSecret } = config;\n\n return async function handler(request: Request): Promise<Response> {\n try {\n // 1. Extract signature header\n const signatureHeader = request.headers.get('x-support-signature');\n if (!signatureHeader) {\n return jsonResponse({ error: 'Missing signature header' }, 401);\n }\n\n // 2. Parse signature header (format: timestamp=1234567890,v1=hex_signature)\n const parts = signatureHeader.split(',');\n const timestampPart = parts.find((p) => p.startsWith('timestamp='));\n const signaturePart = parts.find((p) => p.startsWith('v1='));\n\n if (!timestampPart || !signaturePart) {\n return jsonResponse({ error: 'Invalid signature format' }, 401);\n }\n\n const timestampValue = timestampPart.split('=')[1];\n const signatureValue = signaturePart.split('=')[1];\n\n if (!timestampValue || !signatureValue) {\n return jsonResponse({ error: 'Invalid signature format' }, 401);\n }\n\n const timestamp = parseInt(timestampValue, 10);\n const receivedSignature = signatureValue;\n\n // 3. Verify timestamp (replay protection - 5 minute window)\n const now = Math.floor(Date.now() / 1000);\n const maxAge = 300; // 5 minutes in seconds\n if (now - timestamp > maxAge) {\n return jsonResponse({ error: 'Signature expired' }, 401);\n }\n\n // 4. Read and parse body\n const bodyText = await request.text();\n let body: WebhookRequest;\n\n try {\n body = JSON.parse(bodyText);\n } catch (err) {\n return jsonResponse({ error: 'Invalid JSON body' }, 400);\n }\n\n // 5. Compute expected signature\n const crypto = await import('crypto');\n const payload = `${timestamp}.${bodyText}`;\n const expectedSignature = crypto\n .createHmac('sha256', webhookSecret)\n .update(payload)\n .digest('hex');\n\n // 6. Timing-safe comparison to prevent timing attacks\n if (\n !timingSafeEqual(\n Buffer.from(receivedSignature),\n Buffer.from(expectedSignature),\n )\n ) {\n return jsonResponse({ error: 'Invalid signature' }, 401);\n }\n\n // 7. Extract action field\n const { action } = body;\n if (!action || typeof action !== 'string') {\n return jsonResponse({ error: 'Missing action field' }, 400);\n }\n\n // 8. Route to integration method\n const result = await routeAction(integration, action, body);\n return jsonResponse(result.data, result.status);\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error';\n return jsonResponse({ error: `Internal error: ${message}` }, 500);\n }\n };\n}\n\n/**\n * Routes action to appropriate integration method\n */\nasync function routeAction(\n integration: SupportIntegration,\n action: string,\n body: WebhookRequest,\n): Promise<{ data: unknown; status: number }> {\n try {\n switch (action) {\n case 'lookupUser': {\n const email = (body as unknown as { email: string }).email;\n const result = await integration.lookupUser(email);\n return { data: result, status: 200 };\n }\n\n case 'getPurchases': {\n const userId = (body as unknown as { userId: string }).userId;\n const result = await integration.getPurchases(userId);\n return { data: result, status: 200 };\n }\n\n case 'revokeAccess': {\n const params = body as unknown as {\n purchaseId: string;\n reason: string;\n refundId: string;\n };\n const result = await integration.revokeAccess({\n purchaseId: params.purchaseId,\n reason: params.reason,\n refundId: params.refundId,\n });\n return { data: result, status: 200 };\n }\n\n case 'transferPurchase': {\n const params = body as unknown as {\n purchaseId: string;\n fromUserId: string;\n toEmail: string;\n };\n const result = await integration.transferPurchase({\n purchaseId: params.purchaseId,\n fromUserId: params.fromUserId,\n toEmail: params.toEmail,\n });\n return { data: result, status: 200 };\n }\n\n case 'generateMagicLink': {\n const params = body as unknown as {\n email: string;\n expiresIn: number;\n };\n const result = await integration.generateMagicLink({\n email: params.email,\n expiresIn: params.expiresIn,\n });\n return { data: result, status: 200 };\n }\n\n // Optional methods\n case 'getSubscriptions': {\n if (!integration.getSubscriptions) {\n return {\n data: { error: 'Method not implemented: getSubscriptions' },\n status: 501,\n };\n }\n const userId = (body as unknown as { userId: string }).userId;\n const result = await integration.getSubscriptions(userId);\n return { data: result, status: 200 };\n }\n\n case 'updateEmail': {\n if (!integration.updateEmail) {\n return {\n data: { error: 'Method not implemented: updateEmail' },\n status: 501,\n };\n }\n const params = body as unknown as {\n userId: string;\n newEmail: string;\n };\n const result = await integration.updateEmail({\n userId: params.userId,\n newEmail: params.newEmail,\n });\n return { data: result, status: 200 };\n }\n\n case 'updateName': {\n if (!integration.updateName) {\n return {\n data: { error: 'Method not implemented: updateName' },\n status: 501,\n };\n }\n const params = body as unknown as {\n userId: string;\n newName: string;\n };\n const result = await integration.updateName({\n userId: params.userId,\n newName: params.newName,\n });\n return { data: result, status: 200 };\n }\n\n case 'getClaimedSeats': {\n if (!integration.getClaimedSeats) {\n return {\n data: { error: 'Method not implemented: getClaimedSeats' },\n status: 501,\n };\n }\n const bulkCouponId = (body as unknown as { bulkCouponId: string })\n .bulkCouponId;\n const result = await integration.getClaimedSeats(bulkCouponId);\n return { data: result, status: 200 };\n }\n\n default:\n return {\n data: { error: `Unknown action: ${action}` },\n status: 400,\n };\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error';\n return {\n data: { error: message },\n status: 500,\n };\n }\n}\n\n/**\n * Helper to create JSON responses\n */\nfunction jsonResponse(data: unknown, status: number): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: {\n 'content-type': 'application/json',\n },\n });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/handler.ts"],"sourcesContent":["import { timingSafeEqual } from 'crypto'\nimport type { SupportIntegration } from './integration'\nimport type { ContentSearchRequest, ContentSearchResponse } from './types'\n\n/**\n * Configuration for createSupportHandler\n */\nexport interface SupportHandlerConfig {\n integration: SupportIntegration\n webhookSecret: string\n}\n\n/**\n * Request body for webhook actions\n */\ninterface WebhookRequest {\n action: string\n [key: string]: unknown\n}\n\n/**\n * Creates a Next.js API route handler for SupportIntegration.\n * Verifies HMAC-SHA256 signature and routes actions to integration methods.\n *\n * Signature format: timestamp=1234567890,v1=hex_signature\n * Payload to sign: timestamp.JSON.stringify(body)\n * Replay protection: 5 minute window\n *\n * @example\n * ```typescript\n * import { createSupportHandler } from '@skillrecordings/sdk/handler'\n * import { integration } from './integration'\n *\n * export const POST = createSupportHandler({\n * integration,\n * webhookSecret: process.env.SUPPORT_WEBHOOK_SECRET!,\n * })\n * ```\n */\nexport function createSupportHandler(\n config: SupportHandlerConfig\n): (request: Request) => Promise<Response> {\n const { integration, webhookSecret } = config\n\n return async function handler(request: Request): Promise<Response> {\n try {\n // 1. Extract signature header\n const signatureHeader = request.headers.get('x-support-signature')\n if (!signatureHeader) {\n return jsonResponse({ error: 'Missing signature header' }, 401)\n }\n\n // 2. Parse signature header (format: timestamp=1234567890,v1=hex_signature)\n const parts = signatureHeader.split(',')\n const timestampPart = parts.find((p) => p.startsWith('timestamp='))\n const signaturePart = parts.find((p) => p.startsWith('v1='))\n\n if (!timestampPart || !signaturePart) {\n return jsonResponse({ error: 'Invalid signature format' }, 401)\n }\n\n const timestampValue = timestampPart.split('=')[1]\n const signatureValue = signaturePart.split('=')[1]\n\n if (!timestampValue || !signatureValue) {\n return jsonResponse({ error: 'Invalid signature format' }, 401)\n }\n\n const timestamp = parseInt(timestampValue, 10)\n const receivedSignature = signatureValue\n\n // 3. Verify timestamp (replay protection - 5 minute window)\n const now = Math.floor(Date.now() / 1000)\n const maxAge = 300 // 5 minutes in seconds\n if (now - timestamp > maxAge) {\n return jsonResponse({ error: 'Signature expired' }, 401)\n }\n\n // 4. Read and parse body\n const bodyText = await request.text()\n let body: WebhookRequest\n\n try {\n body = JSON.parse(bodyText)\n } catch (err) {\n return jsonResponse({ error: 'Invalid JSON body' }, 400)\n }\n\n // 5. Compute expected signature\n const crypto = await import('crypto')\n const payload = `${timestamp}.${bodyText}`\n const expectedSignature = crypto\n .createHmac('sha256', webhookSecret)\n .update(payload)\n .digest('hex')\n\n // 6. Timing-safe comparison to prevent timing attacks\n if (\n !timingSafeEqual(\n Buffer.from(receivedSignature),\n Buffer.from(expectedSignature)\n )\n ) {\n return jsonResponse({ error: 'Invalid signature' }, 401)\n }\n\n // 7. Extract action field\n const { action } = body\n if (!action || typeof action !== 'string') {\n return jsonResponse({ error: 'Missing action field' }, 400)\n }\n\n // 8. Route to integration method\n const result = await routeAction(integration, action, body)\n return jsonResponse(result.data, result.status)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n return jsonResponse({ error: `Internal error: ${message}` }, 500)\n }\n }\n}\n\n/**\n * Routes action to appropriate integration method\n */\nasync function routeAction(\n integration: SupportIntegration,\n action: string,\n body: WebhookRequest\n): Promise<{ data: unknown; status: number }> {\n try {\n switch (action) {\n case 'lookupUser': {\n const email = (body as unknown as { email: string }).email\n const result = await integration.lookupUser(email)\n return { data: result, status: 200 }\n }\n\n case 'getPurchases': {\n const userId = (body as unknown as { userId: string }).userId\n const result = await integration.getPurchases(userId)\n return { data: result, status: 200 }\n }\n\n case 'revokeAccess': {\n const params = body as unknown as {\n purchaseId: string\n reason: string\n refundId: string\n }\n const result = await integration.revokeAccess({\n purchaseId: params.purchaseId,\n reason: params.reason,\n refundId: params.refundId,\n })\n return { data: result, status: 200 }\n }\n\n case 'transferPurchase': {\n const params = body as unknown as {\n purchaseId: string\n fromUserId: string\n toEmail: string\n }\n const result = await integration.transferPurchase({\n purchaseId: params.purchaseId,\n fromUserId: params.fromUserId,\n toEmail: params.toEmail,\n })\n return { data: result, status: 200 }\n }\n\n case 'generateMagicLink': {\n const params = body as unknown as {\n email: string\n expiresIn: number\n }\n const result = await integration.generateMagicLink({\n email: params.email,\n expiresIn: params.expiresIn,\n })\n return { data: result, status: 200 }\n }\n\n // Optional methods\n case 'getSubscriptions': {\n if (!integration.getSubscriptions) {\n return {\n data: { error: 'Method not implemented: getSubscriptions' },\n status: 501,\n }\n }\n const userId = (body as unknown as { userId: string }).userId\n const result = await integration.getSubscriptions(userId)\n return { data: result, status: 200 }\n }\n\n case 'updateEmail': {\n if (!integration.updateEmail) {\n return {\n data: { error: 'Method not implemented: updateEmail' },\n status: 501,\n }\n }\n const params = body as unknown as {\n userId: string\n newEmail: string\n }\n const result = await integration.updateEmail({\n userId: params.userId,\n newEmail: params.newEmail,\n })\n return { data: result, status: 200 }\n }\n\n case 'updateName': {\n if (!integration.updateName) {\n return {\n data: { error: 'Method not implemented: updateName' },\n status: 501,\n }\n }\n const params = body as unknown as {\n userId: string\n newName: string\n }\n const result = await integration.updateName({\n userId: params.userId,\n newName: params.newName,\n })\n return { data: result, status: 200 }\n }\n\n case 'getClaimedSeats': {\n if (!integration.getClaimedSeats) {\n return {\n data: { error: 'Method not implemented: getClaimedSeats' },\n status: 501,\n }\n }\n const bulkCouponId = (body as unknown as { bulkCouponId: string })\n .bulkCouponId\n const result = await integration.getClaimedSeats(bulkCouponId)\n return { data: result, status: 200 }\n }\n\n case 'searchContent': {\n if (!integration.searchContent) {\n return {\n data: { error: 'Method not implemented: searchContent' },\n status: 501,\n }\n }\n const params = body as unknown as ContentSearchRequest\n const result: ContentSearchResponse =\n await integration.searchContent(params)\n return { data: result, status: 200 }\n }\n\n default:\n return {\n data: { error: `Unknown action: ${action}` },\n status: 400,\n }\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Unknown error'\n return {\n data: { error: message },\n status: 500,\n }\n }\n}\n\n/**\n * Helper to create JSON responses\n */\nfunction jsonResponse(data: unknown, status: number): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: {\n 'content-type': 'application/json',\n },\n })\n}\n"],"mappings":"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","names":[]}
|
package/dist/integration.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { User, Purchase, Subscription, ActionResult, ClaimedSeat } from './types.js';
|
|
1
|
+
import { User, Purchase, Subscription, ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse } from './types.js';
|
|
2
|
+
export { ContentSearchResult } from './types.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* SupportIntegration interface that apps must implement.
|
|
@@ -140,6 +141,32 @@ interface SupportIntegration {
|
|
|
140
141
|
* @returns Array of claimed seats with user info
|
|
141
142
|
*/
|
|
142
143
|
getClaimedSeats?(bulkCouponId: string): Promise<ClaimedSeat[]>;
|
|
144
|
+
/**
|
|
145
|
+
* Search product content (courses, lessons, articles, etc.) for agent recommendations.
|
|
146
|
+
* Optional method - implement if app wants agent to recommend specific resources.
|
|
147
|
+
*
|
|
148
|
+
* The agent will call this when customers ask about topics, features, or how to do something.
|
|
149
|
+
* Return relevant content the agent can share via links.
|
|
150
|
+
*
|
|
151
|
+
* @param request - Search query with optional filters and customer context
|
|
152
|
+
* @returns Search results with content items and optional quick links
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* // Customer asks: "How do I use generics in TypeScript?"
|
|
157
|
+
* const results = await integration.searchContent({
|
|
158
|
+
* query: "TypeScript generics tutorial",
|
|
159
|
+
* types: ["lesson", "article"],
|
|
160
|
+
* limit: 5
|
|
161
|
+
* })
|
|
162
|
+
*
|
|
163
|
+
* // Agent shares results:
|
|
164
|
+
* // "Here are some resources on TypeScript generics:"
|
|
165
|
+
* // - [Understanding Generics](https://totalts.com/lessons/generics)
|
|
166
|
+
* // - [Generic Constraints](https://totalts.com/lessons/constraints)
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
searchContent?(request: ContentSearchRequest): Promise<ContentSearchResponse>;
|
|
143
170
|
}
|
|
144
171
|
|
|
145
|
-
export { ActionResult, ClaimedSeat, Purchase, Subscription, type SupportIntegration, User };
|
|
172
|
+
export { ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse, Purchase, Subscription, type SupportIntegration, User };
|
package/dist/types.d.ts
CHANGED
|
@@ -73,5 +73,70 @@ interface RefundResult {
|
|
|
73
73
|
refundId?: string;
|
|
74
74
|
error?: string;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Content search result for agent recommendations.
|
|
78
|
+
* Agent queries products to find relevant resources to share with customers.
|
|
79
|
+
*/
|
|
80
|
+
interface ContentSearchResult {
|
|
81
|
+
/** Unique identifier for deduplication */
|
|
82
|
+
id: string;
|
|
83
|
+
/** Resource type for filtering/display */
|
|
84
|
+
type: 'course' | 'module' | 'lesson' | 'article' | 'exercise' | 'resource' | 'social';
|
|
85
|
+
/** Human-readable title */
|
|
86
|
+
title: string;
|
|
87
|
+
/** Brief description (1-2 sentences) */
|
|
88
|
+
description?: string;
|
|
89
|
+
/** Canonical URL to share with customer */
|
|
90
|
+
url: string;
|
|
91
|
+
/** Relevance score 0-1 (optional, for ranking) */
|
|
92
|
+
score?: number;
|
|
93
|
+
/** Product-specific metadata (agent can reference but doesn't parse) */
|
|
94
|
+
metadata?: {
|
|
95
|
+
/** Duration in minutes (for courses/lessons) */
|
|
96
|
+
duration?: number;
|
|
97
|
+
/** Difficulty level */
|
|
98
|
+
difficulty?: 'beginner' | 'intermediate' | 'advanced';
|
|
99
|
+
/** Tags/topics */
|
|
100
|
+
tags?: string[];
|
|
101
|
+
/** Author/instructor name */
|
|
102
|
+
author?: string;
|
|
103
|
+
/** Last updated date */
|
|
104
|
+
updatedAt?: string;
|
|
105
|
+
/** Free vs paid */
|
|
106
|
+
accessLevel?: 'free' | 'paid' | 'preview';
|
|
107
|
+
/** Arbitrary product-specific data */
|
|
108
|
+
[key: string]: unknown;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Content search request from agent.
|
|
113
|
+
*/
|
|
114
|
+
interface ContentSearchRequest {
|
|
115
|
+
/** Natural language query */
|
|
116
|
+
query: string;
|
|
117
|
+
/** Filter by content type */
|
|
118
|
+
types?: ContentSearchResult['type'][];
|
|
119
|
+
/** Max results to return */
|
|
120
|
+
limit?: number;
|
|
121
|
+
/** Customer context (for personalization) */
|
|
122
|
+
customer?: {
|
|
123
|
+
email?: string;
|
|
124
|
+
hasPurchased?: boolean;
|
|
125
|
+
purchasedProducts?: string[];
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Content search response to agent.
|
|
130
|
+
*/
|
|
131
|
+
interface ContentSearchResponse {
|
|
132
|
+
results: ContentSearchResult[];
|
|
133
|
+
/** Quick links always returned (social, support, etc.) */
|
|
134
|
+
quickLinks?: ContentSearchResult[];
|
|
135
|
+
/** Search metadata */
|
|
136
|
+
meta?: {
|
|
137
|
+
totalResults?: number;
|
|
138
|
+
searchTimeMs?: number;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
76
141
|
|
|
77
|
-
export type { ActionResult, ClaimedSeat, Customer, Purchase, RefundRequest, RefundResult, Subscription, User };
|
|
142
|
+
export type { ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse, ContentSearchResult, Customer, Purchase, RefundRequest, RefundResult, Subscription, User };
|
package/package.json
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillrecordings/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": {
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"import": "./dist/index.js"
|
|
10
|
-
},
|
|
11
7
|
"./types": {
|
|
12
8
|
"types": "./dist/types.d.ts",
|
|
13
9
|
"import": "./dist/types.js"
|
|
@@ -31,7 +27,6 @@
|
|
|
31
27
|
},
|
|
32
28
|
"typesVersions": {
|
|
33
29
|
"*": {
|
|
34
|
-
".": ["./dist/index.d.ts"],
|
|
35
30
|
"types": ["./dist/types.d.ts"],
|
|
36
31
|
"integration": ["./dist/integration.d.ts"],
|
|
37
32
|
"adapter": ["./dist/adapter.d.ts"],
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|