@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.
- package/dist/actions.d.ts +6 -1
- package/dist/actions.js +58 -6
- package/dist/actions.js.map +1 -1
- package/dist/checkout-utils.d.ts +9 -0
- package/dist/checkout-utils.js +46 -0
- package/dist/checkout-utils.js.map +1 -1
- package/dist/client-actions.d.ts +6 -1
- package/dist/client-actions.js +10 -1
- package/dist/client-actions.js.map +1 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/components/checkout/PendingPaymentCheckout.js +6 -2
- package/dist/components/checkout/PendingPaymentCheckout.js.map +1 -1
- package/dist/components/checkout/UnconfirmedCheckout.js +10 -2
- package/dist/components/checkout/UnconfirmedCheckout.js.map +1 -1
- package/dist/handlers/checkout.js +28 -2
- package/dist/handlers/checkout.js.map +1 -1
- package/dist/handlers/customer.d.ts +1 -0
- package/dist/handlers/customer.js +39 -0
- package/dist/handlers/customer.js.map +1 -0
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +1 -0
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/subscription.d.ts +95 -0
- package/dist/handlers/subscription.js +208 -0
- package/dist/handlers/subscription.js.map +1 -0
- package/dist/handlers/webhooks.js +144 -23
- package/dist/handlers/webhooks.js.map +1 -1
- package/dist/hooks/useCustomer.d.ts +16 -0
- package/dist/hooks/useCustomer.js +52 -0
- package/dist/hooks/useCustomer.js.map +1 -0
- package/dist/hooks/useProducts.d.ts +14 -3
- package/dist/hooks/useProducts.js +11 -0
- package/dist/hooks/useProducts.js.map +1 -1
- package/dist/lightning-node.d.ts +16 -0
- package/dist/lightning-node.js +23 -0
- package/dist/lightning-node.js.map +1 -1
- package/dist/mdk-client.d.ts +19 -1
- package/dist/mdk-client.js +21 -0
- package/dist/mdk-client.js.map +1 -1
- package/dist/route.d.ts +17 -4
- package/dist/route.js +45 -21
- package/dist/route.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
package/dist/handlers/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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;
|