@wopr-network/platform-core 1.14.7 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.env.example +10 -0
  2. package/dist/account/deletion-executor-repository.d.ts +2 -2
  3. package/dist/account/deletion-executor-repository.js +5 -5
  4. package/dist/{monetization/payram → billing/crypto}/cents-credits-boundary.test.js +14 -17
  5. package/dist/billing/{payram → crypto}/charge-store.d.ts +17 -13
  6. package/dist/billing/{payram → crypto}/charge-store.js +21 -18
  7. package/dist/billing/crypto/charge-store.test.js +64 -0
  8. package/dist/billing/crypto/checkout.d.ts +18 -0
  9. package/dist/billing/crypto/checkout.js +35 -0
  10. package/dist/billing/crypto/checkout.test.js +71 -0
  11. package/dist/billing/crypto/client.d.ts +39 -0
  12. package/dist/billing/crypto/client.js +72 -0
  13. package/dist/billing/crypto/client.test.js +100 -0
  14. package/dist/billing/crypto/index.d.ts +9 -0
  15. package/dist/billing/crypto/index.js +5 -0
  16. package/dist/billing/crypto/types.d.ts +61 -0
  17. package/dist/billing/crypto/types.js +24 -0
  18. package/dist/billing/crypto/webhook.d.ts +34 -0
  19. package/dist/billing/crypto/webhook.js +107 -0
  20. package/dist/billing/crypto/webhook.test.js +266 -0
  21. package/dist/billing/index.d.ts +1 -1
  22. package/dist/billing/index.js +2 -2
  23. package/dist/billing/payment-processor.d.ts +3 -3
  24. package/dist/config/provider-endpoints.d.ts +4 -0
  25. package/dist/config/provider-endpoints.js +10 -6
  26. package/dist/credits/credit-ledger.d.ts +3 -3
  27. package/dist/credits/credit-ledger.js +3 -3
  28. package/dist/db/index.d.ts +1 -1
  29. package/dist/db/index.js +1 -1
  30. package/dist/db/schema/credits.js +1 -1
  31. package/dist/db/schema/{payram.d.ts → crypto.d.ts} +17 -13
  32. package/dist/db/schema/crypto.js +25 -0
  33. package/dist/db/schema/index.d.ts +1 -1
  34. package/dist/db/schema/index.js +1 -1
  35. package/dist/fleet/node-repository.d.ts +1 -3
  36. package/dist/monetization/crypto/__tests__/webhook.test.js +249 -0
  37. package/dist/monetization/crypto/index.d.ts +4 -0
  38. package/dist/monetization/crypto/index.js +2 -0
  39. package/dist/monetization/crypto/webhook.d.ts +24 -0
  40. package/dist/monetization/crypto/webhook.js +88 -0
  41. package/dist/monetization/index.d.ts +3 -3
  42. package/dist/monetization/index.js +1 -1
  43. package/dist/monetization/repository-types.d.ts +1 -1
  44. package/dist/observability/pagerduty.test.js +1 -0
  45. package/dist/security/key-validation.test.js +65 -8
  46. package/drizzle/migrations/0004_crypto_charges.sql +25 -0
  47. package/drizzle/migrations/meta/_journal.json +7 -0
  48. package/package.json +1 -3
  49. package/src/account/deletion-executor-repository.ts +6 -6
  50. package/src/billing/{payram → crypto}/cents-credits-boundary.test.ts +14 -17
  51. package/src/billing/crypto/charge-store.test.ts +81 -0
  52. package/src/billing/{payram → crypto}/charge-store.ts +28 -25
  53. package/src/billing/crypto/checkout.test.ts +93 -0
  54. package/src/billing/crypto/checkout.ts +48 -0
  55. package/src/billing/crypto/client.test.ts +132 -0
  56. package/src/billing/crypto/client.ts +86 -0
  57. package/src/billing/crypto/index.ts +15 -0
  58. package/src/billing/crypto/types.ts +83 -0
  59. package/src/billing/crypto/webhook.test.ts +340 -0
  60. package/src/billing/crypto/webhook.ts +136 -0
  61. package/src/billing/index.ts +2 -2
  62. package/src/billing/payment-processor.ts +3 -3
  63. package/src/config/provider-endpoints.ts +10 -6
  64. package/src/credits/credit-ledger.ts +3 -3
  65. package/src/db/index.ts +1 -2
  66. package/src/db/schema/credits.ts +1 -1
  67. package/src/db/schema/crypto.ts +30 -0
  68. package/src/db/schema/index.ts +1 -1
  69. package/src/fleet/node-repository.ts +8 -3
  70. package/src/monetization/crypto/__tests__/webhook.test.ts +327 -0
  71. package/src/monetization/crypto/index.ts +23 -0
  72. package/src/monetization/crypto/webhook.ts +115 -0
  73. package/src/monetization/index.ts +23 -21
  74. package/src/monetization/repository-types.ts +2 -2
  75. package/src/observability/pagerduty.test.ts +1 -0
  76. package/src/security/key-validation.test.ts +74 -8
  77. package/dist/billing/payram/cents-credits-boundary.test.js +0 -75
  78. package/dist/billing/payram/charge-store.test.js +0 -64
  79. package/dist/billing/payram/checkout.d.ts +0 -15
  80. package/dist/billing/payram/checkout.js +0 -24
  81. package/dist/billing/payram/checkout.test.js +0 -74
  82. package/dist/billing/payram/client.d.ts +0 -7
  83. package/dist/billing/payram/client.js +0 -15
  84. package/dist/billing/payram/client.test.js +0 -52
  85. package/dist/billing/payram/index.d.ts +0 -8
  86. package/dist/billing/payram/index.js +0 -4
  87. package/dist/billing/payram/types.d.ts +0 -40
  88. package/dist/billing/payram/webhook.d.ts +0 -19
  89. package/dist/billing/payram/webhook.js +0 -71
  90. package/dist/billing/payram/webhook.test.d.ts +0 -7
  91. package/dist/billing/payram/webhook.test.js +0 -249
  92. package/dist/db/schema/payram.js +0 -21
  93. package/dist/monetization/payram/charge-store.test.d.ts +0 -1
  94. package/dist/monetization/payram/charge-store.test.js +0 -64
  95. package/dist/monetization/payram/checkout.test.d.ts +0 -1
  96. package/dist/monetization/payram/checkout.test.js +0 -73
  97. package/dist/monetization/payram/client.test.d.ts +0 -1
  98. package/dist/monetization/payram/client.test.js +0 -52
  99. package/dist/monetization/payram/index.d.ts +0 -4
  100. package/dist/monetization/payram/index.js +0 -2
  101. package/dist/monetization/payram/webhook.d.ts +0 -17
  102. package/dist/monetization/payram/webhook.js +0 -71
  103. package/dist/monetization/payram/webhook.test.d.ts +0 -7
  104. package/dist/monetization/payram/webhook.test.js +0 -247
  105. package/src/billing/payram/charge-store.test.ts +0 -84
  106. package/src/billing/payram/checkout.test.ts +0 -99
  107. package/src/billing/payram/checkout.ts +0 -40
  108. package/src/billing/payram/client.test.ts +0 -62
  109. package/src/billing/payram/client.ts +0 -21
  110. package/src/billing/payram/index.ts +0 -14
  111. package/src/billing/payram/types.ts +0 -44
  112. package/src/billing/payram/webhook.test.ts +0 -320
  113. package/src/billing/payram/webhook.ts +0 -94
  114. package/src/db/schema/payram.ts +0 -26
  115. package/src/monetization/payram/cents-credits-boundary.test.ts +0 -84
  116. package/src/monetization/payram/charge-store.test.ts +0 -84
  117. package/src/monetization/payram/checkout.test.ts +0 -98
  118. package/src/monetization/payram/client.test.ts +0 -62
  119. package/src/monetization/payram/index.ts +0 -20
  120. package/src/monetization/payram/webhook.test.ts +0 -327
  121. package/src/monetization/payram/webhook.ts +0 -97
  122. /package/dist/billing/{payram → crypto}/cents-credits-boundary.test.d.ts +0 -0
  123. /package/dist/billing/{payram → crypto}/charge-store.test.d.ts +0 -0
  124. /package/dist/billing/{payram → crypto}/checkout.test.d.ts +0 -0
  125. /package/dist/billing/{payram → crypto}/client.test.d.ts +0 -0
  126. /package/dist/billing/{payram/types.js → crypto/webhook.test.d.ts} +0 -0
  127. /package/dist/monetization/{payram/cents-credits-boundary.test.d.ts → crypto/__tests__/webhook.test.d.ts} +0 -0
@@ -0,0 +1,93 @@
1
+ import type { PGlite } from "@electric-sql/pglite";
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { PlatformDb } from "../../db/index.js";
4
+ import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../../test/db.js";
5
+ import { CryptoChargeRepository } from "./charge-store.js";
6
+ import { createCryptoCheckout, MIN_PAYMENT_USD } from "./checkout.js";
7
+ import type { BTCPayClient } from "./client.js";
8
+
9
+ function createMockClient(overrides: { createInvoice?: ReturnType<typeof vi.fn> } = {}): BTCPayClient {
10
+ return {
11
+ createInvoice:
12
+ overrides.createInvoice ??
13
+ vi.fn().mockResolvedValue({
14
+ id: "inv-mock-001",
15
+ checkoutLink: "https://btcpay.example.com/i/inv-mock-001",
16
+ }),
17
+ } as unknown as BTCPayClient;
18
+ }
19
+
20
+ describe("createCryptoCheckout", () => {
21
+ let pool: PGlite;
22
+ let db: PlatformDb;
23
+ let chargeStore: CryptoChargeRepository;
24
+ let client: BTCPayClient;
25
+
26
+ beforeAll(async () => {
27
+ ({ db, pool } = await createTestDb());
28
+ await beginTestTransaction(pool);
29
+ });
30
+
31
+ afterAll(async () => {
32
+ await endTestTransaction(pool);
33
+ await pool.close();
34
+ });
35
+
36
+ beforeEach(async () => {
37
+ await rollbackTestTransaction(pool);
38
+ chargeStore = new CryptoChargeRepository(db);
39
+ client = createMockClient();
40
+ });
41
+
42
+ it("rejects amounts below $10 minimum", async () => {
43
+ await expect(createCryptoCheckout(client, chargeStore, { tenant: "t-1", amountUsd: 5 })).rejects.toThrow(
44
+ `Minimum payment amount is $${MIN_PAYMENT_USD}`,
45
+ );
46
+ });
47
+
48
+ it("rejects amounts of exactly $0", async () => {
49
+ await expect(createCryptoCheckout(client, chargeStore, { tenant: "t-1", amountUsd: 0 })).rejects.toThrow();
50
+ });
51
+
52
+ it("calls client.createInvoice with correct params", async () => {
53
+ const createInvoice = vi.fn().mockResolvedValue({
54
+ id: "inv-abc",
55
+ checkoutLink: "https://btcpay.example.com/i/inv-abc",
56
+ });
57
+ const mockClient = createMockClient({ createInvoice });
58
+
59
+ await createCryptoCheckout(mockClient, chargeStore, { tenant: "t-test", amountUsd: 25 });
60
+
61
+ expect(createInvoice).toHaveBeenCalledOnce();
62
+ const args = createInvoice.mock.calls[0][0];
63
+ expect(args.amountUsd).toBe(25);
64
+ expect(args.buyerEmail).toContain("t-test@");
65
+ });
66
+
67
+ it("stores the charge with correct amountUsdCents (converts from USD)", async () => {
68
+ const createInvoice = vi.fn().mockResolvedValue({
69
+ id: "inv-store-test",
70
+ checkoutLink: "https://btcpay.example.com/i/inv-store-test",
71
+ });
72
+ const mockClient = createMockClient({ createInvoice });
73
+
74
+ await createCryptoCheckout(mockClient, chargeStore, { tenant: "t-2", amountUsd: 25 });
75
+
76
+ const charge = await chargeStore.getByReferenceId("inv-store-test");
77
+ expect(charge).not.toBeNull();
78
+ expect(charge?.tenantId).toBe("t-2");
79
+ expect(charge?.amountUsdCents).toBe(2500); // $25.00 = 2500 cents
80
+ expect(charge?.status).toBe("New");
81
+ });
82
+
83
+ it("returns referenceId and url", async () => {
84
+ const result = await createCryptoCheckout(client, chargeStore, { tenant: "t-3", amountUsd: 10 });
85
+
86
+ expect(result.referenceId).toBe("inv-mock-001");
87
+ expect(result.url).toBe("https://btcpay.example.com/i/inv-mock-001");
88
+ });
89
+
90
+ it("accepts exactly $10 (minimum boundary)", async () => {
91
+ await expect(createCryptoCheckout(client, chargeStore, { tenant: "t-4", amountUsd: 10 })).resolves.not.toBeNull();
92
+ });
93
+ });
@@ -0,0 +1,48 @@
1
+ import crypto from "node:crypto";
2
+ import { Credit } from "../../credits/credit.js";
3
+ import type { ICryptoChargeRepository } from "./charge-store.js";
4
+ import type { BTCPayClient } from "./client.js";
5
+ import type { CryptoCheckoutOpts } from "./types.js";
6
+
7
+ /** Minimum payment amount in USD. */
8
+ export const MIN_PAYMENT_USD = 10;
9
+
10
+ /**
11
+ * Create a BTCPay invoice and store the charge record.
12
+ *
13
+ * Returns the BTCPay-hosted checkout page URL and invoice ID.
14
+ * The user is redirected to checkoutLink to complete the crypto payment.
15
+ *
16
+ * NOTE: amountUsd is converted to cents (integer) for the charge store.
17
+ * The charge store holds USD cents, NOT nanodollars.
18
+ */
19
+ export async function createCryptoCheckout(
20
+ client: BTCPayClient,
21
+ chargeStore: ICryptoChargeRepository,
22
+ opts: CryptoCheckoutOpts,
23
+ ): Promise<{ referenceId: string; url: string }> {
24
+ if (opts.amountUsd < MIN_PAYMENT_USD) {
25
+ throw new Error(`Minimum payment amount is $${MIN_PAYMENT_USD}`);
26
+ }
27
+
28
+ const orderId = `crypto:${opts.tenant}:${crypto.randomUUID()}`;
29
+
30
+ const invoice = await client.createInvoice({
31
+ amountUsd: opts.amountUsd,
32
+ orderId,
33
+ buyerEmail: `${opts.tenant}@${process.env.PLATFORM_DOMAIN ?? "wopr.bot"}`,
34
+ });
35
+
36
+ // Store the charge record for webhook correlation.
37
+ // amountUsdCents = USD * 100 (cents, NOT nanodollars).
38
+ // Credit.fromDollars() handles the float → integer boundary safely via Math.round
39
+ // on the nanodollar scale, then toCentsRounded() converts back to integer cents.
40
+ // This avoids direct floating-point multiplication for the cents conversion.
41
+ const amountUsdCents = Credit.fromDollars(opts.amountUsd).toCentsRounded();
42
+ await chargeStore.create(invoice.id, opts.tenant, amountUsdCents);
43
+
44
+ return {
45
+ referenceId: invoice.id,
46
+ url: invoice.checkoutLink,
47
+ };
48
+ }
@@ -0,0 +1,132 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { BTCPayClient, loadCryptoConfig } from "./client.js";
3
+
4
+ describe("BTCPayClient", () => {
5
+ it("createInvoice sends correct request and returns id + checkoutLink", async () => {
6
+ const mockResponse = { id: "inv-001", checkoutLink: "https://btcpay.example.com/i/inv-001" };
7
+ const fetchSpy = vi
8
+ .spyOn(globalThis, "fetch")
9
+ .mockResolvedValue(new Response(JSON.stringify(mockResponse), { status: 200 }));
10
+
11
+ const client = new BTCPayClient({
12
+ apiKey: "test-key",
13
+ baseUrl: "https://btcpay.example.com",
14
+ storeId: "store-abc",
15
+ });
16
+
17
+ const result = await client.createInvoice({
18
+ amountUsd: 25,
19
+ orderId: "order-123",
20
+ buyerEmail: "test@example.com",
21
+ });
22
+
23
+ expect(result.id).toBe("inv-001");
24
+ expect(result.checkoutLink).toBe("https://btcpay.example.com/i/inv-001");
25
+
26
+ expect(fetchSpy).toHaveBeenCalledOnce();
27
+ const [url, opts] = fetchSpy.mock.calls[0];
28
+ expect(url).toBe("https://btcpay.example.com/api/v1/stores/store-abc/invoices");
29
+ expect(opts?.method).toBe("POST");
30
+
31
+ const headers = opts?.headers as Record<string, string>;
32
+ expect(headers.Authorization).toBe("token test-key");
33
+ expect(headers["Content-Type"]).toBe("application/json");
34
+
35
+ const body = JSON.parse(opts?.body as string);
36
+ expect(body.amount).toBe("25");
37
+ expect(body.currency).toBe("USD");
38
+ expect(body.metadata.orderId).toBe("order-123");
39
+ expect(body.metadata.buyerEmail).toBe("test@example.com");
40
+ expect(body.checkout.speedPolicy).toBe("MediumSpeed");
41
+
42
+ fetchSpy.mockRestore();
43
+ });
44
+
45
+ it("createInvoice includes redirectURL when provided", async () => {
46
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
47
+ new Response(JSON.stringify({ id: "inv-002", checkoutLink: "https://btcpay.example.com/i/inv-002" }), {
48
+ status: 200,
49
+ }),
50
+ );
51
+
52
+ const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://btcpay.example.com", storeId: "s" });
53
+ await client.createInvoice({ amountUsd: 10, orderId: "o", redirectURL: "https://app.example.com/success" });
54
+
55
+ const body = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string);
56
+ expect(body.checkout.redirectURL).toBe("https://app.example.com/success");
57
+
58
+ fetchSpy.mockRestore();
59
+ });
60
+
61
+ it("createInvoice throws on non-ok response", async () => {
62
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Unauthorized", { status: 401 }));
63
+
64
+ const client = new BTCPayClient({ apiKey: "bad-key", baseUrl: "https://btcpay.example.com", storeId: "s" });
65
+ await expect(client.createInvoice({ amountUsd: 10, orderId: "o" })).rejects.toThrow(
66
+ "BTCPay createInvoice failed (401)",
67
+ );
68
+
69
+ fetchSpy.mockRestore();
70
+ });
71
+
72
+ it("getInvoice sends correct request", async () => {
73
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
74
+ new Response(JSON.stringify({ id: "inv-001", status: "Settled", amount: "25", currency: "USD" }), {
75
+ status: 200,
76
+ }),
77
+ );
78
+
79
+ const client = new BTCPayClient({ apiKey: "k", baseUrl: "https://btcpay.example.com", storeId: "store-abc" });
80
+ const result = await client.getInvoice("inv-001");
81
+
82
+ expect(result.status).toBe("Settled");
83
+ expect(fetchSpy.mock.calls[0][0]).toBe("https://btcpay.example.com/api/v1/stores/store-abc/invoices/inv-001");
84
+
85
+ fetchSpy.mockRestore();
86
+ });
87
+ });
88
+
89
+ describe("loadCryptoConfig", () => {
90
+ beforeEach(() => {
91
+ delete process.env.BTCPAY_API_KEY;
92
+ delete process.env.BTCPAY_BASE_URL;
93
+ delete process.env.BTCPAY_STORE_ID;
94
+ });
95
+
96
+ afterEach(() => {
97
+ vi.unstubAllEnvs();
98
+ });
99
+
100
+ it("returns null when BTCPAY_API_KEY is missing", () => {
101
+ vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
102
+ vi.stubEnv("BTCPAY_STORE_ID", "store-1");
103
+ expect(loadCryptoConfig()).toBeNull();
104
+ });
105
+
106
+ it("returns null when BTCPAY_BASE_URL is missing", () => {
107
+ vi.stubEnv("BTCPAY_API_KEY", "test-key");
108
+ vi.stubEnv("BTCPAY_STORE_ID", "store-1");
109
+ expect(loadCryptoConfig()).toBeNull();
110
+ });
111
+
112
+ it("returns null when BTCPAY_STORE_ID is missing", () => {
113
+ vi.stubEnv("BTCPAY_API_KEY", "test-key");
114
+ vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
115
+ expect(loadCryptoConfig()).toBeNull();
116
+ });
117
+
118
+ it("returns config when all env vars are set", () => {
119
+ vi.stubEnv("BTCPAY_API_KEY", "test-key");
120
+ vi.stubEnv("BTCPAY_BASE_URL", "https://btcpay.test");
121
+ vi.stubEnv("BTCPAY_STORE_ID", "store-1");
122
+ expect(loadCryptoConfig()).toEqual({
123
+ apiKey: "test-key",
124
+ baseUrl: "https://btcpay.test",
125
+ storeId: "store-1",
126
+ });
127
+ });
128
+
129
+ it("returns null when all env vars are missing", () => {
130
+ expect(loadCryptoConfig()).toBeNull();
131
+ });
132
+ });
@@ -0,0 +1,86 @@
1
+ import type { CryptoBillingConfig } from "./types.js";
2
+
3
+ export type { CryptoBillingConfig as CryptoConfig };
4
+
5
+ /**
6
+ * Lightweight BTCPay Server Greenfield API client.
7
+ *
8
+ * Uses plain fetch — zero vendor dependencies.
9
+ * Auth header format: "token <apiKey>" (NOT "Bearer").
10
+ */
11
+ export class BTCPayClient {
12
+ constructor(private readonly config: CryptoBillingConfig) {}
13
+
14
+ private headers(): Record<string, string> {
15
+ return {
16
+ "Content-Type": "application/json",
17
+ Authorization: `token ${this.config.apiKey}`,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Create an invoice on the BTCPay store.
23
+ *
24
+ * Returns the invoice ID and checkout link (URL to redirect the user).
25
+ */
26
+ async createInvoice(opts: {
27
+ amountUsd: number;
28
+ orderId: string;
29
+ buyerEmail?: string;
30
+ redirectURL?: string;
31
+ }): Promise<{ id: string; checkoutLink: string }> {
32
+ const url = `${this.config.baseUrl}/api/v1/stores/${this.config.storeId}/invoices`;
33
+ const body = {
34
+ amount: String(opts.amountUsd),
35
+ currency: "USD",
36
+ metadata: {
37
+ orderId: opts.orderId,
38
+ buyerEmail: opts.buyerEmail,
39
+ },
40
+ checkout: {
41
+ speedPolicy: "MediumSpeed",
42
+ expirationMinutes: 30,
43
+ ...(opts.redirectURL ? { redirectURL: opts.redirectURL } : {}),
44
+ },
45
+ };
46
+
47
+ const res = await fetch(url, {
48
+ method: "POST",
49
+ headers: this.headers(),
50
+ body: JSON.stringify(body),
51
+ });
52
+
53
+ if (!res.ok) {
54
+ const text = await res.text().catch(() => "");
55
+ throw new Error(`BTCPay createInvoice failed (${res.status}): ${text}`);
56
+ }
57
+
58
+ const data = (await res.json()) as { id: string; checkoutLink: string };
59
+ return { id: data.id, checkoutLink: data.checkoutLink };
60
+ }
61
+
62
+ /** Get invoice status by ID. */
63
+ async getInvoice(invoiceId: string): Promise<{ id: string; status: string; amount: string; currency: string }> {
64
+ const url = `${this.config.baseUrl}/api/v1/stores/${this.config.storeId}/invoices/${invoiceId}`;
65
+ const res = await fetch(url, { headers: this.headers() });
66
+
67
+ if (!res.ok) {
68
+ const text = await res.text().catch(() => "");
69
+ throw new Error(`BTCPay getInvoice failed (${res.status}): ${text}`);
70
+ }
71
+
72
+ return (await res.json()) as { id: string; status: string; amount: string; currency: string };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Load BTCPay config from environment variables.
78
+ * Returns null if any required var is missing.
79
+ */
80
+ export function loadCryptoConfig(): CryptoBillingConfig | null {
81
+ const apiKey = process.env.BTCPAY_API_KEY;
82
+ const baseUrl = process.env.BTCPAY_BASE_URL;
83
+ const storeId = process.env.BTCPAY_STORE_ID;
84
+ if (!apiKey || !baseUrl || !storeId) return null;
85
+ return { apiKey, baseUrl, storeId };
86
+ }
@@ -0,0 +1,15 @@
1
+ export type { CryptoChargeRecord, ICryptoChargeRepository } from "./charge-store.js";
2
+ export { CryptoChargeRepository, DrizzleCryptoChargeRepository } from "./charge-store.js";
3
+ export { createCryptoCheckout, MIN_PAYMENT_USD } from "./checkout.js";
4
+ export type { CryptoConfig } from "./client.js";
5
+ export { BTCPayClient, loadCryptoConfig } from "./client.js";
6
+ export type {
7
+ CryptoBillingConfig,
8
+ CryptoCheckoutOpts,
9
+ CryptoPaymentState,
10
+ CryptoWebhookPayload,
11
+ CryptoWebhookResult,
12
+ } from "./types.js";
13
+ export { mapBtcPayEventToStatus } from "./types.js";
14
+ export type { CryptoWebhookDeps } from "./webhook.js";
15
+ export { handleCryptoWebhook, verifyCryptoWebhookSignature } from "./webhook.js";
@@ -0,0 +1,83 @@
1
+ /** BTCPay Server invoice states (Greenfield API v1). */
2
+ export type CryptoPaymentState = "New" | "Processing" | "Expired" | "Invalid" | "Settled";
3
+
4
+ /** Options for creating a crypto payment session. */
5
+ export interface CryptoCheckoutOpts {
6
+ /** Internal tenant ID. */
7
+ tenant: string;
8
+ /** Amount in USD (minimum $10). */
9
+ amountUsd: number;
10
+ }
11
+
12
+ /** Webhook payload received from BTCPay Server (InvoiceSettled event). */
13
+ export interface CryptoWebhookPayload {
14
+ /** BTCPay delivery ID (for deduplication). */
15
+ deliveryId: string;
16
+ /** Webhook ID. */
17
+ webhookId: string;
18
+ /** Original delivery ID (same as deliveryId on first delivery). */
19
+ originalDeliveryId: string;
20
+ /** Whether this is a redelivery. */
21
+ isRedelivery: boolean;
22
+ /** Event type (e.g. "InvoiceSettled", "InvoiceProcessing", "InvoiceExpired"). */
23
+ type: string;
24
+ /** Unix timestamp. */
25
+ timestamp: number;
26
+ /** BTCPay store ID. */
27
+ storeId: string;
28
+ /** BTCPay invoice ID. */
29
+ invoiceId: string;
30
+ /** Invoice metadata (echoed from creation). */
31
+ metadata: Record<string, unknown>;
32
+ /** Whether admin manually marked as settled (InvoiceSettled only). */
33
+ manuallyMarked?: boolean;
34
+ /** Whether customer overpaid (InvoiceSettled only). */
35
+ overPaid?: boolean;
36
+ /** Whether invoice was partially paid (InvoiceExpired only). */
37
+ partiallyPaid?: boolean;
38
+ }
39
+
40
+ /** Configuration for BTCPay Server integration. */
41
+ export interface CryptoBillingConfig {
42
+ /** BTCPay API key (from Account > API keys). */
43
+ apiKey: string;
44
+ /** BTCPay Server base URL. */
45
+ baseUrl: string;
46
+ /** BTCPay store ID. */
47
+ storeId: string;
48
+ }
49
+
50
+ /** Result of processing a crypto webhook event. */
51
+ export interface CryptoWebhookResult {
52
+ handled: boolean;
53
+ status: string;
54
+ tenant?: string;
55
+ creditedCents?: number;
56
+ reactivatedBots?: string[];
57
+ duplicate?: boolean;
58
+ }
59
+
60
+ /**
61
+ * Map BTCPay webhook event type string to a CryptoPaymentState.
62
+ *
63
+ * Shared between the core (billing) and consumer (monetization) webhook handlers.
64
+ * Throws on unrecognized event types to surface integration errors early.
65
+ */
66
+ export function mapBtcPayEventToStatus(eventType: string): CryptoPaymentState {
67
+ switch (eventType) {
68
+ case "InvoiceCreated":
69
+ return "New";
70
+ case "InvoiceReceivedPayment":
71
+ case "InvoiceProcessing":
72
+ return "Processing";
73
+ case "InvoiceSettled":
74
+ case "InvoicePaymentSettled":
75
+ return "Settled";
76
+ case "InvoiceExpired":
77
+ return "Expired";
78
+ case "InvoiceInvalid":
79
+ return "Invalid";
80
+ default:
81
+ throw new Error(`Unknown BTCPay event type: ${eventType}`);
82
+ }
83
+ }