fresh-squeezy 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -1,81 +1,32 @@
1
1
  # fresh-squeezy
2
2
 
3
- Validator-first Lemon Squeezy setup doctor. Verify your integration before it ships.
3
+ Validator-first Lemon Squeezy doctor. Catches misconfigurations before they ship. CLI + library, Node 20+.
4
4
 
5
- Library + CLI. One call, one structured report, one exit code.
6
-
7
- ---
8
-
9
- ## Why this exists
10
-
11
- The official [`@lemonsqueezy/lemonsqueezy.js`](https://github.com/lmsqueezy/lemonsqueezy.js) SDK is great at making API calls. But integration bugs almost never live in the API calls — they live in configuration. Wrong store. Wrong mode. Unpublished product. Webhook registered but missing the one event your refund flow depends on. Live API key accidentally loaded in staging.
12
-
13
- `fresh-squeezy` answers a different question fast:
14
-
15
- > Is my API key pointed at the right store, in the right mode, with a product that is actually published and a webhook subscribed to the events my app relies on?
16
-
17
- ### The pain it removes
18
-
19
- - **Postman + dashboard ping-pong.** Today you copy IDs out of the Lemon Squeezy UI, paste them into env files, and hit Postman to verify each one. One CLI call replaces that loop.
20
- - **SDK lag.** The official JS SDK last shipped v4.0.0 on 2024-11-05. The platform has added three behaviors since (`affiliate_activated`, `payment_processor`, `customer_updated`) that the SDK does not surface. `fresh-squeezy` tracks them in `src/support/manifest.ts` and a weekly drift workflow files an issue the moment the changelog moves again.
21
- - **The scariest bug: prod-in-staging.** `fresh-squeezy` calls `/v1/users/me` and compares the `meta.test_mode` flag (API changelog 2024-01-05) against the mode you declared. Mismatch = `MODE_MISMATCH` error, doctor exits 1. Neither the SDK nor a hand-rolled wrapper catches this by default.
22
- - **Repeat work across products.** Every new SaaS inside a company repeats the same billing setup dance. This is one place for the checks so the next product integration is a five-minute job, not a week of yak-shaving.
23
-
24
- ### Why a validator, not another SDK
25
-
26
- Most teams already use the official SDK or plain `fetch` for API calls. They don't need another wrapper — they need **fewer silent misconfigurations**. `fresh-squeezy` is intentionally thin:
27
-
28
- - 7 validators (`connection`, `store`, `product`, `webhook`, `discount`, `licenseKey`, `subscriptionPlan`) that return the same `ValidationResult` shape every time
29
- - `doctor()` composes them into one report
30
- - A raw `request()` escape hatch so you never hit a wall when the platform adds something we haven't wrapped yet
31
- - Static, reviewed support manifest — no live scraping in runtime code
32
- - Stable issue codes so CI can branch on findings
33
-
34
- If a check feels magical, something is wrong.
35
-
36
- ### Open source by design
37
-
38
- This project is MIT-licensed and deliberately scoped to stay small. The goal is to make it easy for any team (ours or yours) to drop it into a new product, wire it into CI, and trust the output. Contributions that keep it boring — more coverage, more validators, better error messages — are exactly the shape we want. See [CONTRIBUTING.md](./CONTRIBUTING.md).
39
-
40
- ---
41
-
42
- ## What it checks
43
-
44
- | Validator | Catches |
45
- | ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
46
- | `connection` | Invalid key, unreachable account, no stores, **declared mode ≠ key's actual mode** (`MODE_MISMATCH`) |
47
- | `store` | Wrong store ID, store owned by a different account |
48
- | `product` | Unpublished product, product on the wrong store, missing or all-draft variants, missing buy URL |
49
- | `webhook` | Webhook URL not registered, missing recommended events (order lifecycle, subscription lifecycle, refunds), missing optional newer events |
50
- | `discount` | Draft discounts, expired or not-yet-active windows, invalid amounts (≤0 or >100%), store ownership mismatch |
51
- | `licenseKey` | Disabled keys, expired keys, keys at activation limit, store ownership mismatch |
52
- | `subscriptionPlan` | Non-subscription variants, invalid billing intervals, zero-price plans, inconsistent trial settings, draft variants, store ownership mismatch |
53
-
54
- Every validator returns the same `ValidationResult` — stable public contract, switchable by `issue.code`.
55
-
56
- ---
57
-
58
- ## Install
5
+ ## 30-second start
59
6
 
60
7
  ```bash
61
- npm install fresh-squeezy
62
- # or
63
- pnpm add fresh-squeezy
8
+ npm i -D fresh-squeezy
9
+ cp .env.example .env.local # fill in LEMON_SQUEEZY_API_KEY
10
+ npx fresh-squeezy doctor --all-stores
64
11
  ```
65
12
 
66
- Requires Node.js 20+.
13
+ No store ID to copy from the dashboard — the CLI discovers reachable stores itself.
67
14
 
68
- ## Setup (90 seconds)
15
+ | Exit | Meaning |
16
+ |------|---------|
17
+ | `0` | All validators passed |
18
+ | `1` | One or more validators reported `error`-level issues |
19
+ | `2` | Fatal (missing key, invalid flags, network failure) |
69
20
 
70
- ```bash
71
- cp .env.example .env.local
72
- # fill in LEMON_SQUEEZY_API_KEY (test or live)
73
- npx fresh-squeezy doctor --all-stores
74
- ```
21
+ ## What it catches that Postman and the official SDK won't
75
22
 
76
- That's the entire onboarding. No store ID to copy from the dashboard the CLI discovers reachable stores itself.
23
+ - **Prod key pointed at staging.** `MODE_MISMATCH` fires when the key's true `meta.test_mode` (API changelog 2024-01-05) disagrees with the declared mode. Doctor exits 1. Neither the SDK nor a hand-rolled wrapper catches this by default.
24
+ - **Silent store-ownership mismatches.** Products, discounts, license keys, and subscription plans whose `store_id` doesn't match the store you scoped the run to. Stable codes: `PRODUCT_WRONG_STORE`, `DISCOUNT_STORE_MISMATCH`, `LICENSE_KEY_STORE_MISMATCH`, `PLAN_STORE_MISMATCH`.
25
+ - **Webhook subscribed to the wrong events.** Diff against a manifest of recommended events (order/subscription lifecycle, refunds) and newer-but-optional events the SDK doesn't ship.
26
+ - **Platform drift.** A weekly GitHub Action hashes the [Lemon Squeezy API changelog](https://docs.lemonsqueezy.com/api/getting-started/changelog) against `src/support/changelog-snapshot.json` and opens an issue when it moves. Tracked additions beyond the official SDK as of 2026-04-24: `customer_updated` (2026-02-25), `payment_processor` on Subscription (2025-06-11), Affiliates + `affiliate_activated` (2025-01-21), `test_mode` on `/v1/users/me` (2024-01-05).
27
+ - **Postman + dashboard ping-pong.** One `doctor` call replaces the loop of copying IDs out of the UI, pasting them into env files, and verifying each one by hand.
77
28
 
78
- ## Quick start — CLI
29
+ ## CLI
79
30
 
80
31
  ```bash
81
32
  # TTY: multi-select stores interactively, run doctor on each
@@ -87,7 +38,7 @@ npx fresh-squeezy doctor --all-stores
87
38
  # Specific stores
88
39
  npx fresh-squeezy doctor --store-ids 12,34,56
89
40
 
90
- # Scope the run to a product + webhook
41
+ # Scope to a product + webhook
91
42
  npx fresh-squeezy doctor --store-ids 12 \
92
43
  --product-id 987 \
93
44
  --webhook-url https://app.example.com/api/webhooks/lemon-squeezy
@@ -101,24 +52,14 @@ npx fresh-squeezy validate webhook \
101
52
  npx fresh-squeezy doctor --all-stores --json
102
53
  ```
103
54
 
104
- Exit codes:
105
-
106
- | Code | Meaning |
107
- | ---- | ---------------------------------------------- |
108
- | `0` | All validators passed |
109
- | `1` | One or more validators reported `error`-level |
110
- | `2` | Fatal error (missing key, invalid flags, etc.) |
111
-
112
- ### Store resolution
113
-
114
- Resolution order used by every store-scoped command:
55
+ Store resolution order, used by every store-scoped command:
115
56
 
116
57
  1. `--store-ids 1,2,3` (comma-separated, explicit)
117
58
  2. `--all-stores` (every store reachable with the key)
118
- 3. TTY: inquirer multi-select
119
- 4. No TTY + no flag: run connection-only (useful as a CI smoke check)
59
+ 3. TTY: inquirer multi-select prompt
60
+ 4. No TTY + no flag: connection-only run (useful as a CI smoke check)
120
61
 
121
- ## Quick start — library
62
+ ## Library
122
63
 
123
64
  ```ts
124
65
  import { createFreshSqueezy } from "fresh-squeezy";
@@ -126,7 +67,7 @@ import { createFreshSqueezy } from "fresh-squeezy";
126
67
  const lemon = createFreshSqueezy(); // reads LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_MODE
127
68
 
128
69
  const report = await lemon.doctor({
129
- storeId: 12, // library is single-store per call
70
+ storeId: 12, // library is single-store per call
130
71
  productId: 987,
131
72
  webhookUrl: "https://app.example.com/api/webhooks/lemon-squeezy",
132
73
  });
@@ -141,75 +82,41 @@ if (!report.ok) {
141
82
  }
142
83
  ```
143
84
 
144
- For multi-store runs at the library layer, call `doctor()` in a loop across the store IDs you care about — the CLI does exactly this.
85
+ For multi-store runs at the library layer, call `doctor()` in a loop. The CLI does exactly this.
86
+
87
+ Public types: [`FreshSqueezyClient`](src/createFreshSqueezy.ts), [`ValidationResult<T>`](src/core/types.ts), [`DoctorReport`](src/core/types.ts). Switch on `issue.code` in CI logic — codes are stable across minor versions.
145
88
 
146
89
  ## Sandbox vs live
147
90
 
148
- Lemon Squeezy serves both modes from the same API host. Mode is determined by which key you use. `fresh-squeezy` surfaces the mode on every result **and** cross-checks the declared mode against the key's actual mode using `meta.test_mode` from `/v1/users/me` (API changelog 2024-01-05). If they disagree, `validateConnection` fires `MODE_MISMATCH` as an error and `doctor` exits 1 — that's the fastest way to catch a prod key pointed at staging (or vice versa) before it does damage.
91
+ Lemon Squeezy serves both modes from the same API host; mode is determined by the key. `fresh-squeezy` cross-checks the declared mode against `meta.test_mode` from `/v1/users/me`. Mismatch = `MODE_MISMATCH`, doctor exits 1 — the fastest way to catch a prod key pointed at staging before it does damage.
149
92
 
150
93
  ```ts
151
94
  const lemon = createFreshSqueezy({ mode: "test" });
152
95
  const result = await lemon.validateConnection();
153
- console.log(result.mode); // "test" (declared)
154
- console.log(result.resource?.actualMode); // "live" — alarm bell
155
- ```
156
-
157
- The CLI default is `--mode test`. Override with `--mode live`.
158
-
159
- Live smoke testing in CI: the repo ships an opt-in `npm run test:live` target gated on `LEMON_SQUEEZY_LIVE_SMOKE=1`. Run it nightly with a secret test-mode key so platform drift surfaces before a release.
160
-
161
- ## API
162
-
163
- ### `createFreshSqueezy(config?)`
164
-
165
- ```ts
166
- createFreshSqueezy({
167
- apiKey?: string; // default: process.env.LEMON_SQUEEZY_API_KEY
168
- storeId?: string | number; // optional — also read from env for lib consumers
169
- mode?: "test" | "live"; // default: process.env.LEMON_SQUEEZY_MODE ?? "test"
170
- baseUrl?: string; // default: "https://api.lemonsqueezy.com"
171
- fetch?: typeof fetch; // default: globalThis.fetch
172
- });
96
+ result.mode; // "test" (declared)
97
+ result.resource?.actualMode; // "live" — alarm bell
173
98
  ```
174
99
 
175
- Returns a `FreshSqueezyClient`:
100
+ The CLI default is `--mode test`. Override with `--mode live`. For nightly platform-drift checks in CI, run `npm run test:live` with `LEMON_SQUEEZY_LIVE_SMOKE=1` and a test-mode key.
176
101
 
177
- ```ts
178
- client.mode
179
- client.request(options) // raw escape hatch
180
- client.validateConnection()
181
- client.validateStore(id)
182
- client.validateProduct({ productId, expectedStoreId? })
183
- client.validateWebhook({ storeId, url })
184
- client.validateDiscount({ storeId, discountId })
185
- client.validateLicenseKey({ storeId, licenseKeyId })
186
- client.validateSubscriptionPlan({ storeId, variantId })
187
- client.doctor({ storeId?, productId?, webhookUrl?, discountId?, licenseKeyId?, variantId? })
188
- ```
102
+ ## Issue codes
189
103
 
190
- ### `ValidationResult<T>`
104
+ Switch on `issue.code` in CI. All codes are stable across minor versions.
191
105
 
192
- ```ts
193
- {
194
- ok: boolean;
195
- mode: "test" | "live";
196
- name: string;
197
- resource?: T;
198
- issues: Array<{
199
- code: string;
200
- severity: "info" | "warning" | "error";
201
- message: string;
202
- suggestedFix?: string;
203
- context?: Record<string, string | number | boolean | null>;
204
- }>;
205
- }
206
- ```
106
+ | Code | Meaning |
107
+ |------|---------|
108
+ | `AUTH_FAILED` | Invalid or missing API key |
109
+ | `MODE_MISMATCH` | Declared mode doesn't match key's `meta.test_mode` |
110
+ | `NETWORK_ERROR` | Lemon Squeezy unreachable |
111
+ | `STORE_NOT_FOUND` / `STORE_NOT_OWNED` | Store ID invalid or owned by another account |
112
+ | `PRODUCT_UNPUBLISHED` / `PRODUCT_WRONG_STORE` / `PRODUCT_NO_BUY_URL` | Product can't accept checkout |
113
+ | `VARIANT_MISSING` / `VARIANT_UNPUBLISHED` | Product has no live variants |
114
+ | `WEBHOOK_NOT_FOUND` / `WEBHOOK_EVENTS_MISSING` / `WEBHOOK_OPTIONAL_EVENTS` | Webhook URL not registered or under-subscribed |
115
+ | `DISCOUNT_DRAFT` / `DISCOUNT_EXPIRED` / `DISCOUNT_NOT_STARTED` / `DISCOUNT_INVALID_AMOUNT` / `DISCOUNT_REDEMPTIONS_EXHAUSTED` / `DISCOUNT_STORE_MISMATCH` | Discount won't apply at checkout |
116
+ | `LICENSE_KEY_DISABLED` / `LICENSE_KEY_EXPIRED` / `LICENSE_KEY_AT_ACTIVATION_LIMIT` / `LICENSE_KEY_STORE_MISMATCH` | License key won't activate |
117
+ | `PLAN_NOT_SUBSCRIPTION` / `PLAN_INVALID_INTERVAL` / `PLAN_FREE_PRICE` / `PLAN_TRIAL_INCONSISTENT` / `PLAN_DRAFT` / `PLAN_STORE_MISMATCH` | Subscription plan misconfigured |
207
118
 
208
- Switch on `issue.code` in CI logic codes are stable across minor versions.
209
-
210
- ### Raw escape hatch
211
-
212
- For endpoints not yet wrapped (new changelog entries, License API, affiliates):
119
+ For endpoints not yet wrapped, use the raw escape hatch:
213
120
 
214
121
  ```ts
215
122
  const user = await lemon.request({ path: "/v1/users/me" });
@@ -217,36 +124,36 @@ const user = await lemon.request({ path: "/v1/users/me" });
217
124
 
218
125
  ## Environment variables
219
126
 
220
- Only two matter to the CLI:
221
-
222
- | Variable | Required | Used by | Purpose |
223
- | ------------------------ | -------- | ----------------- | ------------------------------------------ |
224
- | `LEMON_SQUEEZY_API_KEY` | yes | library + CLI | Bearer token |
225
- | `LEMON_SQUEEZY_MODE` | no | library + CLI | `test` (default) or `live` |
226
- | `LEMON_SQUEEZY_STORE_ID` | no | library consumers | Convenience default for `client.doctor()` |
227
-
228
- The CLI does **not** read `LEMON_SQUEEZY_STORE_ID` — use `--store-ids` or `--all-stores` so store selection stays explicit per-command.
229
-
230
- ## Changelog drift watcher
127
+ | Variable | Required | Purpose |
128
+ |----------|----------|---------|
129
+ | `LEMON_SQUEEZY_API_KEY` | yes | Bearer token (library + CLI) |
130
+ | `LEMON_SQUEEZY_MODE` | no | `test` (default) or `live` |
131
+ | `LEMON_SQUEEZY_STORE_ID` | no | Convenience default for `client.doctor()` — library only |
231
132
 
232
- A weekly GitHub Action fetches the [Lemon Squeezy API changelog](https://docs.lemonsqueezy.com/api/getting-started/changelog), hashes the normalized content, and compares it against `src/support/changelog-snapshot.json`. On drift, it opens a labeled issue with previous/current hashes, a link to the workflow run, and refresh instructions. Advisory only — no runtime code ever scrapes the changelog.
133
+ The CLI does not read `LEMON_SQUEEZY_STORE_ID`; use `--store-ids` or `--all-stores` so store selection stays explicit per command.
233
134
 
234
- Tracked platform additions beyond the official SDK (as of 2026-04-24):
135
+ ## Reference
235
136
 
236
- - `customer_updated` webhook event — added **2026-02-25**
237
- - `payment_processor` property on Subscription — added **2025-06-11**
238
- - Affiliates endpoints + `affiliate_activated` webhook — added **2025-01-21**
239
- - `test_mode` flag on `/v1/users/me` — added **2024-01-05** (powers `MODE_MISMATCH`)
137
+ ### Validators
240
138
 
241
- ## Scope (v1)
139
+ - **`validateConnection`** — Reachability, key validity, store presence, declared-vs-actual mode. [→ source](src/validate/connection.ts)
140
+ - **`validateStore`** — Store ID exists and is owned by the key's account. [→ source](src/validate/store.ts)
141
+ - **`validateProduct`** — Published, on the expected store, has live variants and a buy URL. [→ source](src/validate/product.ts)
142
+ - **`validateWebhook`** — Webhook URL registered and subscribed to recommended events. [→ source](src/validate/webhook.ts)
143
+ - **`validateDiscount`** — Active, in-window, valid amount, store ownership matches. [→ source](src/validate/discount.ts)
144
+ - **`validateLicenseKey`** — Enabled, not expired, activations available, store ownership matches. [→ source](src/validate/licenseKey.ts)
145
+ - **`validateSubscriptionPlan`** — Subscription type, valid interval, non-zero price, consistent trial. [→ source](src/validate/subscriptionPlan.ts)
146
+ - **`doctor`** — Composes the above into one `DoctorReport`. [→ source](src/validate/doctor.ts)
242
147
 
243
- **In**: connection, store, product, webhook validators; `doctor()`; library + CLI; sandbox-mode fixture tests + opt-in live smoke.
148
+ ### CLI commands
244
149
 
245
- **Out**: License API, affiliates, changelog scraper in runtime, dashboard UI. On the roadmap for v2 if demand is real.
150
+ - **`doctor`** Run every configured validator and emit a report. [→ source](src/cli/commands/doctor.ts)
151
+ - **`validate <name>`** — Run a single validator. [→ source](src/cli/commands/validate.ts)
152
+ - **`init`** — Interactive setup: ask for credentials, pick a store, run doctor. [→ source](src/cli/commands/init.ts)
246
153
 
247
154
  ## Contributing
248
155
 
249
- See [CONTRIBUTING.md](./CONTRIBUTING.md). TL;DR: clone, `npm install`, `npm test`. Manual QA steps in [docs/MANUAL_QA.md](./docs/MANUAL_QA.md).
156
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). Clone, `npm install`, `npm test`.
250
157
 
251
158
  ## License
252
159