@moneydevkit/core 0.9.0-beta.2 → 0.9.0-beta.4

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 (47) hide show
  1. package/dist/actions.d.ts +6 -1
  2. package/dist/actions.js +58 -6
  3. package/dist/actions.js.map +1 -1
  4. package/dist/checkout-utils.d.ts +9 -0
  5. package/dist/checkout-utils.js +46 -0
  6. package/dist/checkout-utils.js.map +1 -1
  7. package/dist/client-actions.d.ts +6 -1
  8. package/dist/client-actions.js +10 -1
  9. package/dist/client-actions.js.map +1 -1
  10. package/dist/client.d.ts +3 -0
  11. package/dist/client.js +1 -0
  12. package/dist/client.js.map +1 -1
  13. package/dist/components/checkout/PendingPaymentCheckout.js +6 -2
  14. package/dist/components/checkout/PendingPaymentCheckout.js.map +1 -1
  15. package/dist/components/checkout/UnconfirmedCheckout.js +10 -2
  16. package/dist/components/checkout/UnconfirmedCheckout.js.map +1 -1
  17. package/dist/handlers/checkout.js +28 -2
  18. package/dist/handlers/checkout.js.map +1 -1
  19. package/dist/handlers/customer.d.ts +1 -0
  20. package/dist/handlers/customer.js +39 -0
  21. package/dist/handlers/customer.js.map +1 -0
  22. package/dist/handlers/index.d.ts +1 -0
  23. package/dist/handlers/index.js +1 -0
  24. package/dist/handlers/index.js.map +1 -1
  25. package/dist/handlers/subscription.d.ts +95 -0
  26. package/dist/handlers/subscription.js +208 -0
  27. package/dist/handlers/subscription.js.map +1 -0
  28. package/dist/handlers/webhooks.js +144 -23
  29. package/dist/handlers/webhooks.js.map +1 -1
  30. package/dist/hooks/useCustomer.d.ts +16 -0
  31. package/dist/hooks/useCustomer.js +52 -0
  32. package/dist/hooks/useCustomer.js.map +1 -0
  33. package/dist/hooks/useProducts.d.ts +14 -3
  34. package/dist/hooks/useProducts.js +11 -0
  35. package/dist/hooks/useProducts.js.map +1 -1
  36. package/dist/lightning-node.d.ts +16 -0
  37. package/dist/lightning-node.js +23 -0
  38. package/dist/lightning-node.js.map +1 -1
  39. package/dist/mdk-client.d.ts +19 -1
  40. package/dist/mdk-client.js +21 -0
  41. package/dist/mdk-client.js.map +1 -1
  42. package/dist/route.d.ts +17 -4
  43. package/dist/route.js +45 -21
  44. package/dist/route.js.map +1 -1
  45. package/dist/types.d.ts +1 -0
  46. package/dist/types.js.map +1 -1
  47. package/package.json +3 -3
@@ -4,6 +4,7 @@ export * from './pay_bolt_11';
4
4
  export * from './pay_bolt_12';
5
5
  export * from './pay_ln_url';
6
6
  export * from './ping';
7
+ export * from './subscription';
7
8
  export * from './sync_rgs';
8
9
  export * from './webhooks';
9
10
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,QAAQ,CAAA;AACtB,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,QAAQ,CAAA;AACtB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Options for creating a subscription renewal URL.
3
+ */
4
+ export interface CreateRenewalSubscriptionUrlOptions {
5
+ /** The subscription ID to renew */
6
+ subscriptionId: string;
7
+ /** Base path for the MDK API (default: /api/mdk) */
8
+ basePath?: string;
9
+ /** Custom checkout path (default: /checkout) */
10
+ checkoutPath?: string;
11
+ }
12
+ /**
13
+ * Options for creating a subscription cancel URL.
14
+ */
15
+ export interface CreateCancelSubscriptionUrlOptions {
16
+ /** The subscription ID to cancel */
17
+ subscriptionId: string;
18
+ }
19
+ /**
20
+ * Generate an HMAC-SHA256 signature for subscription action URL params.
21
+ * This is the core signing function used by both SDK helpers and backend services.
22
+ *
23
+ * @param action - The action type ('cancelSubscription' or 'renewSubscription')
24
+ * @param subscriptionId - The subscription ID
25
+ * @param secret - The HMAC secret (MDK_ACCESS_TOKEN / app.apiKey)
26
+ * @param additionalParams - Optional additional params to include (e.g., checkoutPath)
27
+ * @returns The hex-encoded HMAC signature
28
+ *
29
+ * @example
30
+ * // Backend usage with app.apiKey from database
31
+ * const signature = generateSubscriptionSignature(
32
+ * 'cancelSubscription',
33
+ * subscriptionId,
34
+ * app.apiKey
35
+ * )
36
+ *
37
+ * @example
38
+ * // SDK usage with MDK_ACCESS_TOKEN from env
39
+ * const signature = generateSubscriptionSignature(
40
+ * 'renewSubscription',
41
+ * subscriptionId,
42
+ * process.env.MDK_ACCESS_TOKEN!,
43
+ * { checkoutPath: '/checkout' }
44
+ * )
45
+ */
46
+ export declare function generateSubscriptionSignature(action: 'cancelSubscription' | 'renewSubscription', subscriptionId: string, secret: string, additionalParams?: Record<string, string>): string;
47
+ /**
48
+ * Verify an HMAC-SHA256 signature for subscription action URL params.
49
+ * Uses constant-time comparison to prevent timing attacks.
50
+ *
51
+ * @param params - The URL params (action, subscriptionId, optionally checkoutPath)
52
+ * @param signature - The signature to verify
53
+ * @param secret - The HMAC secret (MDK_ACCESS_TOKEN / app.apiKey)
54
+ * @returns True if signature is valid
55
+ *
56
+ * @example
57
+ * // Verify with explicit secret
58
+ * const isValid = verifySubscriptionSignatureWithSecret(params, signature, app.apiKey)
59
+ */
60
+ export declare function verifySubscriptionSignatureWithSecret(params: URLSearchParams, signature: string, secret: string): boolean;
61
+ /**
62
+ * Generate a signed URL for renewing a subscription.
63
+ * When visited, creates a renewal checkout and redirects to the checkout page.
64
+ * Uses MDK_ACCESS_TOKEN from environment.
65
+ *
66
+ * @example
67
+ * const url = createRenewalSubscriptionUrl({
68
+ * subscriptionId: 'sub_abc123',
69
+ * })
70
+ * // Returns: /api/mdk?action=renewSubscription&subscriptionId=sub_abc123&signature=...
71
+ */
72
+ export declare function createRenewalSubscriptionUrl(options: CreateRenewalSubscriptionUrlOptions): string;
73
+ /**
74
+ * Generate a signed URL for canceling a subscription.
75
+ * Points directly to the MDK-hosted cancel page.
76
+ * Uses MDK_ACCESS_TOKEN from environment.
77
+ *
78
+ * @example
79
+ * const url = createCancelSubscriptionUrl({
80
+ * subscriptionId: 'sub_abc123',
81
+ * })
82
+ * // Returns: https://www.moneydevkit.com/subscription/cancel?subscriptionId=sub_abc123&signature=...
83
+ */
84
+ export declare function createCancelSubscriptionUrl(options: CreateCancelSubscriptionUrlOptions): string;
85
+ /**
86
+ * Verify the HMAC signature of subscription URL params.
87
+ * Uses constant-time comparison to prevent timing attacks.
88
+ * Uses MDK_ACCESS_TOKEN from environment.
89
+ */
90
+ export declare function verifySubscriptionSignature(params: URLSearchParams, signature: string): boolean;
91
+ /**
92
+ * Handle a renewal subscription request from a signed URL.
93
+ * Creates a renewal checkout and redirects to the checkout page.
94
+ */
95
+ export declare function handleRenewSubscriptionFromUrl(request: Request, url: URL, params: URLSearchParams): Promise<Response>;
@@ -0,0 +1,208 @@
1
+ import { createHmac, timingSafeEqual } from 'crypto';
2
+ import { createMoneyDevKitClient } from '../mdk';
3
+ import { sanitizeCheckoutPath } from './checkout';
4
+ /**
5
+ * The base URL for MoneyDevKit hosted pages.
6
+ * In production this is https://www.moneydevkit.com
7
+ * Can be overridden via MDK_HOST environment variable for development.
8
+ */
9
+ const MDK_HOST = process.env.MDK_HOST ?? 'https://www.moneydevkit.com';
10
+ // ────────────────────────────────────────────────────────────────────────────────
11
+ // Core signature utilities - parameterized versions for use by backend or SDK
12
+ // ────────────────────────────────────────────────────────────────────────────────
13
+ /**
14
+ * Generate an HMAC-SHA256 signature for subscription action URL params.
15
+ * This is the core signing function used by both SDK helpers and backend services.
16
+ *
17
+ * @param action - The action type ('cancelSubscription' or 'renewSubscription')
18
+ * @param subscriptionId - The subscription ID
19
+ * @param secret - The HMAC secret (MDK_ACCESS_TOKEN / app.apiKey)
20
+ * @param additionalParams - Optional additional params to include (e.g., checkoutPath)
21
+ * @returns The hex-encoded HMAC signature
22
+ *
23
+ * @example
24
+ * // Backend usage with app.apiKey from database
25
+ * const signature = generateSubscriptionSignature(
26
+ * 'cancelSubscription',
27
+ * subscriptionId,
28
+ * app.apiKey
29
+ * )
30
+ *
31
+ * @example
32
+ * // SDK usage with MDK_ACCESS_TOKEN from env
33
+ * const signature = generateSubscriptionSignature(
34
+ * 'renewSubscription',
35
+ * subscriptionId,
36
+ * process.env.MDK_ACCESS_TOKEN!,
37
+ * { checkoutPath: '/checkout' }
38
+ * )
39
+ */
40
+ export function generateSubscriptionSignature(action, subscriptionId, secret, additionalParams) {
41
+ const urlParams = new URLSearchParams();
42
+ urlParams.set('action', action);
43
+ urlParams.set('subscriptionId', subscriptionId);
44
+ if (additionalParams) {
45
+ for (const [key, value] of Object.entries(additionalParams)) {
46
+ urlParams.set(key, value);
47
+ }
48
+ }
49
+ urlParams.sort();
50
+ const canonicalString = urlParams.toString();
51
+ return createHmac('sha256', secret)
52
+ .update(canonicalString)
53
+ .digest('hex');
54
+ }
55
+ /**
56
+ * Verify an HMAC-SHA256 signature for subscription action URL params.
57
+ * Uses constant-time comparison to prevent timing attacks.
58
+ *
59
+ * @param params - The URL params (action, subscriptionId, optionally checkoutPath)
60
+ * @param signature - The signature to verify
61
+ * @param secret - The HMAC secret (MDK_ACCESS_TOKEN / app.apiKey)
62
+ * @returns True if signature is valid
63
+ *
64
+ * @example
65
+ * // Verify with explicit secret
66
+ * const isValid = verifySubscriptionSignatureWithSecret(params, signature, app.apiKey)
67
+ */
68
+ export function verifySubscriptionSignatureWithSecret(params, signature, secret) {
69
+ const paramsToVerify = new URLSearchParams(params);
70
+ paramsToVerify.delete('signature');
71
+ paramsToVerify.sort();
72
+ const canonicalString = paramsToVerify.toString();
73
+ const expectedSignature = createHmac('sha256', secret)
74
+ .update(canonicalString)
75
+ .digest('hex');
76
+ try {
77
+ const sigBuffer = Buffer.from(signature, 'hex');
78
+ const expectedBuffer = Buffer.from(expectedSignature, 'hex');
79
+ if (sigBuffer.length !== expectedBuffer.length) {
80
+ return false;
81
+ }
82
+ return timingSafeEqual(sigBuffer, expectedBuffer);
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ // ────────────────────────────────────────────────────────────────────────────────
89
+ // SDK helpers - use MDK_ACCESS_TOKEN from environment
90
+ // ────────────────────────────────────────────────────────────────────────────────
91
+ /**
92
+ * Generate a signed URL for renewing a subscription.
93
+ * When visited, creates a renewal checkout and redirects to the checkout page.
94
+ * Uses MDK_ACCESS_TOKEN from environment.
95
+ *
96
+ * @example
97
+ * const url = createRenewalSubscriptionUrl({
98
+ * subscriptionId: 'sub_abc123',
99
+ * })
100
+ * // Returns: /api/mdk?action=renewSubscription&subscriptionId=sub_abc123&signature=...
101
+ */
102
+ export function createRenewalSubscriptionUrl(options) {
103
+ const basePath = options.basePath ?? '/api/mdk';
104
+ const accessToken = process.env.MDK_ACCESS_TOKEN;
105
+ if (!accessToken) {
106
+ throw new Error('MDK_ACCESS_TOKEN is required for creating subscription URLs');
107
+ }
108
+ const additionalParams = options.checkoutPath ? { checkoutPath: options.checkoutPath } : undefined;
109
+ const signature = generateSubscriptionSignature('renewSubscription', options.subscriptionId, accessToken, additionalParams);
110
+ const urlParams = new URLSearchParams();
111
+ urlParams.set('action', 'renewSubscription');
112
+ urlParams.set('subscriptionId', options.subscriptionId);
113
+ if (options.checkoutPath) {
114
+ urlParams.set('checkoutPath', options.checkoutPath);
115
+ }
116
+ urlParams.set('signature', signature);
117
+ return `${basePath}?${urlParams.toString()}`;
118
+ }
119
+ /**
120
+ * Generate a signed URL for canceling a subscription.
121
+ * Points directly to the MDK-hosted cancel page.
122
+ * Uses MDK_ACCESS_TOKEN from environment.
123
+ *
124
+ * @example
125
+ * const url = createCancelSubscriptionUrl({
126
+ * subscriptionId: 'sub_abc123',
127
+ * })
128
+ * // Returns: https://www.moneydevkit.com/subscription/cancel?subscriptionId=sub_abc123&signature=...
129
+ */
130
+ export function createCancelSubscriptionUrl(options) {
131
+ const accessToken = process.env.MDK_ACCESS_TOKEN;
132
+ if (!accessToken) {
133
+ throw new Error('MDK_ACCESS_TOKEN is required for creating subscription URLs');
134
+ }
135
+ const signature = generateSubscriptionSignature('cancelSubscription', options.subscriptionId, accessToken);
136
+ const urlParams = new URLSearchParams();
137
+ urlParams.set('subscriptionId', options.subscriptionId);
138
+ urlParams.set('signature', signature);
139
+ return `${MDK_HOST}/subscription/cancel?${urlParams.toString()}`;
140
+ }
141
+ /**
142
+ * Verify the HMAC signature of subscription URL params.
143
+ * Uses constant-time comparison to prevent timing attacks.
144
+ * Uses MDK_ACCESS_TOKEN from environment.
145
+ */
146
+ export function verifySubscriptionSignature(params, signature) {
147
+ const accessToken = process.env.MDK_ACCESS_TOKEN;
148
+ if (!accessToken)
149
+ return false;
150
+ return verifySubscriptionSignatureWithSecret(params, signature, accessToken);
151
+ }
152
+ /**
153
+ * Safely join a base path with a segment, avoiding double slashes.
154
+ */
155
+ function joinPath(base, segment) {
156
+ if (base === '/')
157
+ return `/${segment}`;
158
+ return `${base}/${segment}`;
159
+ }
160
+ /**
161
+ * Helper to redirect to checkout error page.
162
+ */
163
+ function redirectToCheckoutError(origin, checkoutPath, code, message) {
164
+ const errorUrl = new URL(joinPath(checkoutPath, 'error'), origin);
165
+ errorUrl.searchParams.set('error', code);
166
+ errorUrl.searchParams.set('message', message);
167
+ return Response.redirect(errorUrl.toString(), 302);
168
+ }
169
+ /**
170
+ * Get the origin from a request, respecting proxy headers.
171
+ * Uses Host header to handle Docker port mapping correctly.
172
+ */
173
+ function getRequestOrigin(request, fallbackUrl) {
174
+ const host = request.headers.get('host');
175
+ if (host) {
176
+ const protocol = request.headers.get('x-forwarded-proto') ?? fallbackUrl.protocol.replace(':', '');
177
+ return `${protocol}://${host}`;
178
+ }
179
+ return fallbackUrl.origin;
180
+ }
181
+ /**
182
+ * Handle a renewal subscription request from a signed URL.
183
+ * Creates a renewal checkout and redirects to the checkout page.
184
+ */
185
+ export async function handleRenewSubscriptionFromUrl(request, url, params) {
186
+ const subscriptionId = params.get('subscriptionId');
187
+ const checkoutPath = sanitizeCheckoutPath(params.get('checkoutPath'));
188
+ const origin = getRequestOrigin(request, url);
189
+ if (!subscriptionId) {
190
+ return redirectToCheckoutError(origin, checkoutPath, 'missing_subscription_id', 'Missing subscription ID');
191
+ }
192
+ try {
193
+ const client = createMoneyDevKitClient();
194
+ const result = await client.subscriptions.createRenewalCheckout({ subscriptionId });
195
+ // Redirect to checkout page
196
+ const checkoutUrl = new URL(joinPath(checkoutPath, result.checkoutId), origin);
197
+ return Response.redirect(checkoutUrl.toString(), 302);
198
+ }
199
+ catch (err) {
200
+ const message = err instanceof Error ? err.message : 'Failed to create renewal checkout';
201
+ // Check for "already renewed" scenario
202
+ if (message.includes('already renewed') || message.includes('not renewable')) {
203
+ return redirectToCheckoutError(origin, checkoutPath, 'already_renewed', 'This subscription has already been renewed');
204
+ }
205
+ return redirectToCheckoutError(origin, checkoutPath, 'renewal_failed', message);
206
+ }
207
+ }
208
+ //# sourceMappingURL=subscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription.js","sourceRoot":"","sources":["../../src/handlers/subscription.ts"],"names":[],"mappings":"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"}
@@ -1,46 +1,167 @@
1
1
  import { z } from "zod";
2
- import { warn } from "../logging";
2
+ import { SubscriptionSchema, SubscriptionWebhookEventSchema, } from "@moneydevkit/api-contract";
3
+ import { log, warn } from "../logging";
3
4
  import { createMoneyDevKitClient, createMoneyDevKitNode } from "../mdk";
4
5
  import { markPaymentReceived } from "../payment-state";
5
- const webhookSchema = z.object({
6
- event: z.enum(["incoming-payment"]),
6
+ // Incoming payment events - have nodeId, no subscription
7
+ const incomingPaymentEventSchema = z.object({
8
+ handler: z.literal("webhooks"),
9
+ event: z.literal("incoming-payment"),
7
10
  nodeId: z.string(),
8
11
  });
9
- async function handleIncomingPayment() {
12
+ // Subscription events - have subscription, no nodeId
13
+ const subscriptionEventSchema = z.object({
14
+ handler: z.literal("webhooks"),
15
+ event: SubscriptionWebhookEventSchema,
16
+ subscription: SubscriptionSchema,
17
+ });
18
+ // Discriminated union - TypeScript will narrow based on `event`
19
+ const webhookSchema = z.union([
20
+ incomingPaymentEventSchema,
21
+ subscriptionEventSchema,
22
+ ]);
23
+ // How often to poll for new LDK events
24
+ const POLL_INTERVAL_MS = 100;
25
+ // Minimum time that we'll run the lightning node when handling incoming payments.
26
+ // This needs to be long enough for JIT channel opening and HTLC commitment exchanges.
27
+ const MIN_WAIT_BEFORE_QUIET_MS = 15_000;
28
+ // After the minimum wait, this is how long we'll wait for new events if there are no pending claims.
29
+ const QUIET_THRESHOLD_MS = 5_000;
30
+ // Maximum time we'll run the lightning node when handling incoming payments.
31
+ // Vercel has a hard timeout of 60 seconds for the hobby plan so this should not
32
+ // be longer than that.
33
+ const MAX_WAIT_MS = 60_000;
34
+ /**
35
+ * Handles incoming-payment webhooks from MoneyDevKit platform.
36
+ *
37
+ * This function is invoked when the MDK node sends a webhook notification
38
+ * indicating that new payments have arrived ("incoming-payment" event).
39
+ */
40
+ async function handleIncomingPaymentEvents() {
41
+ const webhookStartTime = Date.now();
42
+ log("[webhook] handleIncomingPayment started");
10
43
  const node = createMoneyDevKitNode();
11
44
  const client = createMoneyDevKitClient();
12
- const payments = node.receivePayments();
13
- if (payments.length === 0) {
14
- return;
15
- }
16
- payments.forEach((payment) => {
17
- markPaymentReceived(payment.paymentHash);
18
- });
45
+ log("[webhook] Starting node and syncing...");
46
+ const syncStartTime = Date.now();
47
+ node.startReceiving();
48
+ const syncDuration = Date.now() - syncStartTime;
49
+ log(`[webhook] Node started and synced in ${syncDuration}ms`);
50
+ const pendingClaims = new Set();
51
+ let eventsProcessed = 0;
52
+ let paymentsReceived = 0;
53
+ let paymentsFailed = 0;
54
+ const startTime = Date.now();
55
+ let lastEventTime = startTime;
19
56
  try {
20
- await client.checkouts.paymentReceived({
21
- payments: payments.map((payment) => ({
22
- paymentHash: payment.paymentHash,
23
- // amount comes in msat from the node, convert to sats
24
- amountSats: payment.amount / 1000,
25
- sandbox: false,
26
- })),
27
- });
57
+ while (true) {
58
+ const event = node.nextEvent();
59
+ if (event) {
60
+ lastEventTime = Date.now();
61
+ eventsProcessed++;
62
+ switch (event.eventType) {
63
+ case 0 /* PaymentEventType.Claimable */:
64
+ log(`[webhook] PaymentClaimable hash=${event.paymentHash} amount=${event.amountMsat}msat pending=${pendingClaims.size + 1}`);
65
+ pendingClaims.add(event.paymentHash);
66
+ node.ackEvent();
67
+ break;
68
+ case 1 /* PaymentEventType.Received */: {
69
+ paymentsReceived++;
70
+ log(`[webhook] PaymentReceived hash=${event.paymentHash} amount=${event.amountMsat}msat`);
71
+ pendingClaims.delete(event.paymentHash);
72
+ markPaymentReceived(event.paymentHash);
73
+ try {
74
+ await client.checkouts.paymentReceived({
75
+ payments: [{
76
+ paymentHash: event.paymentHash,
77
+ amountSats: Math.floor(event.amountMsat / 1000),
78
+ sandbox: false,
79
+ }],
80
+ });
81
+ log(`[webhook] Payment confirmed to API hash=${event.paymentHash}`);
82
+ }
83
+ catch (error) {
84
+ // TODO (austin): Investigate retry strategy for API failures. Currently we ack
85
+ // regardless of API success (matching existing behavior). However,
86
+ // this leaves us in a state where the payment is received but not
87
+ // confirmed to the paying customer or reflected on moneydevkit.com.
88
+ // Consider having the checkout update based on the global payment state
89
+ // and some sort of reconciliation process to backfill the database.
90
+ warn(`[webhook] Failed to confirm payment ${event.paymentHash} to API`, error);
91
+ }
92
+ node.ackEvent();
93
+ break;
94
+ }
95
+ case 2 /* PaymentEventType.Failed */:
96
+ paymentsFailed++;
97
+ log(`[webhook] PaymentFailed hash=${event.paymentHash} reason=${event.reason}`);
98
+ pendingClaims.delete(event.paymentHash);
99
+ node.ackEvent();
100
+ break;
101
+ }
102
+ // Continue immediately to process next event
103
+ continue;
104
+ }
105
+ // No event available - check exit conditions
106
+ const now = Date.now();
107
+ const totalElapsed = now - startTime;
108
+ const quietElapsed = now - lastEventTime;
109
+ if (totalElapsed >= MAX_WAIT_MS) {
110
+ if (pendingClaims.size > 0) {
111
+ warn(`[webhook] Hard timeout after ${totalElapsed}ms with ${pendingClaims.size} pending claims`);
112
+ }
113
+ else {
114
+ log(`[webhook] Hard timeout after ${totalElapsed}ms (no pending)`);
115
+ }
116
+ break;
117
+ }
118
+ const canQuietExit = pendingClaims.size === 0 &&
119
+ quietElapsed >= QUIET_THRESHOLD_MS &&
120
+ totalElapsed >= MIN_WAIT_BEFORE_QUIET_MS;
121
+ if (canQuietExit) {
122
+ log(`[webhook] Quiet exit after ${totalElapsed}ms (quiet=${quietElapsed}ms)`);
123
+ break;
124
+ }
125
+ // Log occasionally while waiting for minimum time
126
+ if (pendingClaims.size === 0 &&
127
+ quietElapsed >= QUIET_THRESHOLD_MS &&
128
+ totalElapsed < MIN_WAIT_BEFORE_QUIET_MS &&
129
+ totalElapsed % 1000 < POLL_INTERVAL_MS) {
130
+ log(`[webhook] Waiting for min time: ${totalElapsed}/${MIN_WAIT_BEFORE_QUIET_MS}ms`);
131
+ }
132
+ // Yield to JS event loop
133
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
134
+ }
28
135
  }
29
- catch (error) {
30
- warn("Failed to notify MoneyDevKit checkout about received payments. Will rely on local state and retry on next webhook.", error);
136
+ finally {
137
+ log("[webhook] Stopping node...");
138
+ node.stopReceiving();
139
+ const totalDuration = Date.now() - webhookStartTime;
140
+ log(`[webhook] Complete: duration=${totalDuration}ms events=${eventsProcessed} received=${paymentsReceived} failed=${paymentsFailed}`);
31
141
  }
32
142
  }
33
143
  export async function handleMdkWebhook(request) {
144
+ const requestStartTime = Date.now();
145
+ log("[webhook] Received webhook request");
34
146
  try {
35
147
  const body = await request.json();
36
148
  const parsed = webhookSchema.parse(body);
37
149
  if (parsed.event === "incoming-payment") {
38
- await handleIncomingPayment();
150
+ log(`[webhook] Parsed event=${parsed.event} nodeId=${parsed.nodeId}`);
151
+ await handleIncomingPaymentEvents();
152
+ }
153
+ else {
154
+ // Subscription events - SDK acknowledges but doesn't handle
155
+ // (merchant handles via their own webhook logic)
156
+ log(`[webhook] Parsed event=${parsed.event} subscriptionId=${parsed.subscription.id}`);
39
157
  }
158
+ const duration = Date.now() - requestStartTime;
159
+ log(`[webhook] Response OK in ${duration}ms`);
40
160
  return new Response("OK", { status: 200 });
41
161
  }
42
162
  catch (error) {
43
- console.error(error);
163
+ const duration = Date.now() - requestStartTime;
164
+ warn(`[webhook] Error after ${duration}ms:`, error);
44
165
  return new Response("Internal Server Error", { status: 500 });
45
166
  }
46
167
  }
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/handlers/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7B,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC,CAAC;IACnC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CACnB,CAAC,CAAC;AAEH,KAAK,UAAU,qBAAqB;IAClC,MAAM,IAAI,GAAG,qBAAqB,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,uBAAuB,EAAE,CAAC;IACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;IAExC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO;IACT,CAAC;IAED,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC3B,mBAAmB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC;YACrC,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBACnC,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,sDAAsD;gBACtD,UAAU,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;gBACjC,OAAO,EAAE,KAAK;aACf,CAAC,CAAC;SACJ,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CACF,oHAAoH,EACpH,KAAK,CACN,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB;IACrD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAEzC,IAAI,MAAM,CAAC,KAAK,KAAK,kBAAkB,EAAE,CAAC;YACxC,MAAM,qBAAqB,EAAE,CAAC;QAChC,CAAC;QAED,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,IAAI,QAAQ,CAAC,uBAAuB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/handlers/webhooks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { CustomerWithSubscriptions, GetCustomerInput } from '@moneydevkit/api-contract';
2
+ import type { MdkError } from '../types';
3
+ export type CustomerIdentifier = GetCustomerInput;
4
+ export interface CustomerData extends CustomerWithSubscriptions {
5
+ hasActiveSubscription: boolean;
6
+ }
7
+ export interface UseCustomerOptions {
8
+ /** Include sandbox subscriptions in the response. Defaults to false. */
9
+ includeSandbox?: boolean;
10
+ }
11
+ export declare function useCustomer(identifier: GetCustomerInput | null | undefined, options?: UseCustomerOptions): {
12
+ customer: CustomerData | null;
13
+ isLoading: boolean;
14
+ error: MdkError | null;
15
+ refetch: () => Promise<void>;
16
+ };
@@ -0,0 +1,52 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { clientGetCustomer } from '../client-actions';
3
+ import { log } from '../logging';
4
+ export function useCustomer(identifier, options) {
5
+ const [customer, setCustomer] = useState(null);
6
+ const [isLoading, setIsLoading] = useState(false);
7
+ const [error, setError] = useState(null);
8
+ // Create stable identifier key for dependency tracking
9
+ const identifierKey = identifier
10
+ ? 'externalId' in identifier
11
+ ? `externalId:${identifier.externalId ?? ''}`
12
+ : 'email' in identifier
13
+ ? `email:${identifier.email ?? ''}`
14
+ : `id:${identifier.id ?? ''}`
15
+ : null;
16
+ // Include options in the dependency key
17
+ const optionsKey = options?.includeSandbox ? 'sandbox:true' : 'sandbox:false';
18
+ const fetchCustomer = useCallback(async () => {
19
+ if (!identifier) {
20
+ setCustomer(null);
21
+ setError(null);
22
+ setIsLoading(false);
23
+ return;
24
+ }
25
+ setIsLoading(true);
26
+ setError(null);
27
+ const result = await clientGetCustomer(identifier, options);
28
+ if (result.error) {
29
+ log('Customer fetch error:', result.error);
30
+ setError(result.error);
31
+ setCustomer(null);
32
+ }
33
+ else {
34
+ setCustomer({
35
+ ...result.data,
36
+ // Include 'past_due' because users retain access during the grace period
37
+ hasActiveSubscription: result.data.subscriptions.some((s) => s.status === 'active' || s.status === 'past_due'),
38
+ });
39
+ }
40
+ setIsLoading(false);
41
+ }, [identifierKey, optionsKey]);
42
+ useEffect(() => {
43
+ fetchCustomer();
44
+ }, [fetchCustomer]);
45
+ return {
46
+ customer,
47
+ isLoading,
48
+ error,
49
+ refetch: fetchCustomer,
50
+ };
51
+ }
52
+ //# sourceMappingURL=useCustomer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCustomer.js","sourceRoot":"","sources":["../../src/hooks/useCustomer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAExD,OAAO,EAAE,iBAAiB,EAA2B,MAAM,mBAAmB,CAAA;AAE9E,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAchC,MAAM,UAAU,WAAW,CACzB,UAA+C,EAC/C,OAA4B;IAE5B,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAA;IACnE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACjD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAkB,IAAI,CAAC,CAAA;IAEzD,uDAAuD;IACvD,MAAM,aAAa,GAAG,UAAU;QAC9B,CAAC,CAAC,YAAY,IAAI,UAAU;YAC1B,CAAC,CAAC,cAAc,UAAU,CAAC,UAAU,IAAI,EAAE,EAAE;YAC7C,CAAC,CAAC,OAAO,IAAI,UAAU;gBACrB,CAAC,CAAC,SAAS,UAAU,CAAC,KAAK,IAAI,EAAE,EAAE;gBACnC,CAAC,CAAC,MAAM,UAAU,CAAC,EAAE,IAAI,EAAE,EAAE;QACjC,CAAC,CAAC,IAAI,CAAA;IAER,wCAAwC;IACxC,MAAM,UAAU,GAAG,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,eAAe,CAAA;IAE7E,MAAM,aAAa,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC3C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,WAAW,CAAC,IAAI,CAAC,CAAA;YACjB,QAAQ,CAAC,IAAI,CAAC,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,OAAM;QACR,CAAC;QAED,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,QAAQ,CAAC,IAAI,CAAC,CAAA;QAEd,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;QAE3D,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;YAC1C,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACtB,WAAW,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;aAAM,CAAC;YACN,WAAW,CAAC;gBACV,GAAG,MAAM,CAAC,IAAI;gBACd,yEAAyE;gBACzE,qBAAqB,EAAE,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CACnD,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU,CACxD;aACF,CAAC,CAAA;QACJ,CAAC;QAED,YAAY,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC,EAAE,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC,CAAA;IAE/B,SAAS,CAAC,GAAG,EAAE;QACb,aAAa,EAAE,CAAA;IACjB,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAA;IAEnB,OAAO;QACL,QAAQ;QACR,SAAS;QACT,KAAK;QACL,OAAO,EAAE,aAAa;KACvB,CAAA;AACH,CAAC"}
@@ -1,14 +1,25 @@
1
+ /**
2
+ * Hook to fetch available products from the MDK API.
3
+ *
4
+ * @returns Object containing products, loading state, error, and refetch function
5
+ *
6
+ * @example
7
+ * const { products } = useProducts()
8
+ * // USD prices are in cents - divide by 100 for display
9
+ * const displayPrice = (product.prices[0]?.priceAmount ?? 0) / 100
10
+ * // SAT amounts are in satoshis - no conversion needed
11
+ */
1
12
  export declare function useProducts(): {
2
13
  products: {
3
- id: string;
4
14
  name: string;
15
+ id: string;
5
16
  description: string | null;
6
17
  recurringInterval: "MONTH" | "QUARTER" | "YEAR" | null;
7
18
  prices: {
8
- id: string;
9
19
  currency: "USD" | "SAT";
10
- amountType: "FIXED" | "CUSTOM";
11
20
  priceAmount: number | null;
21
+ id: string;
22
+ amountType: "FIXED" | "CUSTOM";
12
23
  }[];
13
24
  }[];
14
25
  isLoading: boolean;