@gurulu/node 0.1.1 → 1.0.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.
Files changed (48) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +64 -31
  3. package/dist/context.d.ts +12 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/core.d.ts +60 -0
  6. package/dist/core.d.ts.map +1 -0
  7. package/dist/errors.d.ts +32 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/identify.d.ts +9 -0
  10. package/dist/identify.d.ts.map +1 -0
  11. package/dist/index.d.ts +13 -12
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +947 -21
  14. package/dist/middleware/express.d.ts +25 -0
  15. package/dist/middleware/express.d.ts.map +1 -0
  16. package/dist/middleware/express.js +66 -0
  17. package/dist/middleware/fastify.d.ts +20 -0
  18. package/dist/middleware/fastify.d.ts.map +1 -0
  19. package/dist/middleware/fastify.js +68 -0
  20. package/dist/middleware/next.d.ts +10 -0
  21. package/dist/middleware/next.d.ts.map +1 -0
  22. package/dist/middleware/next.js +69 -0
  23. package/dist/queue.d.ts +20 -0
  24. package/dist/queue.d.ts.map +1 -0
  25. package/dist/track.d.ts +10 -0
  26. package/dist/track.d.ts.map +1 -0
  27. package/dist/transport.d.ts +16 -0
  28. package/dist/transport.d.ts.map +1 -0
  29. package/dist/types.d.ts +129 -43
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/webhooks/custom.d.ts +30 -0
  32. package/dist/webhooks/custom.d.ts.map +1 -0
  33. package/dist/webhooks/custom.js +123 -0
  34. package/dist/webhooks/lemonsqueezy.d.ts +30 -0
  35. package/dist/webhooks/lemonsqueezy.d.ts.map +1 -0
  36. package/dist/webhooks/lemonsqueezy.js +140 -0
  37. package/dist/webhooks/shopify.d.ts +18 -0
  38. package/dist/webhooks/shopify.d.ts.map +1 -0
  39. package/dist/webhooks/shopify.js +142 -0
  40. package/dist/webhooks/stripe.d.ts +31 -0
  41. package/dist/webhooks/stripe.d.ts.map +1 -0
  42. package/dist/webhooks/stripe.js +160 -0
  43. package/package.json +105 -11
  44. package/dist/business-events.d.ts +0 -73
  45. package/dist/business-events.js +0 -113
  46. package/dist/client.d.ts +0 -90
  47. package/dist/client.js +0 -307
  48. package/dist/types.js +0 -30
@@ -0,0 +1,123 @@
1
+ // src/webhooks/custom.ts
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+
4
+ // src/errors.ts
5
+ class GuruluSDKError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.name = "GuruluSDKError";
10
+ this.code = code;
11
+ }
12
+ }
13
+
14
+ class NotInitializedError extends GuruluSDKError {
15
+ constructor() {
16
+ super("SDK_NOT_INITIALIZED", "Gurulu.init() must be called before track/identify");
17
+ }
18
+ }
19
+
20
+ class InvalidWorkspaceKeyError extends GuruluSDKError {
21
+ constructor(message) {
22
+ super("SDK_INVALID_WORKSPACE_KEY", message);
23
+ }
24
+ }
25
+
26
+ class WebhookSignatureError extends GuruluSDKError {
27
+ constructor(message) {
28
+ super("WEBHOOK_SIGNATURE_INVALID", message);
29
+ }
30
+ }
31
+
32
+ class WebhookMappingNotFoundError extends GuruluSDKError {
33
+ constructor(message) {
34
+ super("WEBHOOK_MAPPING_NOT_FOUND", message);
35
+ }
36
+ }
37
+
38
+ class TransportError extends GuruluSDKError {
39
+ status;
40
+ attempts;
41
+ constructor(message, attempts, status) {
42
+ super("SDK_TRANSPORT_ERROR", message);
43
+ this.attempts = attempts;
44
+ this.status = status;
45
+ }
46
+ }
47
+
48
+ class QueueOverflowError extends GuruluSDKError {
49
+ dropped;
50
+ constructor(dropped) {
51
+ super("SDK_QUEUE_OVERFLOW", `Queue overflow — dropped ${dropped} oldest events`);
52
+ this.dropped = dropped;
53
+ }
54
+ }
55
+
56
+ // src/webhooks/custom.ts
57
+ function defaultVerify(rawBody, signature, secret) {
58
+ const expected = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
59
+ const actualBuf = Buffer.from(signature, "hex");
60
+ const expectedBuf = Buffer.from(expected, "hex");
61
+ if (actualBuf.length !== expectedBuf.length)
62
+ return false;
63
+ return timingSafeEqual(actualBuf, expectedBuf);
64
+ }
65
+ function toOutgoing(result) {
66
+ if (!result.event_key) {
67
+ throw new WebhookMappingNotFoundError("Custom mapPayload returned no event_key");
68
+ }
69
+ const consent = {
70
+ analytics: true,
71
+ marketing: true,
72
+ functional: true,
73
+ personalization: false,
74
+ source: "webhook"
75
+ };
76
+ const event = {
77
+ anonymous_id: result.anonymous_id ?? `custom_anon_${result.event_id ?? Date.now()}`,
78
+ event_id: result.event_id,
79
+ event_key: result.event_key,
80
+ event_type: "outcome",
81
+ occurred_at: result.occurred_at ?? new Date().toISOString(),
82
+ producer: "webhook",
83
+ producer_version: "0.1.0",
84
+ properties: result.properties ?? {},
85
+ consent_state: consent
86
+ };
87
+ if (result.person_id)
88
+ event.person_id = result.person_id;
89
+ return event;
90
+ }
91
+ function createCustomWebhook(config, enqueue, options) {
92
+ const headerName = options.signatureHeader.toLowerCase();
93
+ const verifier = options.verifySignature ?? defaultVerify;
94
+ return {
95
+ verify(rawBody, signature) {
96
+ return verifier(rawBody, signature, options.secret);
97
+ },
98
+ async handle(req) {
99
+ const sigRaw = req.headers[headerName];
100
+ const sig = Array.isArray(sigRaw) ? sigRaw[0] : sigRaw;
101
+ if (!sig) {
102
+ throw new WebhookSignatureError(`Header '${options.signatureHeader}' missing`);
103
+ }
104
+ if (!verifier(req.body, sig, options.secret)) {
105
+ throw new WebhookSignatureError("Custom webhook signature mismatch");
106
+ }
107
+ const payload = JSON.parse(req.body);
108
+ const mapped = options.mapPayload(payload);
109
+ const results = Array.isArray(mapped) ? mapped : [mapped];
110
+ const events = results.map(toOutgoing);
111
+ for (const e of events)
112
+ enqueue(e);
113
+ return {
114
+ events_emitted: events.length,
115
+ vendor: "custom",
116
+ event_keys: events.map((e) => e.event_key)
117
+ };
118
+ }
119
+ };
120
+ }
121
+ export {
122
+ createCustomWebhook
123
+ };
@@ -0,0 +1,30 @@
1
+ import type { HandleResult, OutgoingEvent, ResolvedConfig } from '../types.ts';
2
+ /** 6 default Lemon Squeezy event → Gurulu event_key mapping. */
3
+ export declare const LEMONSQUEEZY_DEFAULT_MAPPING: Record<string, string>;
4
+ export interface LemonSqueezyHandleReq {
5
+ body: string;
6
+ headers: Record<string, string | string[] | undefined>;
7
+ }
8
+ export interface LemonSqueezyHandleOptions {
9
+ customMapping?: Record<string, string>;
10
+ }
11
+ interface LemonSqueezyPayload {
12
+ meta?: {
13
+ event_name?: string;
14
+ custom_data?: Record<string, unknown>;
15
+ };
16
+ data?: {
17
+ id?: string;
18
+ type?: string;
19
+ attributes?: Record<string, unknown>;
20
+ };
21
+ }
22
+ export declare function verifyLemonSqueezy(rawBody: string, signatureHeader: string | undefined, secret: string): void;
23
+ export declare function mapLemonSqueezyEvent(payload: LemonSqueezyPayload, options?: LemonSqueezyHandleOptions): OutgoingEvent;
24
+ export declare function createLemonSqueezyHandler(config: ResolvedConfig, enqueue: (event: OutgoingEvent) => void): {
25
+ verify: typeof verifyLemonSqueezy;
26
+ map: typeof mapLemonSqueezyEvent;
27
+ handle: (req: LemonSqueezyHandleReq, secret: string, options?: LemonSqueezyHandleOptions) => Promise<HandleResult>;
28
+ };
29
+ export {};
30
+ //# sourceMappingURL=lemonsqueezy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lemonsqueezy.d.ts","sourceRoot":"","sources":["../../src/webhooks/lemonsqueezy.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,cAAc,EAEf,MAAM,aAAa,CAAC;AAErB,gEAAgE;AAChE,eAAO,MAAM,4BAA4B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAO/D,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,UAAU,mBAAmB;IAC3B,IAAI,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IACtE,IAAI,CAAC,EAAE;QACL,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,CAAC;CACH;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,MAAM,EAAE,MAAM,GACb,IAAI,CAQN;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,yBAA8B,GACtC,aAAa,CAkDf;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GACtC;IACD,MAAM,EAAE,OAAO,kBAAkB,CAAC;IAClC,GAAG,EAAE,OAAO,oBAAoB,CAAC;IACjC,MAAM,EAAE,CACN,GAAG,EAAE,qBAAqB,EAC1B,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,yBAAyB,KAChC,OAAO,CAAC,YAAY,CAAC,CAAC;CAC5B,CAmBA"}
@@ -0,0 +1,140 @@
1
+ // src/webhooks/lemonsqueezy.ts
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+
4
+ // src/errors.ts
5
+ class GuruluSDKError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.name = "GuruluSDKError";
10
+ this.code = code;
11
+ }
12
+ }
13
+
14
+ class NotInitializedError extends GuruluSDKError {
15
+ constructor() {
16
+ super("SDK_NOT_INITIALIZED", "Gurulu.init() must be called before track/identify");
17
+ }
18
+ }
19
+
20
+ class InvalidWorkspaceKeyError extends GuruluSDKError {
21
+ constructor(message) {
22
+ super("SDK_INVALID_WORKSPACE_KEY", message);
23
+ }
24
+ }
25
+
26
+ class WebhookSignatureError extends GuruluSDKError {
27
+ constructor(message) {
28
+ super("WEBHOOK_SIGNATURE_INVALID", message);
29
+ }
30
+ }
31
+
32
+ class WebhookMappingNotFoundError extends GuruluSDKError {
33
+ constructor(message) {
34
+ super("WEBHOOK_MAPPING_NOT_FOUND", message);
35
+ }
36
+ }
37
+
38
+ class TransportError extends GuruluSDKError {
39
+ status;
40
+ attempts;
41
+ constructor(message, attempts, status) {
42
+ super("SDK_TRANSPORT_ERROR", message);
43
+ this.attempts = attempts;
44
+ this.status = status;
45
+ }
46
+ }
47
+
48
+ class QueueOverflowError extends GuruluSDKError {
49
+ dropped;
50
+ constructor(dropped) {
51
+ super("SDK_QUEUE_OVERFLOW", `Queue overflow — dropped ${dropped} oldest events`);
52
+ this.dropped = dropped;
53
+ }
54
+ }
55
+
56
+ // src/webhooks/lemonsqueezy.ts
57
+ var LEMONSQUEEZY_DEFAULT_MAPPING = {
58
+ order_created: "purchase_completed",
59
+ order_refunded: "refund_processed",
60
+ subscription_created: "subscription_started",
61
+ subscription_updated: "subscription_updated",
62
+ subscription_cancelled: "subscription_cancelled",
63
+ subscription_payment_success: "payment_succeeded"
64
+ };
65
+ function verifyLemonSqueezy(rawBody, signatureHeader, secret) {
66
+ if (!signatureHeader)
67
+ throw new WebhookSignatureError("X-Signature header missing");
68
+ const expected = createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
69
+ const actualBuf = Buffer.from(signatureHeader, "hex");
70
+ const expectedBuf = Buffer.from(expected, "hex");
71
+ if (actualBuf.length !== expectedBuf.length || !timingSafeEqual(actualBuf, expectedBuf)) {
72
+ throw new WebhookSignatureError("Lemon Squeezy signature mismatch");
73
+ }
74
+ }
75
+ function mapLemonSqueezyEvent(payload, options = {}) {
76
+ const eventName = payload.meta?.event_name ?? "";
77
+ const mapping = { ...LEMONSQUEEZY_DEFAULT_MAPPING, ...options.customMapping ?? {} };
78
+ const eventKey = mapping[eventName];
79
+ if (!eventKey) {
80
+ throw new WebhookMappingNotFoundError(`Lemon Squeezy event_name '${eventName}' has no Gurulu mapping`);
81
+ }
82
+ const attr = payload.data?.attributes ?? {};
83
+ const objectId = payload.data?.id ?? "unknown";
84
+ const occurredAt = typeof attr.updated_at === "string" ? attr.updated_at : typeof attr.created_at === "string" ? attr.created_at : new Date().toISOString();
85
+ const customerId = typeof attr.customer_id === "string" || typeof attr.customer_id === "number" ? String(attr.customer_id) : undefined;
86
+ const props = {
87
+ lemonsqueezy_event_name: eventName,
88
+ lemonsqueezy_object_id: objectId,
89
+ lemonsqueezy_object_type: payload.data?.type
90
+ };
91
+ if (typeof attr.total === "number")
92
+ props.amount = attr.total / 100;
93
+ if (typeof attr.currency === "string")
94
+ props.currency = attr.currency;
95
+ if (typeof attr.status === "string")
96
+ props.status = attr.status;
97
+ const consent = {
98
+ analytics: true,
99
+ marketing: true,
100
+ functional: true,
101
+ personalization: false,
102
+ source: "webhook"
103
+ };
104
+ return {
105
+ anonymous_id: customerId ? `lemonsqueezy_${customerId}` : `lemonsqueezy_anon_${objectId}`,
106
+ event_id: `lemonsqueezy_${eventName}_${objectId}`,
107
+ event_key: eventKey,
108
+ event_type: "outcome",
109
+ occurred_at: occurredAt,
110
+ producer: "webhook",
111
+ producer_version: "0.1.0",
112
+ properties: props,
113
+ consent_state: consent
114
+ };
115
+ }
116
+ function createLemonSqueezyHandler(config, enqueue) {
117
+ return {
118
+ verify: verifyLemonSqueezy,
119
+ map: mapLemonSqueezyEvent,
120
+ async handle(req, secret, options) {
121
+ const sigHeader = req.headers["x-signature"];
122
+ const sig = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader;
123
+ verifyLemonSqueezy(req.body, sig, secret);
124
+ const payload = JSON.parse(req.body);
125
+ const event = mapLemonSqueezyEvent(payload, options);
126
+ enqueue(event);
127
+ return {
128
+ events_emitted: 1,
129
+ vendor: "lemonsqueezy",
130
+ event_keys: [event.event_key]
131
+ };
132
+ }
133
+ };
134
+ }
135
+ export {
136
+ verifyLemonSqueezy,
137
+ mapLemonSqueezyEvent,
138
+ createLemonSqueezyHandler,
139
+ LEMONSQUEEZY_DEFAULT_MAPPING
140
+ };
@@ -0,0 +1,18 @@
1
+ import type { HandleResult, OutgoingEvent, ResolvedConfig } from '../types.ts';
2
+ /** 8 default Shopify topic → Gurulu event_key mapping. */
3
+ export declare const SHOPIFY_DEFAULT_MAPPING: Record<string, string>;
4
+ export interface ShopifyHandleReq {
5
+ body: string;
6
+ headers: Record<string, string | string[] | undefined>;
7
+ }
8
+ export interface ShopifyHandleOptions {
9
+ customMapping?: Record<string, string>;
10
+ }
11
+ export declare function verifyShopify(rawBody: string, signatureHeader: string | undefined, secret: string): void;
12
+ export declare function mapShopifyEvent(topic: string | undefined, payload: Record<string, unknown>, options?: ShopifyHandleOptions): OutgoingEvent;
13
+ export declare function createShopifyHandler(config: ResolvedConfig, enqueue: (event: OutgoingEvent) => void): {
14
+ verify: typeof verifyShopify;
15
+ map: typeof mapShopifyEvent;
16
+ handle: (req: ShopifyHandleReq, secret: string, options?: ShopifyHandleOptions) => Promise<HandleResult>;
17
+ };
18
+ //# sourceMappingURL=shopify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shopify.d.ts","sourceRoot":"","sources":["../../src/webhooks/shopify.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,cAAc,EAEf,MAAM,aAAa,CAAC;AAErB,0DAA0D;AAC1D,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAS1D,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,MAAM,EAAE,MAAM,GACb,IAAI,CAQN;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,GAAE,oBAAyB,GACjC,aAAa,CA+Cf;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GACtC;IACD,MAAM,EAAE,OAAO,aAAa,CAAC;IAC7B,GAAG,EAAE,OAAO,eAAe,CAAC;IAC5B,MAAM,EAAE,CACN,GAAG,EAAE,gBAAgB,EACrB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,oBAAoB,KAC3B,OAAO,CAAC,YAAY,CAAC,CAAC;CAC5B,CAqBA"}
@@ -0,0 +1,142 @@
1
+ // src/webhooks/shopify.ts
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+
4
+ // src/errors.ts
5
+ class GuruluSDKError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.name = "GuruluSDKError";
10
+ this.code = code;
11
+ }
12
+ }
13
+
14
+ class NotInitializedError extends GuruluSDKError {
15
+ constructor() {
16
+ super("SDK_NOT_INITIALIZED", "Gurulu.init() must be called before track/identify");
17
+ }
18
+ }
19
+
20
+ class InvalidWorkspaceKeyError extends GuruluSDKError {
21
+ constructor(message) {
22
+ super("SDK_INVALID_WORKSPACE_KEY", message);
23
+ }
24
+ }
25
+
26
+ class WebhookSignatureError extends GuruluSDKError {
27
+ constructor(message) {
28
+ super("WEBHOOK_SIGNATURE_INVALID", message);
29
+ }
30
+ }
31
+
32
+ class WebhookMappingNotFoundError extends GuruluSDKError {
33
+ constructor(message) {
34
+ super("WEBHOOK_MAPPING_NOT_FOUND", message);
35
+ }
36
+ }
37
+
38
+ class TransportError extends GuruluSDKError {
39
+ status;
40
+ attempts;
41
+ constructor(message, attempts, status) {
42
+ super("SDK_TRANSPORT_ERROR", message);
43
+ this.attempts = attempts;
44
+ this.status = status;
45
+ }
46
+ }
47
+
48
+ class QueueOverflowError extends GuruluSDKError {
49
+ dropped;
50
+ constructor(dropped) {
51
+ super("SDK_QUEUE_OVERFLOW", `Queue overflow — dropped ${dropped} oldest events`);
52
+ this.dropped = dropped;
53
+ }
54
+ }
55
+
56
+ // src/webhooks/shopify.ts
57
+ var SHOPIFY_DEFAULT_MAPPING = {
58
+ "orders/create": "purchase_completed",
59
+ "orders/paid": "purchase_completed",
60
+ "orders/fulfilled": "order_fulfilled",
61
+ "orders/cancelled": "order_cancelled",
62
+ "customers/create": "signup_completed",
63
+ "carts/create": "cart_started",
64
+ "carts/update": "cart_updated",
65
+ "checkouts/create": "checkout_started"
66
+ };
67
+ function verifyShopify(rawBody, signatureHeader, secret) {
68
+ if (!signatureHeader)
69
+ throw new WebhookSignatureError("X-Shopify-Hmac-Sha256 header missing");
70
+ const expected = createHmac("sha256", secret).update(rawBody, "utf8").digest("base64");
71
+ const actualBuf = Buffer.from(signatureHeader, "base64");
72
+ const expectedBuf = Buffer.from(expected, "base64");
73
+ if (actualBuf.length !== expectedBuf.length || !timingSafeEqual(actualBuf, expectedBuf)) {
74
+ throw new WebhookSignatureError("Shopify HMAC mismatch");
75
+ }
76
+ }
77
+ function mapShopifyEvent(topic, payload, options = {}) {
78
+ if (!topic)
79
+ throw new WebhookMappingNotFoundError("X-Shopify-Topic header required");
80
+ const mapping = { ...SHOPIFY_DEFAULT_MAPPING, ...options.customMapping ?? {} };
81
+ const eventKey = mapping[topic];
82
+ if (!eventKey) {
83
+ throw new WebhookMappingNotFoundError(`Shopify topic '${topic}' has no Gurulu mapping`);
84
+ }
85
+ const occurredAt = typeof payload.updated_at === "string" ? payload.updated_at : typeof payload.created_at === "string" ? payload.created_at : new Date().toISOString();
86
+ const customer = typeof payload.customer === "object" && payload.customer !== null ? payload.customer.id : undefined;
87
+ const props = {
88
+ shopify_topic: topic,
89
+ shopify_object_id: payload.id
90
+ };
91
+ if (typeof payload.total_price === "string")
92
+ props.amount = Number(payload.total_price);
93
+ if (typeof payload.currency === "string")
94
+ props.currency = payload.currency;
95
+ if (typeof payload.financial_status === "string")
96
+ props.status = payload.financial_status;
97
+ const consent = {
98
+ analytics: true,
99
+ marketing: true,
100
+ functional: true,
101
+ personalization: false,
102
+ source: "webhook"
103
+ };
104
+ return {
105
+ anonymous_id: customer ? `shopify_${customer}` : `shopify_anon_${payload.id ?? "unknown"}`,
106
+ event_id: typeof payload.id === "string" || typeof payload.id === "number" ? `shopify_${topic.replace("/", "_")}_${payload.id}` : undefined,
107
+ event_key: eventKey,
108
+ event_type: "outcome",
109
+ occurred_at: occurredAt,
110
+ producer: "webhook",
111
+ producer_version: "0.1.0",
112
+ properties: props,
113
+ consent_state: consent
114
+ };
115
+ }
116
+ function createShopifyHandler(config, enqueue) {
117
+ return {
118
+ verify: verifyShopify,
119
+ map: mapShopifyEvent,
120
+ async handle(req, secret, options) {
121
+ const sigHeader = req.headers["x-shopify-hmac-sha256"];
122
+ const topicHeader = req.headers["x-shopify-topic"];
123
+ const sig = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader;
124
+ const topic = Array.isArray(topicHeader) ? topicHeader[0] : topicHeader;
125
+ verifyShopify(req.body, sig, secret);
126
+ const payload = JSON.parse(req.body);
127
+ const event = mapShopifyEvent(topic, payload, options);
128
+ enqueue(event);
129
+ return {
130
+ events_emitted: 1,
131
+ vendor: "shopify",
132
+ event_keys: [event.event_key]
133
+ };
134
+ }
135
+ };
136
+ }
137
+ export {
138
+ verifyShopify,
139
+ mapShopifyEvent,
140
+ createShopifyHandler,
141
+ SHOPIFY_DEFAULT_MAPPING
142
+ };
@@ -0,0 +1,31 @@
1
+ import type { HandleResult, OutgoingEvent, ResolvedConfig } from '../types.ts';
2
+ export declare const STRIPE_REPLAY_TOLERANCE_SECONDS = 300;
3
+ /** 9 default Stripe → Gurulu event_key mapping. */
4
+ export declare const STRIPE_DEFAULT_MAPPING: Record<string, string>;
5
+ export interface StripeHandleReq {
6
+ body: string;
7
+ headers: Record<string, string | string[] | undefined>;
8
+ }
9
+ export interface StripeHandleOptions {
10
+ customMapping?: Record<string, string>;
11
+ now?: () => Date;
12
+ }
13
+ export interface StripeEventPayload {
14
+ id?: string;
15
+ type?: string;
16
+ created?: number;
17
+ data?: {
18
+ object?: Record<string, unknown>;
19
+ };
20
+ }
21
+ /** Verify Stripe-Signature header. Throws WebhookSignatureError on mismatch. */
22
+ export declare function verifyStripe(rawBody: string, signatureHeader: string | undefined, secret: string, now?: () => Date): void;
23
+ /** Map verified Stripe event → Gurulu OutgoingEvent. */
24
+ export declare function mapStripeEvent(payload: StripeEventPayload, options?: StripeHandleOptions): OutgoingEvent;
25
+ /** Factory bound to a Gurulu instance enqueue callback. */
26
+ export declare function createStripeHandler(config: ResolvedConfig, enqueue: (event: OutgoingEvent) => void): {
27
+ verify: (rawBody: string, sig: string | undefined, secret: string) => void;
28
+ map: (payload: StripeEventPayload, options?: StripeHandleOptions) => OutgoingEvent;
29
+ handle: (req: StripeHandleReq, secret: string, options?: StripeHandleOptions) => Promise<HandleResult>;
30
+ };
31
+ //# sourceMappingURL=stripe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe.d.ts","sourceRoot":"","sources":["../../src/webhooks/stripe.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,cAAc,EAEf,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,mDAAmD;AACnD,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAUzD,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;CAC7C;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAM,IAAuB,GACjC,IAAI,CAmBN;AAED,wDAAwD;AACxD,wBAAgB,cAAc,CAC5B,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,mBAAwB,GAChC,aAAa,CAyCf;AAED,2DAA2D;AAC3D,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GACtC;IACD,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3E,GAAG,EAAE,CAAC,OAAO,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,aAAa,CAAC;IACnF,MAAM,EAAE,CACN,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,mBAAmB,KAC1B,OAAO,CAAC,YAAY,CAAC,CAAC;CAC5B,CAmBA"}
@@ -0,0 +1,160 @@
1
+ // src/webhooks/stripe.ts
2
+ import { createHmac, timingSafeEqual } from "node:crypto";
3
+
4
+ // src/errors.ts
5
+ class GuruluSDKError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.name = "GuruluSDKError";
10
+ this.code = code;
11
+ }
12
+ }
13
+
14
+ class NotInitializedError extends GuruluSDKError {
15
+ constructor() {
16
+ super("SDK_NOT_INITIALIZED", "Gurulu.init() must be called before track/identify");
17
+ }
18
+ }
19
+
20
+ class InvalidWorkspaceKeyError extends GuruluSDKError {
21
+ constructor(message) {
22
+ super("SDK_INVALID_WORKSPACE_KEY", message);
23
+ }
24
+ }
25
+
26
+ class WebhookSignatureError extends GuruluSDKError {
27
+ constructor(message) {
28
+ super("WEBHOOK_SIGNATURE_INVALID", message);
29
+ }
30
+ }
31
+
32
+ class WebhookMappingNotFoundError extends GuruluSDKError {
33
+ constructor(message) {
34
+ super("WEBHOOK_MAPPING_NOT_FOUND", message);
35
+ }
36
+ }
37
+
38
+ class TransportError extends GuruluSDKError {
39
+ status;
40
+ attempts;
41
+ constructor(message, attempts, status) {
42
+ super("SDK_TRANSPORT_ERROR", message);
43
+ this.attempts = attempts;
44
+ this.status = status;
45
+ }
46
+ }
47
+
48
+ class QueueOverflowError extends GuruluSDKError {
49
+ dropped;
50
+ constructor(dropped) {
51
+ super("SDK_QUEUE_OVERFLOW", `Queue overflow — dropped ${dropped} oldest events`);
52
+ this.dropped = dropped;
53
+ }
54
+ }
55
+
56
+ // src/webhooks/stripe.ts
57
+ var STRIPE_REPLAY_TOLERANCE_SECONDS = 300;
58
+ var STRIPE_DEFAULT_MAPPING = {
59
+ "charge.succeeded": "purchase_completed",
60
+ "charge.refunded": "refund_processed",
61
+ "payment_intent.succeeded": "payment_succeeded",
62
+ "invoice.paid": "payment_succeeded",
63
+ "invoice.payment_failed": "payment_failed",
64
+ "customer.subscription.created": "subscription_started",
65
+ "customer.subscription.updated": "subscription_updated",
66
+ "customer.subscription.deleted": "subscription_cancelled",
67
+ "checkout.session.completed": "checkout_completed"
68
+ };
69
+ function verifyStripe(rawBody, signatureHeader, secret, now = () => new Date) {
70
+ if (!signatureHeader)
71
+ throw new WebhookSignatureError("Stripe-Signature header missing");
72
+ let timestamp = null;
73
+ let v1 = null;
74
+ for (const part of signatureHeader.split(",").map((s) => s.trim())) {
75
+ const [k, v] = part.split("=", 2);
76
+ if (k === "t")
77
+ timestamp = Number(v);
78
+ if (k === "v1")
79
+ v1 = v ?? null;
80
+ }
81
+ if (!timestamp || !v1)
82
+ throw new WebhookSignatureError("Stripe-Signature missing t= or v1=");
83
+ if (Math.abs(now().getTime() / 1000 - timestamp) > STRIPE_REPLAY_TOLERANCE_SECONDS) {
84
+ throw new WebhookSignatureError("Stripe-Signature timestamp out of tolerance");
85
+ }
86
+ const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
87
+ const actualBuf = Buffer.from(v1, "hex");
88
+ const expectedBuf = Buffer.from(expected, "hex");
89
+ if (actualBuf.length !== expectedBuf.length || !timingSafeEqual(actualBuf, expectedBuf)) {
90
+ throw new WebhookSignatureError("Stripe-Signature mismatch");
91
+ }
92
+ }
93
+ function mapStripeEvent(payload, options = {}) {
94
+ const type = payload.type ?? "";
95
+ const mapping = { ...STRIPE_DEFAULT_MAPPING, ...options.customMapping ?? {} };
96
+ const eventKey = mapping[type];
97
+ if (!eventKey) {
98
+ throw new WebhookMappingNotFoundError(`Stripe event type '${type}' has no Gurulu mapping`);
99
+ }
100
+ const obj = payload.data?.object ?? {};
101
+ const customer = typeof obj.customer === "string" ? obj.customer : undefined;
102
+ const occurredAt = payload.created ? new Date(payload.created * 1000).toISOString() : new Date().toISOString();
103
+ const props = {
104
+ stripe_event_id: payload.id,
105
+ stripe_event_type: type
106
+ };
107
+ if (typeof obj.amount === "number")
108
+ props.amount = obj.amount / 100;
109
+ if (typeof obj.currency === "string")
110
+ props.currency = obj.currency.toUpperCase();
111
+ if (typeof obj.id === "string")
112
+ props.stripe_object_id = obj.id;
113
+ if (typeof obj.status === "string")
114
+ props.status = obj.status;
115
+ const consent = {
116
+ analytics: true,
117
+ marketing: true,
118
+ functional: true,
119
+ personalization: false,
120
+ source: "webhook"
121
+ };
122
+ const event = {
123
+ anonymous_id: customer ? `stripe_${customer}` : `stripe_anon_${payload.id ?? "unknown"}`,
124
+ event_id: payload.id,
125
+ event_key: eventKey,
126
+ event_type: "outcome",
127
+ occurred_at: occurredAt,
128
+ producer: "webhook",
129
+ producer_version: "0.1.0",
130
+ properties: props,
131
+ consent_state: consent
132
+ };
133
+ return event;
134
+ }
135
+ function createStripeHandler(config, enqueue) {
136
+ return {
137
+ verify: verifyStripe,
138
+ map: mapStripeEvent,
139
+ async handle(req, secret, options) {
140
+ const sigHeader = req.headers["stripe-signature"];
141
+ const sig = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader;
142
+ verifyStripe(req.body, sig, secret, options?.now);
143
+ const payload = JSON.parse(req.body);
144
+ const event = mapStripeEvent(payload, options);
145
+ enqueue(event);
146
+ return {
147
+ events_emitted: 1,
148
+ vendor: "stripe",
149
+ event_keys: [event.event_key]
150
+ };
151
+ }
152
+ };
153
+ }
154
+ export {
155
+ verifyStripe,
156
+ mapStripeEvent,
157
+ createStripeHandler,
158
+ STRIPE_REPLAY_TOLERANCE_SECONDS,
159
+ STRIPE_DEFAULT_MAPPING
160
+ };