fresh-squeezy 0.1.7 → 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/dist/cli.js +391 -207
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +337 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +426 -26
- package/dist/index.d.ts +426 -26
- package/dist/index.js +330 -210
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,155 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import dotenv from "dotenv";
|
|
6
6
|
|
|
7
|
+
// src/cli/commands/augment.ts
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
10
|
+
import { dirname, resolve } from "path";
|
|
11
|
+
async function runAugmentCommand(options = {}) {
|
|
12
|
+
const cwd = options.cwd ?? process.cwd();
|
|
13
|
+
const outPath = resolve(cwd, options.out ?? "lemonsqueezy.augment.d.ts");
|
|
14
|
+
try {
|
|
15
|
+
const flavour = await detectFlavour(cwd, options.force ?? false);
|
|
16
|
+
const contents = renderAugmentationFile(flavour);
|
|
17
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
18
|
+
await writeFile(outPath, contents, "utf8");
|
|
19
|
+
process.stdout.write(`fresh-squeezy: wrote ${outPath} (${flavour}).
|
|
20
|
+
`);
|
|
21
|
+
if (flavour === "with-sdk") {
|
|
22
|
+
process.stdout.write(
|
|
23
|
+
`Use it via: import type { LemonSqueezyAugmented } from "./lemonsqueezy.augment";
|
|
24
|
+
`
|
|
25
|
+
);
|
|
26
|
+
} else {
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
`Use it via: import type { WithLatestLemonSqueezyFields } from "./lemonsqueezy.augment";
|
|
29
|
+
`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
process.stderr.write(`fresh-squeezy: ${message}
|
|
36
|
+
`);
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function detectFlavour(cwd, force) {
|
|
41
|
+
if (force) return "generic";
|
|
42
|
+
const pkgPath = resolve(cwd, "package.json");
|
|
43
|
+
if (!existsSync(pkgPath)) return "generic";
|
|
44
|
+
const raw = await readFile(pkgPath, "utf8");
|
|
45
|
+
const pkg = JSON.parse(raw);
|
|
46
|
+
const deps = {
|
|
47
|
+
...pkg.dependencies ?? {},
|
|
48
|
+
...pkg.devDependencies ?? {},
|
|
49
|
+
...pkg.peerDependencies ?? {}
|
|
50
|
+
};
|
|
51
|
+
return "@lemonsqueezy/lemonsqueezy.js" in deps ? "with-sdk" : "generic";
|
|
52
|
+
}
|
|
53
|
+
function renderAugmentationFile(flavour) {
|
|
54
|
+
const header = `/**
|
|
55
|
+
* Generated by \`fresh-squeezy types:augment\` \u2014 re-run after upgrading
|
|
56
|
+
* fresh-squeezy to refresh the field set.
|
|
57
|
+
*
|
|
58
|
+
* Why this file exists: the live Lemon Squeezy API has acquired fields
|
|
59
|
+
* since the SDK / hand-rolled types in this repo were last updated
|
|
60
|
+
* (e.g. \`payment_processor\`, \`tax_inclusive\`, \`refunded_amount*\`,
|
|
61
|
+
* \`setup_fee*\`, \`unit_price_decimal\`). fresh-squeezy tracks them in its
|
|
62
|
+
* manifest; this file makes them appear on your existing resource types
|
|
63
|
+
* so you don't hand-roll the additions.
|
|
64
|
+
*
|
|
65
|
+
* To wire it in:
|
|
66
|
+
* 1. Make sure your tsconfig.json's "include" covers this file (most
|
|
67
|
+
* do by default if it's in the project root or under "src").
|
|
68
|
+
* 2. Import the augmented type from this file instead of the raw SDK
|
|
69
|
+
* type \u2014 see the example at the bottom of this file.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
`;
|
|
73
|
+
if (flavour === "with-sdk") {
|
|
74
|
+
return header + `import type {
|
|
75
|
+
Subscription,
|
|
76
|
+
Order,
|
|
77
|
+
Variant,
|
|
78
|
+
Price,
|
|
79
|
+
} from "@lemonsqueezy/lemonsqueezy.js";
|
|
80
|
+
import type {
|
|
81
|
+
LatestSubscriptionFields,
|
|
82
|
+
LatestOrderFields,
|
|
83
|
+
LatestVariantFields,
|
|
84
|
+
LatestPriceFields,
|
|
85
|
+
} from "fresh-squeezy";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One-stop reference for resources fresh-squeezy has wider knowledge of
|
|
89
|
+
* than the official SDK. The shape mirrors the SDK's response envelope
|
|
90
|
+
* (\`{ data: { attributes: ... } }\`) \u2014 only the inner attributes are
|
|
91
|
+
* widened.
|
|
92
|
+
*/
|
|
93
|
+
export interface LemonSqueezyAugmented {
|
|
94
|
+
subscription: AugmentResponse<Subscription, LatestSubscriptionFields>;
|
|
95
|
+
order: AugmentResponse<Order, LatestOrderFields>;
|
|
96
|
+
variant: AugmentResponse<Variant, LatestVariantFields>;
|
|
97
|
+
price: AugmentResponse<Price, LatestPriceFields>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Helper that walks the official SDK's response envelope and intersects
|
|
102
|
+
* the inner \`attributes\` record with the supplied field set.
|
|
103
|
+
*/
|
|
104
|
+
type AugmentResponse<T, Fields> = T extends {
|
|
105
|
+
data: infer Data;
|
|
106
|
+
}
|
|
107
|
+
? Data extends { attributes: infer Attrs }
|
|
108
|
+
? Omit<T, "data"> & {
|
|
109
|
+
data: Omit<Data, "attributes"> & { attributes: Attrs & Fields };
|
|
110
|
+
}
|
|
111
|
+
: T
|
|
112
|
+
: T;
|
|
113
|
+
|
|
114
|
+
/* Example:
|
|
115
|
+
*
|
|
116
|
+
* import type { LemonSqueezyAugmented } from "./lemonsqueezy.augment";
|
|
117
|
+
*
|
|
118
|
+
* function describe(sub: LemonSqueezyAugmented["subscription"]) {
|
|
119
|
+
* const attrs = sub.data.attributes;
|
|
120
|
+
* console.log(attrs.payment_processor); // \u2190 typed, no manual extension
|
|
121
|
+
* console.log(attrs.urls?.update_customer_portal);
|
|
122
|
+
* }
|
|
123
|
+
*/
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
return header + `import type {
|
|
127
|
+
WithLatestLemonSqueezyFields,
|
|
128
|
+
LatestSubscriptionFields,
|
|
129
|
+
LatestOrderFields,
|
|
130
|
+
LatestVariantFields,
|
|
131
|
+
LatestPriceFields,
|
|
132
|
+
} from "fresh-squeezy";
|
|
133
|
+
|
|
134
|
+
export type {
|
|
135
|
+
WithLatestLemonSqueezyFields,
|
|
136
|
+
LatestSubscriptionFields,
|
|
137
|
+
LatestOrderFields,
|
|
138
|
+
LatestVariantFields,
|
|
139
|
+
LatestPriceFields,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/* Example with a hand-rolled or third-party type:
|
|
143
|
+
*
|
|
144
|
+
* import type { WithLatestLemonSqueezyFields } from "./lemonsqueezy.augment";
|
|
145
|
+
*
|
|
146
|
+
* type MySubscription = WithLatestLemonSqueezyFields<MyExistingSubscription, "subscription">;
|
|
147
|
+
*
|
|
148
|
+
* function describe(sub: MySubscription) {
|
|
149
|
+
* console.log(sub.payment_processor); // typed
|
|
150
|
+
* console.log(sub.tax_inclusive); // typed
|
|
151
|
+
* }
|
|
152
|
+
*/
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
|
|
7
156
|
// src/core/errors.ts
|
|
8
157
|
var FreshSqueezyError = class _FreshSqueezyError extends Error {
|
|
9
158
|
code;
|
|
@@ -97,14 +246,41 @@ var HttpClient = class {
|
|
|
97
246
|
return doc.data;
|
|
98
247
|
}
|
|
99
248
|
/**
|
|
100
|
-
* Fetch a JSON:API collection
|
|
101
|
-
*
|
|
102
|
-
*
|
|
249
|
+
* Fetch a single page of a JSON:API collection. Most callers want
|
|
250
|
+
* `paginate()` instead — this method is kept for one-shot lookups where
|
|
251
|
+
* the caller knows the result fits in a single page.
|
|
103
252
|
*/
|
|
104
253
|
async getCollection(path2, query) {
|
|
105
254
|
const doc = await this.request({ path: path2, query });
|
|
106
255
|
return doc.data;
|
|
107
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Walk a JSON:API collection across every page and concatenate the
|
|
259
|
+
* results. Lemon Squeezy returns 25 items per page by default; without
|
|
260
|
+
* pagination, validators silently miss anything past the first page (e.g.
|
|
261
|
+
* `validateWebhook` on a store with 26+ webhooks would falsely report
|
|
262
|
+
* `WEBHOOK_NOT_FOUND` for any webhook on page 2).
|
|
263
|
+
*
|
|
264
|
+
* Stops as soon as `meta.page.lastPage` is reached. Falls back to a
|
|
265
|
+
* single-page fetch if the API does not surface page metadata.
|
|
266
|
+
*
|
|
267
|
+
* Caller-supplied `page[number]` is honored as the starting page; we
|
|
268
|
+
* advance from there. `page[size]` is left untouched so callers can tune
|
|
269
|
+
* batch size (max 100 per Lemon Squeezy docs).
|
|
270
|
+
*/
|
|
271
|
+
async paginate(path2, query) {
|
|
272
|
+
const startPage = Number(query?.["page[number]"] ?? 1);
|
|
273
|
+
const all = [];
|
|
274
|
+
let pageNumber = startPage;
|
|
275
|
+
while (true) {
|
|
276
|
+
const pagedQuery = { ...query ?? {}, "page[number]": pageNumber };
|
|
277
|
+
const doc = await this.request({ path: path2, query: pagedQuery });
|
|
278
|
+
all.push(...doc.data);
|
|
279
|
+
const lastPage = doc.meta?.page?.lastPage;
|
|
280
|
+
if (lastPage === void 0 || pageNumber >= lastPage) return all;
|
|
281
|
+
pageNumber += 1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
108
284
|
buildUrl(path2, query) {
|
|
109
285
|
const url = new URL(path2, this.config.baseUrl);
|
|
110
286
|
if (query) {
|
|
@@ -144,12 +320,19 @@ async function getAuthenticatedUser(http) {
|
|
|
144
320
|
return http.request({ path: "/v1/users/me" });
|
|
145
321
|
}
|
|
146
322
|
|
|
323
|
+
// src/core/mode.ts
|
|
324
|
+
function resolveActualMode(testMode) {
|
|
325
|
+
if (testMode === true) return "test";
|
|
326
|
+
if (testMode === false) return "live";
|
|
327
|
+
return void 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
147
330
|
// src/resources/stores.ts
|
|
148
331
|
async function getStore(http, storeId) {
|
|
149
332
|
return http.getResource(`/v1/stores/${storeId}`);
|
|
150
333
|
}
|
|
151
334
|
async function listStores(http) {
|
|
152
|
-
return http.
|
|
335
|
+
return http.paginate("/v1/stores");
|
|
153
336
|
}
|
|
154
337
|
|
|
155
338
|
// src/validate/rules.ts
|
|
@@ -209,6 +392,72 @@ function buildResult(name, mode, issues, resource) {
|
|
|
209
392
|
return result;
|
|
210
393
|
}
|
|
211
394
|
|
|
395
|
+
// src/validate/probe.ts
|
|
396
|
+
function mapErrorToIssue(err, mapping = {}) {
|
|
397
|
+
if (err instanceof FreshSqueezyError) {
|
|
398
|
+
if (mapping.unauthorized && (err.status === 401 || err.code === "UNAUTHORIZED")) {
|
|
399
|
+
return issue(mapping.unauthorized.code, "error", mapping.unauthorized.message, {
|
|
400
|
+
...mapping.unauthorized.suggestedFix !== void 0 ? { suggestedFix: mapping.unauthorized.suggestedFix } : {},
|
|
401
|
+
context: { status: err.status ?? null }
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (mapping.notFound && err.status === 404) {
|
|
405
|
+
return issue(mapping.notFound.code, "error", mapping.notFound.message, {
|
|
406
|
+
...mapping.notFound.suggestedFix !== void 0 ? { suggestedFix: mapping.notFound.suggestedFix } : {},
|
|
407
|
+
...mapping.notFound.context !== void 0 ? { context: mapping.notFound.context } : {}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
if (err.code === "NETWORK_ERROR") {
|
|
411
|
+
return issue(
|
|
412
|
+
ISSUE_CODES.NETWORK_ERROR,
|
|
413
|
+
"error",
|
|
414
|
+
`Could not reach Lemon Squeezy: ${err.message}`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
|
|
418
|
+
context: { status: err.status ?? null, code: err.code }
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
422
|
+
return issue(ISSUE_CODES.UNKNOWN, "error", message);
|
|
423
|
+
}
|
|
424
|
+
async function probeFetch(fetcher, options) {
|
|
425
|
+
try {
|
|
426
|
+
return { ok: true, resource: await fetcher() };
|
|
427
|
+
} catch (err) {
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
issue: mapErrorToIssue(err, {
|
|
431
|
+
notFound: {
|
|
432
|
+
code: options.notFoundCode,
|
|
433
|
+
message: options.notFoundMessage,
|
|
434
|
+
...options.notFoundFix !== void 0 ? { suggestedFix: options.notFoundFix } : {},
|
|
435
|
+
...options.notFoundContext !== void 0 ? { context: options.notFoundContext } : {}
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function checkStoreOwnership(check) {
|
|
442
|
+
const expected = String(check.expectedStoreId);
|
|
443
|
+
const actual = String(check.actualStoreId);
|
|
444
|
+
if (expected === actual) return null;
|
|
445
|
+
const context = {
|
|
446
|
+
expectedStoreId: expected,
|
|
447
|
+
actualStoreId: actual,
|
|
448
|
+
...check.extraContext ?? {}
|
|
449
|
+
};
|
|
450
|
+
return issue(
|
|
451
|
+
check.code,
|
|
452
|
+
"error",
|
|
453
|
+
`${check.label} belongs to store ${actual}, expected ${expected}.`,
|
|
454
|
+
{
|
|
455
|
+
...check.suggestedFix !== void 0 ? { suggestedFix: check.suggestedFix } : {},
|
|
456
|
+
context
|
|
457
|
+
}
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
212
461
|
// src/validate/connection.ts
|
|
213
462
|
async function validateConnection(http, mode) {
|
|
214
463
|
const issues = [];
|
|
@@ -248,67 +497,33 @@ async function validateConnection(http, mode) {
|
|
|
248
497
|
}
|
|
249
498
|
return buildResult("connection", mode, issues, summary);
|
|
250
499
|
} catch (err) {
|
|
251
|
-
issues.push(
|
|
500
|
+
issues.push(
|
|
501
|
+
mapErrorToIssue(err, {
|
|
502
|
+
unauthorized: {
|
|
503
|
+
code: ISSUE_CODES.AUTH_FAILED,
|
|
504
|
+
message: "API key rejected by Lemon Squeezy.",
|
|
505
|
+
suggestedFix: "Regenerate the key at https://app.lemonsqueezy.com/settings/api."
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
);
|
|
252
509
|
return buildResult("connection", mode, issues);
|
|
253
510
|
}
|
|
254
511
|
}
|
|
255
|
-
function resolveActualMode(testMode) {
|
|
256
|
-
if (testMode === true) return "test";
|
|
257
|
-
if (testMode === false) return "live";
|
|
258
|
-
return void 0;
|
|
259
|
-
}
|
|
260
|
-
function toConnectionIssue(err) {
|
|
261
|
-
if (err instanceof FreshSqueezyError) {
|
|
262
|
-
if (err.code === "UNAUTHORIZED") {
|
|
263
|
-
return issue(ISSUE_CODES.AUTH_FAILED, "error", "API key rejected by Lemon Squeezy.", {
|
|
264
|
-
suggestedFix: "Regenerate the key at https://app.lemonsqueezy.com/settings/api.",
|
|
265
|
-
context: { status: err.status ?? null }
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
if (err.code === "NETWORK_ERROR") {
|
|
269
|
-
return issue(ISSUE_CODES.NETWORK_ERROR, "error", `Could not reach Lemon Squeezy: ${err.message}`);
|
|
270
|
-
}
|
|
271
|
-
return issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
|
|
272
|
-
context: { status: err.status ?? null, code: err.code }
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
276
|
-
return issue(ISSUE_CODES.UNKNOWN, "error", message);
|
|
277
|
-
}
|
|
278
512
|
|
|
279
513
|
// src/validate/store.ts
|
|
280
514
|
async function validateStore(http, mode, storeId) {
|
|
281
515
|
const issues = [];
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
"error",
|
|
291
|
-
`Store ${storeId} not found with the current API key.`,
|
|
292
|
-
{
|
|
293
|
-
suggestedFix: "Check the store ID and confirm the API key belongs to the account that owns it.",
|
|
294
|
-
context: { storeId: String(storeId) }
|
|
295
|
-
}
|
|
296
|
-
)
|
|
297
|
-
);
|
|
298
|
-
return buildResult("store", mode, issues);
|
|
299
|
-
}
|
|
300
|
-
if (err instanceof FreshSqueezyError) {
|
|
301
|
-
issues.push(
|
|
302
|
-
issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
|
|
303
|
-
context: { status: err.status ?? null, code: err.code }
|
|
304
|
-
})
|
|
305
|
-
);
|
|
306
|
-
return buildResult("store", mode, issues);
|
|
307
|
-
}
|
|
308
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
309
|
-
issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
|
|
516
|
+
const fetched = await probeFetch(() => getStore(http, storeId), {
|
|
517
|
+
notFoundCode: ISSUE_CODES.STORE_NOT_FOUND,
|
|
518
|
+
notFoundMessage: `Store ${storeId} not found with the current API key.`,
|
|
519
|
+
notFoundFix: "Check the store ID and confirm the API key belongs to the account that owns it.",
|
|
520
|
+
notFoundContext: { storeId: String(storeId) }
|
|
521
|
+
});
|
|
522
|
+
if (!fetched.ok) {
|
|
523
|
+
issues.push(fetched.issue);
|
|
310
524
|
return buildResult("store", mode, issues);
|
|
311
525
|
}
|
|
526
|
+
return buildResult("store", mode, issues, fetched.resource.attributes);
|
|
312
527
|
}
|
|
313
528
|
|
|
314
529
|
// src/resources/products.ts
|
|
@@ -321,7 +536,7 @@ async function getVariant(http, variantId) {
|
|
|
321
536
|
return http.getResource(`/v1/variants/${variantId}`);
|
|
322
537
|
}
|
|
323
538
|
async function listVariantsForProduct(http, productId) {
|
|
324
|
-
return http.
|
|
539
|
+
return http.paginate("/v1/variants", {
|
|
325
540
|
"filter[product_id]": String(productId)
|
|
326
541
|
});
|
|
327
542
|
}
|
|
@@ -329,45 +544,26 @@ async function listVariantsForProduct(http, productId) {
|
|
|
329
544
|
// src/validate/product.ts
|
|
330
545
|
async function validateProduct(http, mode, options) {
|
|
331
546
|
const issues = [];
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
"error",
|
|
341
|
-
`Product ${options.productId} not found.`,
|
|
342
|
-
{
|
|
343
|
-
suggestedFix: "Verify the product ID in the Lemon Squeezy dashboard.",
|
|
344
|
-
context: { productId: String(options.productId) }
|
|
345
|
-
}
|
|
346
|
-
)
|
|
347
|
-
);
|
|
348
|
-
return buildResult("product", mode, issues);
|
|
349
|
-
}
|
|
350
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
351
|
-
issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
|
|
547
|
+
const fetched = await probeFetch(() => getProduct(http, options.productId), {
|
|
548
|
+
notFoundCode: ISSUE_CODES.PRODUCT_NOT_FOUND,
|
|
549
|
+
notFoundMessage: `Product ${options.productId} not found.`,
|
|
550
|
+
notFoundFix: "Verify the product ID in the Lemon Squeezy dashboard.",
|
|
551
|
+
notFoundContext: { productId: String(options.productId) }
|
|
552
|
+
});
|
|
553
|
+
if (!fetched.ok) {
|
|
554
|
+
issues.push(fetched.issue);
|
|
352
555
|
return buildResult("product", mode, issues);
|
|
353
556
|
}
|
|
354
|
-
const attrs =
|
|
557
|
+
const attrs = fetched.resource.attributes;
|
|
355
558
|
if (options.expectedStoreId !== void 0) {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
{
|
|
365
|
-
suggestedFix: "Either use the correct store ID or the correct product ID \u2014 IDs should not cross stores.",
|
|
366
|
-
context: { expectedStoreId: expected, actualStoreId: actual }
|
|
367
|
-
}
|
|
368
|
-
)
|
|
369
|
-
);
|
|
370
|
-
}
|
|
559
|
+
const mismatch = checkStoreOwnership({
|
|
560
|
+
expectedStoreId: options.expectedStoreId,
|
|
561
|
+
actualStoreId: attrs.store_id,
|
|
562
|
+
code: ISSUE_CODES.PRODUCT_WRONG_STORE,
|
|
563
|
+
label: "Product",
|
|
564
|
+
suggestedFix: "Either use the correct store ID or the correct product ID \u2014 IDs should not cross stores."
|
|
565
|
+
});
|
|
566
|
+
if (mismatch) issues.push(mismatch);
|
|
371
567
|
}
|
|
372
568
|
if (attrs.status !== "published") {
|
|
373
569
|
issues.push(
|
|
@@ -420,9 +616,17 @@ async function validateProduct(http, mode, options) {
|
|
|
420
616
|
return buildResult("product", mode, issues, attrs);
|
|
421
617
|
}
|
|
422
618
|
|
|
619
|
+
// src/core/equality.ts
|
|
620
|
+
function sameWebhookUrl(a, b) {
|
|
621
|
+
return normalizeWebhookUrl(a) === normalizeWebhookUrl(b);
|
|
622
|
+
}
|
|
623
|
+
function normalizeWebhookUrl(raw) {
|
|
624
|
+
return raw.replace(/\/+$/, "").toLowerCase();
|
|
625
|
+
}
|
|
626
|
+
|
|
423
627
|
// src/resources/webhooks.ts
|
|
424
628
|
async function listWebhooksForStore(http, storeId) {
|
|
425
|
-
return http.
|
|
629
|
+
return http.paginate("/v1/webhooks", {
|
|
426
630
|
"filter[store_id]": String(storeId)
|
|
427
631
|
});
|
|
428
632
|
}
|
|
@@ -465,7 +669,7 @@ async function validateWebhook(http, mode, options) {
|
|
|
465
669
|
issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
|
|
466
670
|
return buildResult("webhook", mode, issues);
|
|
467
671
|
}
|
|
468
|
-
const match = webhooks.find((webhook) =>
|
|
672
|
+
const match = webhooks.find((webhook) => sameWebhookUrl(webhook.attributes.url, options.url));
|
|
469
673
|
if (!match) {
|
|
470
674
|
issues.push(
|
|
471
675
|
issue(
|
|
@@ -508,9 +712,6 @@ async function validateWebhook(http, mode, options) {
|
|
|
508
712
|
}
|
|
509
713
|
return buildResult("webhook", mode, issues, match.attributes);
|
|
510
714
|
}
|
|
511
|
-
function normalizeUrl(raw) {
|
|
512
|
-
return raw.replace(/\/+$/, "").toLowerCase();
|
|
513
|
-
}
|
|
514
715
|
|
|
515
716
|
// src/resources/discounts.ts
|
|
516
717
|
async function getDiscount(http, discountId) {
|
|
@@ -520,39 +721,25 @@ async function getDiscount(http, discountId) {
|
|
|
520
721
|
// src/validate/discount.ts
|
|
521
722
|
async function validateDiscount(http, mode, options) {
|
|
522
723
|
const issues = [];
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
context: { discountId: String(options.discountId) }
|
|
532
|
-
})
|
|
533
|
-
);
|
|
534
|
-
return buildResult("discount", mode, issues);
|
|
535
|
-
}
|
|
536
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
537
|
-
issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
|
|
724
|
+
const fetched = await probeFetch(() => getDiscount(http, options.discountId), {
|
|
725
|
+
notFoundCode: ISSUE_CODES.DISCOUNT_NOT_FOUND,
|
|
726
|
+
notFoundMessage: `Discount ${options.discountId} not found.`,
|
|
727
|
+
notFoundFix: "Verify the discount ID in the Lemon Squeezy dashboard.",
|
|
728
|
+
notFoundContext: { discountId: String(options.discountId) }
|
|
729
|
+
});
|
|
730
|
+
if (!fetched.ok) {
|
|
731
|
+
issues.push(fetched.issue);
|
|
538
732
|
return buildResult("discount", mode, issues);
|
|
539
733
|
}
|
|
540
|
-
const attrs =
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
{
|
|
550
|
-
suggestedFix: "Use the correct store ID or discount ID \u2014 discounts should not cross stores.",
|
|
551
|
-
context: { expectedStoreId: expectedStore, actualStoreId: actualStore }
|
|
552
|
-
}
|
|
553
|
-
)
|
|
554
|
-
);
|
|
555
|
-
}
|
|
734
|
+
const attrs = fetched.resource.attributes;
|
|
735
|
+
const mismatch = checkStoreOwnership({
|
|
736
|
+
expectedStoreId: options.storeId,
|
|
737
|
+
actualStoreId: attrs.store_id,
|
|
738
|
+
code: ISSUE_CODES.DISCOUNT_STORE_MISMATCH,
|
|
739
|
+
label: "Discount",
|
|
740
|
+
suggestedFix: "Use the correct store ID or discount ID \u2014 discounts should not cross stores."
|
|
741
|
+
});
|
|
742
|
+
if (mismatch) issues.push(mismatch);
|
|
556
743
|
if (attrs.status === "draft") {
|
|
557
744
|
issues.push(
|
|
558
745
|
issue(
|
|
@@ -642,44 +829,25 @@ async function getLicenseKey(http, licenseKeyId) {
|
|
|
642
829
|
// src/validate/licenseKey.ts
|
|
643
830
|
async function validateLicenseKey(http, mode, options) {
|
|
644
831
|
const issues = [];
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
"error",
|
|
654
|
-
`License key ${options.licenseKeyId} not found.`,
|
|
655
|
-
{
|
|
656
|
-
suggestedFix: "Verify the license key ID in the Lemon Squeezy dashboard.",
|
|
657
|
-
context: { licenseKeyId: String(options.licenseKeyId) }
|
|
658
|
-
}
|
|
659
|
-
)
|
|
660
|
-
);
|
|
661
|
-
return buildResult("licenseKey", mode, issues);
|
|
662
|
-
}
|
|
663
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
664
|
-
issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
|
|
832
|
+
const fetched = await probeFetch(() => getLicenseKey(http, options.licenseKeyId), {
|
|
833
|
+
notFoundCode: ISSUE_CODES.LICENSE_KEY_NOT_FOUND,
|
|
834
|
+
notFoundMessage: `License key ${options.licenseKeyId} not found.`,
|
|
835
|
+
notFoundFix: "Verify the license key ID in the Lemon Squeezy dashboard.",
|
|
836
|
+
notFoundContext: { licenseKeyId: String(options.licenseKeyId) }
|
|
837
|
+
});
|
|
838
|
+
if (!fetched.ok) {
|
|
839
|
+
issues.push(fetched.issue);
|
|
665
840
|
return buildResult("licenseKey", mode, issues);
|
|
666
841
|
}
|
|
667
|
-
const attrs =
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
{
|
|
677
|
-
suggestedFix: "Use the correct store ID or license key ID \u2014 keys should not cross stores.",
|
|
678
|
-
context: { expectedStoreId: expectedStore, actualStoreId: actualStore }
|
|
679
|
-
}
|
|
680
|
-
)
|
|
681
|
-
);
|
|
682
|
-
}
|
|
842
|
+
const attrs = fetched.resource.attributes;
|
|
843
|
+
const mismatch = checkStoreOwnership({
|
|
844
|
+
expectedStoreId: options.storeId,
|
|
845
|
+
actualStoreId: attrs.store_id,
|
|
846
|
+
code: ISSUE_CODES.LICENSE_KEY_STORE_MISMATCH,
|
|
847
|
+
label: "License key",
|
|
848
|
+
suggestedFix: "Use the correct store ID or license key ID \u2014 keys should not cross stores."
|
|
849
|
+
});
|
|
850
|
+
if (mismatch) issues.push(mismatch);
|
|
683
851
|
if (attrs.disabled) {
|
|
684
852
|
issues.push(
|
|
685
853
|
issue(
|
|
@@ -730,28 +898,20 @@ async function validateLicenseKey(http, mode, options) {
|
|
|
730
898
|
var VALID_INTERVALS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
|
|
731
899
|
async function validateSubscriptionPlan(http, mode, options) {
|
|
732
900
|
const issues = [];
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
ISSUE_CODES.PLAN_VARIANT_NOT_FOUND,
|
|
741
|
-
"error",
|
|
742
|
-
`Variant ${options.variantId} not found.`,
|
|
743
|
-
{
|
|
744
|
-
suggestedFix: "Verify the variant ID in the Lemon Squeezy dashboard.",
|
|
745
|
-
context: { variantId: String(options.variantId) }
|
|
746
|
-
}
|
|
747
|
-
)
|
|
748
|
-
);
|
|
749
|
-
return buildResult("subscriptionPlan", mode, issues);
|
|
901
|
+
const fetched = await probeFetch(
|
|
902
|
+
() => getVariant(http, options.variantId),
|
|
903
|
+
{
|
|
904
|
+
notFoundCode: ISSUE_CODES.PLAN_VARIANT_NOT_FOUND,
|
|
905
|
+
notFoundMessage: `Variant ${options.variantId} not found.`,
|
|
906
|
+
notFoundFix: "Verify the variant ID in the Lemon Squeezy dashboard.",
|
|
907
|
+
notFoundContext: { variantId: String(options.variantId) }
|
|
750
908
|
}
|
|
751
|
-
|
|
752
|
-
|
|
909
|
+
);
|
|
910
|
+
if (!fetched.ok) {
|
|
911
|
+
issues.push(fetched.issue);
|
|
753
912
|
return buildResult("subscriptionPlan", mode, issues);
|
|
754
913
|
}
|
|
914
|
+
const variant = fetched.resource;
|
|
755
915
|
const attrs = variant.attributes;
|
|
756
916
|
if (!attrs.is_subscription) {
|
|
757
917
|
issues.push(
|
|
@@ -836,21 +996,15 @@ async function validateSubscriptionPlan(http, mode, options) {
|
|
|
836
996
|
}
|
|
837
997
|
try {
|
|
838
998
|
const product = await getProduct(http, attrs.product_id);
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
|
|
849
|
-
context: { expectedStoreId: expectedStore, actualStoreId: actualStore, productId: String(attrs.product_id) }
|
|
850
|
-
}
|
|
851
|
-
)
|
|
852
|
-
);
|
|
853
|
-
}
|
|
999
|
+
const mismatch = checkStoreOwnership({
|
|
1000
|
+
expectedStoreId: options.storeId,
|
|
1001
|
+
actualStoreId: product.attributes.store_id,
|
|
1002
|
+
code: ISSUE_CODES.PLAN_STORE_MISMATCH,
|
|
1003
|
+
label: `Subscription variant (via product ${attrs.product_id})`,
|
|
1004
|
+
suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
|
|
1005
|
+
extraContext: { productId: String(attrs.product_id) }
|
|
1006
|
+
});
|
|
1007
|
+
if (mismatch) issues.push(mismatch);
|
|
854
1008
|
} catch {
|
|
855
1009
|
}
|
|
856
1010
|
const summary = {
|
|
@@ -865,19 +1019,29 @@ async function validateSubscriptionPlan(http, mode, options) {
|
|
|
865
1019
|
}
|
|
866
1020
|
|
|
867
1021
|
// src/validate/doctor.ts
|
|
868
|
-
|
|
1022
|
+
var DEFAULT_VALIDATORS = {
|
|
1023
|
+
connection: validateConnection,
|
|
1024
|
+
store: validateStore,
|
|
1025
|
+
product: validateProduct,
|
|
1026
|
+
webhook: validateWebhook,
|
|
1027
|
+
discount: validateDiscount,
|
|
1028
|
+
licenseKey: validateLicenseKey,
|
|
1029
|
+
subscriptionPlan: validateSubscriptionPlan
|
|
1030
|
+
};
|
|
1031
|
+
async function doctor(http, mode, options = {}, validators = DEFAULT_VALIDATORS) {
|
|
1032
|
+
assertOptionsCoherent(options);
|
|
869
1033
|
const results = [];
|
|
870
|
-
const connection = await
|
|
1034
|
+
const connection = await validators.connection(http, mode);
|
|
871
1035
|
results.push(connection);
|
|
872
1036
|
if (!connection.ok) {
|
|
873
1037
|
return { ok: false, mode, results };
|
|
874
1038
|
}
|
|
875
1039
|
if (options.storeId !== void 0) {
|
|
876
|
-
results.push(await
|
|
1040
|
+
results.push(await validators.store(http, mode, options.storeId));
|
|
877
1041
|
}
|
|
878
1042
|
if (options.productId !== void 0) {
|
|
879
1043
|
results.push(
|
|
880
|
-
await
|
|
1044
|
+
await validators.product(http, mode, {
|
|
881
1045
|
productId: options.productId,
|
|
882
1046
|
expectedStoreId: options.storeId
|
|
883
1047
|
})
|
|
@@ -885,7 +1049,7 @@ async function doctor(http, mode, options = {}) {
|
|
|
885
1049
|
}
|
|
886
1050
|
if (options.storeId !== void 0 && options.webhookUrl !== void 0) {
|
|
887
1051
|
results.push(
|
|
888
|
-
await
|
|
1052
|
+
await validators.webhook(http, mode, {
|
|
889
1053
|
storeId: options.storeId,
|
|
890
1054
|
url: options.webhookUrl
|
|
891
1055
|
})
|
|
@@ -893,7 +1057,7 @@ async function doctor(http, mode, options = {}) {
|
|
|
893
1057
|
}
|
|
894
1058
|
if (options.storeId !== void 0 && options.discountId !== void 0) {
|
|
895
1059
|
results.push(
|
|
896
|
-
await
|
|
1060
|
+
await validators.discount(http, mode, {
|
|
897
1061
|
storeId: options.storeId,
|
|
898
1062
|
discountId: options.discountId
|
|
899
1063
|
})
|
|
@@ -901,7 +1065,7 @@ async function doctor(http, mode, options = {}) {
|
|
|
901
1065
|
}
|
|
902
1066
|
if (options.storeId !== void 0 && options.licenseKeyId !== void 0) {
|
|
903
1067
|
results.push(
|
|
904
|
-
await
|
|
1068
|
+
await validators.licenseKey(http, mode, {
|
|
905
1069
|
storeId: options.storeId,
|
|
906
1070
|
licenseKeyId: options.licenseKeyId
|
|
907
1071
|
})
|
|
@@ -909,7 +1073,7 @@ async function doctor(http, mode, options = {}) {
|
|
|
909
1073
|
}
|
|
910
1074
|
if (options.storeId !== void 0 && options.variantId !== void 0) {
|
|
911
1075
|
results.push(
|
|
912
|
-
await
|
|
1076
|
+
await validators.subscriptionPlan(http, mode, {
|
|
913
1077
|
storeId: options.storeId,
|
|
914
1078
|
variantId: options.variantId
|
|
915
1079
|
})
|
|
@@ -918,6 +1082,19 @@ async function doctor(http, mode, options = {}) {
|
|
|
918
1082
|
const ok = results.every((result) => result.ok);
|
|
919
1083
|
return { ok, mode, results };
|
|
920
1084
|
}
|
|
1085
|
+
function assertOptionsCoherent(options) {
|
|
1086
|
+
if (options.storeId !== void 0) return;
|
|
1087
|
+
const dependent = [];
|
|
1088
|
+
if (options.webhookUrl !== void 0) dependent.push("webhookUrl");
|
|
1089
|
+
if (options.discountId !== void 0) dependent.push("discountId");
|
|
1090
|
+
if (options.licenseKeyId !== void 0) dependent.push("licenseKeyId");
|
|
1091
|
+
if (options.variantId !== void 0) dependent.push("variantId");
|
|
1092
|
+
if (dependent.length === 0) return;
|
|
1093
|
+
throw new FreshSqueezyError({
|
|
1094
|
+
code: "DOCTOR_OPTIONS_INVALID",
|
|
1095
|
+
message: `doctor() received ${dependent.join(", ")} without storeId. These validators cannot run without a store; pass \`storeId\` or remove the dependent options.`
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
921
1098
|
|
|
922
1099
|
// src/createFreshSqueezy.ts
|
|
923
1100
|
function createFreshSqueezy(config = {}) {
|
|
@@ -1363,6 +1540,13 @@ program.command("init").description("Interactive setup: ask for credentials, pic
|
|
|
1363
1540
|
const code = await runInitCommand({ envFile: opts.envFile });
|
|
1364
1541
|
process.exit(code);
|
|
1365
1542
|
});
|
|
1543
|
+
var types = program.command("types").description("Type-augmentation utilities for the official Lemon Squeezy SDK and hand-rolled types");
|
|
1544
|
+
types.command("augment").description(
|
|
1545
|
+
"Generate a .d.ts that intersects your Lemon Squeezy resource types with fields fresh-squeezy already tracks (e.g. payment_processor, tax_inclusive, refunded_amount*)."
|
|
1546
|
+
).option("--out <path>", "Output path (default: lemonsqueezy.augment.d.ts in CWD)").option("--force", "Emit the generic file even when @lemonsqueezy/lemonsqueezy.js is detected").action(async (opts) => {
|
|
1547
|
+
const code = await runAugmentCommand({ out: opts.out, force: Boolean(opts.force) });
|
|
1548
|
+
process.exit(code);
|
|
1549
|
+
});
|
|
1366
1550
|
program.parseAsync(process.argv).catch((err) => {
|
|
1367
1551
|
const message = err instanceof Error ? err.message : String(err);
|
|
1368
1552
|
process.stderr.write(`fresh-squeezy: ${message}
|