@pithy-sh/payments 0.1.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/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/paddle-prices.iife.js +1 -0
- package/dist/pithy.manifest.json +68 -0
- package/docs/lemon-squeezy.md +167 -0
- package/docs/paddle.md +486 -0
- package/package.json +73 -0
- package/pithy.manifest.json +68 -0
- package/src/admin/coverage.ts +107 -0
- package/src/admin/read.ts +360 -0
- package/src/audit/actions.ts +214 -0
- package/src/capability.ts +377 -0
- package/src/client/api.ts +734 -0
- package/src/client/checkout.ts +252 -0
- package/src/client/fixtures/price-preview-de.json +54 -0
- package/src/client/fixtures/price-preview-gb.json +54 -0
- package/src/client/fixtures/price-preview-jp-yen.json +54 -0
- package/src/client/fixtures/price-preview-us-country-only.json +54 -0
- package/src/client/fixtures/price-preview-us-new-york.json +54 -0
- package/src/client/fixtures/pricePreview.ts +67 -0
- package/src/client/hooks.ts +590 -0
- package/src/client/paddle.ts +733 -0
- package/src/client/paddleCache.ts +265 -0
- package/src/client/paddlePrices.iife.ts +43 -0
- package/src/client/paddlePrices.ts +143 -0
- package/src/client/paddlePricesTag.ts +332 -0
- package/src/client/projection.ts +133 -0
- package/src/client/test-utils/cacheStore.ts +41 -0
- package/src/client/wholeUnits.ts +147 -0
- package/src/cloudflare-test.d.ts +16 -0
- package/src/config/config.ts +563 -0
- package/src/data/discount.ts +238 -0
- package/src/data/entitlement.ts +67 -0
- package/src/data/money.ts +75 -0
- package/src/data/pause.ts +148 -0
- package/src/data/providerAccount.ts +47 -0
- package/src/data/purchase.ts +131 -0
- package/src/data/rail.ts +74 -0
- package/src/data/reconcileRun.ts +208 -0
- package/src/data/renderMoney.ts +180 -0
- package/src/data/status.ts +67 -0
- package/src/data/subject.ts +148 -0
- package/src/data/subscription.ts +675 -0
- package/src/data/syncCursor.ts +44 -0
- package/src/data/tables.ts +61 -0
- package/src/data/webhookEvent.ts +196 -0
- package/src/entitlement/manual.ts +209 -0
- package/src/entitlement/resolver.ts +125 -0
- package/src/entitlement/subjectSeam.ts +171 -0
- package/src/error/errors.ts +421 -0
- package/src/grants/apply.ts +207 -0
- package/src/grants/clawback.ts +128 -0
- package/src/grants/ledgerSeam.ts +141 -0
- package/src/http/dispatch.ts +48 -0
- package/src/http/guards.ts +58 -0
- package/src/http/manifestConfig.ts +56 -0
- package/src/http/responses.ts +1147 -0
- package/src/http/routes.ts +2911 -0
- package/src/http/schemas.ts +577 -0
- package/src/http/scopes.ts +260 -0
- package/src/http/view.ts +188 -0
- package/src/http/webhookGuard.ts +341 -0
- package/src/index.ts +231 -0
- package/src/migrations/0001_purchases.ts +312 -0
- package/src/pricing/location.ts +175 -0
- package/src/pricing/visitor.ts +82 -0
- package/src/projection/event.ts +101 -0
- package/src/projection/orphans.ts +226 -0
- package/src/projection/owner.ts +247 -0
- package/src/projection/resolve.ts +57 -0
- package/src/projection/writer.ts +554 -0
- package/src/provision/resolvePaymentsConfig.ts +91 -0
- package/src/rails/apple/certs.ts +66 -0
- package/src/rails/apple/der.ts +249 -0
- package/src/rails/apple/fixtures/chain.ts +412 -0
- package/src/rails/apple/fixtures/did-fail-to-renew-grace.json +43 -0
- package/src/rails/apple/fixtures/did-renew.json +48 -0
- package/src/rails/apple/fixtures/one-time-charge.json +32 -0
- package/src/rails/apple/fixtures/refund.json +33 -0
- package/src/rails/apple/fixtures/subscribed-initial-buy-sandbox.json +38 -0
- package/src/rails/apple/fixtures/test.json +14 -0
- package/src/rails/apple/http.ts +128 -0
- package/src/rails/apple/jws.ts +123 -0
- package/src/rails/apple/notification.ts +357 -0
- package/src/rails/apple/rail.ts +114 -0
- package/src/rails/apple/serverApi.ts +412 -0
- package/src/rails/apple/verify.ts +93 -0
- package/src/rails/apple/x509.ts +424 -0
- package/src/rails/contract.ts +939 -0
- package/src/rails/google/fixtures/play-product-purchased.json +14 -0
- package/src/rails/google/fixtures/play-subscription-active.json +23 -0
- package/src/rails/google/fixtures/push.ts +152 -0
- package/src/rails/google/fixtures/rtdn-one-time-purchased.json +11 -0
- package/src/rails/google/fixtures/rtdn-subscription-renewed.json +11 -0
- package/src/rails/google/fixtures/rtdn-subscription-revoked.json +11 -0
- package/src/rails/google/fixtures/rtdn-test.json +8 -0
- package/src/rails/google/fixtures/rtdn-voided-purchase.json +11 -0
- package/src/rails/google/http.ts +124 -0
- package/src/rails/google/jwt.ts +127 -0
- package/src/rails/google/oidc.ts +308 -0
- package/src/rails/google/playApi.ts +598 -0
- package/src/rails/google/rail.ts +174 -0
- package/src/rails/google/rtdn.ts +331 -0
- package/src/rails/google/verify.ts +127 -0
- package/src/rails/lemonSqueezy/api.ts +192 -0
- package/src/rails/lemonSqueezy/checkout.ts +186 -0
- package/src/rails/lemonSqueezy/discounts.ts +166 -0
- package/src/rails/lemonSqueezy/fixtures/events.ts +180 -0
- package/src/rails/lemonSqueezy/objects.ts +511 -0
- package/src/rails/lemonSqueezy/portal.ts +82 -0
- package/src/rails/lemonSqueezy/pricing.ts +124 -0
- package/src/rails/lemonSqueezy/rail.ts +137 -0
- package/src/rails/lemonSqueezy/read.ts +72 -0
- package/src/rails/lemonSqueezy/refresh.ts +86 -0
- package/src/rails/lemonSqueezy/signature.ts +119 -0
- package/src/rails/lemonSqueezy/verify.ts +45 -0
- package/src/rails/lemonSqueezy/webhook.ts +326 -0
- package/src/rails/paddle/adjustments.ts +94 -0
- package/src/rails/paddle/api.ts +266 -0
- package/src/rails/paddle/checkout.ts +240 -0
- package/src/rails/paddle/discounts.ts +176 -0
- package/src/rails/paddle/events.ts +285 -0
- package/src/rails/paddle/fixtures/browserForged.ts +54 -0
- package/src/rails/paddle/fixtures/pausedSubscription.ts +62 -0
- package/src/rails/paddle/objects.ts +883 -0
- package/src/rails/paddle/portal.ts +130 -0
- package/src/rails/paddle/rail.ts +219 -0
- package/src/rails/paddle/read.ts +108 -0
- package/src/rails/paddle/recorded.ts +83 -0
- package/src/rails/paddle/refresh.ts +153 -0
- package/src/rails/paddle/refund.ts +297 -0
- package/src/rails/paddle/signature.ts +225 -0
- package/src/rails/paddle/subscription.ts +793 -0
- package/src/rails/paddle/verify.ts +120 -0
- package/src/rails/paddle/webhook.ts +405 -0
- package/src/rails/providers.ts +168 -0
- package/src/rails/stripe/api.ts +253 -0
- package/src/rails/stripe/checkout.ts +175 -0
- package/src/rails/stripe/discounts.ts +169 -0
- package/src/rails/stripe/fixtures/event-charge-refunded.json +23 -0
- package/src/rails/stripe/fixtures/event-invoice-paid.json +19 -0
- package/src/rails/stripe/fixtures/event-session-completed-payment.json +27 -0
- package/src/rails/stripe/fixtures/event-session-completed-subscription.json +27 -0
- package/src/rails/stripe/fixtures/event-subscription-canceled.json +43 -0
- package/src/rails/stripe/fixtures/event-subscription-created.json +42 -0
- package/src/rails/stripe/fixtures/event-subscription-deleted.json +44 -0
- package/src/rails/stripe/fixtures/events.ts +111 -0
- package/src/rails/stripe/objects.ts +642 -0
- package/src/rails/stripe/portal.ts +45 -0
- package/src/rails/stripe/pricing.ts +123 -0
- package/src/rails/stripe/rail.ts +139 -0
- package/src/rails/stripe/refresh.ts +79 -0
- package/src/rails/stripe/signature.ts +127 -0
- package/src/rails/stripe/verify.ts +105 -0
- package/src/rails/stripe/webhook.ts +79 -0
- package/src/secret/registry.ts +275 -0
- package/src/seeds/example.ts +181 -0
- package/src/version.generated.ts +16 -0
- package/src/workflows/paddleSweep.ts +717 -0
- package/src/workflows/railAccess.ts +87 -0
- package/src/workflows/reconcile.ts +648 -0
- package/src/workflows/report.ts +64 -0
- package/src/workflows/retryPolicy.ts +40 -0
- package/src/workflows/specs.ts +143 -0
- package/src/workflows/worker.ts +166 -0
- package/src/workflows/workerConfig.ts +73 -0
- package/src/workflows/wrangler.jsonc +55 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
accountReferenceProof,
|
|
6
|
+
LEMON_SQUEEZY_CUSTOM_ACCOUNT,
|
|
7
|
+
LEMON_SQUEEZY_CUSTOM_ENV,
|
|
8
|
+
LEMON_SQUEEZY_CUSTOM_PROOF,
|
|
9
|
+
} from "../objects";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Lemon Squeezy deliveries, built rather than checked in as JSON.
|
|
13
|
+
*
|
|
14
|
+
* Built because every one of them has to be *signed*, and a signature is over exact bytes: a checked-in
|
|
15
|
+
* JSON file plus a checked-in signature would be a pair that silently stops matching the moment either is
|
|
16
|
+
* reformatted, and the test would still pass by verifying the wrong thing. So a fixture produces the body
|
|
17
|
+
* and the test signs it with the same secret it hands the rail — verification is exercised for real.
|
|
18
|
+
*
|
|
19
|
+
* The shapes are Lemon Squeezy's own, trimmed to the fields this rail reads and the ones that prove it
|
|
20
|
+
* reads the right one. `test_mode: false` throughout, because the projection refuses across environments
|
|
21
|
+
* and every test that is not about that axis wants a production purchase.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The store's ids, fixed so a test can assert on the exact namespaced key a row is written under. */
|
|
25
|
+
export const SUBSCRIPTION_ID = "90001";
|
|
26
|
+
export const INVOICE_ONE = "8001";
|
|
27
|
+
export const INVOICE_TWO = "8002";
|
|
28
|
+
export const ORDER_ID = "7001";
|
|
29
|
+
export const CUSTOMER_ID = 700;
|
|
30
|
+
export const VARIANT_ID = 55555;
|
|
31
|
+
|
|
32
|
+
/** The purchaser this deployment stamped into the checkout, echoed back by the store on every delivery. */
|
|
33
|
+
// An encoded subject reference, the exact string `encodeSubjectReference` produces (#412). A bare id is
|
|
34
|
+
// what this was, and the strict decoder now refuses one — a fixture carrying the old shape would prove a
|
|
35
|
+
// binding that production no longer makes.
|
|
36
|
+
export const ACCOUNT_REFERENCE = "user:ada";
|
|
37
|
+
|
|
38
|
+
/** What the fences are tested against. */
|
|
39
|
+
export const THIS_DEPLOYMENT = "staging";
|
|
40
|
+
|
|
41
|
+
/** The signing secret every fixture is minted against, matching what the tests hand the rail. */
|
|
42
|
+
export const FIXTURE_WEBHOOK_SECRET = "ls_whsec_test";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The `meta.custom_data` a delivery carries when our own checkout created the purchase — **proof included**.
|
|
46
|
+
*
|
|
47
|
+
* The proof is computed with the real function rather than hard-coded, so a fixture cannot drift from the
|
|
48
|
+
* verifier and quietly stop exercising it. `forge` mints the two public values with no proof, which is
|
|
49
|
+
* exactly what a stranger can build from a storefront buy link.
|
|
50
|
+
*/
|
|
51
|
+
async function custom(
|
|
52
|
+
deployment: string | undefined = THIS_DEPLOYMENT,
|
|
53
|
+
options: { forge?: boolean } = {},
|
|
54
|
+
): Promise<Record<string, string>> {
|
|
55
|
+
const data: Record<string, string> = { [LEMON_SQUEEZY_CUSTOM_ACCOUNT]: ACCOUNT_REFERENCE };
|
|
56
|
+
if (deployment !== undefined) {
|
|
57
|
+
data[LEMON_SQUEEZY_CUSTOM_ENV] = deployment;
|
|
58
|
+
if (options.forge !== true) {
|
|
59
|
+
data[LEMON_SQUEEZY_CUSTOM_PROOF] = await accountReferenceProof(
|
|
60
|
+
ACCOUNT_REFERENCE,
|
|
61
|
+
deployment,
|
|
62
|
+
FIXTURE_WEBHOOK_SECRET,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One delivery envelope. */
|
|
70
|
+
async function delivery(
|
|
71
|
+
eventName: string,
|
|
72
|
+
id: string,
|
|
73
|
+
type: string,
|
|
74
|
+
attributes: Record<string, unknown>,
|
|
75
|
+
options: { deployment?: string | undefined; stamped?: boolean; forge?: boolean } = {},
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
const meta: Record<string, unknown> = { event_name: eventName, test_mode: false };
|
|
78
|
+
if (options.stamped !== false) meta.custom_data = await custom(options.deployment, { forge: options.forge });
|
|
79
|
+
return JSON.stringify({ meta, data: { type, id, attributes } });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A subscription object, as `subscription_*` delivers it. */
|
|
83
|
+
export function subscription(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
84
|
+
return {
|
|
85
|
+
store_id: 1,
|
|
86
|
+
customer_id: CUSTOMER_ID,
|
|
87
|
+
variant_id: VARIANT_ID,
|
|
88
|
+
status: "active",
|
|
89
|
+
renews_at: "2026-02-01T10:00:00.000000Z",
|
|
90
|
+
ends_at: null,
|
|
91
|
+
created_at: "2026-01-01T10:00:00.000000Z",
|
|
92
|
+
updated_at: "2026-01-01T10:00:00.000000Z",
|
|
93
|
+
test_mode: false,
|
|
94
|
+
...overrides,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A subscription-invoice object, as `subscription_payment_*` delivers it. Carries no `variant_id`. */
|
|
99
|
+
export function invoice(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
100
|
+
return {
|
|
101
|
+
store_id: 1,
|
|
102
|
+
subscription_id: Number(SUBSCRIPTION_ID),
|
|
103
|
+
customer_id: CUSTOMER_ID,
|
|
104
|
+
status: "paid",
|
|
105
|
+
refunded: false,
|
|
106
|
+
refunded_at: null,
|
|
107
|
+
currency: "USD",
|
|
108
|
+
total: 999,
|
|
109
|
+
created_at: "2026-01-01T10:00:05.000000Z",
|
|
110
|
+
updated_at: "2026-01-01T10:00:05.000000Z",
|
|
111
|
+
test_mode: false,
|
|
112
|
+
...overrides,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** An order object, as `order_*` delivers it. */
|
|
117
|
+
export function order(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
118
|
+
return {
|
|
119
|
+
store_id: 1,
|
|
120
|
+
customer_id: CUSTOMER_ID,
|
|
121
|
+
status: "paid",
|
|
122
|
+
refunded: false,
|
|
123
|
+
refunded_at: null,
|
|
124
|
+
currency: "USD",
|
|
125
|
+
total: 4900,
|
|
126
|
+
first_order_item: { variant_id: VARIANT_ID },
|
|
127
|
+
created_at: "2026-03-01T09:00:00.000000Z",
|
|
128
|
+
updated_at: "2026-03-01T09:00:00.000000Z",
|
|
129
|
+
test_mode: false,
|
|
130
|
+
...overrides,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A subscription-domain delivery. */
|
|
135
|
+
export async function subscriptionDelivery(
|
|
136
|
+
eventName: string,
|
|
137
|
+
overrides: Record<string, unknown> = {},
|
|
138
|
+
options: { deployment?: string | undefined; stamped?: boolean; forge?: boolean } = {},
|
|
139
|
+
): Promise<string> {
|
|
140
|
+
return await delivery(eventName, SUBSCRIPTION_ID, "subscriptions", subscription(overrides), options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** An invoice-domain delivery. */
|
|
144
|
+
export async function invoiceDelivery(
|
|
145
|
+
eventName: string,
|
|
146
|
+
id: string = INVOICE_ONE,
|
|
147
|
+
overrides: Record<string, unknown> = {},
|
|
148
|
+
options: { deployment?: string | undefined; stamped?: boolean; forge?: boolean } = {},
|
|
149
|
+
): Promise<string> {
|
|
150
|
+
return await delivery(eventName, id, "subscription-invoices", invoice(overrides), options);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** An order-domain delivery. */
|
|
154
|
+
export async function orderDelivery(
|
|
155
|
+
eventName: string,
|
|
156
|
+
overrides: Record<string, unknown> = {},
|
|
157
|
+
options: { deployment?: string | undefined; stamped?: boolean; forge?: boolean } = {},
|
|
158
|
+
): Promise<string> {
|
|
159
|
+
return await delivery(eventName, ORDER_ID, "orders", order(overrides), options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A delivery of a type this build does not map — a license key, say. */
|
|
163
|
+
export async function unknownDelivery(): Promise<string> {
|
|
164
|
+
return await delivery("license_key_created", "42", "license-keys", {
|
|
165
|
+
status: "active",
|
|
166
|
+
created_at: "2026-04-01T00:00:00Z",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The JSON:API answer `GET /v1/subscriptions/{id}` gives, for the transport stub. */
|
|
171
|
+
export function subscriptionResponse(overrides: Record<string, unknown> = {}): string {
|
|
172
|
+
return JSON.stringify({
|
|
173
|
+
data: { type: "subscriptions", id: SUBSCRIPTION_ID, attributes: subscription(overrides) },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The JSON:API answer `GET /v1/orders/{id}` gives. */
|
|
178
|
+
export function orderResponse(overrides: Record<string, unknown> = {}): string {
|
|
179
|
+
return JSON.stringify({ data: { type: "orders", id: ORDER_ID, attributes: order(overrides) } });
|
|
180
|
+
}
|