@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,87 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { PaymentsConfig } from "../config/config";
|
|
5
|
+
import type { PaymentsRail } from "../data/rail";
|
|
6
|
+
import { mintAppleApiToken } from "../rails/apple/serverApi";
|
|
7
|
+
import type { PaymentsRailProvider } from "../rails/contract";
|
|
8
|
+
import { mintPlayAccessToken } from "../rails/google/playApi";
|
|
9
|
+
import { type RailTrustOptions, resolveRailProvider } from "../rails/providers";
|
|
10
|
+
import { type PaymentsProviderCredentials, railCredentials } from "../secret/registry";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How the reconciliation pass gets a rail, and why it is not simply `resolveRailProvider`.
|
|
14
|
+
*
|
|
15
|
+
* Two of the three rails buy their API access with a signature and a round-trip: Apple mints an ES256 App
|
|
16
|
+
* Store Connect token, Google signs a service-account assertion and POSTs it for an OAuth token. Resolving a
|
|
17
|
+
* rail per purchase would double every lookup — a hundred subscriptions would be two hundred round-trips and a
|
|
18
|
+
* hundred signatures — so a batch mints once and passes the token down through {@link RailTrustOptions}.
|
|
19
|
+
*
|
|
20
|
+
* **Once per page, not once per run.** A minted token expires (Apple's inside the hour, Google's on the hour)
|
|
21
|
+
* and a durable Workflow can be retried long after its first step ran, so a token held for a whole pass would
|
|
22
|
+
* start failing part way through the very run that is meant to repair things. A page is bounded work inside
|
|
23
|
+
* one step, which is exactly the lifetime a token comfortably covers — so the access object is built inside
|
|
24
|
+
* each step and dropped with it.
|
|
25
|
+
*
|
|
26
|
+
* That also keeps the secret handling honest: a bearer credential derived from a secret is never cached in a
|
|
27
|
+
* module variable, which is what CLAUDE.md requires and what a run-scoped cache would quietly become.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** One page's rail access: a provider per rail, with its batch token already minted. */
|
|
31
|
+
export interface ReconcileRailAccess {
|
|
32
|
+
/** The provider for one rail. Throws `payments/rail_not_configured` when the rail is off or unprovisioned. */
|
|
33
|
+
providerFor(rail: PaymentsRail): Promise<PaymentsRailProvider>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What building one page's rail access needs: the catalog, the credentials, the clock, and the seams. */
|
|
37
|
+
export interface RailAccessOptions {
|
|
38
|
+
/** The resolved catalog. Decides whether a rail is enabled at all. */
|
|
39
|
+
config: PaymentsConfig;
|
|
40
|
+
/** Every rail's credentials, read through the one reader by the caller. */
|
|
41
|
+
credentials: PaymentsProviderCredentials;
|
|
42
|
+
/** The clock. Dates each minted token. */
|
|
43
|
+
now: Date;
|
|
44
|
+
/** Transports and additional trust, forwarded to each rail. Empty in production. */
|
|
45
|
+
trust?: RailTrustOptions;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build one page's rail access, minting each rail's batch token on first use.
|
|
50
|
+
*
|
|
51
|
+
* Lazy per rail: a page holding only Stripe purchases never signs an Apple assertion, and a project that
|
|
52
|
+
* enabled one rail never pays for the other two. The mint happens inside `providerFor`, so a rail whose
|
|
53
|
+
* credentials are missing raises `payments/rail_not_configured` at the same place a disabled rail does — one
|
|
54
|
+
* refusal for the caller to handle rather than two.
|
|
55
|
+
*/
|
|
56
|
+
export function batchedRailAccess(options: RailAccessOptions): ReconcileRailAccess {
|
|
57
|
+
const { config, credentials, now } = options;
|
|
58
|
+
const base = options.trust ?? {};
|
|
59
|
+
const built = new Map<PaymentsRail, Promise<PaymentsRailProvider>>();
|
|
60
|
+
|
|
61
|
+
async function build(rail: PaymentsRail): Promise<PaymentsRailProvider> {
|
|
62
|
+
const trust: RailTrustOptions = { ...base };
|
|
63
|
+
if (rail === "apple" && trust.appleApiToken === undefined) {
|
|
64
|
+
trust.appleApiToken = await mintAppleApiToken(railCredentials(credentials, "apple"), { now });
|
|
65
|
+
}
|
|
66
|
+
if (rail === "google" && trust.googleAccessToken === undefined) {
|
|
67
|
+
trust.googleAccessToken = await mintPlayAccessToken(railCredentials(credentials, "google"), {
|
|
68
|
+
now,
|
|
69
|
+
transport: base.googleTransport,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Stripe authenticates every request with the secret key directly, so there is nothing to mint for it.
|
|
73
|
+
return resolveRailProvider(rail, config, credentials, trust);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
providerFor(rail: PaymentsRail): Promise<PaymentsRailProvider> {
|
|
78
|
+
// The promise is memoized rather than its result, so two purchases on one rail in the same page cannot
|
|
79
|
+
// both start a mint before either finishes.
|
|
80
|
+
const existing = built.get(rail);
|
|
81
|
+
if (existing) return existing;
|
|
82
|
+
const pending = build(rail);
|
|
83
|
+
built.set(rail, pending);
|
|
84
|
+
return pending;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|