@skillrecordings/sdk 0.3.0 → 0.4.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 CHANGED
@@ -19,6 +19,7 @@ interface SupportIntegration {
19
19
  updateEmail?(params: { userId, newEmail }): Promise<ActionResult>
20
20
  updateName?(params: { userId, newName }): Promise<ActionResult>
21
21
  getClaimedSeats?(bulkCouponId: string): Promise<ClaimedSeat[]>
22
+ searchContent?(request: ContentSearchRequest): Promise<ContentSearchResponse> // v0.3.0+
22
23
  }
23
24
  ```
24
25
 
package/dist/adapter.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Customer, Purchase, RefundRequest, RefundResult } from './types.js';
2
+ import 'zod';
2
3
 
3
4
  /**
4
5
  * Base adapter interface that apps must implement to integrate with the support platform.
@@ -0,0 +1,10 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };
10
+ //# sourceMappingURL=chunk-PZ5AY32C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/client.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { SupportIntegration } from './integration.js';
2
- import { User, Purchase, Subscription, ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse } from './types.js';
2
+ import { User, Purchase, Subscription, ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse, ProductStatus } from './types.js';
3
+ import 'zod';
3
4
 
4
5
  /**
5
6
  * Client for calling app integration endpoints with HMAC-signed requests.
@@ -28,13 +29,14 @@ declare class IntegrationClient implements SupportIntegration {
28
29
  });
29
30
  /**
30
31
  * Generate HMAC-SHA256 signature for request body.
31
- * Format: `t=<timestamp>,v1=<signature>`
32
+ * Format: `timestamp=<timestamp>,v1=<signature>`
32
33
  *
33
34
  * Signature is computed as: HMAC-SHA256(timestamp + "." + body, secret)
34
35
  */
35
36
  private generateSignature;
36
37
  /**
37
38
  * Make signed POST request to app integration endpoint.
39
+ * Uses action-based routing: all requests go to /api/support with action in body.
38
40
  */
39
41
  private request;
40
42
  lookupUser(email: string): Promise<User | null>;
@@ -66,6 +68,7 @@ declare class IntegrationClient implements SupportIntegration {
66
68
  }): Promise<ActionResult>;
67
69
  getClaimedSeats(bulkCouponId: string): Promise<ClaimedSeat[]>;
68
70
  searchContent(request: ContentSearchRequest): Promise<ContentSearchResponse>;
71
+ getProductStatus(productId: string): Promise<ProductStatus | null>;
69
72
  }
70
73
 
71
74
  export { IntegrationClient };
package/dist/client.js CHANGED
@@ -1,3 +1,5 @@
1
+ import "./chunk-PZ5AY32C.js";
2
+
1
3
  // src/client.ts
2
4
  import { createHmac } from "crypto";
3
5
  var IntegrationClient = class {
@@ -9,7 +11,7 @@ var IntegrationClient = class {
9
11
  }
10
12
  /**
11
13
  * Generate HMAC-SHA256 signature for request body.
12
- * Format: `t=<timestamp>,v1=<signature>`
14
+ * Format: `timestamp=<timestamp>,v1=<signature>`
13
15
  *
14
16
  * Signature is computed as: HMAC-SHA256(timestamp + "." + body, secret)
15
17
  */
@@ -17,15 +19,16 @@ var IntegrationClient = class {
17
19
  const timestamp = Math.floor(Date.now() / 1e3);
18
20
  const signedPayload = `${timestamp}.${body}`;
19
21
  const signature = createHmac("sha256", this.webhookSecret).update(signedPayload).digest("hex");
20
- return `t=${timestamp},v1=${signature}`;
22
+ return `timestamp=${timestamp},v1=${signature}`;
21
23
  }
22
24
  /**
23
25
  * Make signed POST request to app integration endpoint.
26
+ * Uses action-based routing: all requests go to /api/support with action in body.
24
27
  */
25
- async request(endpoint, payload) {
26
- const body = JSON.stringify(payload);
28
+ async request(action, payload) {
29
+ const body = JSON.stringify({ action, ...payload });
27
30
  const signature = this.generateSignature(body);
28
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
31
+ const response = await fetch(this.baseUrl, {
29
32
  method: "POST",
30
33
  headers: {
31
34
  "Content-Type": "application/json",
@@ -52,34 +55,40 @@ var IntegrationClient = class {
52
55
  return await response.json();
53
56
  }
54
57
  async lookupUser(email) {
55
- return this.request("/api/support/lookup-user", { email });
58
+ return this.request("lookupUser", { email });
56
59
  }
57
60
  async getPurchases(userId) {
58
- return this.request("/api/support/get-purchases", { userId });
61
+ return this.request("getPurchases", { userId });
59
62
  }
60
63
  async getSubscriptions(userId) {
61
- return this.request("/api/support/get-subscriptions", { userId });
64
+ return this.request("getSubscriptions", { userId });
62
65
  }
63
66
  async revokeAccess(params) {
64
- return this.request("/api/support/revoke-access", params);
67
+ return this.request("revokeAccess", params);
65
68
  }
66
69
  async transferPurchase(params) {
67
- return this.request("/api/support/transfer-purchase", params);
70
+ return this.request("transferPurchase", params);
68
71
  }
69
72
  async generateMagicLink(params) {
70
- return this.request("/api/support/generate-magic-link", params);
73
+ return this.request("generateMagicLink", params);
71
74
  }
72
75
  async updateEmail(params) {
73
- return this.request("/api/support/update-email", params);
76
+ return this.request("updateEmail", params);
74
77
  }
75
78
  async updateName(params) {
76
- return this.request("/api/support/update-name", params);
79
+ return this.request("updateName", params);
77
80
  }
78
81
  async getClaimedSeats(bulkCouponId) {
79
- return this.request("/api/support/get-claimed-seats", { bulkCouponId });
82
+ return this.request("getClaimedSeats", { bulkCouponId });
80
83
  }
81
84
  async searchContent(request) {
82
- return this.request("/api/support/search-content", request);
85
+ return this.request(
86
+ "searchContent",
87
+ request
88
+ );
89
+ }
90
+ async getProductStatus(productId) {
91
+ return this.request("getProductStatus", { productId });
83
92
  }
84
93
  };
85
94
  export {
@@ -1 +1 @@
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":[]}
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { createHmac } from 'node:crypto'\nimport type {\n ActionResult,\n ClaimedSeat,\n ContentSearchRequest,\n ContentSearchResponse,\n ProductStatus,\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: `timestamp=<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 `timestamp=${timestamp},v1=${signature}`\n }\n\n /**\n * Make signed POST request to app integration endpoint.\n * Uses action-based routing: all requests go to /api/support with action in body.\n */\n private async request<T>(\n action: string,\n payload: Record<string, unknown>\n ): Promise<T> {\n const body = JSON.stringify({ action, ...payload })\n const signature = this.generateSignature(body)\n\n // baseUrl should be the complete endpoint URL (e.g., https://example.com/api/support)\n const response = await fetch(this.baseUrl, {\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('lookupUser', { email })\n }\n\n async getPurchases(userId: string): Promise<Purchase[]> {\n return this.request('getPurchases', { userId })\n }\n\n async getSubscriptions(userId: string): Promise<Subscription[]> {\n return this.request('getSubscriptions', { userId })\n }\n\n async revokeAccess(params: {\n purchaseId: string\n reason: string\n refundId: string\n }): Promise<ActionResult> {\n return this.request('revokeAccess', params)\n }\n\n async transferPurchase(params: {\n purchaseId: string\n fromUserId: string\n toEmail: string\n }): Promise<ActionResult> {\n return this.request('transferPurchase', params)\n }\n\n async generateMagicLink(params: {\n email: string\n expiresIn: number\n }): Promise<{ url: string }> {\n return this.request('generateMagicLink', params)\n }\n\n async updateEmail(params: {\n userId: string\n newEmail: string\n }): Promise<ActionResult> {\n return this.request('updateEmail', params)\n }\n\n async updateName(params: {\n userId: string\n newName: string\n }): Promise<ActionResult> {\n return this.request('updateName', params)\n }\n\n async getClaimedSeats(bulkCouponId: string): Promise<ClaimedSeat[]> {\n return this.request('getClaimedSeats', { bulkCouponId })\n }\n\n async searchContent(\n request: ContentSearchRequest\n ): Promise<ContentSearchResponse> {\n return this.request(\n 'searchContent',\n request as unknown as Record<string, unknown>\n )\n }\n\n async getProductStatus(productId: string): Promise<ProductStatus | null> {\n return this.request('getProductStatus', { productId })\n }\n}\n"],"mappings":";;;AAAA,SAAS,kBAAkB;AA+BpB,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,aAAa,SAAS,OAAO,SAAS;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,QACZ,QACA,SACY;AACZ,UAAM,OAAO,KAAK,UAAU,EAAE,QAAQ,GAAG,QAAQ,CAAC;AAClD,UAAM,YAAY,KAAK,kBAAkB,IAAI;AAG7C,UAAM,WAAW,MAAM,MAAM,KAAK,SAAS;AAAA,MACzC,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,cAAc,EAAE,MAAM,CAAC;AAAA,EAC7C;AAAA,EAEA,MAAM,aAAa,QAAqC;AACtD,WAAO,KAAK,QAAQ,gBAAgB,EAAE,OAAO,CAAC;AAAA,EAChD;AAAA,EAEA,MAAM,iBAAiB,QAAyC;AAC9D,WAAO,KAAK,QAAQ,oBAAoB,EAAE,OAAO,CAAC;AAAA,EACpD;AAAA,EAEA,MAAM,aAAa,QAIO;AACxB,WAAO,KAAK,QAAQ,gBAAgB,MAAM;AAAA,EAC5C;AAAA,EAEA,MAAM,iBAAiB,QAIG;AACxB,WAAO,KAAK,QAAQ,oBAAoB,MAAM;AAAA,EAChD;AAAA,EAEA,MAAM,kBAAkB,QAGK;AAC3B,WAAO,KAAK,QAAQ,qBAAqB,MAAM;AAAA,EACjD;AAAA,EAEA,MAAM,YAAY,QAGQ;AACxB,WAAO,KAAK,QAAQ,eAAe,MAAM;AAAA,EAC3C;AAAA,EAEA,MAAM,WAAW,QAGS;AACxB,WAAO,KAAK,QAAQ,cAAc,MAAM;AAAA,EAC1C;AAAA,EAEA,MAAM,gBAAgB,cAA8C;AAClE,WAAO,KAAK,QAAQ,mBAAmB,EAAE,aAAa,CAAC;AAAA,EACzD;AAAA,EAEA,MAAM,cACJ,SACgC;AAChC,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,WAAkD;AACvE,WAAO,KAAK,QAAQ,oBAAoB,EAAE,UAAU,CAAC;AAAA,EACvD;AACF;","names":[]}
package/dist/handler.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { SupportIntegration } from './integration.js';
2
2
  import './types.js';
3
+ import 'zod';
3
4
 
4
5
  /**
5
6
  * Configuration for createSupportHandler
package/dist/handler.js CHANGED
@@ -1,3 +1,5 @@
1
+ import "./chunk-PZ5AY32C.js";
2
+
1
3
  // src/handler.ts
2
4
  import { timingSafeEqual } from "crypto";
3
5
  function createSupportHandler(config) {
@@ -155,6 +157,17 @@ async function routeAction(integration, action, body) {
155
157
  const result = await integration.searchContent(params);
156
158
  return { data: result, status: 200 };
157
159
  }
160
+ case "getProductStatus": {
161
+ if (!integration.getProductStatus) {
162
+ return {
163
+ data: { error: "Method not implemented: getProductStatus" },
164
+ status: 501
165
+ };
166
+ }
167
+ const productId = body.productId;
168
+ const result = await integration.getProductStatus(productId);
169
+ return { data: result, status: 200 };
170
+ }
158
171
  default:
159
172
  return {
160
173
  data: { error: `Unknown action: ${action}` },
@@ -1 +1 @@
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":[]}
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 case 'getProductStatus': {\n if (!integration.getProductStatus) {\n return {\n data: { error: 'Method not implemented: getProductStatus' },\n status: 501,\n }\n }\n const productId = (body as unknown as { productId: string }).productId\n const result = await integration.getProductStatus(productId)\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,5 +1,6 @@
1
- import { User, Purchase, Subscription, ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse } from './types.js';
1
+ import { User, Purchase, Subscription, ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse, ProductStatus } from './types.js';
2
2
  export { ContentSearchResult } from './types.js';
3
+ import 'zod';
3
4
 
4
5
  /**
5
6
  * SupportIntegration interface that apps must implement.
@@ -167,6 +168,29 @@ interface SupportIntegration {
167
168
  * ```
168
169
  */
169
170
  searchContent?(request: ContentSearchRequest): Promise<ContentSearchResponse>;
171
+ /**
172
+ * Get product availability/inventory status.
173
+ * Optional method - implement to let the agent accurately report availability.
174
+ *
175
+ * IMPORTANT: The agent should call this BEFORE claiming a product is sold out
176
+ * or unavailable. Without this, the agent may give incorrect availability info.
177
+ *
178
+ * @param productId - Product identifier (slug or ID)
179
+ * @returns ProductStatus with availability, inventory, and enrollment info
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * // Customer asks: "Can I still sign up for the TypeScript workshop?"
184
+ * const status = await integration.getProductStatus('ts-workshop-feb-2026')
185
+ *
186
+ * if (status?.soldOut) {
187
+ * // Agent: "Sorry, this workshop is sold out (0 of 50 seats remaining)"
188
+ * } else if (status?.available && status.quantityRemaining > 0) {
189
+ * // Agent: "Yes! There are still 12 seats available."
190
+ * }
191
+ * ```
192
+ */
193
+ getProductStatus?(productId: string): Promise<ProductStatus | null>;
170
194
  }
171
195
 
172
- export { ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse, Purchase, Subscription, type SupportIntegration, User };
196
+ export { ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse, ProductStatus, Purchase, Subscription, type SupportIntegration, User };
package/dist/types.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  /**
2
4
  * User entity returned by app integration.
3
5
  * Replaces Customer for consistency with SupportIntegration interface.
@@ -138,5 +140,79 @@ interface ContentSearchResponse {
138
140
  searchTimeMs?: number;
139
141
  };
140
142
  }
143
+ /**
144
+ * Product type for availability checking.
145
+ * Self-paced is always available, live/cohort have limited seats.
146
+ */
147
+ type ProductType = 'self-paced' | 'live' | 'cohort' | 'membership' | 'source-code-access' | (string & {});
148
+ /**
149
+ * Product state lifecycle.
150
+ */
151
+ type ProductState = 'draft' | 'active' | 'unavailable' | 'archived';
152
+ /**
153
+ * Zod schema for ProductType validation
154
+ */
155
+ declare const ProductTypeSchema: z.ZodUnion<readonly [z.ZodLiteral<"self-paced">, z.ZodLiteral<"live">, z.ZodLiteral<"cohort">, z.ZodLiteral<"membership">, z.ZodLiteral<"source-code-access">, z.ZodString]>;
156
+ /**
157
+ * Zod schema for ProductState validation
158
+ */
159
+ declare const ProductStateSchema: z.ZodEnum<{
160
+ active: "active";
161
+ draft: "draft";
162
+ unavailable: "unavailable";
163
+ archived: "archived";
164
+ }>;
165
+ /**
166
+ * Zod schema for ProductStatus validation
167
+ */
168
+ declare const ProductStatusSchema: z.ZodObject<{
169
+ productId: z.ZodString;
170
+ productType: z.ZodUnion<readonly [z.ZodLiteral<"self-paced">, z.ZodLiteral<"live">, z.ZodLiteral<"cohort">, z.ZodLiteral<"membership">, z.ZodLiteral<"source-code-access">, z.ZodString]>;
171
+ available: z.ZodBoolean;
172
+ soldOut: z.ZodBoolean;
173
+ quantityAvailable: z.ZodNumber;
174
+ quantityRemaining: z.ZodNumber;
175
+ state: z.ZodEnum<{
176
+ active: "active";
177
+ draft: "draft";
178
+ unavailable: "unavailable";
179
+ archived: "archived";
180
+ }>;
181
+ startsAt: z.ZodOptional<z.ZodString>;
182
+ endsAt: z.ZodOptional<z.ZodString>;
183
+ enrollmentOpen: z.ZodOptional<z.ZodString>;
184
+ enrollmentClose: z.ZodOptional<z.ZodString>;
185
+ }, z.core.$strip>;
186
+ /**
187
+ * Product availability/inventory status.
188
+ * Used by agent to accurately report whether products can be purchased.
189
+ *
190
+ * For live events/cohorts: check soldOut and quantityRemaining
191
+ * For self-paced: typically available=true, quantityAvailable=-1 (unlimited)
192
+ */
193
+ interface ProductStatus {
194
+ /** Product identifier */
195
+ productId: string;
196
+ /** Type of product determines availability semantics */
197
+ productType: ProductType;
198
+ /** Whether the product can currently be purchased */
199
+ available: boolean;
200
+ /** Whether all seats/inventory are sold */
201
+ soldOut: boolean;
202
+ /** Total quantity available for sale (-1 = unlimited) */
203
+ quantityAvailable: number;
204
+ /** Remaining quantity not yet sold */
205
+ quantityRemaining: number;
206
+ /** Product lifecycle state */
207
+ state: ProductState;
208
+ /** For live events: when the event starts */
209
+ startsAt?: string;
210
+ /** For live events: when the event ends */
211
+ endsAt?: string;
212
+ /** For cohorts: when enrollment opens */
213
+ enrollmentOpen?: string;
214
+ /** For cohorts: when enrollment closes */
215
+ enrollmentClose?: string;
216
+ }
141
217
 
142
- export type { ActionResult, ClaimedSeat, ContentSearchRequest, ContentSearchResponse, ContentSearchResult, Customer, Purchase, RefundRequest, RefundResult, Subscription, User };
218
+ export { type ActionResult, type ClaimedSeat, type ContentSearchRequest, type ContentSearchResponse, type ContentSearchResult, type Customer, type ProductState, ProductStateSchema, type ProductStatus, ProductStatusSchema, type ProductType, ProductTypeSchema, type Purchase, type RefundRequest, type RefundResult, type Subscription, type User };