@openbilling/dodo 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/index.cjs +232 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +212 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 George Dimitrov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @openbilling/dodo
|
|
2
|
+
|
|
3
|
+
Dodo Payments adapter for OpenBilling.
|
|
4
|
+
|
|
5
|
+
`@openbilling/dodo` provides a fetch-based Dodo Payments adapter that implements the shared `@openbilling/core` billing contract. It supports the current OpenBilling MVP workflows for hosted checkout creation, customer portal links, webhook verification, and normalized webhook mapping.
|
|
6
|
+
|
|
7
|
+
This package keeps Dodo-specific behavior visible where it matters. Checkout creation is product-based and currently requires `productId`; Dodo determines whether the checkout is one-time or recurring from the configured product.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @openbilling/core @openbilling/dodo
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Links
|
|
16
|
+
|
|
17
|
+
- Documentation: [openbilling.geodim.dev](https://openbilling.geodim.dev)
|
|
18
|
+
- Repository: [github.com/gndimitro/openbilling](https://github.com/gndimitro/openbilling)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DodoProviderError: () => DodoProviderError,
|
|
24
|
+
createDodoProvider: () => createDodoProvider
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_standardwebhooks = require("standardwebhooks");
|
|
28
|
+
var import_core = require("@openbilling/core");
|
|
29
|
+
var DEFAULT_BASE_URL = "https://live.dodopayments.com";
|
|
30
|
+
var textDecoder = new TextDecoder();
|
|
31
|
+
var DodoProviderError = class extends Error {
|
|
32
|
+
/** Provider-specific error classification. */
|
|
33
|
+
code;
|
|
34
|
+
/** HTTP status returned by Dodo when the error came from an API response. */
|
|
35
|
+
status;
|
|
36
|
+
constructor(message, code, status, options) {
|
|
37
|
+
super(message, options);
|
|
38
|
+
this.name = "DodoProviderError";
|
|
39
|
+
this.code = code;
|
|
40
|
+
if (status !== void 0) {
|
|
41
|
+
this.status = status;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function createDodoProvider(config) {
|
|
46
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
47
|
+
return (0, import_core.createBilling)({
|
|
48
|
+
async createCheckout(input) {
|
|
49
|
+
const response = await postJson(
|
|
50
|
+
`${baseUrl}/checkouts`,
|
|
51
|
+
config.apiKey,
|
|
52
|
+
buildCheckoutRequest(input)
|
|
53
|
+
);
|
|
54
|
+
if (!response.checkout_url) {
|
|
55
|
+
throw new DodoProviderError("Dodo checkout session did not include a checkout URL.", "api_error");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
id: response.session_id,
|
|
59
|
+
url: response.checkout_url,
|
|
60
|
+
provider: import_core.Provider.Dodo,
|
|
61
|
+
raw: response
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
async createPortalLink(input) {
|
|
65
|
+
const endpoint = new URL(
|
|
66
|
+
`${baseUrl}/customers/${encodeURIComponent(input.customerId)}/customer-portal/session`
|
|
67
|
+
);
|
|
68
|
+
endpoint.searchParams.set("return_url", input.returnUrl);
|
|
69
|
+
const response = await postJson(endpoint.toString(), config.apiKey);
|
|
70
|
+
return {
|
|
71
|
+
url: response.link,
|
|
72
|
+
provider: import_core.Provider.Dodo,
|
|
73
|
+
raw: response
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
async verifyWebhook(input) {
|
|
77
|
+
const headers = getRequiredWebhookHeaders(input.headers);
|
|
78
|
+
const secret = input.secret ?? config.webhookSecret;
|
|
79
|
+
const payload = toWebhookPayload(input.payload);
|
|
80
|
+
try {
|
|
81
|
+
const event = new import_standardwebhooks.Webhook(secret).verify(payload, headers);
|
|
82
|
+
return normalizeWebhookEvent(event);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new DodoProviderError("Dodo webhook signature verification failed.", "invalid_webhook_signature", void 0, {
|
|
85
|
+
cause: error
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function normalizeBaseUrl(baseUrl) {
|
|
92
|
+
return (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
93
|
+
}
|
|
94
|
+
function buildCheckoutRequest(input) {
|
|
95
|
+
if (!input.productId) {
|
|
96
|
+
throw new DodoProviderError(
|
|
97
|
+
input.priceId ? "Dodo checkout sessions currently require a productId instead of a priceId." : "Dodo checkout sessions require a productId.",
|
|
98
|
+
"unsupported_input"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const request = {
|
|
102
|
+
product_cart: [
|
|
103
|
+
{
|
|
104
|
+
product_id: input.productId,
|
|
105
|
+
quantity: 1
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
return_url: input.successUrl,
|
|
109
|
+
cancel_url: input.cancelUrl
|
|
110
|
+
};
|
|
111
|
+
if (input.customerId) {
|
|
112
|
+
request.customer_id = input.customerId;
|
|
113
|
+
} else if (input.customerEmail) {
|
|
114
|
+
request.customer = {
|
|
115
|
+
email: input.customerEmail
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (input.metadata) {
|
|
119
|
+
request.metadata = input.metadata;
|
|
120
|
+
}
|
|
121
|
+
return request;
|
|
122
|
+
}
|
|
123
|
+
function getRequiredWebhookHeaders(headers) {
|
|
124
|
+
const webhookId = headers?.["webhook-id"];
|
|
125
|
+
const webhookSignature = headers?.["webhook-signature"];
|
|
126
|
+
const webhookTimestamp = headers?.["webhook-timestamp"];
|
|
127
|
+
if (!webhookId || !webhookSignature || !webhookTimestamp) {
|
|
128
|
+
throw new DodoProviderError(
|
|
129
|
+
"Dodo webhook verification requires webhook-id, webhook-signature, and webhook-timestamp headers.",
|
|
130
|
+
"invalid_webhook_headers"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
"webhook-id": webhookId,
|
|
135
|
+
"webhook-signature": webhookSignature,
|
|
136
|
+
"webhook-timestamp": webhookTimestamp
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function toWebhookPayload(payload) {
|
|
140
|
+
return typeof payload === "string" ? payload : textDecoder.decode(payload);
|
|
141
|
+
}
|
|
142
|
+
function normalizeWebhookEvent(event) {
|
|
143
|
+
const payload = event;
|
|
144
|
+
const customerId = payload.data?.customer?.customer_id;
|
|
145
|
+
switch (payload.type) {
|
|
146
|
+
case import_core.Payment.Succeeded:
|
|
147
|
+
if (!payload.data?.payment_id) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
type: import_core.Payment.Succeeded,
|
|
152
|
+
provider: import_core.Provider.Dodo,
|
|
153
|
+
paymentId: payload.data.payment_id,
|
|
154
|
+
...customerId ? { customerId } : {},
|
|
155
|
+
raw: event
|
|
156
|
+
};
|
|
157
|
+
case import_core.Subscription.Active:
|
|
158
|
+
if (customerId && payload.data?.subscription_id) {
|
|
159
|
+
return {
|
|
160
|
+
type: import_core.Subscription.Active,
|
|
161
|
+
provider: import_core.Provider.Dodo,
|
|
162
|
+
customerId,
|
|
163
|
+
subscriptionId: payload.data.subscription_id,
|
|
164
|
+
raw: event
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
case import_core.Subscription.Cancelled:
|
|
169
|
+
if (customerId && payload.data?.subscription_id) {
|
|
170
|
+
return {
|
|
171
|
+
type: import_core.Subscription.Cancelled,
|
|
172
|
+
provider: import_core.Provider.Dodo,
|
|
173
|
+
customerId,
|
|
174
|
+
subscriptionId: payload.data.subscription_id,
|
|
175
|
+
raw: event
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
type: import_core.Webhook.Unknown,
|
|
182
|
+
provider: import_core.Provider.Dodo,
|
|
183
|
+
raw: event
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async function postJson(url, apiKey, body) {
|
|
187
|
+
const headers = {
|
|
188
|
+
authorization: `Bearer ${apiKey}`
|
|
189
|
+
};
|
|
190
|
+
if (body !== void 0) {
|
|
191
|
+
headers["content-type"] = "application/json";
|
|
192
|
+
}
|
|
193
|
+
const requestInit = {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers,
|
|
196
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
197
|
+
};
|
|
198
|
+
const response = await fetch(url, requestInit);
|
|
199
|
+
const responseBody = await readResponseBody(response);
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw createApiError(response, responseBody);
|
|
202
|
+
}
|
|
203
|
+
return responseBody;
|
|
204
|
+
}
|
|
205
|
+
async function readResponseBody(response) {
|
|
206
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
207
|
+
if (contentType.includes("application/json")) {
|
|
208
|
+
return response.json();
|
|
209
|
+
}
|
|
210
|
+
return response.text();
|
|
211
|
+
}
|
|
212
|
+
function createApiError(response, responseBody) {
|
|
213
|
+
const message = getApiErrorMessage(responseBody) ?? `Dodo API request failed with status ${response.status}${response.statusText ? ` ${response.statusText}` : ""}.`;
|
|
214
|
+
return new DodoProviderError(message, "api_error", response.status);
|
|
215
|
+
}
|
|
216
|
+
function getApiErrorMessage(responseBody) {
|
|
217
|
+
if (!responseBody || typeof responseBody !== "object") {
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
if ("message" in responseBody && typeof responseBody.message === "string") {
|
|
221
|
+
return responseBody.message;
|
|
222
|
+
}
|
|
223
|
+
if ("error" in responseBody && typeof responseBody.error === "string") {
|
|
224
|
+
return responseBody.error;
|
|
225
|
+
}
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
229
|
+
0 && (module.exports = {
|
|
230
|
+
DodoProviderError,
|
|
231
|
+
createDodoProvider
|
|
232
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { BillingProvider } from '@openbilling/core';
|
|
2
|
+
|
|
3
|
+
type DodoErrorCode = "unsupported_input" | "api_error" | "invalid_webhook_headers" | "invalid_webhook_signature";
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for the Dodo Payments provider adapter.
|
|
6
|
+
*
|
|
7
|
+
* The current Dodo adapter is intentionally narrow and only covers the hosted
|
|
8
|
+
* checkout, customer portal, and webhook flows documented in the root README.
|
|
9
|
+
*/
|
|
10
|
+
interface DodoProviderConfig {
|
|
11
|
+
/** Secret API key used for Dodo REST API requests. */
|
|
12
|
+
apiKey: string;
|
|
13
|
+
/** Webhook signing secret used by `standardwebhooks` verification. */
|
|
14
|
+
webhookSecret: string;
|
|
15
|
+
/** Optional API host override. Defaults to Dodo's live API base URL. */
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Error raised by the Dodo provider adapter.
|
|
20
|
+
*
|
|
21
|
+
* The `code` field is stable enough for app-level branching, while `status`
|
|
22
|
+
* is included when the failure originated from the Dodo HTTP API.
|
|
23
|
+
*/
|
|
24
|
+
declare class DodoProviderError extends Error {
|
|
25
|
+
/** Provider-specific error classification. */
|
|
26
|
+
readonly code: DodoErrorCode;
|
|
27
|
+
/** HTTP status returned by Dodo when the error came from an API response. */
|
|
28
|
+
readonly status?: number;
|
|
29
|
+
constructor(message: string, code: DodoErrorCode, status?: number, options?: ErrorOptions);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a fetch-based Dodo Payments adapter that implements the shared
|
|
33
|
+
* `@openbilling/core` billing contract.
|
|
34
|
+
*
|
|
35
|
+
* The current MVP intentionally supports a narrow Dodo surface:
|
|
36
|
+
* - checkout creation through `POST /checkouts`
|
|
37
|
+
* - customer billing management through customer portal sessions
|
|
38
|
+
* - webhook verification plus normalization for a small set of events
|
|
39
|
+
*
|
|
40
|
+
* Important provider caveats:
|
|
41
|
+
* - Dodo currently requires `productId` for checkout creation
|
|
42
|
+
* - `priceId` alone is not treated as a portable substitute
|
|
43
|
+
* - the Dodo product determines whether a checkout is one-time or recurring
|
|
44
|
+
* - the adapter defaults to `https://live.dodopayments.com`, with `baseUrl`
|
|
45
|
+
* available for test mode or custom hosts
|
|
46
|
+
*
|
|
47
|
+
* Supported normalized Dodo webhook coverage:
|
|
48
|
+
* - `payment.succeeded`
|
|
49
|
+
* - `subscription.active`
|
|
50
|
+
* - `subscription.cancelled`
|
|
51
|
+
*
|
|
52
|
+
* Unsupported Dodo events resolve to {@link Webhook.Unknown} instead of
|
|
53
|
+
* throwing purely because the event is outside the current MVP.
|
|
54
|
+
*
|
|
55
|
+
* @throws {DodoProviderError} When input is unsupported, webhook verification
|
|
56
|
+
* fails, or the Dodo API returns an error response.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const billing = createDodoProvider({
|
|
61
|
+
* apiKey: "dodo_api_key",
|
|
62
|
+
* webhookSecret: "whsec_123"
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* const checkout = await billing.createCheckout({
|
|
66
|
+
* productId: "prod_123",
|
|
67
|
+
* customerEmail: "demo@example.com",
|
|
68
|
+
* successUrl: "https://example.com/success",
|
|
69
|
+
* cancelUrl: "https://example.com/cancel",
|
|
70
|
+
* mode: "subscription"
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
declare function createDodoProvider(config: DodoProviderConfig): BillingProvider;
|
|
75
|
+
|
|
76
|
+
export { type DodoProviderConfig, DodoProviderError, createDodoProvider };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { BillingProvider } from '@openbilling/core';
|
|
2
|
+
|
|
3
|
+
type DodoErrorCode = "unsupported_input" | "api_error" | "invalid_webhook_headers" | "invalid_webhook_signature";
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for the Dodo Payments provider adapter.
|
|
6
|
+
*
|
|
7
|
+
* The current Dodo adapter is intentionally narrow and only covers the hosted
|
|
8
|
+
* checkout, customer portal, and webhook flows documented in the root README.
|
|
9
|
+
*/
|
|
10
|
+
interface DodoProviderConfig {
|
|
11
|
+
/** Secret API key used for Dodo REST API requests. */
|
|
12
|
+
apiKey: string;
|
|
13
|
+
/** Webhook signing secret used by `standardwebhooks` verification. */
|
|
14
|
+
webhookSecret: string;
|
|
15
|
+
/** Optional API host override. Defaults to Dodo's live API base URL. */
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Error raised by the Dodo provider adapter.
|
|
20
|
+
*
|
|
21
|
+
* The `code` field is stable enough for app-level branching, while `status`
|
|
22
|
+
* is included when the failure originated from the Dodo HTTP API.
|
|
23
|
+
*/
|
|
24
|
+
declare class DodoProviderError extends Error {
|
|
25
|
+
/** Provider-specific error classification. */
|
|
26
|
+
readonly code: DodoErrorCode;
|
|
27
|
+
/** HTTP status returned by Dodo when the error came from an API response. */
|
|
28
|
+
readonly status?: number;
|
|
29
|
+
constructor(message: string, code: DodoErrorCode, status?: number, options?: ErrorOptions);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a fetch-based Dodo Payments adapter that implements the shared
|
|
33
|
+
* `@openbilling/core` billing contract.
|
|
34
|
+
*
|
|
35
|
+
* The current MVP intentionally supports a narrow Dodo surface:
|
|
36
|
+
* - checkout creation through `POST /checkouts`
|
|
37
|
+
* - customer billing management through customer portal sessions
|
|
38
|
+
* - webhook verification plus normalization for a small set of events
|
|
39
|
+
*
|
|
40
|
+
* Important provider caveats:
|
|
41
|
+
* - Dodo currently requires `productId` for checkout creation
|
|
42
|
+
* - `priceId` alone is not treated as a portable substitute
|
|
43
|
+
* - the Dodo product determines whether a checkout is one-time or recurring
|
|
44
|
+
* - the adapter defaults to `https://live.dodopayments.com`, with `baseUrl`
|
|
45
|
+
* available for test mode or custom hosts
|
|
46
|
+
*
|
|
47
|
+
* Supported normalized Dodo webhook coverage:
|
|
48
|
+
* - `payment.succeeded`
|
|
49
|
+
* - `subscription.active`
|
|
50
|
+
* - `subscription.cancelled`
|
|
51
|
+
*
|
|
52
|
+
* Unsupported Dodo events resolve to {@link Webhook.Unknown} instead of
|
|
53
|
+
* throwing purely because the event is outside the current MVP.
|
|
54
|
+
*
|
|
55
|
+
* @throws {DodoProviderError} When input is unsupported, webhook verification
|
|
56
|
+
* fails, or the Dodo API returns an error response.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const billing = createDodoProvider({
|
|
61
|
+
* apiKey: "dodo_api_key",
|
|
62
|
+
* webhookSecret: "whsec_123"
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* const checkout = await billing.createCheckout({
|
|
66
|
+
* productId: "prod_123",
|
|
67
|
+
* customerEmail: "demo@example.com",
|
|
68
|
+
* successUrl: "https://example.com/success",
|
|
69
|
+
* cancelUrl: "https://example.com/cancel",
|
|
70
|
+
* mode: "subscription"
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
declare function createDodoProvider(config: DodoProviderConfig): BillingProvider;
|
|
75
|
+
|
|
76
|
+
export { type DodoProviderConfig, DodoProviderError, createDodoProvider };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Webhook as StandardWebhook } from "standardwebhooks";
|
|
3
|
+
import {
|
|
4
|
+
Payment,
|
|
5
|
+
Provider,
|
|
6
|
+
Subscription,
|
|
7
|
+
Webhook,
|
|
8
|
+
createBilling
|
|
9
|
+
} from "@openbilling/core";
|
|
10
|
+
var DEFAULT_BASE_URL = "https://live.dodopayments.com";
|
|
11
|
+
var textDecoder = new TextDecoder();
|
|
12
|
+
var DodoProviderError = class extends Error {
|
|
13
|
+
/** Provider-specific error classification. */
|
|
14
|
+
code;
|
|
15
|
+
/** HTTP status returned by Dodo when the error came from an API response. */
|
|
16
|
+
status;
|
|
17
|
+
constructor(message, code, status, options) {
|
|
18
|
+
super(message, options);
|
|
19
|
+
this.name = "DodoProviderError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
if (status !== void 0) {
|
|
22
|
+
this.status = status;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
function createDodoProvider(config) {
|
|
27
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
28
|
+
return createBilling({
|
|
29
|
+
async createCheckout(input) {
|
|
30
|
+
const response = await postJson(
|
|
31
|
+
`${baseUrl}/checkouts`,
|
|
32
|
+
config.apiKey,
|
|
33
|
+
buildCheckoutRequest(input)
|
|
34
|
+
);
|
|
35
|
+
if (!response.checkout_url) {
|
|
36
|
+
throw new DodoProviderError("Dodo checkout session did not include a checkout URL.", "api_error");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id: response.session_id,
|
|
40
|
+
url: response.checkout_url,
|
|
41
|
+
provider: Provider.Dodo,
|
|
42
|
+
raw: response
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
async createPortalLink(input) {
|
|
46
|
+
const endpoint = new URL(
|
|
47
|
+
`${baseUrl}/customers/${encodeURIComponent(input.customerId)}/customer-portal/session`
|
|
48
|
+
);
|
|
49
|
+
endpoint.searchParams.set("return_url", input.returnUrl);
|
|
50
|
+
const response = await postJson(endpoint.toString(), config.apiKey);
|
|
51
|
+
return {
|
|
52
|
+
url: response.link,
|
|
53
|
+
provider: Provider.Dodo,
|
|
54
|
+
raw: response
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
async verifyWebhook(input) {
|
|
58
|
+
const headers = getRequiredWebhookHeaders(input.headers);
|
|
59
|
+
const secret = input.secret ?? config.webhookSecret;
|
|
60
|
+
const payload = toWebhookPayload(input.payload);
|
|
61
|
+
try {
|
|
62
|
+
const event = new StandardWebhook(secret).verify(payload, headers);
|
|
63
|
+
return normalizeWebhookEvent(event);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new DodoProviderError("Dodo webhook signature verification failed.", "invalid_webhook_signature", void 0, {
|
|
66
|
+
cause: error
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function normalizeBaseUrl(baseUrl) {
|
|
73
|
+
return (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
74
|
+
}
|
|
75
|
+
function buildCheckoutRequest(input) {
|
|
76
|
+
if (!input.productId) {
|
|
77
|
+
throw new DodoProviderError(
|
|
78
|
+
input.priceId ? "Dodo checkout sessions currently require a productId instead of a priceId." : "Dodo checkout sessions require a productId.",
|
|
79
|
+
"unsupported_input"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const request = {
|
|
83
|
+
product_cart: [
|
|
84
|
+
{
|
|
85
|
+
product_id: input.productId,
|
|
86
|
+
quantity: 1
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
return_url: input.successUrl,
|
|
90
|
+
cancel_url: input.cancelUrl
|
|
91
|
+
};
|
|
92
|
+
if (input.customerId) {
|
|
93
|
+
request.customer_id = input.customerId;
|
|
94
|
+
} else if (input.customerEmail) {
|
|
95
|
+
request.customer = {
|
|
96
|
+
email: input.customerEmail
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (input.metadata) {
|
|
100
|
+
request.metadata = input.metadata;
|
|
101
|
+
}
|
|
102
|
+
return request;
|
|
103
|
+
}
|
|
104
|
+
function getRequiredWebhookHeaders(headers) {
|
|
105
|
+
const webhookId = headers?.["webhook-id"];
|
|
106
|
+
const webhookSignature = headers?.["webhook-signature"];
|
|
107
|
+
const webhookTimestamp = headers?.["webhook-timestamp"];
|
|
108
|
+
if (!webhookId || !webhookSignature || !webhookTimestamp) {
|
|
109
|
+
throw new DodoProviderError(
|
|
110
|
+
"Dodo webhook verification requires webhook-id, webhook-signature, and webhook-timestamp headers.",
|
|
111
|
+
"invalid_webhook_headers"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
"webhook-id": webhookId,
|
|
116
|
+
"webhook-signature": webhookSignature,
|
|
117
|
+
"webhook-timestamp": webhookTimestamp
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function toWebhookPayload(payload) {
|
|
121
|
+
return typeof payload === "string" ? payload : textDecoder.decode(payload);
|
|
122
|
+
}
|
|
123
|
+
function normalizeWebhookEvent(event) {
|
|
124
|
+
const payload = event;
|
|
125
|
+
const customerId = payload.data?.customer?.customer_id;
|
|
126
|
+
switch (payload.type) {
|
|
127
|
+
case Payment.Succeeded:
|
|
128
|
+
if (!payload.data?.payment_id) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
type: Payment.Succeeded,
|
|
133
|
+
provider: Provider.Dodo,
|
|
134
|
+
paymentId: payload.data.payment_id,
|
|
135
|
+
...customerId ? { customerId } : {},
|
|
136
|
+
raw: event
|
|
137
|
+
};
|
|
138
|
+
case Subscription.Active:
|
|
139
|
+
if (customerId && payload.data?.subscription_id) {
|
|
140
|
+
return {
|
|
141
|
+
type: Subscription.Active,
|
|
142
|
+
provider: Provider.Dodo,
|
|
143
|
+
customerId,
|
|
144
|
+
subscriptionId: payload.data.subscription_id,
|
|
145
|
+
raw: event
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
case Subscription.Cancelled:
|
|
150
|
+
if (customerId && payload.data?.subscription_id) {
|
|
151
|
+
return {
|
|
152
|
+
type: Subscription.Cancelled,
|
|
153
|
+
provider: Provider.Dodo,
|
|
154
|
+
customerId,
|
|
155
|
+
subscriptionId: payload.data.subscription_id,
|
|
156
|
+
raw: event
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
type: Webhook.Unknown,
|
|
163
|
+
provider: Provider.Dodo,
|
|
164
|
+
raw: event
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async function postJson(url, apiKey, body) {
|
|
168
|
+
const headers = {
|
|
169
|
+
authorization: `Bearer ${apiKey}`
|
|
170
|
+
};
|
|
171
|
+
if (body !== void 0) {
|
|
172
|
+
headers["content-type"] = "application/json";
|
|
173
|
+
}
|
|
174
|
+
const requestInit = {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers,
|
|
177
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
178
|
+
};
|
|
179
|
+
const response = await fetch(url, requestInit);
|
|
180
|
+
const responseBody = await readResponseBody(response);
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw createApiError(response, responseBody);
|
|
183
|
+
}
|
|
184
|
+
return responseBody;
|
|
185
|
+
}
|
|
186
|
+
async function readResponseBody(response) {
|
|
187
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
188
|
+
if (contentType.includes("application/json")) {
|
|
189
|
+
return response.json();
|
|
190
|
+
}
|
|
191
|
+
return response.text();
|
|
192
|
+
}
|
|
193
|
+
function createApiError(response, responseBody) {
|
|
194
|
+
const message = getApiErrorMessage(responseBody) ?? `Dodo API request failed with status ${response.status}${response.statusText ? ` ${response.statusText}` : ""}.`;
|
|
195
|
+
return new DodoProviderError(message, "api_error", response.status);
|
|
196
|
+
}
|
|
197
|
+
function getApiErrorMessage(responseBody) {
|
|
198
|
+
if (!responseBody || typeof responseBody !== "object") {
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
if ("message" in responseBody && typeof responseBody.message === "string") {
|
|
202
|
+
return responseBody.message;
|
|
203
|
+
}
|
|
204
|
+
if ("error" in responseBody && typeof responseBody.error === "string") {
|
|
205
|
+
return responseBody.error;
|
|
206
|
+
}
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
export {
|
|
210
|
+
DodoProviderError,
|
|
211
|
+
createDodoProvider
|
|
212
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openbilling/dodo",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"homepage": "https://openbilling.geodim.dev",
|
|
5
|
+
"description": "Switch between Stripe and Dodo Payments without rewriting your billing logic",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/gndimitro/openbilling",
|
|
9
|
+
"directory": "packages/dodo"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"billing",
|
|
14
|
+
"payments",
|
|
15
|
+
"stripe",
|
|
16
|
+
"dodo"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"standardwebhooks": "^1.0.0",
|
|
34
|
+
"@openbilling/core": "0.1.0-alpha.1"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
41
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --clean --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --project tsconfig.json --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|