@skillrecordings/sdk 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +43 -0
- package/dist/adapter.js +1 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +70 -0
- package/dist/client.js +85 -0
- package/dist/client.js.map +1 -0
- package/dist/handler.d.ts +32 -0
- package/dist/handler.js +172 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +145 -0
- package/dist/integration.js +1 -0
- package/dist/integration.js.map +1 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -7
- package/CHANGELOG.md +0 -12
- package/src/__tests__/client.test.ts +0 -351
- package/src/__tests__/handler.test.ts +0 -442
- package/src/__tests__/types.test.ts +0 -121
- package/src/adapter.ts +0 -43
- package/src/client.ts +0 -146
- package/src/handler.ts +0 -271
- package/src/index.ts +0 -19
- package/src/integration.ts +0 -164
- package/src/types.ts +0 -82
- package/tsconfig.json +0 -8
- package/vitest.config.ts +0 -10
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Customer, Purchase, RefundRequest, RefundResult } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base adapter interface that apps must implement to integrate with the support platform.
|
|
5
|
+
*
|
|
6
|
+
* @deprecated Use SupportIntegration interface from './integration' instead.
|
|
7
|
+
* This interface is kept for backwards compatibility during migration.
|
|
8
|
+
*
|
|
9
|
+
* Migration path:
|
|
10
|
+
* - Replace AppAdapter with SupportIntegration
|
|
11
|
+
* - Rename getCustomer to lookupUser
|
|
12
|
+
* - Replace processRefund with revokeAccess
|
|
13
|
+
* - Add required methods: transferPurchase, generateMagicLink
|
|
14
|
+
*
|
|
15
|
+
* @see {@link SupportIntegration} for the new interface
|
|
16
|
+
*
|
|
17
|
+
* Each app (egghead, Total TypeScript, etc.) provides:
|
|
18
|
+
* - Customer lookup by email
|
|
19
|
+
* - Purchase history retrieval
|
|
20
|
+
* - Refund processing capabilities
|
|
21
|
+
*/
|
|
22
|
+
interface AppAdapter {
|
|
23
|
+
/**
|
|
24
|
+
* Fetch customer by email address
|
|
25
|
+
* @deprecated Use lookupUser from SupportIntegration
|
|
26
|
+
* @returns Customer if found, null otherwise
|
|
27
|
+
*/
|
|
28
|
+
getCustomer(email: string): Promise<Customer | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Fetch all purchases for a given customer
|
|
31
|
+
* @deprecated Use getPurchases from SupportIntegration (same signature)
|
|
32
|
+
* @returns Array of purchases, empty if none found
|
|
33
|
+
*/
|
|
34
|
+
getPurchases(customerId: string): Promise<Purchase[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Process a refund for a purchase
|
|
37
|
+
* @deprecated Use revokeAccess from SupportIntegration instead
|
|
38
|
+
* @returns RefundResult indicating success/failure
|
|
39
|
+
*/
|
|
40
|
+
processRefund(request: RefundRequest): Promise<RefundResult>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type { AppAdapter };
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { SupportIntegration } from './integration.js';
|
|
2
|
+
import { User, Purchase, Subscription, ActionResult, ClaimedSeat } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client for calling app integration endpoints with HMAC-signed requests.
|
|
6
|
+
*
|
|
7
|
+
* Used by core to call app-specific support actions (lookupUser, getPurchases, etc.)
|
|
8
|
+
* with Stripe-style HMAC-SHA256 signature verification.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { IntegrationClient } from '@skillrecordings/sdk/client'
|
|
13
|
+
*
|
|
14
|
+
* const client = new IntegrationClient({
|
|
15
|
+
* baseUrl: 'https://totaltypescript.com',
|
|
16
|
+
* webhookSecret: 'whsec_abc123',
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* const user = await client.lookupUser('test@example.com')
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare class IntegrationClient implements SupportIntegration {
|
|
23
|
+
private readonly baseUrl;
|
|
24
|
+
private readonly webhookSecret;
|
|
25
|
+
constructor(config: {
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
webhookSecret: string;
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Generate HMAC-SHA256 signature for request body.
|
|
31
|
+
* Format: `t=<timestamp>,v1=<signature>`
|
|
32
|
+
*
|
|
33
|
+
* Signature is computed as: HMAC-SHA256(timestamp + "." + body, secret)
|
|
34
|
+
*/
|
|
35
|
+
private generateSignature;
|
|
36
|
+
/**
|
|
37
|
+
* Make signed POST request to app integration endpoint.
|
|
38
|
+
*/
|
|
39
|
+
private request;
|
|
40
|
+
lookupUser(email: string): Promise<User | null>;
|
|
41
|
+
getPurchases(userId: string): Promise<Purchase[]>;
|
|
42
|
+
getSubscriptions(userId: string): Promise<Subscription[]>;
|
|
43
|
+
revokeAccess(params: {
|
|
44
|
+
purchaseId: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
refundId: string;
|
|
47
|
+
}): Promise<ActionResult>;
|
|
48
|
+
transferPurchase(params: {
|
|
49
|
+
purchaseId: string;
|
|
50
|
+
fromUserId: string;
|
|
51
|
+
toEmail: string;
|
|
52
|
+
}): Promise<ActionResult>;
|
|
53
|
+
generateMagicLink(params: {
|
|
54
|
+
email: string;
|
|
55
|
+
expiresIn: number;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
url: string;
|
|
58
|
+
}>;
|
|
59
|
+
updateEmail(params: {
|
|
60
|
+
userId: string;
|
|
61
|
+
newEmail: string;
|
|
62
|
+
}): Promise<ActionResult>;
|
|
63
|
+
updateName(params: {
|
|
64
|
+
userId: string;
|
|
65
|
+
newName: string;
|
|
66
|
+
}): Promise<ActionResult>;
|
|
67
|
+
getClaimedSeats(bulkCouponId: string): Promise<ClaimedSeat[]>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { IntegrationClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { createHmac } from "crypto";
|
|
3
|
+
var IntegrationClient = class {
|
|
4
|
+
baseUrl;
|
|
5
|
+
webhookSecret;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
8
|
+
this.webhookSecret = config.webhookSecret;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generate HMAC-SHA256 signature for request body.
|
|
12
|
+
* Format: `t=<timestamp>,v1=<signature>`
|
|
13
|
+
*
|
|
14
|
+
* Signature is computed as: HMAC-SHA256(timestamp + "." + body, secret)
|
|
15
|
+
*/
|
|
16
|
+
generateSignature(body) {
|
|
17
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
18
|
+
const signedPayload = `${timestamp}.${body}`;
|
|
19
|
+
const signature = createHmac("sha256", this.webhookSecret).update(signedPayload).digest("hex");
|
|
20
|
+
return `t=${timestamp},v1=${signature}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Make signed POST request to app integration endpoint.
|
|
24
|
+
*/
|
|
25
|
+
async request(endpoint, payload) {
|
|
26
|
+
const body = JSON.stringify(payload);
|
|
27
|
+
const signature = this.generateSignature(body);
|
|
28
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"X-Support-Signature": signature
|
|
33
|
+
},
|
|
34
|
+
body
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
let errorMessage;
|
|
38
|
+
try {
|
|
39
|
+
const errorBody = await response.json();
|
|
40
|
+
if (errorBody?.error) {
|
|
41
|
+
errorMessage = errorBody.error;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
if (errorMessage) {
|
|
46
|
+
throw new Error(errorMessage);
|
|
47
|
+
}
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Integration request failed: ${response.status} ${response.statusText}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return await response.json();
|
|
53
|
+
}
|
|
54
|
+
async lookupUser(email) {
|
|
55
|
+
return this.request("/api/support/lookup-user", { email });
|
|
56
|
+
}
|
|
57
|
+
async getPurchases(userId) {
|
|
58
|
+
return this.request("/api/support/get-purchases", { userId });
|
|
59
|
+
}
|
|
60
|
+
async getSubscriptions(userId) {
|
|
61
|
+
return this.request("/api/support/get-subscriptions", { userId });
|
|
62
|
+
}
|
|
63
|
+
async revokeAccess(params) {
|
|
64
|
+
return this.request("/api/support/revoke-access", params);
|
|
65
|
+
}
|
|
66
|
+
async transferPurchase(params) {
|
|
67
|
+
return this.request("/api/support/transfer-purchase", params);
|
|
68
|
+
}
|
|
69
|
+
async generateMagicLink(params) {
|
|
70
|
+
return this.request("/api/support/generate-magic-link", params);
|
|
71
|
+
}
|
|
72
|
+
async updateEmail(params) {
|
|
73
|
+
return this.request("/api/support/update-email", params);
|
|
74
|
+
}
|
|
75
|
+
async updateName(params) {
|
|
76
|
+
return this.request("/api/support/update-name", params);
|
|
77
|
+
}
|
|
78
|
+
async getClaimedSeats(bulkCouponId) {
|
|
79
|
+
return this.request("/api/support/get-claimed-seats", { bulkCouponId });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export {
|
|
83
|
+
IntegrationClient
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +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;AA4BpB,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;AACF;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SupportIntegration } from './integration.js';
|
|
2
|
+
import './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for createSupportHandler
|
|
6
|
+
*/
|
|
7
|
+
interface SupportHandlerConfig {
|
|
8
|
+
integration: SupportIntegration;
|
|
9
|
+
webhookSecret: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a Next.js API route handler for SupportIntegration.
|
|
13
|
+
* Verifies HMAC-SHA256 signature and routes actions to integration methods.
|
|
14
|
+
*
|
|
15
|
+
* Signature format: timestamp=1234567890,v1=hex_signature
|
|
16
|
+
* Payload to sign: timestamp.JSON.stringify(body)
|
|
17
|
+
* Replay protection: 5 minute window
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { createSupportHandler } from '@skillrecordings/sdk/handler'
|
|
22
|
+
* import { integration } from './integration'
|
|
23
|
+
*
|
|
24
|
+
* export const POST = createSupportHandler({
|
|
25
|
+
* integration,
|
|
26
|
+
* webhookSecret: process.env.SUPPORT_WEBHOOK_SECRET!,
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function createSupportHandler(config: SupportHandlerConfig): (request: Request) => Promise<Response>;
|
|
31
|
+
|
|
32
|
+
export { type SupportHandlerConfig, createSupportHandler };
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/handler.ts
|
|
2
|
+
import { timingSafeEqual } from "crypto";
|
|
3
|
+
function createSupportHandler(config) {
|
|
4
|
+
const { integration, webhookSecret } = config;
|
|
5
|
+
return async function handler(request) {
|
|
6
|
+
try {
|
|
7
|
+
const signatureHeader = request.headers.get("x-support-signature");
|
|
8
|
+
if (!signatureHeader) {
|
|
9
|
+
return jsonResponse({ error: "Missing signature header" }, 401);
|
|
10
|
+
}
|
|
11
|
+
const parts = signatureHeader.split(",");
|
|
12
|
+
const timestampPart = parts.find((p) => p.startsWith("timestamp="));
|
|
13
|
+
const signaturePart = parts.find((p) => p.startsWith("v1="));
|
|
14
|
+
if (!timestampPart || !signaturePart) {
|
|
15
|
+
return jsonResponse({ error: "Invalid signature format" }, 401);
|
|
16
|
+
}
|
|
17
|
+
const timestampValue = timestampPart.split("=")[1];
|
|
18
|
+
const signatureValue = signaturePart.split("=")[1];
|
|
19
|
+
if (!timestampValue || !signatureValue) {
|
|
20
|
+
return jsonResponse({ error: "Invalid signature format" }, 401);
|
|
21
|
+
}
|
|
22
|
+
const timestamp = parseInt(timestampValue, 10);
|
|
23
|
+
const receivedSignature = signatureValue;
|
|
24
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
25
|
+
const maxAge = 300;
|
|
26
|
+
if (now - timestamp > maxAge) {
|
|
27
|
+
return jsonResponse({ error: "Signature expired" }, 401);
|
|
28
|
+
}
|
|
29
|
+
const bodyText = await request.text();
|
|
30
|
+
let body;
|
|
31
|
+
try {
|
|
32
|
+
body = JSON.parse(bodyText);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return jsonResponse({ error: "Invalid JSON body" }, 400);
|
|
35
|
+
}
|
|
36
|
+
const crypto = await import("crypto");
|
|
37
|
+
const payload = `${timestamp}.${bodyText}`;
|
|
38
|
+
const expectedSignature = crypto.createHmac("sha256", webhookSecret).update(payload).digest("hex");
|
|
39
|
+
if (!timingSafeEqual(
|
|
40
|
+
Buffer.from(receivedSignature),
|
|
41
|
+
Buffer.from(expectedSignature)
|
|
42
|
+
)) {
|
|
43
|
+
return jsonResponse({ error: "Invalid signature" }, 401);
|
|
44
|
+
}
|
|
45
|
+
const { action } = body;
|
|
46
|
+
if (!action || typeof action !== "string") {
|
|
47
|
+
return jsonResponse({ error: "Missing action field" }, 400);
|
|
48
|
+
}
|
|
49
|
+
const result = await routeAction(integration, action, body);
|
|
50
|
+
return jsonResponse(result.data, result.status);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
53
|
+
return jsonResponse({ error: `Internal error: ${message}` }, 500);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function routeAction(integration, action, body) {
|
|
58
|
+
try {
|
|
59
|
+
switch (action) {
|
|
60
|
+
case "lookupUser": {
|
|
61
|
+
const email = body.email;
|
|
62
|
+
const result = await integration.lookupUser(email);
|
|
63
|
+
return { data: result, status: 200 };
|
|
64
|
+
}
|
|
65
|
+
case "getPurchases": {
|
|
66
|
+
const userId = body.userId;
|
|
67
|
+
const result = await integration.getPurchases(userId);
|
|
68
|
+
return { data: result, status: 200 };
|
|
69
|
+
}
|
|
70
|
+
case "revokeAccess": {
|
|
71
|
+
const params = body;
|
|
72
|
+
const result = await integration.revokeAccess({
|
|
73
|
+
purchaseId: params.purchaseId,
|
|
74
|
+
reason: params.reason,
|
|
75
|
+
refundId: params.refundId
|
|
76
|
+
});
|
|
77
|
+
return { data: result, status: 200 };
|
|
78
|
+
}
|
|
79
|
+
case "transferPurchase": {
|
|
80
|
+
const params = body;
|
|
81
|
+
const result = await integration.transferPurchase({
|
|
82
|
+
purchaseId: params.purchaseId,
|
|
83
|
+
fromUserId: params.fromUserId,
|
|
84
|
+
toEmail: params.toEmail
|
|
85
|
+
});
|
|
86
|
+
return { data: result, status: 200 };
|
|
87
|
+
}
|
|
88
|
+
case "generateMagicLink": {
|
|
89
|
+
const params = body;
|
|
90
|
+
const result = await integration.generateMagicLink({
|
|
91
|
+
email: params.email,
|
|
92
|
+
expiresIn: params.expiresIn
|
|
93
|
+
});
|
|
94
|
+
return { data: result, status: 200 };
|
|
95
|
+
}
|
|
96
|
+
// Optional methods
|
|
97
|
+
case "getSubscriptions": {
|
|
98
|
+
if (!integration.getSubscriptions) {
|
|
99
|
+
return {
|
|
100
|
+
data: { error: "Method not implemented: getSubscriptions" },
|
|
101
|
+
status: 501
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const userId = body.userId;
|
|
105
|
+
const result = await integration.getSubscriptions(userId);
|
|
106
|
+
return { data: result, status: 200 };
|
|
107
|
+
}
|
|
108
|
+
case "updateEmail": {
|
|
109
|
+
if (!integration.updateEmail) {
|
|
110
|
+
return {
|
|
111
|
+
data: { error: "Method not implemented: updateEmail" },
|
|
112
|
+
status: 501
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const params = body;
|
|
116
|
+
const result = await integration.updateEmail({
|
|
117
|
+
userId: params.userId,
|
|
118
|
+
newEmail: params.newEmail
|
|
119
|
+
});
|
|
120
|
+
return { data: result, status: 200 };
|
|
121
|
+
}
|
|
122
|
+
case "updateName": {
|
|
123
|
+
if (!integration.updateName) {
|
|
124
|
+
return {
|
|
125
|
+
data: { error: "Method not implemented: updateName" },
|
|
126
|
+
status: 501
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const params = body;
|
|
130
|
+
const result = await integration.updateName({
|
|
131
|
+
userId: params.userId,
|
|
132
|
+
newName: params.newName
|
|
133
|
+
});
|
|
134
|
+
return { data: result, status: 200 };
|
|
135
|
+
}
|
|
136
|
+
case "getClaimedSeats": {
|
|
137
|
+
if (!integration.getClaimedSeats) {
|
|
138
|
+
return {
|
|
139
|
+
data: { error: "Method not implemented: getClaimedSeats" },
|
|
140
|
+
status: 501
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const bulkCouponId = body.bulkCouponId;
|
|
144
|
+
const result = await integration.getClaimedSeats(bulkCouponId);
|
|
145
|
+
return { data: result, status: 200 };
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
return {
|
|
149
|
+
data: { error: `Unknown action: ${action}` },
|
|
150
|
+
status: 400
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
155
|
+
return {
|
|
156
|
+
data: { error: message },
|
|
157
|
+
status: 500
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function jsonResponse(data, status) {
|
|
162
|
+
return new Response(JSON.stringify(data), {
|
|
163
|
+
status,
|
|
164
|
+
headers: {
|
|
165
|
+
"content-type": "application/json"
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export {
|
|
170
|
+
createSupportHandler
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { User, Purchase, Subscription, ActionResult, ClaimedSeat } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SupportIntegration interface that apps must implement.
|
|
5
|
+
*
|
|
6
|
+
* Each app (egghead, Total TypeScript, etc.) implements this interface
|
|
7
|
+
* to provide user lookup, purchase/subscription management, and support actions.
|
|
8
|
+
*
|
|
9
|
+
* The support platform calls these methods via IntegrationClient with HMAC auth.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import type { SupportIntegration } from '@skillrecordings/sdk/integration'
|
|
14
|
+
*
|
|
15
|
+
* const integration: SupportIntegration = {
|
|
16
|
+
* async lookupUser(email) {
|
|
17
|
+
* return db.user.findUnique({ where: { email } })
|
|
18
|
+
* },
|
|
19
|
+
* async getPurchases(userId) {
|
|
20
|
+
* return db.purchase.findMany({ where: { userId } })
|
|
21
|
+
* },
|
|
22
|
+
* async revokeAccess({ purchaseId, reason, refundId }) {
|
|
23
|
+
* await db.purchase.update({
|
|
24
|
+
* where: { id: purchaseId },
|
|
25
|
+
* data: { status: 'refunded', refundReason: reason, stripeRefundId: refundId }
|
|
26
|
+
* })
|
|
27
|
+
* return { success: true }
|
|
28
|
+
* },
|
|
29
|
+
* async transferPurchase({ purchaseId, fromUserId, toEmail }) {
|
|
30
|
+
* const toUser = await db.user.findUnique({ where: { email: toEmail } })
|
|
31
|
+
* await db.purchase.update({
|
|
32
|
+
* where: { id: purchaseId },
|
|
33
|
+
* data: { userId: toUser.id }
|
|
34
|
+
* })
|
|
35
|
+
* return { success: true }
|
|
36
|
+
* },
|
|
37
|
+
* async generateMagicLink({ email, expiresIn }) {
|
|
38
|
+
* const token = await createMagicToken(email, expiresIn)
|
|
39
|
+
* return { url: `${APP_URL}/auth/magic?token=${token}` }
|
|
40
|
+
* },
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
interface SupportIntegration {
|
|
45
|
+
/**
|
|
46
|
+
* Look up user by email address.
|
|
47
|
+
* Called by the agent to fetch user context at conversation start.
|
|
48
|
+
*
|
|
49
|
+
* @param email - User's email address
|
|
50
|
+
* @returns User if found, null otherwise
|
|
51
|
+
*/
|
|
52
|
+
lookupUser(email: string): Promise<User | null>;
|
|
53
|
+
/**
|
|
54
|
+
* Fetch all purchases for a given user.
|
|
55
|
+
* Used by agent to display purchase history and validate refund eligibility.
|
|
56
|
+
*
|
|
57
|
+
* @param userId - User's unique identifier
|
|
58
|
+
* @returns Array of purchases, empty if none found
|
|
59
|
+
*/
|
|
60
|
+
getPurchases(userId: string): Promise<Purchase[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Fetch active subscriptions for a user.
|
|
63
|
+
* Optional method - only implement if app supports recurring billing.
|
|
64
|
+
*
|
|
65
|
+
* @param userId - User's unique identifier
|
|
66
|
+
* @returns Array of subscriptions, empty if none found
|
|
67
|
+
*/
|
|
68
|
+
getSubscriptions?(userId: string): Promise<Subscription[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Revoke access to a product after refund.
|
|
71
|
+
* Called after Stripe refund succeeds to remove product access.
|
|
72
|
+
*
|
|
73
|
+
* @param params.purchaseId - Purchase to revoke
|
|
74
|
+
* @param params.reason - Refund reason for audit trail
|
|
75
|
+
* @param params.refundId - Stripe refund ID
|
|
76
|
+
* @returns ActionResult indicating success/failure
|
|
77
|
+
*/
|
|
78
|
+
revokeAccess(params: {
|
|
79
|
+
purchaseId: string;
|
|
80
|
+
reason: string;
|
|
81
|
+
refundId: string;
|
|
82
|
+
}): Promise<ActionResult>;
|
|
83
|
+
/**
|
|
84
|
+
* Transfer purchase to a different user.
|
|
85
|
+
* Updates purchase ownership and moves product access.
|
|
86
|
+
*
|
|
87
|
+
* @param params.purchaseId - Purchase to transfer
|
|
88
|
+
* @param params.fromUserId - Current owner's ID
|
|
89
|
+
* @param params.toEmail - New owner's email address
|
|
90
|
+
* @returns ActionResult indicating success/failure
|
|
91
|
+
*/
|
|
92
|
+
transferPurchase(params: {
|
|
93
|
+
purchaseId: string;
|
|
94
|
+
fromUserId: string;
|
|
95
|
+
toEmail: string;
|
|
96
|
+
}): Promise<ActionResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Generate a magic link for passwordless login.
|
|
99
|
+
* Used by agent to send login links during support conversations.
|
|
100
|
+
*
|
|
101
|
+
* @param params.email - User's email address
|
|
102
|
+
* @param params.expiresIn - Expiration time in seconds (default 3600)
|
|
103
|
+
* @returns Object with magic link URL
|
|
104
|
+
*/
|
|
105
|
+
generateMagicLink(params: {
|
|
106
|
+
email: string;
|
|
107
|
+
expiresIn: number;
|
|
108
|
+
}): Promise<{
|
|
109
|
+
url: string;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Update user's email address.
|
|
113
|
+
* Optional method - not all apps support email changes.
|
|
114
|
+
*
|
|
115
|
+
* @param params.userId - User's unique identifier
|
|
116
|
+
* @param params.newEmail - New email address
|
|
117
|
+
* @returns ActionResult indicating success/failure
|
|
118
|
+
*/
|
|
119
|
+
updateEmail?(params: {
|
|
120
|
+
userId: string;
|
|
121
|
+
newEmail: string;
|
|
122
|
+
}): Promise<ActionResult>;
|
|
123
|
+
/**
|
|
124
|
+
* Update user's display name.
|
|
125
|
+
* Optional method - not all apps support name changes.
|
|
126
|
+
*
|
|
127
|
+
* @param params.userId - User's unique identifier
|
|
128
|
+
* @param params.newName - New display name
|
|
129
|
+
* @returns ActionResult indicating success/failure
|
|
130
|
+
*/
|
|
131
|
+
updateName?(params: {
|
|
132
|
+
userId: string;
|
|
133
|
+
newName: string;
|
|
134
|
+
}): Promise<ActionResult>;
|
|
135
|
+
/**
|
|
136
|
+
* Get all claimed seats for a team/bulk purchase.
|
|
137
|
+
* Optional method - only implement for apps with team features.
|
|
138
|
+
*
|
|
139
|
+
* @param bulkCouponId - Bulk coupon/license identifier
|
|
140
|
+
* @returns Array of claimed seats with user info
|
|
141
|
+
*/
|
|
142
|
+
getClaimedSeats?(bulkCouponId: string): Promise<ClaimedSeat[]>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { ActionResult, ClaimedSeat, Purchase, Subscription, type SupportIntegration, User };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=integration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|