@wopr-network/platform-core 1.14.8 → 1.16.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.
- package/dist/account/deletion-executor-repository.d.ts +2 -2
- package/dist/account/deletion-executor-repository.js +5 -5
- package/dist/{monetization/payram → billing/crypto}/cents-credits-boundary.test.js +14 -17
- package/dist/billing/crypto/charge-store.d.ts +68 -0
- package/dist/billing/crypto/charge-store.js +109 -0
- package/dist/billing/crypto/charge-store.test.js +120 -0
- package/dist/billing/crypto/checkout.d.ts +18 -0
- package/dist/billing/crypto/checkout.js +35 -0
- package/dist/billing/crypto/checkout.test.js +71 -0
- package/dist/billing/crypto/client.d.ts +39 -0
- package/dist/billing/crypto/client.js +72 -0
- package/dist/billing/crypto/client.test.js +100 -0
- package/dist/billing/crypto/evm/__tests__/address-gen.test.js +54 -0
- package/dist/billing/crypto/evm/__tests__/checkout.test.js +54 -0
- package/dist/billing/crypto/evm/__tests__/config.test.js +52 -0
- package/dist/billing/crypto/evm/__tests__/settler.test.js +196 -0
- package/dist/billing/crypto/evm/__tests__/watcher.test.js +109 -0
- package/dist/billing/crypto/evm/address-gen.d.ts +8 -0
- package/dist/billing/crypto/evm/address-gen.js +29 -0
- package/dist/billing/crypto/evm/checkout.d.ts +26 -0
- package/dist/billing/crypto/evm/checkout.js +57 -0
- package/dist/billing/crypto/evm/config.d.ts +13 -0
- package/dist/billing/crypto/evm/config.js +46 -0
- package/dist/billing/crypto/evm/index.d.ts +9 -0
- package/dist/billing/crypto/evm/index.js +5 -0
- package/dist/billing/crypto/evm/settler.d.ts +23 -0
- package/dist/billing/crypto/evm/settler.js +60 -0
- package/dist/billing/crypto/evm/types.d.ts +40 -0
- package/dist/billing/crypto/evm/types.js +1 -0
- package/dist/billing/crypto/evm/watcher.d.ts +31 -0
- package/dist/billing/crypto/evm/watcher.js +91 -0
- package/dist/billing/crypto/index.d.ts +10 -0
- package/dist/billing/crypto/index.js +6 -0
- package/dist/billing/crypto/types.d.ts +61 -0
- package/dist/billing/crypto/types.js +24 -0
- package/dist/billing/crypto/webhook.d.ts +34 -0
- package/dist/billing/crypto/webhook.js +107 -0
- package/dist/billing/crypto/webhook.test.d.ts +1 -0
- package/dist/billing/crypto/webhook.test.js +266 -0
- package/dist/billing/index.d.ts +1 -1
- package/dist/billing/index.js +2 -2
- package/dist/billing/payment-processor.d.ts +3 -3
- package/dist/credits/credit-ledger.d.ts +3 -3
- package/dist/credits/credit-ledger.js +3 -3
- package/dist/db/schema/credits.js +1 -1
- package/dist/db/schema/{payram.d.ts → crypto.d.ts} +85 -13
- package/dist/db/schema/crypto.js +32 -0
- package/dist/db/schema/index.d.ts +1 -1
- package/dist/db/schema/index.js +1 -1
- package/dist/monetization/crypto/__tests__/webhook.test.d.ts +1 -0
- package/dist/monetization/crypto/__tests__/webhook.test.js +249 -0
- package/dist/monetization/crypto/index.d.ts +4 -0
- package/dist/monetization/crypto/index.js +2 -0
- package/dist/monetization/crypto/webhook.d.ts +24 -0
- package/dist/monetization/crypto/webhook.js +88 -0
- package/dist/monetization/index.d.ts +3 -3
- package/dist/monetization/index.js +1 -1
- package/dist/monetization/repository-types.d.ts +1 -1
- package/dist/observability/pagerduty.test.js +1 -0
- package/docs/superpowers/plans/2026-03-14-stablecoin-phase1.md +1413 -0
- package/drizzle/migrations/0004_crypto_charges.sql +25 -0
- package/drizzle/migrations/0005_stablecoin_columns.sql +7 -0
- package/drizzle/migrations/meta/_journal.json +14 -0
- package/package.json +4 -3
- package/src/account/deletion-executor-repository.ts +6 -6
- package/src/billing/{payram → crypto}/cents-credits-boundary.test.ts +14 -17
- package/src/billing/crypto/charge-store.test.ts +142 -0
- package/src/billing/crypto/charge-store.ts +166 -0
- package/src/billing/crypto/checkout.test.ts +93 -0
- package/src/billing/crypto/checkout.ts +48 -0
- package/src/billing/crypto/client.test.ts +132 -0
- package/src/billing/crypto/client.ts +86 -0
- package/src/billing/crypto/evm/__tests__/address-gen.test.ts +63 -0
- package/src/billing/crypto/evm/__tests__/checkout.test.ts +83 -0
- package/src/billing/crypto/evm/__tests__/config.test.ts +63 -0
- package/src/billing/crypto/evm/__tests__/settler.test.ts +218 -0
- package/src/billing/crypto/evm/__tests__/watcher.test.ts +128 -0
- package/src/billing/crypto/evm/address-gen.ts +29 -0
- package/src/billing/crypto/evm/checkout.ts +82 -0
- package/src/billing/crypto/evm/config.ts +50 -0
- package/src/billing/crypto/evm/index.ts +16 -0
- package/src/billing/crypto/evm/settler.ts +79 -0
- package/src/billing/crypto/evm/types.ts +45 -0
- package/src/billing/crypto/evm/watcher.ts +126 -0
- package/src/billing/crypto/index.ts +16 -0
- package/src/billing/crypto/types.ts +83 -0
- package/src/billing/crypto/webhook.test.ts +340 -0
- package/src/billing/crypto/webhook.ts +136 -0
- package/src/billing/index.ts +2 -2
- package/src/billing/payment-processor.ts +3 -3
- package/src/credits/credit-ledger.ts +3 -3
- package/src/db/schema/credits.ts +1 -1
- package/src/db/schema/crypto.ts +37 -0
- package/src/db/schema/index.ts +1 -1
- package/src/monetization/crypto/__tests__/webhook.test.ts +327 -0
- package/src/monetization/crypto/index.ts +23 -0
- package/src/monetization/crypto/webhook.ts +115 -0
- package/src/monetization/index.ts +23 -21
- package/src/monetization/repository-types.ts +2 -2
- package/src/observability/pagerduty.test.ts +1 -0
- package/dist/billing/payram/cents-credits-boundary.test.js +0 -75
- package/dist/billing/payram/charge-store.d.ts +0 -41
- package/dist/billing/payram/charge-store.js +0 -72
- package/dist/billing/payram/charge-store.test.js +0 -64
- package/dist/billing/payram/checkout.d.ts +0 -15
- package/dist/billing/payram/checkout.js +0 -24
- package/dist/billing/payram/checkout.test.js +0 -74
- package/dist/billing/payram/client.d.ts +0 -7
- package/dist/billing/payram/client.js +0 -15
- package/dist/billing/payram/client.test.js +0 -52
- package/dist/billing/payram/index.d.ts +0 -8
- package/dist/billing/payram/index.js +0 -4
- package/dist/billing/payram/types.d.ts +0 -40
- package/dist/billing/payram/webhook.d.ts +0 -19
- package/dist/billing/payram/webhook.js +0 -71
- package/dist/billing/payram/webhook.test.d.ts +0 -7
- package/dist/billing/payram/webhook.test.js +0 -249
- package/dist/db/schema/payram.js +0 -21
- package/dist/monetization/payram/charge-store.test.js +0 -64
- package/dist/monetization/payram/checkout.test.js +0 -73
- package/dist/monetization/payram/client.test.js +0 -52
- package/dist/monetization/payram/index.d.ts +0 -4
- package/dist/monetization/payram/index.js +0 -2
- package/dist/monetization/payram/webhook.d.ts +0 -17
- package/dist/monetization/payram/webhook.js +0 -71
- package/dist/monetization/payram/webhook.test.d.ts +0 -7
- package/dist/monetization/payram/webhook.test.js +0 -247
- package/src/billing/payram/charge-store.test.ts +0 -84
- package/src/billing/payram/charge-store.ts +0 -109
- package/src/billing/payram/checkout.test.ts +0 -99
- package/src/billing/payram/checkout.ts +0 -40
- package/src/billing/payram/client.test.ts +0 -62
- package/src/billing/payram/client.ts +0 -21
- package/src/billing/payram/index.ts +0 -14
- package/src/billing/payram/types.ts +0 -44
- package/src/billing/payram/webhook.test.ts +0 -320
- package/src/billing/payram/webhook.ts +0 -94
- package/src/db/schema/payram.ts +0 -26
- package/src/monetization/payram/cents-credits-boundary.test.ts +0 -84
- package/src/monetization/payram/charge-store.test.ts +0 -84
- package/src/monetization/payram/checkout.test.ts +0 -98
- package/src/monetization/payram/client.test.ts +0 -62
- package/src/monetization/payram/index.ts +0 -20
- package/src/monetization/payram/webhook.test.ts +0 -327
- package/src/monetization/payram/webhook.ts +0 -97
- /package/dist/billing/{payram → crypto}/cents-credits-boundary.test.d.ts +0 -0
- /package/dist/billing/{payram → crypto}/charge-store.test.d.ts +0 -0
- /package/dist/billing/{payram → crypto}/checkout.test.d.ts +0 -0
- /package/dist/billing/{payram → crypto}/client.test.d.ts +0 -0
- /package/dist/billing/{payram/types.js → crypto/evm/__tests__/address-gen.test.d.ts} +0 -0
- /package/dist/{monetization/payram → billing/crypto/evm/__tests__}/checkout.test.d.ts +0 -0
- /package/dist/{monetization/payram/cents-credits-boundary.test.d.ts → billing/crypto/evm/__tests__/config.test.d.ts} +0 -0
- /package/dist/{monetization/payram/charge-store.test.d.ts → billing/crypto/evm/__tests__/settler.test.d.ts} +0 -0
- /package/dist/{monetization/payram/client.test.d.ts → billing/crypto/evm/__tests__/watcher.test.d.ts} +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { DrizzleLedger } from "../../credits/ledger.js";
|
|
4
|
+
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
5
|
+
import { DrizzleWebhookSeenRepository } from "../drizzle-webhook-seen-repository.js";
|
|
6
|
+
import { noOpReplayGuard } from "../webhook-seen-repository.js";
|
|
7
|
+
import { CryptoChargeRepository } from "./charge-store.js";
|
|
8
|
+
import { mapBtcPayEventToStatus } from "./types.js";
|
|
9
|
+
import { handleCryptoWebhook, verifyCryptoWebhookSignature } from "./webhook.js";
|
|
10
|
+
function makePayload(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
deliveryId: "del-001",
|
|
13
|
+
webhookId: "whk-001",
|
|
14
|
+
originalDeliveryId: "del-001",
|
|
15
|
+
isRedelivery: false,
|
|
16
|
+
type: "InvoiceSettled",
|
|
17
|
+
timestamp: Date.now(),
|
|
18
|
+
storeId: "store-test",
|
|
19
|
+
invoiceId: "inv-test-001",
|
|
20
|
+
metadata: { orderId: "order-001" },
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
let pool;
|
|
25
|
+
let db;
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
({ db, pool } = await createTestDb());
|
|
28
|
+
});
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await pool.close();
|
|
31
|
+
});
|
|
32
|
+
describe("handleCryptoWebhook", () => {
|
|
33
|
+
let chargeStore;
|
|
34
|
+
let creditLedger;
|
|
35
|
+
let deps;
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
await truncateAllTables(pool);
|
|
38
|
+
chargeStore = new CryptoChargeRepository(db);
|
|
39
|
+
creditLedger = new DrizzleLedger(db);
|
|
40
|
+
await creditLedger.seedSystemAccounts();
|
|
41
|
+
deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
|
|
42
|
+
// Create a default test charge
|
|
43
|
+
await chargeStore.create("inv-test-001", "tenant-a", 2500);
|
|
44
|
+
});
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// InvoiceSettled — should credit ledger
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
describe("InvoiceSettled", () => {
|
|
49
|
+
it("credits the ledger with the requested USD amount", async () => {
|
|
50
|
+
const result = await handleCryptoWebhook(deps, makePayload({ type: "InvoiceSettled" }));
|
|
51
|
+
expect(result.handled).toBe(true);
|
|
52
|
+
expect(result.status).toBe("Settled");
|
|
53
|
+
expect(result.tenant).toBe("tenant-a");
|
|
54
|
+
expect(result.creditedCents).toBe(2500);
|
|
55
|
+
const balance = await creditLedger.balance("tenant-a");
|
|
56
|
+
expect(balance.toCents()).toBe(2500);
|
|
57
|
+
});
|
|
58
|
+
it("uses crypto: prefix on reference ID in credit transaction", async () => {
|
|
59
|
+
await handleCryptoWebhook(deps, makePayload({ type: "InvoiceSettled" }));
|
|
60
|
+
const history = await creditLedger.history("tenant-a");
|
|
61
|
+
expect(history).toHaveLength(1);
|
|
62
|
+
expect(history[0].referenceId).toBe("crypto:inv-test-001");
|
|
63
|
+
expect(history[0].entryType).toBe("purchase");
|
|
64
|
+
});
|
|
65
|
+
it("records fundingSource as crypto", async () => {
|
|
66
|
+
await handleCryptoWebhook(deps, makePayload({ type: "InvoiceSettled" }));
|
|
67
|
+
const history = await creditLedger.history("tenant-a");
|
|
68
|
+
expect(history[0].metadata?.fundingSource).toBe("crypto");
|
|
69
|
+
});
|
|
70
|
+
it("marks the charge as credited after Settled", async () => {
|
|
71
|
+
await handleCryptoWebhook(deps, makePayload({ type: "InvoiceSettled" }));
|
|
72
|
+
expect(await chargeStore.isCredited("inv-test-001")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
it("is idempotent — duplicate InvoiceSettled does not double-credit", async () => {
|
|
75
|
+
await handleCryptoWebhook(deps, makePayload({ type: "InvoiceSettled" }));
|
|
76
|
+
const result2 = await handleCryptoWebhook(deps, makePayload({ type: "InvoiceSettled" }));
|
|
77
|
+
expect(result2.handled).toBe(true);
|
|
78
|
+
expect(result2.creditedCents).toBe(0);
|
|
79
|
+
const balance = await creditLedger.balance("tenant-a");
|
|
80
|
+
expect(balance.toCents()).toBe(2500); // Only credited once
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Statuses that should NOT credit the ledger
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
describe("InvoiceProcessing", () => {
|
|
87
|
+
it("does NOT credit the ledger", async () => {
|
|
88
|
+
const result = await handleCryptoWebhook(deps, makePayload({ type: "InvoiceProcessing" }));
|
|
89
|
+
expect(result.handled).toBe(true);
|
|
90
|
+
expect(result.tenant).toBe("tenant-a");
|
|
91
|
+
expect(result.creditedCents).toBeUndefined();
|
|
92
|
+
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("InvoiceCreated", () => {
|
|
96
|
+
it("does NOT credit the ledger", async () => {
|
|
97
|
+
const result = await handleCryptoWebhook(deps, makePayload({ type: "InvoiceCreated" }));
|
|
98
|
+
expect(result.handled).toBe(true);
|
|
99
|
+
expect(result.creditedCents).toBeUndefined();
|
|
100
|
+
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("InvoiceExpired", () => {
|
|
104
|
+
it("does NOT credit the ledger", async () => {
|
|
105
|
+
const result = await handleCryptoWebhook(deps, makePayload({ type: "InvoiceExpired" }));
|
|
106
|
+
expect(result.handled).toBe(true);
|
|
107
|
+
expect(result.creditedCents).toBeUndefined();
|
|
108
|
+
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe("InvoiceInvalid", () => {
|
|
112
|
+
it("does NOT credit the ledger", async () => {
|
|
113
|
+
const result = await handleCryptoWebhook(deps, makePayload({ type: "InvoiceInvalid" }));
|
|
114
|
+
expect(result.handled).toBe(true);
|
|
115
|
+
expect(result.creditedCents).toBeUndefined();
|
|
116
|
+
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Unknown invoice ID
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
describe("unknown invoiceId", () => {
|
|
123
|
+
it("returns handled:false when charge not found", async () => {
|
|
124
|
+
const result = await handleCryptoWebhook(deps, makePayload({ invoiceId: "inv-unknown-999" }));
|
|
125
|
+
expect(result.handled).toBe(false);
|
|
126
|
+
expect(result.tenant).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Charge store updates
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
describe("charge store updates", () => {
|
|
133
|
+
it("updates charge status on every webhook call", async () => {
|
|
134
|
+
await handleCryptoWebhook(deps, makePayload({ type: "InvoiceProcessing" }));
|
|
135
|
+
const charge = await chargeStore.getByReferenceId("inv-test-001");
|
|
136
|
+
expect(charge?.status).toBe("Processing");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Multiple tenants
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
describe("different invoices", () => {
|
|
143
|
+
it("processes multiple invoices independently", async () => {
|
|
144
|
+
await chargeStore.create("inv-b-001", "tenant-b", 5000);
|
|
145
|
+
await chargeStore.create("inv-c-001", "tenant-c", 1500);
|
|
146
|
+
await handleCryptoWebhook(deps, makePayload({ invoiceId: "inv-b-001", type: "InvoiceSettled" }));
|
|
147
|
+
await handleCryptoWebhook(deps, makePayload({ invoiceId: "inv-c-001", type: "InvoiceSettled" }));
|
|
148
|
+
expect((await creditLedger.balance("tenant-b")).toCents()).toBe(5000);
|
|
149
|
+
expect((await creditLedger.balance("tenant-c")).toCents()).toBe(1500);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Replay guard
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
describe("replay guard", () => {
|
|
156
|
+
it("blocks duplicate invoiceId + event type combos", async () => {
|
|
157
|
+
const replayGuard = new DrizzleWebhookSeenRepository(db);
|
|
158
|
+
const depsWithGuard = { ...deps, replayGuard };
|
|
159
|
+
const first = await handleCryptoWebhook(depsWithGuard, makePayload({ type: "InvoiceSettled" }));
|
|
160
|
+
expect(first.handled).toBe(true);
|
|
161
|
+
expect(first.creditedCents).toBe(2500);
|
|
162
|
+
expect(first.duplicate).toBeUndefined();
|
|
163
|
+
const second = await handleCryptoWebhook(depsWithGuard, makePayload({ type: "InvoiceSettled" }));
|
|
164
|
+
expect(second.handled).toBe(true);
|
|
165
|
+
expect(second.duplicate).toBe(true);
|
|
166
|
+
expect(second.creditedCents).toBeUndefined();
|
|
167
|
+
expect((await creditLedger.balance("tenant-a")).toCents()).toBe(2500);
|
|
168
|
+
});
|
|
169
|
+
it("same invoice with different event type is not blocked", async () => {
|
|
170
|
+
const replayGuard = new DrizzleWebhookSeenRepository(db);
|
|
171
|
+
const depsWithGuard = { ...deps, replayGuard };
|
|
172
|
+
await handleCryptoWebhook(depsWithGuard, makePayload({ type: "InvoiceProcessing" }));
|
|
173
|
+
const result = await handleCryptoWebhook(depsWithGuard, makePayload({ type: "InvoiceSettled" }));
|
|
174
|
+
expect(result.duplicate).toBeUndefined();
|
|
175
|
+
expect(result.creditedCents).toBe(2500);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Unknown event type
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
describe("unknown event type", () => {
|
|
182
|
+
it("throws on unrecognized BTCPay event type", async () => {
|
|
183
|
+
await expect(handleCryptoWebhook(deps, makePayload({ type: "SomeUnknownEvent" }))).rejects.toThrow("Unknown BTCPay event type: SomeUnknownEvent");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Resource reactivation
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
describe("resource reactivation via onCreditsPurchased", () => {
|
|
190
|
+
it("calls onCreditsPurchased on Settled and includes reactivatedBots", async () => {
|
|
191
|
+
const mockOnCreditsPurchased = vi.fn().mockResolvedValue(["bot-1", "bot-2"]);
|
|
192
|
+
const depsWithCallback = {
|
|
193
|
+
...deps,
|
|
194
|
+
onCreditsPurchased: mockOnCreditsPurchased,
|
|
195
|
+
};
|
|
196
|
+
const result = await handleCryptoWebhook(depsWithCallback, makePayload({ type: "InvoiceSettled" }));
|
|
197
|
+
expect(mockOnCreditsPurchased).toHaveBeenCalledWith("tenant-a", creditLedger);
|
|
198
|
+
expect(result.reactivatedBots).toEqual(["bot-1", "bot-2"]);
|
|
199
|
+
});
|
|
200
|
+
it("does not include reactivatedBots when no resources reactivated", async () => {
|
|
201
|
+
const mockOnCreditsPurchased = vi.fn().mockResolvedValue([]);
|
|
202
|
+
const depsWithCallback = {
|
|
203
|
+
...deps,
|
|
204
|
+
onCreditsPurchased: mockOnCreditsPurchased,
|
|
205
|
+
};
|
|
206
|
+
const result = await handleCryptoWebhook(depsWithCallback, makePayload({ type: "InvoiceSettled" }));
|
|
207
|
+
expect(result.reactivatedBots).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Webhook signature verification
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
describe("verifyCryptoWebhookSignature", () => {
|
|
215
|
+
const secret = "test-webhook-secret";
|
|
216
|
+
const body = '{"type":"InvoiceSettled","invoiceId":"inv-001"}';
|
|
217
|
+
it("returns true for valid signature", () => {
|
|
218
|
+
const sig = `sha256=${crypto.createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
219
|
+
expect(verifyCryptoWebhookSignature(body, sig, secret)).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
it("returns false for invalid signature", () => {
|
|
222
|
+
expect(verifyCryptoWebhookSignature(body, "sha256=badhex", secret)).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
it("returns false for wrong secret", () => {
|
|
225
|
+
const sig = `sha256=${crypto.createHmac("sha256", "wrong-secret").update(body).digest("hex")}`;
|
|
226
|
+
expect(verifyCryptoWebhookSignature(body, sig, secret)).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
it("returns false for tampered body", () => {
|
|
229
|
+
const sig = `sha256=${crypto.createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
230
|
+
expect(verifyCryptoWebhookSignature(`${body}tampered`, sig, secret)).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Replay guard unit tests
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// mapBtcPayEventToStatus
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
describe("mapBtcPayEventToStatus", () => {
|
|
240
|
+
it("maps known event types to CryptoPaymentState", () => {
|
|
241
|
+
expect(mapBtcPayEventToStatus("InvoiceCreated")).toBe("New");
|
|
242
|
+
expect(mapBtcPayEventToStatus("InvoiceReceivedPayment")).toBe("Processing");
|
|
243
|
+
expect(mapBtcPayEventToStatus("InvoiceProcessing")).toBe("Processing");
|
|
244
|
+
expect(mapBtcPayEventToStatus("InvoiceSettled")).toBe("Settled");
|
|
245
|
+
expect(mapBtcPayEventToStatus("InvoicePaymentSettled")).toBe("Settled");
|
|
246
|
+
expect(mapBtcPayEventToStatus("InvoiceExpired")).toBe("Expired");
|
|
247
|
+
expect(mapBtcPayEventToStatus("InvoiceInvalid")).toBe("Invalid");
|
|
248
|
+
});
|
|
249
|
+
it("throws on unknown event type", () => {
|
|
250
|
+
expect(() => mapBtcPayEventToStatus("SomethingElse")).toThrow("Unknown BTCPay event type: SomethingElse");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
describe("DrizzleWebhookSeenRepository (crypto replay guard)", () => {
|
|
254
|
+
beforeEach(async () => {
|
|
255
|
+
await truncateAllTables(pool);
|
|
256
|
+
});
|
|
257
|
+
it("reports unseen keys as not duplicate", async () => {
|
|
258
|
+
const guard = new DrizzleWebhookSeenRepository(db);
|
|
259
|
+
expect(await guard.isDuplicate("inv-001:InvoiceSettled", "crypto")).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
it("reports seen keys as duplicate", async () => {
|
|
262
|
+
const guard = new DrizzleWebhookSeenRepository(db);
|
|
263
|
+
await guard.markSeen("inv-001:InvoiceSettled", "crypto");
|
|
264
|
+
expect(await guard.isDuplicate("inv-001:InvoiceSettled", "crypto")).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
});
|
package/dist/billing/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
export * from "./crypto/index.js";
|
|
1
2
|
export { DrizzleWebhookSeenRepository } from "./drizzle-webhook-seen-repository.js";
|
|
2
3
|
export type { ChargeOpts, ChargeResult, CheckoutOpts, CheckoutSession, Invoice, IPaymentProcessor, PortalOpts, SavedPaymentMethod, SetupResult, WebhookResult, } from "./payment-processor.js";
|
|
3
4
|
export { PaymentMethodOwnershipError } from "./payment-processor.js";
|
|
4
|
-
export * from "./payram/index.js";
|
|
5
5
|
export * from "./stripe/index.js";
|
|
6
6
|
export type { IWebhookSeenRepository } from "./webhook-seen-repository.js";
|
|
7
7
|
export { noOpReplayGuard } from "./webhook-seen-repository.js";
|
package/dist/billing/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
// Crypto (BTCPay Server)
|
|
2
|
+
export * from "./crypto/index.js";
|
|
1
3
|
export { DrizzleWebhookSeenRepository } from "./drizzle-webhook-seen-repository.js";
|
|
2
4
|
export { PaymentMethodOwnershipError } from "./payment-processor.js";
|
|
3
|
-
// PayRam
|
|
4
|
-
export * from "./payram/index.js";
|
|
5
5
|
// Stripe
|
|
6
6
|
export * from "./stripe/index.js";
|
|
7
7
|
export { noOpReplayGuard } from "./webhook-seen-repository.js";
|
|
@@ -6,7 +6,7 @@ export declare class PaymentMethodOwnershipError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
/** A saved payment method on file for a tenant (processor-agnostic). */
|
|
8
8
|
export interface SavedPaymentMethod {
|
|
9
|
-
/** Processor-specific payment method ID (e.g. Stripe pm_xxx,
|
|
9
|
+
/** Processor-specific payment method ID (e.g. Stripe pm_xxx, BTCPay wallet address). */
|
|
10
10
|
id: string;
|
|
11
11
|
/** Human-readable label (e.g. "Visa ending 4242", "ETH wallet"). */
|
|
12
12
|
label: string;
|
|
@@ -77,12 +77,12 @@ export interface WebhookResult {
|
|
|
77
77
|
/**
|
|
78
78
|
* Processor-agnostic payment interface.
|
|
79
79
|
*
|
|
80
|
-
* Each payment processor (Stripe,
|
|
80
|
+
* Each payment processor (Stripe, BTCPay, future processors) implements
|
|
81
81
|
* this interface. The platform layer programs against IPaymentProcessor
|
|
82
82
|
* and never imports processor-specific types.
|
|
83
83
|
*/
|
|
84
84
|
export interface IPaymentProcessor {
|
|
85
|
-
/** Human-readable processor name (e.g. "stripe", "
|
|
85
|
+
/** Human-readable processor name (e.g. "stripe", "crypto"). */
|
|
86
86
|
readonly name: string;
|
|
87
87
|
/** Create a checkout session for a one-time credit purchase. */
|
|
88
88
|
createCheckoutSession(opts: CheckoutOpts): Promise<CheckoutSession>;
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
* NAMING CONVENTION — cents vs credits (WOP-1058)
|
|
3
3
|
*
|
|
4
4
|
* - `_cents` suffix = value denominated in USD cents.
|
|
5
|
-
* Used for: Stripe amounts,
|
|
5
|
+
* Used for: Stripe amounts, BTCPay amounts, ledger balances, ledger transactions.
|
|
6
6
|
* The platform credit unit IS the USD cent (1 credit = 1 cent = $0.01).
|
|
7
7
|
*
|
|
8
8
|
* - `_credits` suffix = platform credit count (used in DB columns and raw storage).
|
|
9
9
|
* Semantically identical to cents but signals "this is a platform concept stored
|
|
10
10
|
* as a Credit.toRaw() nanodollar value in the database."
|
|
11
11
|
*
|
|
12
|
-
* - NEVER rename a Stripe/
|
|
12
|
+
* - NEVER rename a Stripe/BTCPay-facing `_cents` field to `_credits`.
|
|
13
13
|
* Stripe's `amount` parameter and `amount_total` response are always USD cents.
|
|
14
|
-
*
|
|
14
|
+
* BTCPay's `amountUsdCents` is always USD cents.
|
|
15
15
|
*
|
|
16
16
|
* - When adding new fields: if it touches a payment processor API, use `_cents`.
|
|
17
17
|
* If it's a DB column storing Credit.toRaw() values, use `_credits`.
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
* NAMING CONVENTION — cents vs credits (WOP-1058)
|
|
3
3
|
*
|
|
4
4
|
* - `_cents` suffix = value denominated in USD cents.
|
|
5
|
-
* Used for: Stripe amounts,
|
|
5
|
+
* Used for: Stripe amounts, BTCPay amounts, ledger balances, ledger transactions.
|
|
6
6
|
* The platform credit unit IS the USD cent (1 credit = 1 cent = $0.01).
|
|
7
7
|
*
|
|
8
8
|
* - `_credits` suffix = platform credit count (used in DB columns and raw storage).
|
|
9
9
|
* Semantically identical to cents but signals "this is a platform concept stored
|
|
10
10
|
* as a Credit.toRaw() nanodollar value in the database."
|
|
11
11
|
*
|
|
12
|
-
* - NEVER rename a Stripe/
|
|
12
|
+
* - NEVER rename a Stripe/BTCPay-facing `_cents` field to `_credits`.
|
|
13
13
|
* Stripe's `amount` parameter and `amount_total` response are always USD cents.
|
|
14
|
-
*
|
|
14
|
+
* BTCPay's `amountUsdCents` is always USD cents.
|
|
15
15
|
*
|
|
16
16
|
* - When adding new fields: if it touches a payment processor API, use `_cents`.
|
|
17
17
|
* If it's a DB column storing Credit.toRaw() values, use `_credits`.
|
|
@@ -13,7 +13,7 @@ export const creditTransactions = pgTable("credit_transactions", {
|
|
|
13
13
|
type: text("type").notNull(), // signup_grant | purchase | bounty | referral | promo | community_dividend | bot_runtime | adapter_usage | addon | refund | correction
|
|
14
14
|
description: text("description"),
|
|
15
15
|
referenceId: text("reference_id").unique(),
|
|
16
|
-
fundingSource: text("funding_source"), // "stripe" | "
|
|
16
|
+
fundingSource: text("funding_source"), // "stripe" | "crypto" | null (null = legacy/signup)
|
|
17
17
|
attributedUserId: text("attributed_user_id"), // nullable — null for system/bot charges
|
|
18
18
|
createdAt: text("created_at").notNull().default(sql `(now())`),
|
|
19
19
|
expiresAt: text("expires_at"), // nullable — null means never expires
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* reference_id is the
|
|
2
|
+
* Crypto payment charges — tracks the lifecycle of each BTCPay invoice.
|
|
3
|
+
* reference_id is the BTCPay invoice ID.
|
|
4
|
+
*
|
|
5
|
+
* amountUsdCents stores the requested amount in USD cents (integer).
|
|
6
|
+
* This is NOT nanodollars — Credit.fromCents() handles the conversion
|
|
7
|
+
* when crediting the ledger in the webhook handler.
|
|
4
8
|
*/
|
|
5
|
-
export declare const
|
|
6
|
-
name: "
|
|
9
|
+
export declare const cryptoCharges: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
10
|
+
name: "crypto_charges";
|
|
7
11
|
schema: undefined;
|
|
8
12
|
columns: {
|
|
9
13
|
referenceId: import("drizzle-orm/pg-core").PgColumn<{
|
|
10
14
|
name: "reference_id";
|
|
11
|
-
tableName: "
|
|
15
|
+
tableName: "crypto_charges";
|
|
12
16
|
dataType: "string";
|
|
13
17
|
columnType: "PgText";
|
|
14
18
|
data: string;
|
|
@@ -25,7 +29,7 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
25
29
|
}, {}, {}>;
|
|
26
30
|
tenantId: import("drizzle-orm/pg-core").PgColumn<{
|
|
27
31
|
name: "tenant_id";
|
|
28
|
-
tableName: "
|
|
32
|
+
tableName: "crypto_charges";
|
|
29
33
|
dataType: "string";
|
|
30
34
|
columnType: "PgText";
|
|
31
35
|
data: string;
|
|
@@ -42,7 +46,7 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
42
46
|
}, {}, {}>;
|
|
43
47
|
amountUsdCents: import("drizzle-orm/pg-core").PgColumn<{
|
|
44
48
|
name: "amount_usd_cents";
|
|
45
|
-
tableName: "
|
|
49
|
+
tableName: "crypto_charges";
|
|
46
50
|
dataType: "number";
|
|
47
51
|
columnType: "PgInteger";
|
|
48
52
|
data: number;
|
|
@@ -59,7 +63,7 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
59
63
|
}, {}, {}>;
|
|
60
64
|
status: import("drizzle-orm/pg-core").PgColumn<{
|
|
61
65
|
name: "status";
|
|
62
|
-
tableName: "
|
|
66
|
+
tableName: "crypto_charges";
|
|
63
67
|
dataType: "string";
|
|
64
68
|
columnType: "PgText";
|
|
65
69
|
data: string;
|
|
@@ -76,7 +80,7 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
76
80
|
}, {}, {}>;
|
|
77
81
|
currency: import("drizzle-orm/pg-core").PgColumn<{
|
|
78
82
|
name: "currency";
|
|
79
|
-
tableName: "
|
|
83
|
+
tableName: "crypto_charges";
|
|
80
84
|
dataType: "string";
|
|
81
85
|
columnType: "PgText";
|
|
82
86
|
data: string;
|
|
@@ -93,7 +97,7 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
93
97
|
}, {}, {}>;
|
|
94
98
|
filledAmount: import("drizzle-orm/pg-core").PgColumn<{
|
|
95
99
|
name: "filled_amount";
|
|
96
|
-
tableName: "
|
|
100
|
+
tableName: "crypto_charges";
|
|
97
101
|
dataType: "string";
|
|
98
102
|
columnType: "PgText";
|
|
99
103
|
data: string;
|
|
@@ -110,7 +114,7 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
110
114
|
}, {}, {}>;
|
|
111
115
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
112
116
|
name: "created_at";
|
|
113
|
-
tableName: "
|
|
117
|
+
tableName: "crypto_charges";
|
|
114
118
|
dataType: "string";
|
|
115
119
|
columnType: "PgText";
|
|
116
120
|
data: string;
|
|
@@ -127,7 +131,7 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
127
131
|
}, {}, {}>;
|
|
128
132
|
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
129
133
|
name: "updated_at";
|
|
130
|
-
tableName: "
|
|
134
|
+
tableName: "crypto_charges";
|
|
131
135
|
dataType: "string";
|
|
132
136
|
columnType: "PgText";
|
|
133
137
|
data: string;
|
|
@@ -144,7 +148,7 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
144
148
|
}, {}, {}>;
|
|
145
149
|
creditedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
146
150
|
name: "credited_at";
|
|
147
|
-
tableName: "
|
|
151
|
+
tableName: "crypto_charges";
|
|
148
152
|
dataType: "string";
|
|
149
153
|
columnType: "PgText";
|
|
150
154
|
data: string;
|
|
@@ -159,6 +163,74 @@ export declare const payramCharges: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
159
163
|
identity: undefined;
|
|
160
164
|
generated: undefined;
|
|
161
165
|
}, {}, {}>;
|
|
166
|
+
chain: import("drizzle-orm/pg-core").PgColumn<{
|
|
167
|
+
name: "chain";
|
|
168
|
+
tableName: "crypto_charges";
|
|
169
|
+
dataType: "string";
|
|
170
|
+
columnType: "PgText";
|
|
171
|
+
data: string;
|
|
172
|
+
driverParam: string;
|
|
173
|
+
notNull: false;
|
|
174
|
+
hasDefault: false;
|
|
175
|
+
isPrimaryKey: false;
|
|
176
|
+
isAutoincrement: false;
|
|
177
|
+
hasRuntimeDefault: false;
|
|
178
|
+
enumValues: [string, ...string[]];
|
|
179
|
+
baseColumn: never;
|
|
180
|
+
identity: undefined;
|
|
181
|
+
generated: undefined;
|
|
182
|
+
}, {}, {}>;
|
|
183
|
+
token: import("drizzle-orm/pg-core").PgColumn<{
|
|
184
|
+
name: "token";
|
|
185
|
+
tableName: "crypto_charges";
|
|
186
|
+
dataType: "string";
|
|
187
|
+
columnType: "PgText";
|
|
188
|
+
data: string;
|
|
189
|
+
driverParam: string;
|
|
190
|
+
notNull: false;
|
|
191
|
+
hasDefault: false;
|
|
192
|
+
isPrimaryKey: false;
|
|
193
|
+
isAutoincrement: false;
|
|
194
|
+
hasRuntimeDefault: false;
|
|
195
|
+
enumValues: [string, ...string[]];
|
|
196
|
+
baseColumn: never;
|
|
197
|
+
identity: undefined;
|
|
198
|
+
generated: undefined;
|
|
199
|
+
}, {}, {}>;
|
|
200
|
+
depositAddress: import("drizzle-orm/pg-core").PgColumn<{
|
|
201
|
+
name: "deposit_address";
|
|
202
|
+
tableName: "crypto_charges";
|
|
203
|
+
dataType: "string";
|
|
204
|
+
columnType: "PgText";
|
|
205
|
+
data: string;
|
|
206
|
+
driverParam: string;
|
|
207
|
+
notNull: false;
|
|
208
|
+
hasDefault: false;
|
|
209
|
+
isPrimaryKey: false;
|
|
210
|
+
isAutoincrement: false;
|
|
211
|
+
hasRuntimeDefault: false;
|
|
212
|
+
enumValues: [string, ...string[]];
|
|
213
|
+
baseColumn: never;
|
|
214
|
+
identity: undefined;
|
|
215
|
+
generated: undefined;
|
|
216
|
+
}, {}, {}>;
|
|
217
|
+
derivationIndex: import("drizzle-orm/pg-core").PgColumn<{
|
|
218
|
+
name: "derivation_index";
|
|
219
|
+
tableName: "crypto_charges";
|
|
220
|
+
dataType: "number";
|
|
221
|
+
columnType: "PgInteger";
|
|
222
|
+
data: number;
|
|
223
|
+
driverParam: string | number;
|
|
224
|
+
notNull: false;
|
|
225
|
+
hasDefault: false;
|
|
226
|
+
isPrimaryKey: false;
|
|
227
|
+
isAutoincrement: false;
|
|
228
|
+
hasRuntimeDefault: false;
|
|
229
|
+
enumValues: undefined;
|
|
230
|
+
baseColumn: never;
|
|
231
|
+
identity: undefined;
|
|
232
|
+
generated: undefined;
|
|
233
|
+
}, {}, {}>;
|
|
162
234
|
};
|
|
163
235
|
dialect: "pg";
|
|
164
236
|
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { index, integer, pgTable, text } from "drizzle-orm/pg-core";
|
|
3
|
+
/**
|
|
4
|
+
* Crypto payment charges — tracks the lifecycle of each BTCPay invoice.
|
|
5
|
+
* reference_id is the BTCPay invoice ID.
|
|
6
|
+
*
|
|
7
|
+
* amountUsdCents stores the requested amount in USD cents (integer).
|
|
8
|
+
* This is NOT nanodollars — Credit.fromCents() handles the conversion
|
|
9
|
+
* when crediting the ledger in the webhook handler.
|
|
10
|
+
*/
|
|
11
|
+
export const cryptoCharges = pgTable("crypto_charges", {
|
|
12
|
+
referenceId: text("reference_id").primaryKey(),
|
|
13
|
+
tenantId: text("tenant_id").notNull(),
|
|
14
|
+
amountUsdCents: integer("amount_usd_cents").notNull(),
|
|
15
|
+
status: text("status").notNull().default("New"),
|
|
16
|
+
currency: text("currency"),
|
|
17
|
+
filledAmount: text("filled_amount"),
|
|
18
|
+
createdAt: text("created_at").notNull().default(sql `(now())`),
|
|
19
|
+
updatedAt: text("updated_at").notNull().default(sql `(now())`),
|
|
20
|
+
creditedAt: text("credited_at"),
|
|
21
|
+
chain: text("chain"),
|
|
22
|
+
token: text("token"),
|
|
23
|
+
depositAddress: text("deposit_address"),
|
|
24
|
+
derivationIndex: integer("derivation_index"),
|
|
25
|
+
}, (table) => [
|
|
26
|
+
index("idx_crypto_charges_tenant").on(table.tenantId),
|
|
27
|
+
index("idx_crypto_charges_status").on(table.status),
|
|
28
|
+
index("idx_crypto_charges_created").on(table.createdAt),
|
|
29
|
+
index("idx_crypto_charges_deposit_address").on(table.depositAddress),
|
|
30
|
+
// uniqueIndex would be ideal but drizzle pgTable helper doesn't support it inline.
|
|
31
|
+
// Enforced via migration: CREATE UNIQUE INDEX.
|
|
32
|
+
]);
|
|
@@ -16,6 +16,7 @@ export * from "./coupon-codes.js";
|
|
|
16
16
|
export * from "./credit-auto-topup.js";
|
|
17
17
|
export * from "./credit-auto-topup-settings.js";
|
|
18
18
|
export * from "./credits.js";
|
|
19
|
+
export * from "./crypto.js";
|
|
19
20
|
export * from "./dividend-distributions.js";
|
|
20
21
|
export * from "./email-notifications.js";
|
|
21
22
|
export * from "./fleet-event-history.js";
|
|
@@ -39,7 +40,6 @@ export * from "./onboarding-sessions.js";
|
|
|
39
40
|
export * from "./org-memberships.js";
|
|
40
41
|
export * from "./organization-members.js";
|
|
41
42
|
export * from "./page-contexts.js";
|
|
42
|
-
export * from "./payram.js";
|
|
43
43
|
export * from "./platform-api-keys.js";
|
|
44
44
|
export * from "./plugin-configs.js";
|
|
45
45
|
export * from "./plugin-marketplace-content.js";
|
package/dist/db/schema/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./coupon-codes.js";
|
|
|
16
16
|
export * from "./credit-auto-topup.js";
|
|
17
17
|
export * from "./credit-auto-topup-settings.js";
|
|
18
18
|
export * from "./credits.js";
|
|
19
|
+
export * from "./crypto.js";
|
|
19
20
|
export * from "./dividend-distributions.js";
|
|
20
21
|
export * from "./email-notifications.js";
|
|
21
22
|
export * from "./fleet-event-history.js";
|
|
@@ -39,7 +40,6 @@ export * from "./onboarding-sessions.js";
|
|
|
39
40
|
export * from "./org-memberships.js";
|
|
40
41
|
export * from "./organization-members.js";
|
|
41
42
|
export * from "./page-contexts.js";
|
|
42
|
-
export * from "./payram.js";
|
|
43
43
|
export * from "./platform-api-keys.js";
|
|
44
44
|
export * from "./plugin-configs.js";
|
|
45
45
|
export * from "./plugin-marketplace-content.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|