@pithy-sh/payments 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/payments",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,7 @@
32
32
  "reset": "bun run clean && rm -rf node_modules"
33
33
  },
34
34
  "peerDependencies": {
35
- "@pithy-sh/ledger": "workspace:*",
35
+ "@pithy-sh/ledger": "^0.1.2",
36
36
  "react": "^19.0.0"
37
37
  },
38
38
  "peerDependenciesMeta": {
@@ -47,8 +47,8 @@
47
47
  "@cloudflare/workers-types": "^5.20260729.1",
48
48
  "@hono/zod-validator": "^0.9.0",
49
49
  "@paddle/paddle-js": "^1.6.4",
50
- "@pithy-sh/core": "workspace:*",
51
- "@pithy-sh/secrets": "workspace:*",
50
+ "@pithy-sh/core": "^0.1.2",
51
+ "@pithy-sh/secrets": "^0.1.2",
52
52
  "hono": "^4.13.2",
53
53
  "kysely": "^0.29.0",
54
54
  "zod": "^4.0.0"
@@ -13,4 +13,4 @@
13
13
  export const PACKAGE_NAME = "@pithy-sh/payments";
14
14
 
15
15
  /** This package's version, stamped from its own package.json at generation time. */
16
- export const PACKAGE_VERSION = "0.1.0";
16
+ export const PACKAGE_VERSION = "0.1.2";
@@ -1,68 +0,0 @@
1
- {
2
- "name": "payments",
3
- "package": "@pithy-sh/payments",
4
- "requiredBindings": [
5
- { "type": "d1", "name": "DB" },
6
- {
7
- "type": "workflow",
8
- "name": "PAYMENTS_RECONCILE",
9
- "job": "reconcile",
10
- "className": "PaymentsReconcileWorkflow",
11
- "optional": true
12
- }
13
- ],
14
- "peerCapabilities": ["secrets"],
15
- "optionalCapabilities": ["auth", "controlplane", "ledger"],
16
- "migrationNamespace": "payments",
17
- "secrets": [
18
- {
19
- "name": "payments-provider-credentials",
20
- "origin": {
21
- "kind": "obtained",
22
- "issuer": "other",
23
- "documentation": "https://github.com/pithy-sh/pithy/blob/main/docs/commands/payments.md#where-each-rails-credentials-come-from"
24
- },
25
- "rotation": {
26
- "kind": "manual",
27
- "issuer": "other",
28
- "documentation": "https://github.com/pithy-sh/pithy/blob/main/docs/commands/payments.md#where-each-rails-credentials-come-from"
29
- }
30
- }
31
- ],
32
- "whenToEnable": "Four payment rails — Apple, Google, Stripe, Lemon Squeezy — resolving to one cross-rail entitlement, in your own Worker and your own D1. Buy Pro on iOS, be entitled on the web, with no hosted data plane holding your purchase history. A product is not an entitlement: `pro_monthly` and `pro_annual`, across four stores' catalogs, grant one key — `pro` — and gating code names the key, never a SKU. Every write converges on one idempotent projection keyed on (rail, provider transaction id), so a client submission, a provider webhook, and a reconciliation pass produce the identical row: a dropped client call costs nothing and a replayed webhook changes nothing. The projection is monotonic on the provider's own event time, because providers do not guarantee delivery order and a stale `expired` arriving after the `renewed` that superseded it would silently revoke a paying subscriber. Sandbox purchases are tracked as sandbox and never grant a production entitlement. A cron Workflow re-verifies what the webhooks missed, because webhook-only systems rot silently. Lemon Squeezy is the merchant of record — it owns the sales tax, the EU VAT registration, the invoicing and the chargebacks, which is what a solo developer reaches for when the alternative is registering for VAT in twenty-seven countries. Add auth — every route belongs to an authenticated purchaser or a machine proving authenticity, and there are no public routes. Add secrets: the four rails' credentials are read through it, so payments will not compose without it. Ledger fulfillment is opt-in per product; most products never touch a balance.",
33
- "scaffold": [
34
- "Add a `payments({ rails: {...}, products: {...} })` block to pithy.config.ts. The catalog lives in config, not in D1 — a product's entitlement mapping is policy, so it belongs in git rather than in a mutable table. A new SKU needs a deploy, which is the correct trade.",
35
- "Give every product a `type` (`consumable`, `non_consumable`, or `subscription`), a display `name`, the `entitlements` it grants, and one block per rail it ships on. Omit a rail to ship without it — `remove_ads` on mobile only is expressed by omission, not a flag.",
36
- "Bind a D1 database named DB in wrangler.jsonc — the same app database your other capabilities use.",
37
- "Run `pithy migrate` to create pithy_payments_purchases, pithy_payments_entitlements, pithy_payments_provider_accounts, pithy_payments_webhook_events, and pithy_payments_reconcile_runs.",
38
- "Add `@pithy-sh/secrets` if it is not already installed, then supply each rail's credentials through it: Apple's App Store Connect key with its key and issuer ids, Google's service account, Stripe's secret key and webhook signing secret. Apple's root certificates are public and ship with the package.",
39
- "Add `@pithy-sh/auth` if it is not already installed. Purchases belong to an authenticated user; with no auth capability composed every route denies.",
40
- "Comping and revoking entitlements by hand is a `control-plane` surface, not an admin user, so /payments/entitlements/grant and /payments/entitlements/revoke are reached with a signed management credential rather than a session. A grant must name an entitlement key this project defines — one a product grants, or one declared in `manualEntitlements` — so a console cannot comp `pr` for `pro` and leave a customer locked out; `payments:catalog:read` is the separately-granted read that lets a console offer a list instead of a text box, and it discloses only each product's id, kind, name and keys. Add `controlplane()` to this Worker and run `pithy dashboard connect`, then grant the connection `payments:catalog:read`, `payments:entitlements:grant`, `payments:entitlements:revoke`, or only the one its tooling needs — a refund tool that cannot comp cannot be turned into one, and a comp tool cannot take paid access away from a live customer. With the seam not composed both routes answer `controlplane/not_connected`, and no app session opens either of them whatever scopes it carries.",
41
- "Selling through Stripe also needs a `stripe` block naming where its hosted pages return to — `successUrl`, `cancelUrl`, `portalReturnUrl`. Required whenever the rail is on, and config rather than request input: a client that could name a return URL could send a paying customer to a page it controls. Put `{CHECKOUT_SESSION_ID}` in the success URL's query and the thank-you page can post it to /payments/purchases for an entitlement at once.",
42
- "Set the account link on every purchase your app starts — Apple's appAccountToken and Google's obfuscatedAccountId, in the app's own purchase flow. A webhook arrives carrying the provider's identifier and names no holder of ours, so a purchase made without one arrives orphaned. Stripe needs nothing from you here: /payments/checkout sets `client_reference_id` from the subject the caller acts for, which is the one hook a client must never be allowed to name.",
43
- "Run `pithy payments provision` to deploy the reconciliation Workflow and write its binding. Every pass it runs leaves a row in pithy_payments_reconcile_runs — started, finished, rail, environment and tally, kept ninety days — and `GET /payments/admin/reconcile-runs` reads them behind `payments:reconcile:read`, so an adopter can tell a healthy integration from one whose cron stopped firing without granting anybody the purchase log. `pithy add` touches no Cloudflare account; provisioning is the separate, explicit step. Skipping it costs nothing up front — the binding is optional and every route works without it — but webhook-only systems rot silently, so the nightly pass is what turns a dropped delivery into a repaired row rather than a support ticket.",
44
- "Gate a paid route with `requireEntitlement(\"pro\")` from @pithy-sh/core. The gate lives in core, so a Worker with no payments capability composed denies rather than opens."
45
- ],
46
- "configOptions": [
47
- {
48
- "key": "basePath",
49
- "default": "/payments",
50
- "describe": "Where the payments routes mount, webhooks included. Edit the `rails` and `products` objects by hand — the catalog maps each store's SKU to one logical product and the entitlements it grants, and it is the whole point of the capability."
51
- },
52
- {
53
- "key": "billingSubject",
54
- "choices": ["user", "organization"],
55
- "describe": "Who holds a subscription in this project. user — one person buys, one person is entitled. organization — a company buys, and everybody in it is entitled. It keys the entitlement column and its UNIQUE index, so changing it once anybody has subscribed is a migration, not an edit."
56
- },
57
- {
58
- "key": "manualEntitlements",
59
- "default": [],
60
- "describe": "Entitlement keys the control plane may grant that no product sells — a beta flag, an internal tier, a key that exists only to be comped. Declared, because the alternative to declaring is not checking: with this empty, a manual grant of any key outside the catalog is refused. Only grants are constrained; a revoke of a key the catalog has since dropped stays legal."
61
- },
62
- {
63
- "key": "graceGrantsAccess",
64
- "default": true,
65
- "describe": "Whether a subscription in its billing-retry grace period still grants its entitlements. True, because that is the point of grace — a failed card should not lock a paying subscriber out mid-period. Once grace is exhausted the purchase moves to on_hold, which never grants."
66
- }
67
- ]
68
- }