fresh-squeezy 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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/core/errors.ts","../src/core/config.ts","../src/core/http.ts","../src/resources/users.ts","../src/resources/stores.ts","../src/validate/rules.ts","../src/validate/connection.ts","../src/validate/store.ts","../src/resources/products.ts","../src/resources/variants.ts","../src/validate/product.ts","../src/resources/webhooks.ts","../src/support/manifest.ts","../src/validate/webhook.ts","../src/validate/doctor.ts","../src/createFreshSqueezy.ts"],"sourcesContent":["export * from \"./createFreshSqueezy.js\";\nexport * from \"./core/errors.js\";\nexport * from \"./core/config.js\";\nexport * from \"./core/types.js\";\nexport * from \"./validate/rules.js\";\nexport * from \"./validate/connection.js\";\nexport * from \"./validate/product.js\";\nexport * from \"./validate/webhook.js\";\nexport * from \"./validate/doctor.js\";\nexport * from \"./resources/stores.js\";\nexport * from \"./resources/products.js\";\nexport * from \"./resources/variants.js\";\nexport * from \"./resources/webhooks.js\";\nexport * from \"./resources/users.js\";\nexport * from \"./support/manifest.js\";\n","/**\n * Unified error type for fresh-squeezy. All HTTP and validation failures that\n * bubble up to consumers pass through this class so caller code can branch on\n * a single `instanceof` check.\n *\n * Why a class over a discriminated union: Node's `fetch` rejections interleave\n * with library errors in user stack traces. A class keeps the stack readable\n * and gives one stable prototype chain for consumer `catch` blocks.\n */\nexport class FreshSqueezyError extends Error {\n public readonly code: string;\n public readonly status?: number;\n public readonly detail?: unknown;\n\n constructor(opts: { code: string; message: string; status?: number; detail?: unknown }) {\n super(opts.message);\n this.name = \"FreshSqueezyError\";\n this.code = opts.code;\n this.status = opts.status;\n this.detail = opts.detail;\n Object.setPrototypeOf(this, FreshSqueezyError.prototype);\n }\n}\n","import type { FreshSqueezyConfig, Mode, ResolvedConfig } from \"./types.js\";\nimport { FreshSqueezyError } from \"./errors.js\";\n\n/**\n * Lemon Squeezy API root. Test and live share the same host — mode is determined\n * by which API key is used. Documented at https://docs.lemonsqueezy.com/api.\n */\nconst DEFAULT_BASE_URL = \"https://api.lemonsqueezy.com\";\n\n/**\n * Env variable names read when a field is not passed explicitly. Consuming\n * products rely on these names being stable — treat them as public API.\n */\nexport const ENV_KEYS = {\n apiKey: \"LEMON_SQUEEZY_API_KEY\",\n storeId: \"LEMON_SQUEEZY_STORE_ID\",\n mode: \"LEMON_SQUEEZY_MODE\",\n} as const;\n\n/**\n * Resolve the user-supplied config against environment variables and defaults.\n *\n * Precedence (highest → lowest): explicit argument → env var → built-in default.\n * Throws `FreshSqueezyError` only for fields that cannot be defaulted (currently\n * just `apiKey`), so callers can surface a clear setup error at construction\n * time rather than at first request.\n */\nexport function resolveConfig(input: FreshSqueezyConfig = {}): ResolvedConfig {\n const apiKey = input.apiKey ?? process.env[ENV_KEYS.apiKey];\n if (!apiKey) {\n throw new FreshSqueezyError({\n code: \"MISSING_API_KEY\",\n message: `No API key provided. Pass \\`apiKey\\` or set ${ENV_KEYS.apiKey}.`,\n });\n }\n\n const mode = normalizeMode(input.mode ?? process.env[ENV_KEYS.mode] ?? \"test\");\n const storeIdRaw = input.storeId ?? process.env[ENV_KEYS.storeId];\n\n return {\n apiKey,\n storeId: storeIdRaw == null ? undefined : String(storeIdRaw),\n mode,\n baseUrl: input.baseUrl ?? DEFAULT_BASE_URL,\n fetch: input.fetch ?? globalThis.fetch,\n };\n}\n\nfunction normalizeMode(value: string): Mode {\n if (value === \"test\" || value === \"live\") return value;\n throw new FreshSqueezyError({\n code: \"INVALID_MODE\",\n message: `Mode must be \"test\" or \"live\", got \"${value}\".`,\n });\n}\n","import { FreshSqueezyError } from \"./errors.js\";\nimport type {\n JsonApiCollection,\n JsonApiDocument,\n JsonApiResource,\n ResolvedConfig,\n} from \"./types.js\";\n\n/**\n * Options for a single HTTP request.\n *\n * `path` is a Lemon Squeezy API path starting with `/v1/...`. The `query`\n * record is serialized as URL search params with JSON:API-style bracketed\n * keys left untouched (e.g. `filter[store_id]`).\n */\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\";\n path: string;\n query?: Record<string, string | number | undefined>;\n body?: Record<string, unknown>;\n signal?: AbortSignal;\n}\n\n/**\n * Lemon Squeezy JSON:API error object. Kept loose because the API occasionally\n * includes extra keys; the fields we surface are the stable ones.\n */\ninterface JsonApiError {\n status?: string;\n code?: string;\n title?: string;\n detail?: string;\n}\n\n/**\n * Low-level HTTP client. Callers usually go through resource/validator helpers,\n * but this is also exposed as the public escape hatch so consumers can reach\n * endpoints fresh-squeezy does not wrap yet.\n *\n * Responsibilities kept in this one place (per plan.md \"one source of truth\n * for transport\"):\n * - auth header injection\n * - query string serialization with JSON:API bracket keys preserved\n * - response parsing + error normalization\n * - surfacing HTTP status in `FreshSqueezyError`\n *\n * Retries, pagination helpers, and rate-limit handling live in separate files\n * so this layer stays small and obvious.\n */\nexport class HttpClient {\n constructor(private readonly config: ResolvedConfig) {}\n\n async request<T>(options: RequestOptions): Promise<T> {\n const url = this.buildUrl(options.path, options.query);\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.config.apiKey}`,\n Accept: \"application/vnd.api+json\",\n };\n if (options.body !== undefined) {\n headers[\"Content-Type\"] = \"application/vnd.api+json\";\n }\n\n let response: Response;\n try {\n response = await this.config.fetch(url, {\n method: options.method ?? \"GET\",\n headers,\n body: options.body === undefined ? undefined : JSON.stringify(options.body),\n signal: options.signal,\n });\n } catch (cause) {\n throw new FreshSqueezyError({\n code: \"NETWORK_ERROR\",\n message: cause instanceof Error ? cause.message : \"Network request failed\",\n detail: cause,\n });\n }\n\n const text = await response.text();\n const parsed = text.length > 0 ? safeJsonParse(text) : undefined;\n\n if (!response.ok) {\n throw toApiError(response.status, parsed);\n }\n\n return parsed as T;\n }\n\n /**\n * Fetch a single JSON:API resource and return its `data` object.\n */\n async getResource<TAttr>(path: string): Promise<JsonApiResource<TAttr>> {\n const doc = await this.request<JsonApiDocument<TAttr>>({ path });\n return doc.data;\n }\n\n /**\n * Fetch a JSON:API collection and return its `data` array.\n * Pagination is the caller's responsibility — use `meta.page` on the raw\n * request for multi-page traversal.\n */\n async getCollection<TAttr>(\n path: string,\n query?: RequestOptions[\"query\"]\n ): Promise<JsonApiResource<TAttr>[]> {\n const doc = await this.request<JsonApiCollection<TAttr>>({ path, query });\n return doc.data;\n }\n\n private buildUrl(path: string, query?: RequestOptions[\"query\"]): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.append(key, String(value));\n }\n }\n return url.toString();\n }\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction toApiError(status: number, body: unknown): FreshSqueezyError {\n const errors = extractJsonApiErrors(body);\n const first = errors[0];\n const code =\n status === 401\n ? \"UNAUTHORIZED\"\n : status === 404\n ? \"NOT_FOUND\"\n : status === 429\n ? \"RATE_LIMITED\"\n : (first?.code ?? `HTTP_${status}`);\n const message =\n first?.detail ?? first?.title ?? `Lemon Squeezy request failed with status ${status}`;\n return new FreshSqueezyError({ code, status, message, detail: body });\n}\n\nfunction extractJsonApiErrors(body: unknown): JsonApiError[] {\n if (!body || typeof body !== \"object\") return [];\n const errors = (body as { errors?: unknown }).errors;\n if (!Array.isArray(errors)) return [];\n return errors.filter(\n (entry): entry is JsonApiError => typeof entry === \"object\" && entry !== null\n );\n}\n","import type { HttpClient } from \"../core/http.js\";\nimport type { JsonApiDocument, JsonApiResource } from \"../core/types.js\";\n\n/**\n * Subset of the Lemon Squeezy `users` resource attributes we rely on.\n * Full schema at https://docs.lemonsqueezy.com/api/users.\n */\nexport interface UserAttributes {\n name: string;\n email: string;\n color?: string;\n avatar_url?: string | null;\n has_custom_avatar?: boolean;\n createdAt?: string;\n updatedAt?: string;\n}\n\n/**\n * Document-level metadata on `/v1/users/me`.\n *\n * `test_mode` was added to the endpoint on 2024-01-05 (per the Lemon Squeezy\n * API changelog: https://docs.lemonsqueezy.com/api/getting-started/changelog).\n * It reports whether the *key* being used is a test-mode key, independent of\n * what the caller declared. The connection validator compares this against\n * the caller's declared mode to catch the common \"prod key in staging\"\n * (or vice versa) misconfiguration.\n */\nexport interface UserMeta {\n test_mode?: boolean;\n}\n\n/**\n * Authenticated-user document with the `data` + `meta` block preserved.\n * The connection validator needs the meta flag, so we return the full\n * document here rather than just `data` as the collection helpers do.\n */\nexport type AuthenticatedUserDocument = JsonApiDocument<UserAttributes> & { meta?: UserMeta };\n\n/**\n * Fetch the user associated with the API key. Primary use is the connection\n * validator: a successful call confirms the key is valid, surfaces the\n * account identity for logs, and exposes `meta.test_mode` for mode\n * mismatch detection.\n */\nexport async function getAuthenticatedUser(\n http: HttpClient\n): Promise<AuthenticatedUserDocument> {\n return http.request<AuthenticatedUserDocument>({ path: \"/v1/users/me\" });\n}\n\n/**\n * Backwards-compatible helper if a caller only wants the resource (old\n * `getAuthenticatedUser` shape). Internal — not re-exported from the root.\n */\nexport function userResource(\n doc: AuthenticatedUserDocument\n): JsonApiResource<UserAttributes> {\n return doc.data;\n}\n","import type { HttpClient } from \"../core/http.js\";\nimport type { JsonApiResource } from \"../core/types.js\";\n\n/**\n * Subset of store attributes fresh-squeezy reads.\n * Full schema at https://docs.lemonsqueezy.com/api/stores.\n */\nexport interface StoreAttributes {\n name: string;\n slug: string;\n domain?: string | null;\n url?: string | null;\n country?: string;\n currency?: string;\n plan?: string;\n created_at?: string;\n updated_at?: string;\n}\n\nexport async function getStore(\n http: HttpClient,\n storeId: string | number\n): Promise<JsonApiResource<StoreAttributes>> {\n return http.getResource<StoreAttributes>(`/v1/stores/${storeId}`);\n}\n\nexport async function listStores(http: HttpClient): Promise<JsonApiResource<StoreAttributes>[]> {\n return http.getCollection<StoreAttributes>(\"/v1/stores\");\n}\n","import type { ValidationIssue, ValidationResult, ValidationSeverity } from \"../core/types.js\";\n\n/**\n * Stable issue codes. Consumers may switch on these in CI — do not rename\n * without a major version bump.\n */\nexport const ISSUE_CODES = {\n AUTH_FAILED: \"AUTH_FAILED\",\n MODE_MISMATCH: \"MODE_MISMATCH\",\n STORE_NOT_FOUND: \"STORE_NOT_FOUND\",\n STORE_NOT_OWNED: \"STORE_NOT_OWNED\",\n PRODUCT_NOT_FOUND: \"PRODUCT_NOT_FOUND\",\n PRODUCT_WRONG_STORE: \"PRODUCT_WRONG_STORE\",\n PRODUCT_UNPUBLISHED: \"PRODUCT_UNPUBLISHED\",\n PRODUCT_NO_BUY_URL: \"PRODUCT_NO_BUY_URL\",\n VARIANT_UNPUBLISHED: \"VARIANT_UNPUBLISHED\",\n VARIANT_MISSING: \"VARIANT_MISSING\",\n WEBHOOK_NOT_FOUND: \"WEBHOOK_NOT_FOUND\",\n WEBHOOK_EVENTS_MISSING: \"WEBHOOK_EVENTS_MISSING\",\n WEBHOOK_OPTIONAL_EVENTS: \"WEBHOOK_OPTIONAL_EVENTS\",\n NETWORK_ERROR: \"NETWORK_ERROR\",\n UNKNOWN: \"UNKNOWN\",\n} as const;\n\n/**\n * Build a `ValidationIssue` with defaults for the common case.\n * Extracted so every validator produces consistently shaped issues.\n */\nexport function issue(\n code: string,\n severity: ValidationSeverity,\n message: string,\n extras: { suggestedFix?: string; context?: ValidationIssue[\"context\"] } = {}\n): ValidationIssue {\n const base: ValidationIssue = { code, severity, message };\n if (extras.suggestedFix !== undefined) base.suggestedFix = extras.suggestedFix;\n if (extras.context !== undefined) base.context = extras.context;\n return base;\n}\n\n/**\n * Fold an issue list into a boolean. Used by every validator so `ok` is\n * computed the same way everywhere.\n */\nexport function isOk(issues: ValidationIssue[]): boolean {\n return !issues.some((entry) => entry.severity === \"error\");\n}\n\n/**\n * Compose a `ValidationResult` with the `ok` flag derived from issues.\n */\nexport function buildResult<T>(\n name: string,\n mode: ValidationResult[\"mode\"],\n issues: ValidationIssue[],\n resource?: T\n): ValidationResult<T> {\n const result: ValidationResult<T> = {\n name,\n ok: isOk(issues),\n mode,\n issues,\n };\n if (resource !== undefined) result.resource = resource;\n return result;\n}\n","import { FreshSqueezyError } from \"../core/errors.js\";\nimport type { HttpClient } from \"../core/http.js\";\nimport type { Mode, ValidationIssue, ValidationResult } from \"../core/types.js\";\nimport { getAuthenticatedUser, type UserAttributes } from \"../resources/users.js\";\nimport { listStores } from \"../resources/stores.js\";\nimport { ISSUE_CODES, buildResult, issue } from \"./rules.js\";\n\n/**\n * Connection validator summary attached to the `resource` field. Keeps the\n * validator self-contained — consumers need not call `users/me` again.\n *\n * `actualMode` is derived from the `meta.test_mode` field Lemon Squeezy added\n * to `/v1/users/me` on 2024-01-05 (API changelog). When the caller declared\n * one mode but the key actually belongs to the other, the validator fires a\n * `MODE_MISMATCH` error — the single misconfiguration most likely to cause a\n * prod-in-staging (or vice versa) incident.\n */\nexport interface ConnectionSummary {\n user: UserAttributes;\n storeCount: number;\n storeIds: string[];\n /** The mode the API key actually belongs to (per `/v1/users/me` meta). */\n actualMode?: Mode;\n /** The mode the caller asked for at construction time. */\n declaredMode: Mode;\n}\n\n/**\n * Verify that the API key works, surface the account identity + reachable\n * stores, and cross-check declared mode vs the key's true mode.\n *\n * This is the first check every `doctor()` run performs; if it fails,\n * no downstream validator has anything useful to report.\n */\nexport async function validateConnection(\n http: HttpClient,\n mode: Mode\n): Promise<ValidationResult<ConnectionSummary>> {\n const issues: ValidationIssue[] = [];\n\n try {\n const userDoc = await getAuthenticatedUser(http);\n const stores = await listStores(http);\n\n const actualMode = resolveActualMode(userDoc.meta?.test_mode);\n const summary: ConnectionSummary = {\n user: userDoc.data.attributes,\n storeCount: stores.length,\n storeIds: stores.map((store) => store.id),\n declaredMode: mode,\n ...(actualMode ? { actualMode } : {}),\n };\n\n if (actualMode && actualMode !== mode) {\n issues.push(\n issue(\n ISSUE_CODES.MODE_MISMATCH,\n \"error\",\n `API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,\n {\n suggestedFix: `Either pass --mode ${actualMode} or use a ${mode}-mode key from https://app.lemonsqueezy.com/settings/api.`,\n context: { declared: mode, actual: actualMode },\n }\n )\n );\n }\n\n if (stores.length === 0) {\n issues.push(\n issue(\n ISSUE_CODES.STORE_NOT_FOUND,\n \"warning\",\n \"API key authenticated but no stores are reachable.\",\n { suggestedFix: \"Confirm the API key belongs to an account that owns at least one store.\" }\n )\n );\n }\n\n return buildResult(\"connection\", mode, issues, summary);\n } catch (err) {\n issues.push(toConnectionIssue(err));\n return buildResult(\"connection\", mode, issues);\n }\n}\n\n/**\n * Map the boolean `meta.test_mode` flag to our `Mode` type. Returns\n * `undefined` when the field is absent so older accounts / proxies that\n * don't surface it don't produce spurious MODE_MISMATCH failures.\n */\nfunction resolveActualMode(testMode: boolean | undefined): Mode | undefined {\n if (testMode === true) return \"test\";\n if (testMode === false) return \"live\";\n return undefined;\n}\n\nfunction toConnectionIssue(err: unknown): ValidationIssue {\n if (err instanceof FreshSqueezyError) {\n if (err.code === \"UNAUTHORIZED\") {\n return issue(ISSUE_CODES.AUTH_FAILED, \"error\", \"API key rejected by Lemon Squeezy.\", {\n suggestedFix: \"Regenerate the key at https://app.lemonsqueezy.com/settings/api.\",\n context: { status: err.status ?? null },\n });\n }\n if (err.code === \"NETWORK_ERROR\") {\n return issue(ISSUE_CODES.NETWORK_ERROR, \"error\", `Could not reach Lemon Squeezy: ${err.message}`);\n }\n return issue(ISSUE_CODES.UNKNOWN, \"error\", err.message, {\n context: { status: err.status ?? null, code: err.code },\n });\n }\n const message = err instanceof Error ? err.message : \"Unknown error\";\n return issue(ISSUE_CODES.UNKNOWN, \"error\", message);\n}\n","import { FreshSqueezyError } from \"../core/errors.js\";\nimport type { HttpClient } from \"../core/http.js\";\nimport type { Mode, ValidationIssue, ValidationResult } from \"../core/types.js\";\nimport { getStore, type StoreAttributes } from \"../resources/stores.js\";\nimport { ISSUE_CODES, buildResult, issue } from \"./rules.js\";\n\n/**\n * Verify a store exists and is reachable with the current API key. A 404\n * typically means the key belongs to a different account, not that the store\n * is gone — the suggested fix reflects that.\n */\nexport async function validateStore(\n http: HttpClient,\n mode: Mode,\n storeId: string | number\n): Promise<ValidationResult<StoreAttributes>> {\n const issues: ValidationIssue[] = [];\n\n try {\n const store = await getStore(http, storeId);\n return buildResult(\"store\", mode, issues, store.attributes);\n } catch (err) {\n if (err instanceof FreshSqueezyError && err.status === 404) {\n issues.push(\n issue(\n ISSUE_CODES.STORE_NOT_FOUND,\n \"error\",\n `Store ${storeId} not found with the current API key.`,\n {\n suggestedFix:\n \"Check the store ID and confirm the API key belongs to the account that owns it.\",\n context: { storeId: String(storeId) },\n }\n )\n );\n return buildResult(\"store\", mode, issues);\n }\n if (err instanceof FreshSqueezyError) {\n issues.push(\n issue(ISSUE_CODES.UNKNOWN, \"error\", err.message, {\n context: { status: err.status ?? null, code: err.code },\n })\n );\n return buildResult(\"store\", mode, issues);\n }\n const message = err instanceof Error ? err.message : \"Unknown error\";\n issues.push(issue(ISSUE_CODES.UNKNOWN, \"error\", message));\n return buildResult(\"store\", mode, issues);\n }\n}\n","import type { HttpClient } from \"../core/http.js\";\nimport type { JsonApiResource } from \"../core/types.js\";\n\n/**\n * Subset of product attributes we need for validation. `status` drives the\n * \"unpublished product\" check; `store_id` drives ownership checks.\n */\nexport interface ProductAttributes {\n name: string;\n slug: string;\n description?: string | null;\n status: \"draft\" | \"published\";\n status_formatted?: string;\n store_id: number;\n buy_now_url?: string | null;\n from_price?: number | null;\n to_price?: number | null;\n created_at?: string;\n updated_at?: string;\n}\n\nexport async function getProduct(\n http: HttpClient,\n productId: string | number\n): Promise<JsonApiResource<ProductAttributes>> {\n return http.getResource<ProductAttributes>(`/v1/products/${productId}`);\n}\n\nexport async function listProducts(\n http: HttpClient,\n storeId: string | number\n): Promise<JsonApiResource<ProductAttributes>[]> {\n return http.getCollection<ProductAttributes>(\"/v1/products\", {\n \"filter[store_id]\": String(storeId),\n });\n}\n","import type { HttpClient } from \"../core/http.js\";\nimport type { JsonApiResource } from \"../core/types.js\";\n\n/**\n * Variant attributes used by the product validator to detect\n * variant/price drift from the product they belong to.\n */\nexport interface VariantAttributes {\n product_id: number;\n name: string;\n slug: string;\n description?: string | null;\n status: \"pending\" | \"draft\" | \"published\";\n is_subscription?: boolean;\n interval?: string | null;\n interval_count?: number | null;\n has_license_keys?: boolean;\n created_at?: string;\n updated_at?: string;\n}\n\nexport async function listVariantsForProduct(\n http: HttpClient,\n productId: string | number\n): Promise<JsonApiResource<VariantAttributes>[]> {\n return http.getCollection<VariantAttributes>(\"/v1/variants\", {\n \"filter[product_id]\": String(productId),\n });\n}\n","import { FreshSqueezyError } from \"../core/errors.js\";\nimport type { HttpClient } from \"../core/http.js\";\nimport type { Mode, ValidationIssue, ValidationResult } from \"../core/types.js\";\nimport { getProduct, type ProductAttributes } from \"../resources/products.js\";\nimport { listVariantsForProduct } from \"../resources/variants.js\";\nimport { ISSUE_CODES, buildResult, issue } from \"./rules.js\";\n\nexport interface ProductValidationOptions {\n productId: string | number;\n /** Optional: confirm the product belongs to this store. */\n expectedStoreId?: string | number;\n}\n\n/**\n * Validate a product's publish state, store ownership, and that it has at\n * least one published variant. Surfaces the most common misconfigurations\n * caught in the wild (unpublished product, wrong store, missing variants).\n */\nexport async function validateProduct(\n http: HttpClient,\n mode: Mode,\n options: ProductValidationOptions\n): Promise<ValidationResult<ProductAttributes>> {\n const issues: ValidationIssue[] = [];\n\n let product;\n try {\n product = await getProduct(http, options.productId);\n } catch (err) {\n if (err instanceof FreshSqueezyError && err.status === 404) {\n issues.push(\n issue(\n ISSUE_CODES.PRODUCT_NOT_FOUND,\n \"error\",\n `Product ${options.productId} not found.`,\n {\n suggestedFix: \"Verify the product ID in the Lemon Squeezy dashboard.\",\n context: { productId: String(options.productId) },\n }\n )\n );\n return buildResult(\"product\", mode, issues);\n }\n const message = err instanceof Error ? err.message : \"Unknown error\";\n issues.push(issue(ISSUE_CODES.UNKNOWN, \"error\", message));\n return buildResult(\"product\", mode, issues);\n }\n\n const attrs = product.attributes;\n\n if (options.expectedStoreId !== undefined) {\n const expected = String(options.expectedStoreId);\n const actual = String(attrs.store_id);\n if (expected !== actual) {\n issues.push(\n issue(\n ISSUE_CODES.PRODUCT_WRONG_STORE,\n \"error\",\n `Product belongs to store ${actual}, expected ${expected}.`,\n {\n suggestedFix:\n \"Either use the correct store ID or the correct product ID — IDs should not cross stores.\",\n context: { expectedStoreId: expected, actualStoreId: actual },\n }\n )\n );\n }\n }\n\n if (attrs.status !== \"published\") {\n issues.push(\n issue(\n ISSUE_CODES.PRODUCT_UNPUBLISHED,\n \"error\",\n `Product is in \"${attrs.status}\" state, not \"published\".`,\n {\n suggestedFix: \"Publish the product in the Lemon Squeezy dashboard before selling.\",\n context: { status: attrs.status },\n }\n )\n );\n }\n\n if (!attrs.buy_now_url) {\n issues.push(\n issue(\n ISSUE_CODES.PRODUCT_NO_BUY_URL,\n \"warning\",\n \"Product has no buy-now URL. Hosted checkout may be disabled.\",\n { suggestedFix: \"Enable buy-now in product settings, or use a custom checkout flow.\" }\n )\n );\n }\n\n try {\n const variants = await listVariantsForProduct(http, options.productId);\n if (variants.length === 0) {\n issues.push(\n issue(\n ISSUE_CODES.VARIANT_MISSING,\n \"error\",\n \"Product has no variants. Customers cannot purchase it.\",\n { suggestedFix: \"Add at least one variant in the product configuration.\" }\n )\n );\n } else if (!variants.some((variant) => variant.attributes.status === \"published\")) {\n issues.push(\n issue(\n ISSUE_CODES.VARIANT_UNPUBLISHED,\n \"error\",\n \"Product has variants but none are published.\",\n { suggestedFix: \"Publish at least one variant.\" }\n )\n );\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Unknown error fetching variants\";\n issues.push(issue(ISSUE_CODES.UNKNOWN, \"warning\", message));\n }\n\n return buildResult(\"product\", mode, issues, attrs);\n}\n","import type { HttpClient } from \"../core/http.js\";\nimport type { JsonApiResource } from \"../core/types.js\";\n\n/**\n * Subset of webhook attributes we read. `events` is an ordered list of\n * subscribed event names; the validator cross-references these against the\n * support manifest to catch missing subscriptions.\n */\nexport interface WebhookAttributes {\n store_id: number;\n url: string;\n events: string[];\n last_sent_at?: string | null;\n created_at?: string;\n updated_at?: string;\n test_mode?: boolean;\n}\n\nexport async function listWebhooksForStore(\n http: HttpClient,\n storeId: string | number\n): Promise<JsonApiResource<WebhookAttributes>[]> {\n return http.getCollection<WebhookAttributes>(\"/v1/webhooks\", {\n \"filter[store_id]\": String(storeId),\n });\n}\n","/**\n * Support manifest: the locally reviewed source of truth for what\n * fresh-squeezy explicitly understands on the Lemon Squeezy platform.\n *\n * The plan deliberately favors a static, reviewed manifest over live changelog\n * scraping (see plan.md §Non-goals). When the platform adds new resources,\n * fields, or webhook events, bump the entries below and re-snapshot the\n * changelog page with `npm run check:changelog -- --update`.\n *\n * Changelog source: https://docs.lemonsqueezy.com/api/getting-started/changelog\n * Last reviewed: 2026-04-24\n */\n\n/**\n * Resources fresh-squeezy wraps today. Anything outside this list is still\n * reachable via the raw `request()` escape hatch but has no dedicated\n * validator.\n */\nexport const SUPPORTED_RESOURCES = [\n \"users\",\n \"stores\",\n \"products\",\n \"variants\",\n \"webhooks\",\n] as const;\n\n/**\n * Webhook events fresh-squeezy expects a production integration to subscribe to\n * at minimum. Consumers can still subscribe to more; the validator only flags\n * missing ones from this list.\n *\n * Rationale:\n * - `order_*` covers one-off purchases and refunds.\n * - `subscription_*` covers the recurring-billing lifecycle.\n * - `subscription_payment_*` covers dunning / retry loops.\n *\n * Confirmed present in the Lemon Squeezy webhook topic list as of 2026-04-24.\n */\nexport const RECOMMENDED_WEBHOOK_EVENTS = [\n \"order_created\",\n \"order_refunded\",\n \"subscription_created\",\n \"subscription_updated\",\n \"subscription_cancelled\",\n \"subscription_resumed\",\n \"subscription_expired\",\n \"subscription_payment_success\",\n \"subscription_payment_failed\",\n] as const;\n\n/**\n * Newer or integration-specific events surfaced as info-level suggestions\n * rather than errors. Missing these is common and not necessarily a\n * misconfiguration.\n *\n * Per-entry changelog provenance (source:\n * https://docs.lemonsqueezy.com/api/getting-started/changelog):\n *\n * - `customer_updated` — added 2026-02-25. Fires when a customer record\n * changes (e.g. email, marketing consent). Needed if the app mirrors\n * customer data locally.\n * - `affiliate_activated` — added 2025-01-21 alongside the affiliates\n * endpoints. Only relevant if the store has an affiliate program.\n * - `license_key_created` / `license_key_updated` — License API events.\n * Only relevant when variants have `has_license_keys: true`.\n */\nexport const OPTIONAL_WEBHOOK_EVENTS = [\n \"customer_updated\",\n \"affiliate_activated\",\n \"license_key_created\",\n \"license_key_updated\",\n] as const;\n\n/**\n * Platform additions we have read and decided *not* to validate against yet,\n * documented here so maintainers see the deliberate gap during review.\n *\n * Tracked so the drift workflow has an \"expected state\" to compare against:\n * if the changelog page changes and none of these items explain it, the diff\n * is probably something new that needs a manifest update.\n */\nexport const ACKNOWLEDGED_CHANGELOG_ENTRIES = [\n {\n date: \"2026-02-25\",\n summary: \"Added customer_updated webhook event.\",\n handledBy: \"OPTIONAL_WEBHOOK_EVENTS\",\n },\n {\n date: \"2025-06-11\",\n summary: \"Added payment_processor attribute to Subscription objects.\",\n handledBy:\n \"Not wrapped — reachable via client.request('/v1/subscriptions/:id'). Add a validator only if a real integration needs it.\",\n },\n {\n date: \"2025-01-21\",\n summary: \"Added Affiliates endpoints and affiliate_activated webhook.\",\n handledBy: \"OPTIONAL_WEBHOOK_EVENTS (event only; resource stays v2 scope)\",\n },\n {\n date: \"2024-01-05\",\n summary: \"Added test_mode flag to /v1/users/me meta.\",\n handledBy:\n \"Read in validateConnection to emit MODE_MISMATCH when the key's true mode differs from the caller's declared mode.\",\n },\n] as const;\n\nexport type RecommendedEvent = (typeof RECOMMENDED_WEBHOOK_EVENTS)[number];\nexport type OptionalEvent = (typeof OPTIONAL_WEBHOOK_EVENTS)[number];\n","import { FreshSqueezyError } from \"../core/errors.js\";\nimport type { HttpClient } from \"../core/http.js\";\nimport type { Mode, ValidationIssue, ValidationResult } from \"../core/types.js\";\nimport { listWebhooksForStore, type WebhookAttributes } from \"../resources/webhooks.js\";\nimport { OPTIONAL_WEBHOOK_EVENTS, RECOMMENDED_WEBHOOK_EVENTS } from \"../support/manifest.js\";\nimport { ISSUE_CODES, buildResult, issue } from \"./rules.js\";\n\nexport interface WebhookValidationOptions {\n storeId: string | number;\n /** The public URL your app exposes for Lemon Squeezy to POST to. */\n url: string;\n}\n\n/**\n * Confirm a webhook matching `options.url` is registered against the given\n * store, and cross-reference its subscribed events against the support\n * manifest's recommended + optional lists.\n *\n * Missing recommended events = error. Missing optional events = info, because\n * not every integration needs them.\n */\nexport async function validateWebhook(\n http: HttpClient,\n mode: Mode,\n options: WebhookValidationOptions\n): Promise<ValidationResult<WebhookAttributes>> {\n const issues: ValidationIssue[] = [];\n\n let webhooks;\n try {\n webhooks = await listWebhooksForStore(http, options.storeId);\n } catch (err) {\n if (err instanceof FreshSqueezyError) {\n issues.push(\n issue(ISSUE_CODES.UNKNOWN, \"error\", err.message, {\n context: { status: err.status ?? null, code: err.code },\n })\n );\n return buildResult(\"webhook\", mode, issues);\n }\n const message = err instanceof Error ? err.message : \"Unknown error\";\n issues.push(issue(ISSUE_CODES.UNKNOWN, \"error\", message));\n return buildResult(\"webhook\", mode, issues);\n }\n\n const match = webhooks.find((webhook) => normalizeUrl(webhook.attributes.url) === normalizeUrl(options.url));\n if (!match) {\n issues.push(\n issue(\n ISSUE_CODES.WEBHOOK_NOT_FOUND,\n \"error\",\n `No webhook registered for URL ${options.url} on store ${options.storeId}.`,\n {\n suggestedFix:\n \"Register the webhook in Lemon Squeezy (Settings → Webhooks) and subscribe to the recommended events.\",\n context: { storeId: String(options.storeId), url: options.url },\n }\n )\n );\n return buildResult(\"webhook\", mode, issues);\n }\n\n const subscribed = new Set(match.attributes.events);\n const missingRecommended = RECOMMENDED_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));\n const missingOptional = OPTIONAL_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));\n\n if (missingRecommended.length > 0) {\n issues.push(\n issue(\n ISSUE_CODES.WEBHOOK_EVENTS_MISSING,\n \"error\",\n `Webhook is missing recommended events: ${missingRecommended.join(\", \")}.`,\n {\n suggestedFix: \"Subscribe to all recommended events so the integration survives plan changes and refunds.\",\n context: { missing: missingRecommended.join(\",\") },\n }\n )\n );\n }\n\n if (missingOptional.length > 0) {\n issues.push(\n issue(\n ISSUE_CODES.WEBHOOK_OPTIONAL_EVENTS,\n \"info\",\n `Optional events not subscribed: ${missingOptional.join(\", \")}.`,\n { context: { missing: missingOptional.join(\",\") } }\n )\n );\n }\n\n return buildResult(\"webhook\", mode, issues, match.attributes);\n}\n\n/**\n * Compare webhook URLs without being tripped up by trailing slashes.\n * Lemon Squeezy strips trailing slashes on save; users often pass them in.\n */\nfunction normalizeUrl(raw: string): string {\n return raw.replace(/\\/+$/, \"\").toLowerCase();\n}\n","import type { HttpClient } from \"../core/http.js\";\nimport type { DoctorReport, Mode, ValidationResult } from \"../core/types.js\";\nimport { validateConnection } from \"./connection.js\";\nimport { validateStore } from \"./store.js\";\nimport { validateProduct } from \"./product.js\";\nimport { validateWebhook } from \"./webhook.js\";\n\n/**\n * Optional targets for the doctor run. If a target is omitted, its validator\n * is skipped — consumers only pay for what they configure.\n */\nexport interface DoctorOptions {\n storeId?: string | number;\n productId?: string | number;\n webhookUrl?: string;\n}\n\n/**\n * Compose every configured validator into a single report. This is the\n * primary entry point for CI health checks: one call, one structured result,\n * one exit code decision.\n *\n * Order is meaningful. Connection runs first because downstream validators\n * have nothing useful to say if the API key is broken.\n */\nexport async function doctor(\n http: HttpClient,\n mode: Mode,\n options: DoctorOptions = {}\n): Promise<DoctorReport> {\n const results: ValidationResult[] = [];\n\n const connection = await validateConnection(http, mode);\n results.push(connection);\n\n if (!connection.ok) {\n return { ok: false, mode, results };\n }\n\n if (options.storeId !== undefined) {\n results.push(await validateStore(http, mode, options.storeId));\n }\n\n if (options.productId !== undefined) {\n results.push(\n await validateProduct(http, mode, {\n productId: options.productId,\n expectedStoreId: options.storeId,\n })\n );\n }\n\n if (options.storeId !== undefined && options.webhookUrl !== undefined) {\n results.push(\n await validateWebhook(http, mode, {\n storeId: options.storeId,\n url: options.webhookUrl,\n })\n );\n }\n\n const ok = results.every((result) => result.ok);\n return { ok, mode, results };\n}\n","import { resolveConfig } from \"./core/config.js\";\nimport { HttpClient, type RequestOptions } from \"./core/http.js\";\nimport type { FreshSqueezyConfig, DoctorReport, Mode, ValidationResult } from \"./core/types.js\";\nimport { validateConnection, type ConnectionSummary } from \"./validate/connection.js\";\nimport { validateStore } from \"./validate/store.js\";\nimport { validateProduct, type ProductValidationOptions } from \"./validate/product.js\";\nimport { validateWebhook, type WebhookValidationOptions } from \"./validate/webhook.js\";\nimport { doctor, type DoctorOptions } from \"./validate/doctor.js\";\nimport type { StoreAttributes } from \"./resources/stores.js\";\nimport type { ProductAttributes } from \"./resources/products.js\";\nimport type { WebhookAttributes } from \"./resources/webhooks.js\";\n\n/**\n * The public client. All consumer code flows through the factory below —\n * direct instantiation is intentionally not exposed so we can evolve the\n * internals without breaking callers.\n */\nexport interface FreshSqueezyClient {\n /** Resolved mode (test or live). Surfaced so consumers can log it. */\n readonly mode: Mode;\n\n /** Raw HTTP escape hatch for endpoints fresh-squeezy does not wrap. */\n request<T = unknown>(options: RequestOptions): Promise<T>;\n\n validateConnection(): Promise<ValidationResult<ConnectionSummary>>;\n validateStore(storeId: string | number): Promise<ValidationResult<StoreAttributes>>;\n validateProduct(options: ProductValidationOptions): Promise<ValidationResult<ProductAttributes>>;\n validateWebhook(options: WebhookValidationOptions): Promise<ValidationResult<WebhookAttributes>>;\n doctor(options?: DoctorOptions): Promise<DoctorReport>;\n}\n\n/**\n * Create a fresh-squeezy client. Zero-config usage reads\n * `LEMON_SQUEEZY_API_KEY`, `LEMON_SQUEEZY_STORE_ID`, and `LEMON_SQUEEZY_MODE`\n * from `process.env`.\n *\n * @example\n * ```ts\n * const lemon = createFreshSqueezy();\n * const report = await lemon.doctor();\n * if (!report.ok) process.exit(1);\n * ```\n */\nexport function createFreshSqueezy(config: FreshSqueezyConfig = {}): FreshSqueezyClient {\n const resolved = resolveConfig(config);\n const http = new HttpClient(resolved);\n\n return {\n mode: resolved.mode,\n request: (options) => http.request(options),\n validateConnection: () => validateConnection(http, resolved.mode),\n validateStore: (storeId) => validateStore(http, resolved.mode, storeId),\n validateProduct: (options) => validateProduct(http, resolved.mode, options),\n validateWebhook: (options) => validateWebhook(http, resolved.mode, options),\n doctor: (options) =>\n doctor(http, resolved.mode, {\n storeId: options?.storeId ?? resolved.storeId,\n productId: options?.productId,\n webhookUrl: options?.webhookUrl,\n }),\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,541 @@
1
+ /**
2
+ * Core public types for fresh-squeezy.
3
+ *
4
+ * These shapes are the integration contract with consuming products. Treat them
5
+ * as public API surface — breaking changes require a major version bump.
6
+ */
7
+ /**
8
+ * Severity for validator findings.
9
+ *
10
+ * - `info`: benign observation (e.g. "store reachable").
11
+ * - `warning`: likely misconfiguration that should be reviewed.
12
+ * - `error`: the setup is wrong; consumers should fail CI.
13
+ */
14
+ type ValidationSeverity = "info" | "warning" | "error";
15
+ /**
16
+ * Which Lemon Squeezy environment a validator ran against.
17
+ * Mode is determined by which API key was used, not by a host switch.
18
+ */
19
+ type Mode = "test" | "live";
20
+ /**
21
+ * A single finding surfaced by a validator.
22
+ *
23
+ * `code` is a stable identifier so consumers can match in CI or dashboards
24
+ * without depending on `message` wording. `suggestedFix` is optional prose
25
+ * pointing to the most likely remediation.
26
+ */
27
+ interface ValidationIssue {
28
+ code: string;
29
+ severity: ValidationSeverity;
30
+ message: string;
31
+ suggestedFix?: string;
32
+ context?: Record<string, string | number | boolean | null>;
33
+ }
34
+ /**
35
+ * Return shape for every validator.
36
+ *
37
+ * `ok` is `false` if any issue has `severity: "error"`. `mode` surfaces the
38
+ * environment the validator actually talked to, so callers can detect
39
+ * test/live confusion.
40
+ */
41
+ interface ValidationResult<T = unknown> {
42
+ ok: boolean;
43
+ mode: Mode;
44
+ name: string;
45
+ resource?: T;
46
+ issues: ValidationIssue[];
47
+ }
48
+ /**
49
+ * Aggregate doctor output. Composes the results of every individual validator
50
+ * into one object suitable for CI exit-code decisions, dashboards, or JSON logs.
51
+ */
52
+ interface DoctorReport {
53
+ ok: boolean;
54
+ mode: Mode;
55
+ results: ValidationResult[];
56
+ }
57
+ /**
58
+ * Options used when constructing the client.
59
+ * Every field has an env-based default so consumers can use `createFreshSqueezy()`
60
+ * with no arguments for the common case.
61
+ */
62
+ interface FreshSqueezyConfig {
63
+ apiKey?: string;
64
+ storeId?: string | number;
65
+ mode?: Mode;
66
+ baseUrl?: string;
67
+ fetch?: typeof fetch;
68
+ }
69
+ /**
70
+ * Resolved config after env + defaults are applied. Internal — not exported
71
+ * from the package root.
72
+ */
73
+ interface ResolvedConfig {
74
+ apiKey: string;
75
+ storeId?: string;
76
+ mode: Mode;
77
+ baseUrl: string;
78
+ fetch: typeof fetch;
79
+ }
80
+ /**
81
+ * JSON:API resource envelope returned by the Lemon Squeezy main API.
82
+ *
83
+ * The shape is documented at https://jsonapi.org/ and covers the subset
84
+ * fresh-squeezy relies on.
85
+ */
86
+ interface JsonApiResource<TAttributes = Record<string, unknown>> {
87
+ type: string;
88
+ id: string;
89
+ attributes: TAttributes;
90
+ relationships?: Record<string, unknown>;
91
+ links?: Record<string, string>;
92
+ }
93
+ /**
94
+ * JSON:API top-level document for single-resource responses.
95
+ */
96
+ interface JsonApiDocument<TAttributes = Record<string, unknown>> {
97
+ data: JsonApiResource<TAttributes>;
98
+ links?: Record<string, string>;
99
+ meta?: Record<string, unknown>;
100
+ }
101
+ /**
102
+ * JSON:API top-level document for collection responses.
103
+ */
104
+ interface JsonApiCollection<TAttributes = Record<string, unknown>> {
105
+ data: JsonApiResource<TAttributes>[];
106
+ links?: Record<string, string>;
107
+ meta?: {
108
+ page?: {
109
+ currentPage: number;
110
+ lastPage: number;
111
+ total: number;
112
+ };
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Options for a single HTTP request.
118
+ *
119
+ * `path` is a Lemon Squeezy API path starting with `/v1/...`. The `query`
120
+ * record is serialized as URL search params with JSON:API-style bracketed
121
+ * keys left untouched (e.g. `filter[store_id]`).
122
+ */
123
+ interface RequestOptions {
124
+ method?: "GET" | "POST" | "PATCH" | "DELETE";
125
+ path: string;
126
+ query?: Record<string, string | number | undefined>;
127
+ body?: Record<string, unknown>;
128
+ signal?: AbortSignal;
129
+ }
130
+ /**
131
+ * Low-level HTTP client. Callers usually go through resource/validator helpers,
132
+ * but this is also exposed as the public escape hatch so consumers can reach
133
+ * endpoints fresh-squeezy does not wrap yet.
134
+ *
135
+ * Responsibilities kept in this one place (per plan.md "one source of truth
136
+ * for transport"):
137
+ * - auth header injection
138
+ * - query string serialization with JSON:API bracket keys preserved
139
+ * - response parsing + error normalization
140
+ * - surfacing HTTP status in `FreshSqueezyError`
141
+ *
142
+ * Retries, pagination helpers, and rate-limit handling live in separate files
143
+ * so this layer stays small and obvious.
144
+ */
145
+ declare class HttpClient {
146
+ private readonly config;
147
+ constructor(config: ResolvedConfig);
148
+ request<T>(options: RequestOptions): Promise<T>;
149
+ /**
150
+ * Fetch a single JSON:API resource and return its `data` object.
151
+ */
152
+ getResource<TAttr>(path: string): Promise<JsonApiResource<TAttr>>;
153
+ /**
154
+ * Fetch a JSON:API collection and return its `data` array.
155
+ * Pagination is the caller's responsibility — use `meta.page` on the raw
156
+ * request for multi-page traversal.
157
+ */
158
+ getCollection<TAttr>(path: string, query?: RequestOptions["query"]): Promise<JsonApiResource<TAttr>[]>;
159
+ private buildUrl;
160
+ }
161
+
162
+ /**
163
+ * Subset of the Lemon Squeezy `users` resource attributes we rely on.
164
+ * Full schema at https://docs.lemonsqueezy.com/api/users.
165
+ */
166
+ interface UserAttributes {
167
+ name: string;
168
+ email: string;
169
+ color?: string;
170
+ avatar_url?: string | null;
171
+ has_custom_avatar?: boolean;
172
+ createdAt?: string;
173
+ updatedAt?: string;
174
+ }
175
+ /**
176
+ * Document-level metadata on `/v1/users/me`.
177
+ *
178
+ * `test_mode` was added to the endpoint on 2024-01-05 (per the Lemon Squeezy
179
+ * API changelog: https://docs.lemonsqueezy.com/api/getting-started/changelog).
180
+ * It reports whether the *key* being used is a test-mode key, independent of
181
+ * what the caller declared. The connection validator compares this against
182
+ * the caller's declared mode to catch the common "prod key in staging"
183
+ * (or vice versa) misconfiguration.
184
+ */
185
+ interface UserMeta {
186
+ test_mode?: boolean;
187
+ }
188
+ /**
189
+ * Authenticated-user document with the `data` + `meta` block preserved.
190
+ * The connection validator needs the meta flag, so we return the full
191
+ * document here rather than just `data` as the collection helpers do.
192
+ */
193
+ type AuthenticatedUserDocument = JsonApiDocument<UserAttributes> & {
194
+ meta?: UserMeta;
195
+ };
196
+ /**
197
+ * Fetch the user associated with the API key. Primary use is the connection
198
+ * validator: a successful call confirms the key is valid, surfaces the
199
+ * account identity for logs, and exposes `meta.test_mode` for mode
200
+ * mismatch detection.
201
+ */
202
+ declare function getAuthenticatedUser(http: HttpClient): Promise<AuthenticatedUserDocument>;
203
+ /**
204
+ * Backwards-compatible helper if a caller only wants the resource (old
205
+ * `getAuthenticatedUser` shape). Internal — not re-exported from the root.
206
+ */
207
+ declare function userResource(doc: AuthenticatedUserDocument): JsonApiResource<UserAttributes>;
208
+
209
+ /**
210
+ * Connection validator summary attached to the `resource` field. Keeps the
211
+ * validator self-contained — consumers need not call `users/me` again.
212
+ *
213
+ * `actualMode` is derived from the `meta.test_mode` field Lemon Squeezy added
214
+ * to `/v1/users/me` on 2024-01-05 (API changelog). When the caller declared
215
+ * one mode but the key actually belongs to the other, the validator fires a
216
+ * `MODE_MISMATCH` error — the single misconfiguration most likely to cause a
217
+ * prod-in-staging (or vice versa) incident.
218
+ */
219
+ interface ConnectionSummary {
220
+ user: UserAttributes;
221
+ storeCount: number;
222
+ storeIds: string[];
223
+ /** The mode the API key actually belongs to (per `/v1/users/me` meta). */
224
+ actualMode?: Mode;
225
+ /** The mode the caller asked for at construction time. */
226
+ declaredMode: Mode;
227
+ }
228
+ /**
229
+ * Verify that the API key works, surface the account identity + reachable
230
+ * stores, and cross-check declared mode vs the key's true mode.
231
+ *
232
+ * This is the first check every `doctor()` run performs; if it fails,
233
+ * no downstream validator has anything useful to report.
234
+ */
235
+ declare function validateConnection(http: HttpClient, mode: Mode): Promise<ValidationResult<ConnectionSummary>>;
236
+
237
+ /**
238
+ * Subset of product attributes we need for validation. `status` drives the
239
+ * "unpublished product" check; `store_id` drives ownership checks.
240
+ */
241
+ interface ProductAttributes {
242
+ name: string;
243
+ slug: string;
244
+ description?: string | null;
245
+ status: "draft" | "published";
246
+ status_formatted?: string;
247
+ store_id: number;
248
+ buy_now_url?: string | null;
249
+ from_price?: number | null;
250
+ to_price?: number | null;
251
+ created_at?: string;
252
+ updated_at?: string;
253
+ }
254
+ declare function getProduct(http: HttpClient, productId: string | number): Promise<JsonApiResource<ProductAttributes>>;
255
+ declare function listProducts(http: HttpClient, storeId: string | number): Promise<JsonApiResource<ProductAttributes>[]>;
256
+
257
+ interface ProductValidationOptions {
258
+ productId: string | number;
259
+ /** Optional: confirm the product belongs to this store. */
260
+ expectedStoreId?: string | number;
261
+ }
262
+ /**
263
+ * Validate a product's publish state, store ownership, and that it has at
264
+ * least one published variant. Surfaces the most common misconfigurations
265
+ * caught in the wild (unpublished product, wrong store, missing variants).
266
+ */
267
+ declare function validateProduct(http: HttpClient, mode: Mode, options: ProductValidationOptions): Promise<ValidationResult<ProductAttributes>>;
268
+
269
+ /**
270
+ * Subset of webhook attributes we read. `events` is an ordered list of
271
+ * subscribed event names; the validator cross-references these against the
272
+ * support manifest to catch missing subscriptions.
273
+ */
274
+ interface WebhookAttributes {
275
+ store_id: number;
276
+ url: string;
277
+ events: string[];
278
+ last_sent_at?: string | null;
279
+ created_at?: string;
280
+ updated_at?: string;
281
+ test_mode?: boolean;
282
+ }
283
+ declare function listWebhooksForStore(http: HttpClient, storeId: string | number): Promise<JsonApiResource<WebhookAttributes>[]>;
284
+
285
+ interface WebhookValidationOptions {
286
+ storeId: string | number;
287
+ /** The public URL your app exposes for Lemon Squeezy to POST to. */
288
+ url: string;
289
+ }
290
+ /**
291
+ * Confirm a webhook matching `options.url` is registered against the given
292
+ * store, and cross-reference its subscribed events against the support
293
+ * manifest's recommended + optional lists.
294
+ *
295
+ * Missing recommended events = error. Missing optional events = info, because
296
+ * not every integration needs them.
297
+ */
298
+ declare function validateWebhook(http: HttpClient, mode: Mode, options: WebhookValidationOptions): Promise<ValidationResult<WebhookAttributes>>;
299
+
300
+ /**
301
+ * Optional targets for the doctor run. If a target is omitted, its validator
302
+ * is skipped — consumers only pay for what they configure.
303
+ */
304
+ interface DoctorOptions {
305
+ storeId?: string | number;
306
+ productId?: string | number;
307
+ webhookUrl?: string;
308
+ }
309
+ /**
310
+ * Compose every configured validator into a single report. This is the
311
+ * primary entry point for CI health checks: one call, one structured result,
312
+ * one exit code decision.
313
+ *
314
+ * Order is meaningful. Connection runs first because downstream validators
315
+ * have nothing useful to say if the API key is broken.
316
+ */
317
+ declare function doctor(http: HttpClient, mode: Mode, options?: DoctorOptions): Promise<DoctorReport>;
318
+
319
+ /**
320
+ * Subset of store attributes fresh-squeezy reads.
321
+ * Full schema at https://docs.lemonsqueezy.com/api/stores.
322
+ */
323
+ interface StoreAttributes {
324
+ name: string;
325
+ slug: string;
326
+ domain?: string | null;
327
+ url?: string | null;
328
+ country?: string;
329
+ currency?: string;
330
+ plan?: string;
331
+ created_at?: string;
332
+ updated_at?: string;
333
+ }
334
+ declare function getStore(http: HttpClient, storeId: string | number): Promise<JsonApiResource<StoreAttributes>>;
335
+ declare function listStores(http: HttpClient): Promise<JsonApiResource<StoreAttributes>[]>;
336
+
337
+ /**
338
+ * The public client. All consumer code flows through the factory below —
339
+ * direct instantiation is intentionally not exposed so we can evolve the
340
+ * internals without breaking callers.
341
+ */
342
+ interface FreshSqueezyClient {
343
+ /** Resolved mode (test or live). Surfaced so consumers can log it. */
344
+ readonly mode: Mode;
345
+ /** Raw HTTP escape hatch for endpoints fresh-squeezy does not wrap. */
346
+ request<T = unknown>(options: RequestOptions): Promise<T>;
347
+ validateConnection(): Promise<ValidationResult<ConnectionSummary>>;
348
+ validateStore(storeId: string | number): Promise<ValidationResult<StoreAttributes>>;
349
+ validateProduct(options: ProductValidationOptions): Promise<ValidationResult<ProductAttributes>>;
350
+ validateWebhook(options: WebhookValidationOptions): Promise<ValidationResult<WebhookAttributes>>;
351
+ doctor(options?: DoctorOptions): Promise<DoctorReport>;
352
+ }
353
+ /**
354
+ * Create a fresh-squeezy client. Zero-config usage reads
355
+ * `LEMON_SQUEEZY_API_KEY`, `LEMON_SQUEEZY_STORE_ID`, and `LEMON_SQUEEZY_MODE`
356
+ * from `process.env`.
357
+ *
358
+ * @example
359
+ * ```ts
360
+ * const lemon = createFreshSqueezy();
361
+ * const report = await lemon.doctor();
362
+ * if (!report.ok) process.exit(1);
363
+ * ```
364
+ */
365
+ declare function createFreshSqueezy(config?: FreshSqueezyConfig): FreshSqueezyClient;
366
+
367
+ /**
368
+ * Unified error type for fresh-squeezy. All HTTP and validation failures that
369
+ * bubble up to consumers pass through this class so caller code can branch on
370
+ * a single `instanceof` check.
371
+ *
372
+ * Why a class over a discriminated union: Node's `fetch` rejections interleave
373
+ * with library errors in user stack traces. A class keeps the stack readable
374
+ * and gives one stable prototype chain for consumer `catch` blocks.
375
+ */
376
+ declare class FreshSqueezyError extends Error {
377
+ readonly code: string;
378
+ readonly status?: number;
379
+ readonly detail?: unknown;
380
+ constructor(opts: {
381
+ code: string;
382
+ message: string;
383
+ status?: number;
384
+ detail?: unknown;
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Env variable names read when a field is not passed explicitly. Consuming
390
+ * products rely on these names being stable — treat them as public API.
391
+ */
392
+ declare const ENV_KEYS: {
393
+ readonly apiKey: "LEMON_SQUEEZY_API_KEY";
394
+ readonly storeId: "LEMON_SQUEEZY_STORE_ID";
395
+ readonly mode: "LEMON_SQUEEZY_MODE";
396
+ };
397
+ /**
398
+ * Resolve the user-supplied config against environment variables and defaults.
399
+ *
400
+ * Precedence (highest → lowest): explicit argument → env var → built-in default.
401
+ * Throws `FreshSqueezyError` only for fields that cannot be defaulted (currently
402
+ * just `apiKey`), so callers can surface a clear setup error at construction
403
+ * time rather than at first request.
404
+ */
405
+ declare function resolveConfig(input?: FreshSqueezyConfig): ResolvedConfig;
406
+
407
+ /**
408
+ * Stable issue codes. Consumers may switch on these in CI — do not rename
409
+ * without a major version bump.
410
+ */
411
+ declare const ISSUE_CODES: {
412
+ readonly AUTH_FAILED: "AUTH_FAILED";
413
+ readonly MODE_MISMATCH: "MODE_MISMATCH";
414
+ readonly STORE_NOT_FOUND: "STORE_NOT_FOUND";
415
+ readonly STORE_NOT_OWNED: "STORE_NOT_OWNED";
416
+ readonly PRODUCT_NOT_FOUND: "PRODUCT_NOT_FOUND";
417
+ readonly PRODUCT_WRONG_STORE: "PRODUCT_WRONG_STORE";
418
+ readonly PRODUCT_UNPUBLISHED: "PRODUCT_UNPUBLISHED";
419
+ readonly PRODUCT_NO_BUY_URL: "PRODUCT_NO_BUY_URL";
420
+ readonly VARIANT_UNPUBLISHED: "VARIANT_UNPUBLISHED";
421
+ readonly VARIANT_MISSING: "VARIANT_MISSING";
422
+ readonly WEBHOOK_NOT_FOUND: "WEBHOOK_NOT_FOUND";
423
+ readonly WEBHOOK_EVENTS_MISSING: "WEBHOOK_EVENTS_MISSING";
424
+ readonly WEBHOOK_OPTIONAL_EVENTS: "WEBHOOK_OPTIONAL_EVENTS";
425
+ readonly NETWORK_ERROR: "NETWORK_ERROR";
426
+ readonly UNKNOWN: "UNKNOWN";
427
+ };
428
+ /**
429
+ * Build a `ValidationIssue` with defaults for the common case.
430
+ * Extracted so every validator produces consistently shaped issues.
431
+ */
432
+ declare function issue(code: string, severity: ValidationSeverity, message: string, extras?: {
433
+ suggestedFix?: string;
434
+ context?: ValidationIssue["context"];
435
+ }): ValidationIssue;
436
+ /**
437
+ * Fold an issue list into a boolean. Used by every validator so `ok` is
438
+ * computed the same way everywhere.
439
+ */
440
+ declare function isOk(issues: ValidationIssue[]): boolean;
441
+ /**
442
+ * Compose a `ValidationResult` with the `ok` flag derived from issues.
443
+ */
444
+ declare function buildResult<T>(name: string, mode: ValidationResult["mode"], issues: ValidationIssue[], resource?: T): ValidationResult<T>;
445
+
446
+ /**
447
+ * Variant attributes used by the product validator to detect
448
+ * variant/price drift from the product they belong to.
449
+ */
450
+ interface VariantAttributes {
451
+ product_id: number;
452
+ name: string;
453
+ slug: string;
454
+ description?: string | null;
455
+ status: "pending" | "draft" | "published";
456
+ is_subscription?: boolean;
457
+ interval?: string | null;
458
+ interval_count?: number | null;
459
+ has_license_keys?: boolean;
460
+ created_at?: string;
461
+ updated_at?: string;
462
+ }
463
+ declare function listVariantsForProduct(http: HttpClient, productId: string | number): Promise<JsonApiResource<VariantAttributes>[]>;
464
+
465
+ /**
466
+ * Support manifest: the locally reviewed source of truth for what
467
+ * fresh-squeezy explicitly understands on the Lemon Squeezy platform.
468
+ *
469
+ * The plan deliberately favors a static, reviewed manifest over live changelog
470
+ * scraping (see plan.md §Non-goals). When the platform adds new resources,
471
+ * fields, or webhook events, bump the entries below and re-snapshot the
472
+ * changelog page with `npm run check:changelog -- --update`.
473
+ *
474
+ * Changelog source: https://docs.lemonsqueezy.com/api/getting-started/changelog
475
+ * Last reviewed: 2026-04-24
476
+ */
477
+ /**
478
+ * Resources fresh-squeezy wraps today. Anything outside this list is still
479
+ * reachable via the raw `request()` escape hatch but has no dedicated
480
+ * validator.
481
+ */
482
+ declare const SUPPORTED_RESOURCES: readonly ["users", "stores", "products", "variants", "webhooks"];
483
+ /**
484
+ * Webhook events fresh-squeezy expects a production integration to subscribe to
485
+ * at minimum. Consumers can still subscribe to more; the validator only flags
486
+ * missing ones from this list.
487
+ *
488
+ * Rationale:
489
+ * - `order_*` covers one-off purchases and refunds.
490
+ * - `subscription_*` covers the recurring-billing lifecycle.
491
+ * - `subscription_payment_*` covers dunning / retry loops.
492
+ *
493
+ * Confirmed present in the Lemon Squeezy webhook topic list as of 2026-04-24.
494
+ */
495
+ declare const RECOMMENDED_WEBHOOK_EVENTS: readonly ["order_created", "order_refunded", "subscription_created", "subscription_updated", "subscription_cancelled", "subscription_resumed", "subscription_expired", "subscription_payment_success", "subscription_payment_failed"];
496
+ /**
497
+ * Newer or integration-specific events surfaced as info-level suggestions
498
+ * rather than errors. Missing these is common and not necessarily a
499
+ * misconfiguration.
500
+ *
501
+ * Per-entry changelog provenance (source:
502
+ * https://docs.lemonsqueezy.com/api/getting-started/changelog):
503
+ *
504
+ * - `customer_updated` — added 2026-02-25. Fires when a customer record
505
+ * changes (e.g. email, marketing consent). Needed if the app mirrors
506
+ * customer data locally.
507
+ * - `affiliate_activated` — added 2025-01-21 alongside the affiliates
508
+ * endpoints. Only relevant if the store has an affiliate program.
509
+ * - `license_key_created` / `license_key_updated` — License API events.
510
+ * Only relevant when variants have `has_license_keys: true`.
511
+ */
512
+ declare const OPTIONAL_WEBHOOK_EVENTS: readonly ["customer_updated", "affiliate_activated", "license_key_created", "license_key_updated"];
513
+ /**
514
+ * Platform additions we have read and decided *not* to validate against yet,
515
+ * documented here so maintainers see the deliberate gap during review.
516
+ *
517
+ * Tracked so the drift workflow has an "expected state" to compare against:
518
+ * if the changelog page changes and none of these items explain it, the diff
519
+ * is probably something new that needs a manifest update.
520
+ */
521
+ declare const ACKNOWLEDGED_CHANGELOG_ENTRIES: readonly [{
522
+ readonly date: "2026-02-25";
523
+ readonly summary: "Added customer_updated webhook event.";
524
+ readonly handledBy: "OPTIONAL_WEBHOOK_EVENTS";
525
+ }, {
526
+ readonly date: "2025-06-11";
527
+ readonly summary: "Added payment_processor attribute to Subscription objects.";
528
+ readonly handledBy: "Not wrapped — reachable via client.request('/v1/subscriptions/:id'). Add a validator only if a real integration needs it.";
529
+ }, {
530
+ readonly date: "2025-01-21";
531
+ readonly summary: "Added Affiliates endpoints and affiliate_activated webhook.";
532
+ readonly handledBy: "OPTIONAL_WEBHOOK_EVENTS (event only; resource stays v2 scope)";
533
+ }, {
534
+ readonly date: "2024-01-05";
535
+ readonly summary: "Added test_mode flag to /v1/users/me meta.";
536
+ readonly handledBy: "Read in validateConnection to emit MODE_MISMATCH when the key's true mode differs from the caller's declared mode.";
537
+ }];
538
+ type RecommendedEvent = (typeof RECOMMENDED_WEBHOOK_EVENTS)[number];
539
+ type OptionalEvent = (typeof OPTIONAL_WEBHOOK_EVENTS)[number];
540
+
541
+ export { ACKNOWLEDGED_CHANGELOG_ENTRIES, type AuthenticatedUserDocument, type ConnectionSummary, type DoctorOptions, type DoctorReport, ENV_KEYS, type FreshSqueezyClient, type FreshSqueezyConfig, FreshSqueezyError, ISSUE_CODES, type JsonApiCollection, type JsonApiDocument, type JsonApiResource, type Mode, OPTIONAL_WEBHOOK_EVENTS, type OptionalEvent, type ProductAttributes, type ProductValidationOptions, RECOMMENDED_WEBHOOK_EVENTS, type RecommendedEvent, type ResolvedConfig, SUPPORTED_RESOURCES, type StoreAttributes, type UserAttributes, type UserMeta, type ValidationIssue, type ValidationResult, type ValidationSeverity, type VariantAttributes, type WebhookAttributes, type WebhookValidationOptions, buildResult, createFreshSqueezy, doctor, getAuthenticatedUser, getProduct, getStore, isOk, issue, listProducts, listStores, listVariantsForProduct, listWebhooksForStore, resolveConfig, userResource, validateConnection, validateProduct, validateWebhook };