@checkbox-site/cli 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/checkbox.js +338 -56
- package/package.json +16 -13
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ both exist, the CLI prints which one it used (stderr), and `checkbox whoami` sho
|
|
|
75
75
|
|
|
76
76
|
| Command | What it does |
|
|
77
77
|
|---|---|
|
|
78
|
-
| `checkbox init [--yes] [--json]` | one browser approval (device login), then keys in `.env.local
|
|
78
|
+
| `checkbox init [--yes] [--json]` | one browser approval (device login), then keys in `.env.local` (not when that file is tracked by git or the folder is deployed as it is: the key then stays only in `~/.config/checkbox/config.json`), the SRI-pinned snippet in the root layout, site + Terms / policies registered, Stripe test subscription (with permission; only a Stripe restricted TEST key, `rk_test_…` in `STRIPE_RESTRICTED_KEY`, is used: `sk_test_` and live keys are refused with how to make one), first checks, owner email; `--verify` re-checks after a deploy; `--ci` only adds CI (`checkbox ci init`) |
|
|
79
79
|
| `checkbox login [--api-key <key>\|--stdin]` | verifies the key with `GET /v1/account`, then saves it |
|
|
80
80
|
| `checkbox whoami` | account, mode (test/live) and where the key came from |
|
|
81
81
|
| `checkbox logout` | forgets the stored key |
|
package/dist/checkbox.js
CHANGED
|
@@ -3335,7 +3335,7 @@ var init_operations = __esm({
|
|
|
3335
3335
|
/**
|
|
3336
3336
|
* Register the Checkout Session params your code uses.
|
|
3337
3337
|
*
|
|
3338
|
-
* Registers exactly the object your code passes to stripe.checkout.sessions.create (customer, URLs and secrets are not needed and are ignored; `warnings` says what was not stored). It is judged exactly like checkStripeCheckoutSession, discounts included. A config with the same name, or for the same Price ids, replaces the older one (`replaced`). Test runs mirror it into Stripe test mode; when it matches your latest live checkout, the scanned test page counts as an observation of your live checkout. Re-register after changing your checkout code, then call createTestRun.
|
|
3338
|
+
* Registers exactly the object your code passes to stripe.checkout.sessions.create, as `params` (that object sent alone as the body is accepted too; customer, URLs and secrets are not needed and are ignored; `warnings` says what was not stored). It is judged exactly like checkStripeCheckoutSession, discounts included. A config with the same name, or for the same Price ids, replaces the older one (`replaced`). Test runs mirror it into Stripe test mode; when it matches your latest live checkout, the scanned test page counts as an observation of your live checkout. Re-register after changing your checkout code, then call createTestRun.
|
|
3339
3339
|
*
|
|
3340
3340
|
* @param site Unique identifier of the site (site_…).
|
|
3341
3341
|
* @see POST /v1/sites/{site}/stripe_checkout_configs (operation `createStripeCheckoutConfig`, scope `test_runs:write`)
|
|
@@ -3617,7 +3617,7 @@ var SDK_VERSION;
|
|
|
3617
3617
|
var init_version = __esm({
|
|
3618
3618
|
"../sdk-node/src/version.ts"() {
|
|
3619
3619
|
"use strict";
|
|
3620
|
-
SDK_VERSION = "0.2.
|
|
3620
|
+
SDK_VERSION = "0.2.4";
|
|
3621
3621
|
}
|
|
3622
3622
|
});
|
|
3623
3623
|
|
|
@@ -11321,6 +11321,9 @@ function introAmountOf(amount, o) {
|
|
|
11321
11321
|
return void 0;
|
|
11322
11322
|
}
|
|
11323
11323
|
function unverifiedReason(ctx) {
|
|
11324
|
+
const known = ctx.referencePlans ?? [];
|
|
11325
|
+
if (known.length) return `none of your plans (${known.slice(0, 4).map((p) => p.label ?? p.name ?? p.id).join(", ")}) was matched to this checkout's price (the Price id it sells is not one Checkbox has for them)`;
|
|
11326
|
+
if ((ctx.checkoutConfigs?.length ?? 0) > 0) return "the price this checkout sells is not in your registered Checkout configs or your billing platform's prices";
|
|
11324
11327
|
const chosen = ctx.facts?.planPriceId;
|
|
11325
11328
|
if (chosen && ctx.config) return `the chosen price ${chosen} is not among the active recurring prices read from your billing platform`;
|
|
11326
11329
|
if (chosen) return `the chosen price ${chosen} could not be read (no billing platform connected)`;
|
|
@@ -11461,8 +11464,8 @@ function sameProductName(a, b) {
|
|
|
11461
11464
|
function declaredPriceIds(x) {
|
|
11462
11465
|
return [x.price_id, x.stripe_price, x.priceId, x.id].filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
11463
11466
|
}
|
|
11464
|
-
function reconcileDeclaredPlans(raw, billing, platform = "Stripe") {
|
|
11465
|
-
const prices = billing.filter((p) => p && p.amount > 0 && p.interval);
|
|
11467
|
+
function reconcileDeclaredPlans(raw, billing, platform = "Stripe", checkoutTrials) {
|
|
11468
|
+
const prices = billing.filter((p) => p && p.amount > 0 && p.interval).map((p) => checkoutTrials?.has(p.id) ? { ...p, trialDays: checkoutTrials.get(p.id) } : p);
|
|
11466
11469
|
const out = [...raw];
|
|
11467
11470
|
const conflicts = [];
|
|
11468
11471
|
if (!prices.length || !raw.length) return { plans: out, conflicts };
|
|
@@ -14917,7 +14920,8 @@ function inAppPath(input) {
|
|
|
14917
14920
|
return { kind: "in_app", state: "works", detail: "a test cancellation completed online" };
|
|
14918
14921
|
}
|
|
14919
14922
|
if (hostedStep2) return void 0;
|
|
14920
|
-
const
|
|
14923
|
+
const onHosted = (flow.steps ?? []).some((s2) => s2.source !== "snippet" && isHostedCancelUrl(s2.url));
|
|
14924
|
+
const kinds = (flow.blockers ?? []).map(classifyBlocker).filter((k) => !(onHosted && k === "contact"));
|
|
14921
14925
|
if (kinds.some((k) => k === "contact" || k === "not_found" || k === "obstacle")) {
|
|
14922
14926
|
return { kind: "in_app", state: "broken", detail: `the test cancellation did not complete (${flow.blockers.join("; ")})` };
|
|
14923
14927
|
}
|
|
@@ -14994,7 +14998,7 @@ function offlineOnlyCancellation(input) {
|
|
|
14994
14998
|
const hosted = input.ctx.hostedCancel;
|
|
14995
14999
|
const helpHits = [];
|
|
14996
15000
|
for (const c of captures) {
|
|
14997
|
-
if (isFetchedDocument(c) || c.pageProblem || c.pageKind === "checkout") continue;
|
|
15001
|
+
if (isFetchedDocument(c) || c.pageProblem || c.pageKind === "checkout" || isHostedCancelUrl(c.url)) continue;
|
|
14998
15002
|
const sentence = contactOnlySentence(c.text);
|
|
14999
15003
|
if (!sentence) continue;
|
|
15000
15004
|
if (ownOnlineCancelControls(c, hosted).length) continue;
|
|
@@ -15005,7 +15009,7 @@ function offlineOnlyCancellation(input) {
|
|
|
15005
15009
|
}
|
|
15006
15010
|
return { where: `${c.pageKind === "account" ? "account page" : c.pageKind === "cancel" ? "cancel page" : "page"} ${c.url.split("#")[0]}`, quote: sentence, capture: c };
|
|
15007
15011
|
}
|
|
15008
|
-
const contactOnlyUrls = new Set(captures.filter((c) => !isFetchedDocument(c) && contactOnlySentence(c.text)).map((c) => stripUrl(c.url)));
|
|
15012
|
+
const contactOnlyUrls = new Set(captures.filter((c) => !isFetchedDocument(c) && !isHostedCancelUrl(c.url) && contactOnlySentence(c.text)).map((c) => stripUrl(c.url)));
|
|
15009
15013
|
const onlineControl = captures.some(
|
|
15010
15014
|
(c) => !isFetchedDocument(c) && ownOnlineCancelControls(c, hosted).some((el) => !contactOnlyUrls.has(stripUrl(hrefOf(el) ?? "", c.url)))
|
|
15011
15015
|
);
|
|
@@ -16851,6 +16855,17 @@ function cancelBeforeTrialMatch(text) {
|
|
|
16851
16855
|
if (byDate && TRIAL_ENDS_DATE_RE.test(text)) return byDate[0];
|
|
16852
16856
|
return null;
|
|
16853
16857
|
}
|
|
16858
|
+
function statedTrialMismatch(a, p, offers) {
|
|
16859
|
+
const stated = a.trialLengths.filter((t) => typeof t.days === "number" && t.days > 0);
|
|
16860
|
+
if (!stated.length) return void 0;
|
|
16861
|
+
const own = p?.offer && p.offer.trialDays > 0 ? [p.offer] : p?.offer ? [] : offers.filter((o) => o.trialDays > 0);
|
|
16862
|
+
if (!own.length) return void 0;
|
|
16863
|
+
const fits = (days, trial) => days === trial || days % 30 === 0 && Math.abs(days - trial) <= Math.max(1, days / 30) * 2;
|
|
16864
|
+
if (stated.some((t) => own.some((o) => fits(t.days, o.trialDays)))) return void 0;
|
|
16865
|
+
const lengths = [...new Set(own.map((o) => o.trialDays))].sort((x, y) => x - y);
|
|
16866
|
+
const plan2 = p?.offer ? `${offerLabel(p.offer)} has a ${p.offer.trialDays}-day free trial` : `the plan${lengths.length > 1 ? "s give" : " gives"} a ${lengths.join("- or ")}-day free trial`;
|
|
16867
|
+
return { stated: `\u201C${stated[0].text}\u201D`, plan: plan2 };
|
|
16868
|
+
}
|
|
16854
16869
|
var ACK_MAX_DELAY_HOURS, NO_ACK_EMAIL_MESSAGE, ackCache, SIGNUP_LINK_TOLERANCE_MS, sent, terms, cancellationInstructions, CANCELLING_RE, CANCEL_BEFORE_TRIAL_RE, MONTH, DATE, CANCEL_BEFORE_NO_CHARGE_RE, CANCEL_BEFORE_DATE_RE, TRIAL_ENDS_DATE_RE, trialTerms, CONFIRMATION_CHECKS;
|
|
16855
16870
|
var init_confirmation = __esm({
|
|
16856
16871
|
"../rules/src/checks/confirmation.ts"() {
|
|
@@ -17160,6 +17175,15 @@ ${v.pageText}`);
|
|
|
17160
17175
|
);
|
|
17161
17176
|
const units = trialDocs.map((doc) => {
|
|
17162
17177
|
const a = doc.analysis;
|
|
17178
|
+
const wrongTrial = statedTrialMismatch(a, ackPlan(doc.email, input, offers), offers);
|
|
17179
|
+
if (wrongTrial) {
|
|
17180
|
+
return docUnit(doc, {
|
|
17181
|
+
status: "fail",
|
|
17182
|
+
detail: `says ${wrongTrial.stated}, but ${wrongTrial.plan}: state the trial the subscriber really gets`,
|
|
17183
|
+
confidence: 0.8,
|
|
17184
|
+
meta: { trialLengthMismatch: true }
|
|
17185
|
+
});
|
|
17186
|
+
}
|
|
17163
17187
|
const method = a.cancelMethod[0];
|
|
17164
17188
|
const timing = a.trialLengths[0] ?? a.postTrialCharge[0];
|
|
17165
17189
|
const beforeMatch = cancelBeforeTrialMatch(a.text);
|
|
@@ -22221,7 +22245,7 @@ function classifyRaw(input) {
|
|
|
22221
22245
|
}
|
|
22222
22246
|
}
|
|
22223
22247
|
function fixFromFail(c) {
|
|
22224
|
-
return withPlanTerms(contentFixLine(String(c.checkId), c.data) ?? contentFixFromText(String(c.checkId), c.detail) ?? introFixFromText(c) ?? planFrequencyFix(c) ?? FAIL_FIX[String(c.checkId)] ?? "Apply the change in the task for this check.", c.data);
|
|
22248
|
+
return withPlanTerms(contentFixLine(String(c.checkId), c.data) ?? contentFixFromText(String(c.checkId), c.detail) ?? introFixFromText(c) ?? trialLengthFixFromText(c) ?? planFrequencyFix(c) ?? FAIL_FIX[String(c.checkId)] ?? "Apply the change in the task for this check.", c.data);
|
|
22225
22249
|
}
|
|
22226
22250
|
function withPlanTerms(fix, data) {
|
|
22227
22251
|
if (!/<(?:price|interval)>/.test(fix)) return fix;
|
|
@@ -22241,6 +22265,11 @@ function introFixFromText(c) {
|
|
|
22241
22265
|
const m = /introductory price of (\S+?)(?: \(\d+% off\))? for (the first [^—,]+?)\s*(?:,|—)/i.exec(c.detail);
|
|
22242
22266
|
return m ? `State the introductory price (${m[1]} for ${m[2]}) and when it ends, then the regular price from the next charge, in the renewal line.` : "State the introductory price and how long it lasts, then the regular price from the next charge, in the renewal line.";
|
|
22243
22267
|
}
|
|
22268
|
+
function trialLengthFixFromText(c) {
|
|
22269
|
+
if (c.checkId !== "confirmation.email.trial_terms") return void 0;
|
|
22270
|
+
const m = /says “([^”]+)”, but .*?\b(?:has|gives?) a ([\d -]+?or [\d]+|\d+)-day free trial/.exec(c.detail);
|
|
22271
|
+
return m ? `The acknowledgment says \u201C${m[1]}\u201D, but the plan has a ${m[2]}-day free trial: state the trial the subscriber really gets, and how to cancel before it ends.` : void 0;
|
|
22272
|
+
}
|
|
22244
22273
|
function planFrequencyFix(c) {
|
|
22245
22274
|
if (c.checkId !== "signup.disclosure.frequency") return void 0;
|
|
22246
22275
|
const every = /\bplans? (?:is|are) billed (every [a-z0-9 ]+?)(?= \/ | —|[.;,]|$)/i.exec(c.detail)?.[1];
|
|
@@ -22326,7 +22355,7 @@ function classifyFindings(findings) {
|
|
|
22326
22355
|
info: classify({ checkId: f.checkId, status: f.status, detail: f.detail, missing: f.evidence?.missing, data: f.data, origin: "evaluation", siblings })
|
|
22327
22356
|
}));
|
|
22328
22357
|
}
|
|
22329
|
-
var STATUS_LABELS, CHECK_TIERS, LIVE_CHECKS, has, isStripeInput, HOSTED_BEFORE_LINE_NOTE, HOSTED_CHECKOUT_NOTE, family, isEmailCheck, ask, PLAN_FACTS_FIX, STRIPE_CANCEL_UNREAD_FIX, BILLING_CONTRADICTS_FIX, VERIFIED_FROM_STRIPE_NOTE, VERIFIED_FROM_HOSTED_NOTE, HOSTED_UNLINKED_FIX, NOT_CHECKED_CANCEL_FIX, EMAIL_EXISTS, RULES, STATUS_RULES, NY_EMAIL_ONLY_LABEL, NY_EMAIL_ONLY_NOTE, STRIPE_HOSTED_UNITS, PLATFORM_NAMES2, PORTAL_REQUIRED_FIX, PORTAL_IN_APP_BROKEN_FIX, PORTAL_OFFLINE_FIX, PAYMENT_LINK_PORTAL_FIX, PAYMENT_LINK_OFFLINE_FIX, FAIL_FIX, CHECKS_PASS_UNREAD_LABEL, STATUS_RANK;
|
|
22358
|
+
var STATUS_LABELS, CHECK_TIERS, LIVE_CHECKS, has, isStripeInput, HOSTED_BEFORE_LINE_NOTE, HOSTED_CHECKOUT_NOTE, family, isEmailCheck, ask, PLAN_FACTS_FIX, STRIPE_CANCEL_UNREAD_FIX, BILLING_CONTRADICTS_FIX, VERIFIED_FROM_STRIPE_NOTE, VERIFIED_FROM_HOSTED_NOTE, HOSTED_UNLINKED_FIX, NOT_CHECKED_CANCEL_FIX, EMAIL_EXISTS, RULES, STATUS_RULES, NY_EMAIL_ONLY_LABEL, NY_EMAIL_ONLY_NOTE, STRIPE_HOSTED_UNITS, PLATFORM_NAMES2, PORTAL_REQUIRED_FIX, PORTAL_IN_APP_BROKEN_FIX, PORTAL_OFFLINE_FIX, PAYMENT_LINK_PORTAL_FIX, PAYMENT_LINK_OFFLINE_FIX, RENEWAL_LINE_EXAMPLE, FAIL_FIX, CHECKS_PASS_UNREAD_LABEL, STATUS_RANK;
|
|
22330
22359
|
var init_status = __esm({
|
|
22331
22360
|
"../rules/src/status.ts"() {
|
|
22332
22361
|
"use strict";
|
|
@@ -22522,7 +22551,7 @@ var init_status = __esm({
|
|
|
22522
22551
|
},
|
|
22523
22552
|
{ code: "receipt_without_terms", category: "content", when: has(/only a receipt without subscription terms/), build: () => ({ status: "needs_fix", fix: "Send an acknowledgment that restates the renewal terms (price, frequency, renews until cancelled) and how to cancel; a plain receipt is not enough." }) },
|
|
22524
22553
|
// Audit P2: Stripe wording (custom_text) only for Stripe inputs; any other checkout gets the same line, platform-neutral.
|
|
22525
|
-
{ code: "processor_default_line", category: "content", when: has(/payment processor.s default line|authorization line is not the renewal terms|no automatic-renewal statement was found/), build: (c) => ({ status: "needs_fix", fix: withPlanTerms(`Add your own renewal line next to the pay button${isStripeInput(c) ? " (Stripe custom_text.submit)" : ""}: "
|
|
22554
|
+
{ code: "processor_default_line", category: "content", when: has(/payment processor.s default line|authorization line is not the renewal terms|no automatic-renewal statement was found/), build: (c) => ({ status: "needs_fix", fix: withPlanTerms(`Add your own renewal line next to the pay button${isStripeInput(c) ? " (Stripe custom_text.submit)" : ""}: "${RENEWAL_LINE_EXAMPLE} Cancel anytime with your cancel link."`, c.data) }) },
|
|
22526
22555
|
{ code: "renewal_without_until_cancel", category: "content", when: has(/but not that it continues until the customer cancels/), build: () => ({ status: "needs_fix", fix: 'Add "until you cancel" to the renewal line.' }) },
|
|
22527
22556
|
{ code: "terms_hidden", category: "content", when: has(/collapsed <details>|\(nearly\) the colour of its background|nearly the colour of the background|hidden by a stylesheet|sr-only/), build: () => ({ status: "needs_fix", fix: "Show the renewal line as plain visible text next to the button (not collapsed, not faded into the background)." }) },
|
|
22528
22557
|
// Pilot round 11 (change-guard): an intro / discounted price the Stripe session does not apply.
|
|
@@ -22985,8 +23014,9 @@ var init_status = __esm({
|
|
|
22985
23014
|
PORTAL_OFFLINE_FIX = 'Turn on "Cancel subscriptions" in the Stripe customer portal and link it from your account page, then replace the "email / call us to cancel" line with "Cancel anytime online in your account".';
|
|
22986
23015
|
PAYMENT_LINK_PORTAL_FIX = `Turn on "Cancel subscriptions" in the Stripe customer portal (Stripe Dashboard \u2192 Settings \u2192 Billing \u2192 Customer portal), copy its billing.stripe.com login link and add it to your site's footer as a "Cancel subscription" link.`;
|
|
22987
23016
|
PAYMENT_LINK_OFFLINE_FIX = `${PAYMENT_LINK_PORTAL_FIX} Then replace the "email / call us to cancel" line with "Cancel anytime online" and that link.`;
|
|
23017
|
+
RENEWAL_LINE_EXAMPLE = "<price> every <interval> (plus applicable taxes). Renews automatically until you cancel.";
|
|
22988
23018
|
FAIL_FIX = {
|
|
22989
|
-
"signup.disclosure.autorenew_language":
|
|
23019
|
+
"signup.disclosure.autorenew_language": `Add one line under the price, before the card and email fields: "${RENEWAL_LINE_EXAMPLE}"`,
|
|
22990
23020
|
"signup.disclosure.price": "State the recurring price in the renewal line (the amount actually charged each time).",
|
|
22991
23021
|
"signup.disclosure.frequency": `State how often it is charged, in the plan's own interval ("every month", "every year").`,
|
|
22992
23022
|
"signup.disclosure.cancellation_policy": 'Add "Cancel anytime online" to the renewal line, with your cancel link.',
|
|
@@ -25251,6 +25281,24 @@ var init_stripe2 = __esm({
|
|
|
25251
25281
|
}
|
|
25252
25282
|
});
|
|
25253
25283
|
|
|
25284
|
+
// ../rules/src/preflight/stripe-verdict.ts
|
|
25285
|
+
function stripeParamsVerdict(r) {
|
|
25286
|
+
if (r.verdict === "not_applicable") return { verdict: "not_applicable", open: [] };
|
|
25287
|
+
const open2 = r.checks.filter((c) => c.status === "fail" || c.status === "needs_review" && !(c.compliance_status && SATISFIED_COMPLIANCE.has(c.compliance_status))).map((c) => ({ check_id: c.check_id, status: c.status, title: c.title ?? c.check_id, detail: c.explanation ?? "" })).sort((a, b) => (a.status === "fail" ? 0 : 1) - (b.status === "fail" ? 0 : 1));
|
|
25288
|
+
if (r.verdict === "fail" || r.compliance_status === "needs_fix" || open2.some((c) => c.status === "fail")) return { verdict: "fail", open: open2 };
|
|
25289
|
+
const coupon = (r.warnings ?? []).find((w) => w.code === "coupon_unverified");
|
|
25290
|
+
if (open2.length) return { verdict: "needs_review", open: open2, ...coupon ? { reason: coupon.message } : {} };
|
|
25291
|
+
if (coupon) return { verdict: "needs_review", open: [], reason: coupon.message };
|
|
25292
|
+
return { verdict: "pass", open: [] };
|
|
25293
|
+
}
|
|
25294
|
+
var SATISFIED_COMPLIANCE;
|
|
25295
|
+
var init_stripe_verdict = __esm({
|
|
25296
|
+
"../rules/src/preflight/stripe-verdict.ts"() {
|
|
25297
|
+
"use strict";
|
|
25298
|
+
SATISFIED_COMPLIANCE = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
25299
|
+
}
|
|
25300
|
+
});
|
|
25301
|
+
|
|
25254
25302
|
// ../rules/src/preflight/email.ts
|
|
25255
25303
|
function linkify(escaped) {
|
|
25256
25304
|
return escaped.replace(/https?:\/\/[^\s<]+/g, (raw) => {
|
|
@@ -25434,6 +25482,10 @@ function contextFor(kind, occ, plan2, jurisdictions, asOf, email) {
|
|
|
25434
25482
|
function longDate2(d) {
|
|
25435
25483
|
return d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
25436
25484
|
}
|
|
25485
|
+
function supportContactLine(contact, opts = {}) {
|
|
25486
|
+
if (contact || opts.required) return `Questions? Reply to this email or contact us at ${contact || SUPPORT_CONTACT_PLACEHOLDER}.`;
|
|
25487
|
+
return "Questions? Reply to this email.";
|
|
25488
|
+
}
|
|
25437
25489
|
function emailTemplate(kind, plan2, opts = {}) {
|
|
25438
25490
|
const brand = plan2.merchantName ?? "";
|
|
25439
25491
|
const sub = `${brand ? `${brand} ` : ""}subscription`;
|
|
@@ -25441,7 +25493,7 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25441
25493
|
const every = intervalPhrase(plan2.interval, plan2.intervalCount);
|
|
25442
25494
|
const cancel = plan2.cancelUrl ?? CANCEL_URL_PLACEHOLDER;
|
|
25443
25495
|
const effect = plan2.cancelTiming === "immediately" ? "cancellation takes effect immediately" : "cancellation takes effect at the end of your current billing period";
|
|
25444
|
-
const contact = opts.supportContact
|
|
25496
|
+
const contact = supportContactLine(opts.supportContact);
|
|
25445
25497
|
const lines = (...l) => l.filter(Boolean).join("\n\n");
|
|
25446
25498
|
const intro = plan2.introAmount !== void 0 && plan2.introAmount < plan2.amount ? `${formatMoney(plan2.introAmount, plan2.currency)} for ${plan2.introPeriods && plan2.introPeriods > 1 ? `your first ${plan2.introPeriods} billing periods` : `your first ${every}`}` : "";
|
|
25447
25499
|
const introLine = intro ? `Introductory price: ${intro}, then ${money3} every ${every}.` : "";
|
|
@@ -25449,11 +25501,13 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25449
25501
|
case "acknowledgment":
|
|
25450
25502
|
return {
|
|
25451
25503
|
subject: `Your ${sub} is confirmed`,
|
|
25504
|
+
// Round 31 (pilot 30 item 22): the same sentences, in the same order, as the "You send" acknowledgment template
|
|
25505
|
+
// (apps/api notices/merchant-templates.ts), so the plan, checkEmail and the due notice give one email.
|
|
25452
25506
|
text: lines(
|
|
25453
|
-
`Thanks for subscribing${brand ? ` to ${brand}` : ""}
|
|
25454
|
-
introLine,
|
|
25507
|
+
`Thanks for subscribing${brand ? ` to ${brand}` : ""}. This email confirms your subscription terms.`,
|
|
25455
25508
|
plan2.trialDays ? `Your free trial lasts ${plan2.trialDays} days and ends on {{trial_end_date}}. To avoid being charged, cancel before your trial ends at ${cancel} \u2014 it takes less than a minute. If you do nothing, your subscription starts automatically on {{trial_end_date}} and you will be charged ${money3} every ${every}.` : "",
|
|
25456
|
-
`Your ${sub} automatically renews every ${every} at ${money3} until you cancel. Your next charge is on {{next_charge_date}}.`,
|
|
25509
|
+
`Your ${sub} automatically renews every ${every} at ${money3} until you cancel. Your ${plan2.trialDays ? "first" : "next"} charge is on {{next_charge_date}}.`,
|
|
25510
|
+
introLine,
|
|
25457
25511
|
`How to cancel: go to ${cancel} \u2014 you can cancel anytime online; ${effect}.`,
|
|
25458
25512
|
contact
|
|
25459
25513
|
),
|
|
@@ -25509,7 +25563,7 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25509
25563
|
`To stop future charges, cancel online anytime at ${cancel}.`,
|
|
25510
25564
|
// Pilot round 11 (invariant test): Georgia's charge notice must give the business's contact information;
|
|
25511
25565
|
// "reply to this email" is not one, so without a support contact the template asks for it.
|
|
25512
|
-
opts.supportContact
|
|
25566
|
+
supportContactLine(opts.supportContact, { required: true })
|
|
25513
25567
|
),
|
|
25514
25568
|
// Round 12: the template used {{support_contact}} without listing it as a merge tag to fill.
|
|
25515
25569
|
tags: ["{{charge_date}}", "{{next_charge_date}}", ...opts.supportContact ? [] : [SUPPORT_CONTACT_PLACEHOLDER]]
|
|
@@ -26050,6 +26104,7 @@ __export(preflight_exports, {
|
|
|
26050
26104
|
PREFLIGHT_DISCLAIMER: () => PREFLIGHT_DISCLAIMER,
|
|
26051
26105
|
PREFLIGHT_DOCS_URL: () => PREFLIGHT_DOCS_URL,
|
|
26052
26106
|
PreflightInputError: () => PreflightInputError,
|
|
26107
|
+
SATISFIED_COMPLIANCE: () => SATISFIED_COMPLIANCE,
|
|
26053
26108
|
STRIPE_HOSTED_STYLING_NOTE: () => STRIPE_HOSTED_STYLING_NOTE,
|
|
26054
26109
|
checkDisclosure: () => checkDisclosure,
|
|
26055
26110
|
checkEmail: () => checkEmail,
|
|
@@ -26071,6 +26126,8 @@ __export(preflight_exports, {
|
|
|
26071
26126
|
runChecks: () => runChecks,
|
|
26072
26127
|
shapeChecks: () => shapeChecks,
|
|
26073
26128
|
signupStripeObjects: () => signupStripeObjects,
|
|
26129
|
+
stripeParamsVerdict: () => stripeParamsVerdict,
|
|
26130
|
+
supportContactLine: () => supportContactLine,
|
|
26074
26131
|
withIntroOffers: () => withIntroOffers
|
|
26075
26132
|
});
|
|
26076
26133
|
var init_preflight = __esm({
|
|
@@ -26079,6 +26136,7 @@ var init_preflight = __esm({
|
|
|
26079
26136
|
init_common();
|
|
26080
26137
|
init_disclosure();
|
|
26081
26138
|
init_stripe2();
|
|
26139
|
+
init_stripe_verdict();
|
|
26082
26140
|
init_email();
|
|
26083
26141
|
init_html_page();
|
|
26084
26142
|
init_page();
|
|
@@ -26699,8 +26757,8 @@ var init_config3 = __esm({
|
|
|
26699
26757
|
});
|
|
26700
26758
|
|
|
26701
26759
|
// src/commands/init/literal.ts
|
|
26702
|
-
function parseObjectLiteral(source, env2 = {}) {
|
|
26703
|
-
const p = new Parser(source, env2);
|
|
26760
|
+
function parseObjectLiteral(source, env2 = {}, consts = {}) {
|
|
26761
|
+
const p = new Parser(source, env2, consts);
|
|
26704
26762
|
if (p.peek() !== "{") return { value: null, unresolved: ["<not an object literal>"] };
|
|
26705
26763
|
const v = p.object("");
|
|
26706
26764
|
return { value: v === UNKNOWN ? null : v, unresolved: [...new Set(p.unresolved)], ...Object.keys(p.fromEnv).length ? { fromEnv: p.fromEnv } : {} };
|
|
@@ -26710,10 +26768,11 @@ var init_literal = __esm({
|
|
|
26710
26768
|
"src/commands/init/literal.ts"() {
|
|
26711
26769
|
"use strict";
|
|
26712
26770
|
UNKNOWN = Symbol("unknown");
|
|
26713
|
-
Parser = class {
|
|
26714
|
-
constructor(s2, env2) {
|
|
26771
|
+
Parser = class _Parser {
|
|
26772
|
+
constructor(s2, env2, consts = {}) {
|
|
26715
26773
|
this.s = s2;
|
|
26716
26774
|
this.env = env2;
|
|
26775
|
+
this.consts = consts;
|
|
26717
26776
|
}
|
|
26718
26777
|
i = 0;
|
|
26719
26778
|
unresolved = [];
|
|
@@ -26803,6 +26862,12 @@ var init_literal = __esm({
|
|
|
26803
26862
|
if (name === "false") return false;
|
|
26804
26863
|
if (name === "null") return null;
|
|
26805
26864
|
if (name === "undefined") return void 0;
|
|
26865
|
+
if (Object.prototype.hasOwnProperty.call(this.consts, name)) {
|
|
26866
|
+
const sub = new _Parser(this.consts[name], this.env);
|
|
26867
|
+
const v = sub.value("");
|
|
26868
|
+
if (v !== UNKNOWN && v !== void 0 && path) this.fromEnv[path] = name;
|
|
26869
|
+
return v;
|
|
26870
|
+
}
|
|
26806
26871
|
const env2 = /^(?:process\.env|import\.meta\.env)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(name);
|
|
26807
26872
|
if (env2) {
|
|
26808
26873
|
if (this.env[env2[1]] !== void 0 && path) this.fromEnv[path] = name;
|
|
@@ -27147,7 +27212,13 @@ function sessionLiteral(text, index) {
|
|
|
27147
27212
|
else if (ch === quote2) quote2 = null;
|
|
27148
27213
|
continue;
|
|
27149
27214
|
}
|
|
27150
|
-
if (ch ===
|
|
27215
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
27216
|
+
const nl = text.indexOf("\n", i);
|
|
27217
|
+
i = nl === -1 ? text.length : nl;
|
|
27218
|
+
} else if (ch === "/" && text[i + 1] === "*") {
|
|
27219
|
+
const end = text.indexOf("*/", i + 2);
|
|
27220
|
+
i = end === -1 ? text.length : end + 1;
|
|
27221
|
+
} else if (ch === '"' || ch === "'" || ch === "`") quote2 = ch;
|
|
27151
27222
|
else if (ch === "{") depth++;
|
|
27152
27223
|
else if (ch === "}") {
|
|
27153
27224
|
depth--;
|
|
@@ -27156,6 +27227,58 @@ function sessionLiteral(text, index) {
|
|
|
27156
27227
|
}
|
|
27157
27228
|
return null;
|
|
27158
27229
|
}
|
|
27230
|
+
function declaredExpr(text, from) {
|
|
27231
|
+
let depth = 0;
|
|
27232
|
+
let quote2 = null;
|
|
27233
|
+
for (let i = from; i < text.length && i - from < 2e3; i++) {
|
|
27234
|
+
const ch = text[i];
|
|
27235
|
+
if (quote2) {
|
|
27236
|
+
if (ch === "\\") i++;
|
|
27237
|
+
else if (ch === quote2) quote2 = null;
|
|
27238
|
+
continue;
|
|
27239
|
+
}
|
|
27240
|
+
if (ch === '"' || ch === "'" || ch === "`") quote2 = ch;
|
|
27241
|
+
else if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
27242
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
27243
|
+
else if ((ch === ";" || ch === "\n") && depth <= 0) return text.slice(from, i).trim();
|
|
27244
|
+
}
|
|
27245
|
+
return text.slice(from, from + 2e3).trim();
|
|
27246
|
+
}
|
|
27247
|
+
function constDeclarations(text, exportedOnly) {
|
|
27248
|
+
const out = {};
|
|
27249
|
+
const re = exportedOnly ? /(?:^|\n)[ \t]*export\s+const\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*/g : /(?:^|\n)[ \t]*(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*/g;
|
|
27250
|
+
for (const m of text.matchAll(re)) if (!(m[1] in out)) out[m[1]] = declaredExpr(text, m.index + m[0].length);
|
|
27251
|
+
return out;
|
|
27252
|
+
}
|
|
27253
|
+
function resolveImport(root, fromFile, spec) {
|
|
27254
|
+
const dir = fromFile.includes("/") ? fromFile.slice(0, fromFile.lastIndexOf("/")) : "";
|
|
27255
|
+
const bases = spec.startsWith(".") ? [join4(dir, spec)] : /^[@~]\//.test(spec) ? [spec.slice(2), join4("src", spec.slice(2))] : [];
|
|
27256
|
+
for (const b of bases) {
|
|
27257
|
+
const base = b.split("\\").join("/").replace(/\.(?:[cm]?js|jsx)$/, "");
|
|
27258
|
+
for (const f of [b, ...[".ts", ".tsx", ".js", ".mjs", ".jsx", "/index.ts", "/index.js"].map((e) => `${base}${e}`)]) {
|
|
27259
|
+
if (/\.[cm]?[jt]sx?$/.test(f) && existsSync4(join4(root, f))) return f.split("\\").join("/");
|
|
27260
|
+
}
|
|
27261
|
+
}
|
|
27262
|
+
return void 0;
|
|
27263
|
+
}
|
|
27264
|
+
function constantsFor(root, relFile, text) {
|
|
27265
|
+
const out = {};
|
|
27266
|
+
for (const m of text.matchAll(/import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
27267
|
+
const target = resolveImport(root, relFile, m[2]);
|
|
27268
|
+
if (!target) continue;
|
|
27269
|
+
let exported;
|
|
27270
|
+
try {
|
|
27271
|
+
exported = constDeclarations(readFileSync5(join4(root, target), "utf8"), true);
|
|
27272
|
+
} catch {
|
|
27273
|
+
continue;
|
|
27274
|
+
}
|
|
27275
|
+
for (const part of m[1].split(",")) {
|
|
27276
|
+
const [name, alias] = part.trim().split(/\s+as\s+/).map((x) => x.trim());
|
|
27277
|
+
if (name && exported[name] !== void 0) out[alias || name] = exported[name];
|
|
27278
|
+
}
|
|
27279
|
+
}
|
|
27280
|
+
return { ...out, ...constDeclarations(text, false) };
|
|
27281
|
+
}
|
|
27159
27282
|
function detectProject(root, processEnv = {}) {
|
|
27160
27283
|
const pkg = readJson(join4(root, "package.json"));
|
|
27161
27284
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
@@ -27192,7 +27315,7 @@ function detectProject(root, processEnv = {}) {
|
|
|
27192
27315
|
checkoutSessions.push({ file: rel2, line: line2, unresolved: ["<params are not an object literal>"] });
|
|
27193
27316
|
continue;
|
|
27194
27317
|
}
|
|
27195
|
-
const parsed = parseObjectLiteral(lit, { ...env2, ...Object.fromEntries(Object.entries(processEnv).filter((e) => typeof e[1] === "string" && /^(STRIPE_|NEXT_PUBLIC_STRIPE_|PRICE_)/.test(e[0]))) });
|
|
27318
|
+
const parsed = parseObjectLiteral(lit, { ...env2, ...Object.fromEntries(Object.entries(processEnv).filter((e) => typeof e[1] === "string" && /^(STRIPE_|NEXT_PUBLIC_STRIPE_|PRICE_)/.test(e[0]))) }, constantsFor(root, rel2, text));
|
|
27196
27319
|
checkoutSessions.push({ file: rel2, line: line2, ...parsed.value ? { params: redactSecretsDeep(parsed.value) } : {}, unresolved: parsed.unresolved });
|
|
27197
27320
|
}
|
|
27198
27321
|
});
|
|
@@ -27330,6 +27453,33 @@ function paramsBuilder(root, file, line2) {
|
|
|
27330
27453
|
} else if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fn}\\b|export\\s+const\\s+${fn}\\b`).test(text)) target = file;
|
|
27331
27454
|
return target ? { module: `${target}#${fn}`, takesArgs } : void 0;
|
|
27332
27455
|
}
|
|
27456
|
+
function plansSoldBy(params, plans) {
|
|
27457
|
+
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
27458
|
+
const out = [];
|
|
27459
|
+
for (const li of items) {
|
|
27460
|
+
const pd = li?.price_data;
|
|
27461
|
+
if (!pd || typeof pd.unit_amount !== "number") continue;
|
|
27462
|
+
const match = plans.find((p) => (typeof p.amount === "number" ? p.amount : typeof p.price === "number" ? Math.round(p.price * 100) : NaN) === pd.unit_amount && p.interval === pd.recurring?.interval);
|
|
27463
|
+
if (match && !out.includes(match)) out.push(match);
|
|
27464
|
+
}
|
|
27465
|
+
return out;
|
|
27466
|
+
}
|
|
27467
|
+
function cancelUrlInCode(sessions) {
|
|
27468
|
+
const urls = /* @__PURE__ */ new Set();
|
|
27469
|
+
for (const s2 of sessions) {
|
|
27470
|
+
const text = JSON.stringify(s2.params?.custom_text ?? "");
|
|
27471
|
+
for (const m of text.matchAll(/cancel[^"]{0,80}?(https:\/\/[^\s"'<>)]+?)(?=[.,;:!?]?(?:[\s"'<>)]|$))/gi)) urls.add(m[1]);
|
|
27472
|
+
}
|
|
27473
|
+
return urls.size === 1 ? [...urls][0] : void 0;
|
|
27474
|
+
}
|
|
27475
|
+
function pageSource(root, built) {
|
|
27476
|
+
const route = /(?:^|\/)(?:public|dist|out|build|static|_site|www)\/(?:(.*)\/)?([^/]+?)(?:\/index)?\.html?$/i.exec(built);
|
|
27477
|
+
if (!route) return void 0;
|
|
27478
|
+
const path = [route[1], route[2]].filter(Boolean).join("/");
|
|
27479
|
+
const bases = [`app/${path}/page`, `src/app/${path}/page`, `app/(marketing)/${path}/page`, `pages/${path}`, `src/pages/${path}`, `src/routes/${path}/+page`, `pages/${path}/index`, `src/pages/${path}/index`];
|
|
27480
|
+
const hits = bases.flatMap((b) => [".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".astro", ".mdx"].map((e) => `${b}${e}`)).filter((f) => existsSync5(join5(root, f)));
|
|
27481
|
+
return hits.length === 1 ? hits[0] : void 0;
|
|
27482
|
+
}
|
|
27333
27483
|
function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
27334
27484
|
const det = detectProject(root, env2);
|
|
27335
27485
|
const todos = [];
|
|
@@ -27348,13 +27498,21 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27348
27498
|
merchant.cancel_url = cancelFact;
|
|
27349
27499
|
found.push(`cancel URL ${cancelFact} (from checkbox init)`);
|
|
27350
27500
|
} else if (siteUrl && cancelPath) merchant.cancel_url = `${originOf(siteUrl)}${cancelPath}`;
|
|
27351
|
-
else {
|
|
27501
|
+
else if (cancelUrlInCode(det.checkoutSessions)) {
|
|
27502
|
+
merchant.cancel_url = cancelUrlInCode(det.checkoutSessions);
|
|
27503
|
+
found.push(`cancel URL ${String(merchant.cancel_url)} (from the Checkout custom_text)`);
|
|
27504
|
+
} else {
|
|
27352
27505
|
merchant.cancel_url = "TODO: https://\u2026 page where subscribers cancel online";
|
|
27353
27506
|
todos.push("merchant.cancel_url: the page where subscribers cancel online");
|
|
27354
27507
|
}
|
|
27355
27508
|
if (fact("support_email")) merchant.support_contact = fact("support_email");
|
|
27509
|
+
const initPlans = Array.isArray(facts.plans) ? facts.plans : [];
|
|
27510
|
+
const initName = (p) => {
|
|
27511
|
+
const m = initPlans.find((x) => typeof x.name === "string" && x.amount === p.amount && x.interval === p.interval && (Number(x.interval_count ?? 1) || 1) === (p.interval_count ?? 1));
|
|
27512
|
+
return m ? String(m.name) : void 0;
|
|
27513
|
+
};
|
|
27356
27514
|
const plans = det.plans.map((p, i) => ({
|
|
27357
|
-
name: p.name ?? `Plan ${i + 1}`,
|
|
27515
|
+
name: p.name ?? initName(p) ?? `Plan ${i + 1}`,
|
|
27358
27516
|
amount: p.amount,
|
|
27359
27517
|
currency: p.currency,
|
|
27360
27518
|
interval: p.interval,
|
|
@@ -27393,8 +27551,13 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27393
27551
|
todos.push("plans: the price, interval and trial of each plan (the checkout uses Price ids, which cannot be read from code)");
|
|
27394
27552
|
}
|
|
27395
27553
|
const planNames = plans.map((p) => String(p.name));
|
|
27396
|
-
const
|
|
27397
|
-
|
|
27554
|
+
const oneOf = planNames.map((n) => JSON.stringify(n)).join(", ");
|
|
27555
|
+
let planTodo = false;
|
|
27556
|
+
const planFor = (kind = "entry") => {
|
|
27557
|
+
if (plans.length <= 1) return {};
|
|
27558
|
+
planTodo = true;
|
|
27559
|
+
return { plan: kind === "page" ? `TODO: "all" (the page shows every plan) or one of ${oneOf}` : `TODO: one of ${oneOf}` };
|
|
27560
|
+
};
|
|
27398
27561
|
const stripe = [];
|
|
27399
27562
|
const perFile = /* @__PURE__ */ new Map();
|
|
27400
27563
|
for (const s2 of det.checkoutSessions) {
|
|
@@ -27425,7 +27588,12 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27425
27588
|
} else if (s2.unresolved.length) found.push(`${s2.file}:${s2.line}: Checkout Session (${s2.unresolved.length} value(s) are expressions, read as-is)`);
|
|
27426
27589
|
else found.push(`${s2.file}:${s2.line}: Checkout Session`);
|
|
27427
27590
|
entry.strict = true;
|
|
27428
|
-
|
|
27591
|
+
const sold = s2.params ? plansSoldBy(s2.params, plans) : [];
|
|
27592
|
+
if (plans.length === 1 || sold.length === 1) stripe.push({ ...entry, plan: String((sold[0] ?? plans[0]).name) });
|
|
27593
|
+
else if (s2.params && plans.length > 1 && !plans.some((p) => String(p.name).startsWith("TODO"))) {
|
|
27594
|
+
for (const p of plans) stripe.push({ ...entry, name: unique(names, `${String(entry.name).replace(/\)$/, "")}: ${String(p.name)})`), plan: String(p.name) });
|
|
27595
|
+
names.delete(String(entry.name));
|
|
27596
|
+
} else stripe.push(entry);
|
|
27429
27597
|
}
|
|
27430
27598
|
const emails = [];
|
|
27431
27599
|
const pages = [];
|
|
@@ -27445,7 +27613,8 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27445
27613
|
}
|
|
27446
27614
|
if (PAGE_FILE.test(rel2)) {
|
|
27447
27615
|
const kind = /checkout|subscribe/i.test(rel2) ? "checkout" : "pricing";
|
|
27448
|
-
|
|
27616
|
+
const src = pageSource(root, rel2);
|
|
27617
|
+
pages.push({ name: unique(names, `Page: ${rel2}`), file: rel2, kind, ...planFor("page"), ...src ? { source: src } : {} });
|
|
27449
27618
|
found.push(`${rel2}: ${kind} page`);
|
|
27450
27619
|
return;
|
|
27451
27620
|
}
|
|
@@ -27465,6 +27634,7 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27465
27634
|
if (!disclosures.length && det.payButtons.length) {
|
|
27466
27635
|
todos.push(`The renewal disclosure lives in code (${det.payButtons[0].file}); keep its text in a .txt file the component imports and add it under checks.disclosures, or check the rendered page under checks.pages`);
|
|
27467
27636
|
}
|
|
27637
|
+
if (planTodo) todos.push('plan: pick the plan of each disclosure, page and email entry marked TODO ("all" for a page that shows every plan)');
|
|
27468
27638
|
const checks = {};
|
|
27469
27639
|
if (stripe.length) checks.stripe_checkout = stripe;
|
|
27470
27640
|
if (disclosures.length) checks.disclosures = disclosures;
|
|
@@ -27737,7 +27907,7 @@ function moduleErrorMessage(stderr, dir, flags = []) {
|
|
|
27737
27907
|
const first2 = lines.find((l) => /^(?:[A-Z]\w*)?Error\b.*:/.test(l)) ?? lines.find((l) => /error/i.test(l)) ?? lines[lines.length - 1] ?? "";
|
|
27738
27908
|
return first2.replace(/file:\/\/\S+/g, (u) => rel2(u)).replace(/\s+/g, " ").slice(0, 400);
|
|
27739
27909
|
}
|
|
27740
|
-
function paramsFromSource(text, file, call = 1, env2 = {}) {
|
|
27910
|
+
function paramsFromSource(text, file, call = 1, env2 = {}, consts = {}) {
|
|
27741
27911
|
const calls = [...text.matchAll(/checkout\.sessions\.create\s*\(/g)];
|
|
27742
27912
|
if (!calls.length) throw new InputError(`${file} has no stripe.checkout.sessions.create( call`);
|
|
27743
27913
|
const m = calls[call - 1];
|
|
@@ -27745,7 +27915,7 @@ function paramsFromSource(text, file, call = 1, env2 = {}) {
|
|
|
27745
27915
|
const line2 = text.slice(0, m.index).split("\n").length;
|
|
27746
27916
|
const lit = sessionLiteral(text, m.index + m[0].length);
|
|
27747
27917
|
if (!lit) throw new InputError(`${file}:${line2}: the params are not an object literal; use "module" (a function returning the params) or "file" instead`);
|
|
27748
|
-
const parsed = parseObjectLiteral(lit, env2);
|
|
27918
|
+
const parsed = parseObjectLiteral(lit, env2, consts);
|
|
27749
27919
|
if (!parsed.value) throw new InputError(`${file}:${line2}: could not read the params object`);
|
|
27750
27920
|
return { params: parsed.value, line: line2, unresolved: parsed.unresolved, ...parsed.fromEnv ? { fromEnv: parsed.fromEnv } : {} };
|
|
27751
27921
|
}
|
|
@@ -27849,7 +28019,8 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27849
28019
|
} else {
|
|
27850
28020
|
const src = e.source;
|
|
27851
28021
|
job.inputs.push(src);
|
|
27852
|
-
const
|
|
28022
|
+
const srcText = readRel(dir, src);
|
|
28023
|
+
const r = paramsFromSource(srcText, src, e.call ?? 1, projectEnv(dir), constantsFor(dir, src, srcText));
|
|
27853
28024
|
job.target = { file: rel(loaded, root, src), line: r.line };
|
|
27854
28025
|
job.fromSource = true;
|
|
27855
28026
|
params = r.params;
|
|
@@ -27861,9 +28032,16 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27861
28032
|
}
|
|
27862
28033
|
}
|
|
27863
28034
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new InputError("the Checkout Session params must be a JSON object");
|
|
28035
|
+
const items0 = Array.isArray(params.line_items) ? params.line_items : [];
|
|
28036
|
+
if (!e.plan && (config.plans?.length ?? 0) > 1 && !items0.some((li) => li && typeof li === "object" && li.price_data)) {
|
|
28037
|
+
throw new InputError(`"${e.name}" does not say which plan it sells, and its price is a Price id or an expression Checkbox cannot read: add "plan": one of ${(config.plans ?? []).map((p) => JSON.stringify(p.name)).join(", ")} to this entry (one entry per plan when one call serves several plans).`);
|
|
28038
|
+
}
|
|
27864
28039
|
const merchant = planTerms(config, pickPlan(config, e.plan));
|
|
27865
28040
|
const drift = planDrift(params, pickPlan(config, e.plan));
|
|
27866
28041
|
if (drift) job.warnings.push(drift);
|
|
28042
|
+
const soldPlan = pickPlan(config, e.plan);
|
|
28043
|
+
const overrides = soldPlan ? planAsSold(params, soldPlan) : void 0;
|
|
28044
|
+
if (soldPlan && overrides) job.sold = { plan: soldPlan.name, overrides };
|
|
27867
28045
|
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
27868
28046
|
if (items.length && items.every((li) => li && typeof li === "object" && li.price_data)) for (const k of ["price", "amount", "currency", "interval", "interval_count"]) delete merchant[k];
|
|
27869
28047
|
job.body = { params, ...Object.keys(merchant).length ? { merchant } : {}, ...jurisdictionsOf(config) };
|
|
@@ -27872,6 +28050,29 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27872
28050
|
}
|
|
27873
28051
|
return job;
|
|
27874
28052
|
}
|
|
28053
|
+
function planAsSold(params, plan2) {
|
|
28054
|
+
const sd = params.subscription_data ?? {};
|
|
28055
|
+
const trial = typeof sd.trial_period_days === "number" ? sd.trial_period_days : void 0;
|
|
28056
|
+
const out = {};
|
|
28057
|
+
if (trial !== void 0 && trial !== (plan2.trial_days ?? 0)) out.trial_days = trial;
|
|
28058
|
+
if (trial === void 0 && plan2.trial_days && plan2.trial_days > 0 && sd.trial_end === void 0) out.trial_days = 0;
|
|
28059
|
+
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
28060
|
+
const pd = items.map((li) => li && typeof li === "object" ? li.price_data : void 0).find((x) => x && typeof x === "object");
|
|
28061
|
+
const rec = pd && typeof pd.recurring === "object" ? pd.recurring : void 0;
|
|
28062
|
+
if (rec && typeof rec.interval === "string" && rec.interval !== plan2.interval && ["day", "week", "month", "year"].includes(rec.interval)) out.interval = rec.interval;
|
|
28063
|
+
const price3 = typeof plan2.price === "number" ? plan2.price : typeof plan2.price === "string" && /^[\d.]+$/.test(plan2.price) ? Number(plan2.price) : void 0;
|
|
28064
|
+
if (pd && typeof pd.unit_amount === "number" && price3 !== void 0 && /^(?:usd|eur|gbp|cad|aud|nzd)$/i.test(String(pd.currency ?? plan2.currency ?? "usd")) && (!pd.currency || pd.currency === (plan2.currency ?? "usd")) && Math.abs(pd.unit_amount - Math.round(price3 * 100)) >= 1) {
|
|
28065
|
+
out.price = pd.unit_amount / 100;
|
|
28066
|
+
}
|
|
28067
|
+
return Object.keys(out).length ? out : void 0;
|
|
28068
|
+
}
|
|
28069
|
+
function describeSold(o) {
|
|
28070
|
+
const parts = [];
|
|
28071
|
+
if (o.trial_days !== void 0) parts.push(o.trial_days ? `a ${o.trial_days}-day trial` : "no trial");
|
|
28072
|
+
const charge = [o.price !== void 0 ? Number(o.price).toFixed(2) : "", o.interval ? `every ${o.interval}` : ""].filter(Boolean).join(" ");
|
|
28073
|
+
if (charge) parts.push(charge);
|
|
28074
|
+
return parts.join(", ");
|
|
28075
|
+
}
|
|
27875
28076
|
function planDrift(params, plan2) {
|
|
27876
28077
|
if (!plan2) return void 0;
|
|
27877
28078
|
const sd = params.subscription_data ?? {};
|
|
@@ -27887,7 +28088,7 @@ function planDrift(params, plan2) {
|
|
|
27887
28088
|
if (pd && typeof pd.unit_amount === "number" && price3 !== void 0 && /^(?:usd|eur|gbp|cad|aud|nzd)$/i.test(String(pd.currency ?? plan2.currency ?? "usd")) && (!pd.currency || pd.currency === (plan2.currency ?? "usd")) && Math.abs(pd.unit_amount - Math.round(price3 * 100)) >= 1) {
|
|
27888
28089
|
out.push(`the params charge ${(pd.unit_amount / 100).toFixed(2)}, plan "${plan2.name}" ${price3.toFixed(2)}`);
|
|
27889
28090
|
}
|
|
27890
|
-
return out.length ? `The params and checkbox.json disagree: ${out.join("; ")}. The
|
|
28091
|
+
return out.length ? `The params and checkbox.json disagree: ${out.join("; ")}. The Checkout check uses the params, and the emails, pages and disclosures of plan "${plan2.name}" are checked against both until they agree: update "plans" in checkbox.json to the plan you really sell.` : void 0;
|
|
27891
28092
|
}
|
|
27892
28093
|
function disclosureJob(loaded, e, root) {
|
|
27893
28094
|
const { config, dir } = loaded;
|
|
@@ -27993,9 +28194,35 @@ async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
|
27993
28194
|
}
|
|
27994
28195
|
async function resolveJobs(loaded, o) {
|
|
27995
28196
|
const root = o.root ?? loaded.dir;
|
|
28197
|
+
const jobs = [];
|
|
28198
|
+
for (const e of loaded.config.checks.stripe_checkout ?? []) jobs.push(await stripeJob(loaded, e, o, root));
|
|
28199
|
+
const dependent = await planJobs(loaded, o, root);
|
|
28200
|
+
jobs.push(...dependent);
|
|
28201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28202
|
+
for (const sj of jobs.filter((j) => j.type === "stripe_checkout" && j.sold)) {
|
|
28203
|
+
const key = JSON.stringify(sj.sold);
|
|
28204
|
+
if (seen.has(key)) continue;
|
|
28205
|
+
seen.add(key);
|
|
28206
|
+
const { plan: planName, overrides } = sj.sold;
|
|
28207
|
+
const config = { ...loaded.config, plans: (loaded.config.plans ?? []).map((p) => p.name === planName ? { ...p, ...overrides } : p) };
|
|
28208
|
+
const variant = await planJobs({ ...loaded, config }, o, root);
|
|
28209
|
+
const sold = describeSold(overrides);
|
|
28210
|
+
variant.forEach((v, i) => {
|
|
28211
|
+
const orig = dependent[i];
|
|
28212
|
+
if (orig.error || orig.skip || !v.body || JSON.stringify(v.body) === JSON.stringify(orig.body)) return;
|
|
28213
|
+
jobs.push({
|
|
28214
|
+
...v,
|
|
28215
|
+
name: `${orig.name} (as "${sj.name}" sells it: ${sold})`,
|
|
28216
|
+
inputs: [.../* @__PURE__ */ new Set([...v.inputs, ...sj.inputs])],
|
|
28217
|
+
warnings: [`The params and checkbox.json disagree: "${sj.name}" sells plan "${planName}" with ${sold}, which checkbox.json does not say. This check judges ${orig.name} against what the params sell; the same check against checkbox.json runs too, and both must pass. Update "plans" in checkbox.json (or the params) so they agree.`, ...v.warnings]
|
|
28218
|
+
});
|
|
28219
|
+
});
|
|
28220
|
+
}
|
|
28221
|
+
return jobs;
|
|
28222
|
+
}
|
|
28223
|
+
async function planJobs(loaded, o, root) {
|
|
27996
28224
|
const c = loaded.config.checks;
|
|
27997
28225
|
const jobs = [];
|
|
27998
|
-
for (const e of c.stripe_checkout ?? []) jobs.push(await stripeJob(loaded, e, o, root));
|
|
27999
28226
|
for (const e of c.disclosures ?? []) jobs.push(disclosureJob(loaded, e, root));
|
|
28000
28227
|
for (const e of c.pages ?? []) jobs.push(pageJob(loaded, e, o, root));
|
|
28001
28228
|
for (const e of c.emails ?? []) {
|
|
@@ -28555,7 +28782,7 @@ function onlineEngine(client) {
|
|
|
28555
28782
|
return { mode: "online", run: (kind, body) => client.sdk.call(operationFor2(kind), body) };
|
|
28556
28783
|
}
|
|
28557
28784
|
function ciVocabulary(text) {
|
|
28558
|
-
return text.replace(/Pass every plan the page sells \(plans: \[\.\.\.\]\)/g, 'Set "plan": "all" on this page entry in checkbox.json (every plan the page sells)').replace(/Pass plans with every plan the page sells/g, 'Set "plan": "all" on this page entry in checkbox.json').replace(/\(page_kind "checkout"\)/g, '(a pages entry with "kind": "checkout")').replace(/page_kind "checkout"/g, '"kind": "checkout"').replace(/check the renewal (text|text itself) with check_disclosure/g, 'check the renewal $1 with a "disclosures" entry').replace(/\bcheck_disclosure\b/g, 'a "disclosures" entry').replace(/pass merchant\.price \(e\.g\. 19\.99\), merchant\.currency and merchant\.interval/g, 'add the plan to "plans" in checkbox.json (price, currency, interval) and name it in "plan" on this entry').replace(/merchant\.price \+ merchant\.interval/g, 'a plan in "plans" of checkbox.json (and "plan" on this entry)').replace(/pass merchant\.price and merchant\.interval/gi, 'add the plan to "plans" in checkbox.json and name it in "plan" on this entry').replace(/merchant\.intro_percent_off \(or intro_amount_off\) and merchant\.intro_periods/g, "intro_percent_off (or intro_amount_off) and intro_periods on the plan in checkbox.json").replace(/merchant\.intro_\*/g, "the plan's intro_* in checkbox.json").replace(/merchant\.trial_days \((\d+)\)/g, "trial_days ($1) of the plan in checkbox.json").replace(/merchant\.price was ignored/g, "the plan's price in checkbox.json was ignored").replace(/In test mode, pass the page source as html\./g, 'With a test key, list the built page under "file" in checkbox.json instead of "url" (or run --online with a live key, ck_live_\u2026).');
|
|
28785
|
+
return text.replace(/Pass every plan the page sells \(plans: \[\.\.\.\]\)/g, 'Set "plan": "all" on this page entry in checkbox.json (every plan the page sells)').replace(/Pass plans with every plan the page sells/g, 'Set "plan": "all" on this page entry in checkbox.json').replace(/\(page_kind "checkout"\)/g, '(a pages entry with "kind": "checkout")').replace(/page_kind "checkout"/g, '"kind": "checkout"').replace(/check the renewal (text|text itself) with check_disclosure/g, 'check the renewal $1 with a "disclosures" entry').replace(/\bcheck_disclosure\b/g, 'a "disclosures" entry').replace(/pass merchant\.price \(e\.g\. 19\.99\), merchant\.currency and merchant\.interval/g, 'add the plan to "plans" in checkbox.json (price, currency, interval) and name it in "plan" on this entry').replace(/merchant\.price \+ merchant\.interval/g, 'a plan in "plans" of checkbox.json (and "plan" on this entry)').replace(/pass merchant\.price and merchant\.interval/gi, 'add the plan to "plans" in checkbox.json and name it in "plan" on this entry').replace(/merchant\.intro_percent_off \(or intro_amount_off\) and merchant\.intro_periods/g, "intro_percent_off (or intro_amount_off) and intro_periods on the plan in checkbox.json").replace(/merchant\.intro_\*/g, "the plan's intro_* in checkbox.json").replace(/merchant\.trial_days \((\d+)\)/g, "trial_days ($1) of the plan in checkbox.json").replace(/merchant\.price was ignored/g, "the plan's price in checkbox.json was ignored").replace(/comes from merchant\.price and is not verified against the Price\. Connect Stripe on the site, or register the params with createStripeCheckoutConfig on a site whose Stripe account has this Price\./g, "comes from the plan's price in checkbox.json and is not verified against the Price. Connect Stripe on the site in Checkbox, then run checkbox ci --online (CHECKBOX_SECRET_KEY) so Checkbox reads the Price.").replace(/comes from merchant\.price\b/g, "comes from the plan's price in checkbox.json").replace(/\bmerchant\.price says\b/g, "The plan's price in checkbox.json says").replace(/\(or pass merchant\.cancel_url\)/g, "(or set merchant.cancel_url in checkbox.json)").replace(/In test mode, pass the page source as html\./g, 'With a test key, list the built page under "file" in checkbox.json instead of "url" (or run --online with a live key, ck_live_\u2026).');
|
|
28559
28786
|
}
|
|
28560
28787
|
function pricingPageFixes(fixes) {
|
|
28561
28788
|
const reworded = fixes.map(
|
|
@@ -28565,10 +28792,18 @@ function pricingPageFixes(fixes) {
|
|
|
28565
28792
|
const renewalLineFor = /* @__PURE__ */ new Set();
|
|
28566
28793
|
for (const f of reworded) {
|
|
28567
28794
|
const card = /^(Plan card [^:]+): /.exec(f)?.[1] ?? "";
|
|
28568
|
-
const isRenewalLine = /\b(?:Add one line under the price|Put the renewal line under the price|Move the renewal line under the price)/.test(f);
|
|
28795
|
+
const isRenewalLine = /\b(?:Add one line under the price|Put the renewal line under the price|Move the renewal line under the price|Add your own renewal line next to the (?:pay|plan's) button)/.test(f);
|
|
28569
28796
|
if (isRenewalLine) {
|
|
28570
|
-
|
|
28571
|
-
renewalLineFor.
|
|
28797
|
+
const cards = [...card.matchAll(/“([^”]+)”/g)].map((m) => m[1]);
|
|
28798
|
+
const fresh = cards.filter((c) => !renewalLineFor.has(c));
|
|
28799
|
+
if (cards.length ? !fresh.length : renewalLineFor.has(card)) continue;
|
|
28800
|
+
if (!cards.length) renewalLineFor.add(card);
|
|
28801
|
+
for (const c of fresh) renewalLineFor.add(c);
|
|
28802
|
+
if (cards.length && fresh.length < cards.length) {
|
|
28803
|
+
const g = `Plan card ${fresh.map((c) => `\u201C${c}\u201D`).join(", ")}: ${f.slice(card.length + 2)}`;
|
|
28804
|
+
if (!out.includes(g)) out.push(g);
|
|
28805
|
+
continue;
|
|
28806
|
+
}
|
|
28572
28807
|
}
|
|
28573
28808
|
if (!out.includes(f)) out.push(f);
|
|
28574
28809
|
}
|
|
@@ -28594,12 +28829,23 @@ function fixesOf(r, onlyNeedsFix = false) {
|
|
|
28594
28829
|
failing.sort((a, b) => (a.compliance_status === "needs_fix" ? 0 : 1) - (b.compliance_status === "needs_fix" ? 0 : 1) || (RANK2[a.status] ?? 2) - (RANK2[b.status] ?? 2));
|
|
28595
28830
|
for (const c of failing) {
|
|
28596
28831
|
const base = c.compliance?.fix || c.fix || c.compliance?.question?.text || c.explanation || c.title || c.check_id;
|
|
28597
|
-
|
|
28832
|
+
let cards = failingCards(c.explanation);
|
|
28833
|
+
const plain = cards.length > 1 ? cardClauses(c.explanation).filter((x) => /look like the surrounding text/.test(x.clause)).map((x) => x.card) : [];
|
|
28834
|
+
if (plain.length && plain.length < cards.length && /renewal line/i.test(base)) {
|
|
28835
|
+
cards = cards.filter((x) => !plain.includes(x));
|
|
28836
|
+
const standOut = `Plan card ${plain.map((x) => `\u201C${x}\u201D`).join(", ")}: ${STAND_OUT_FIX}`;
|
|
28837
|
+
if (!out.includes(standOut)) out.push(standOut);
|
|
28838
|
+
}
|
|
28598
28839
|
const text = cards.length && base ? `Plan card ${cards.map((x) => `\u201C${x}\u201D`).join(", ")}: ${base}` : base;
|
|
28599
28840
|
if (text && !out.includes(text)) out.push(text);
|
|
28600
28841
|
}
|
|
28601
28842
|
return out;
|
|
28602
28843
|
}
|
|
28844
|
+
function cardClauses(explanation) {
|
|
28845
|
+
if (!explanation) return [];
|
|
28846
|
+
const ms = [...explanation.matchAll(/plan button “([^”]{1,80})”(?: in the card “([^”]{1,80})”)?/g)];
|
|
28847
|
+
return ms.map((m, i) => ({ card: m[2] ?? m[1], clause: explanation.slice(m.index + m[0].length, ms[i + 1]?.index ?? explanation.length) }));
|
|
28848
|
+
}
|
|
28603
28849
|
function failingCards(explanation) {
|
|
28604
28850
|
if (!explanation) return [];
|
|
28605
28851
|
if (/\(\d+ checkout page views? checked\) — e\.g\./.test(explanation)) return [];
|
|
@@ -28768,18 +29014,29 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28768
29014
|
engine.mode === "offline" ? `The intro price is declared in checkbox.json, not verified: offline, Checkbox cannot read ${what} from Stripe. Run with --online (CHECKBOX_SECRET_KEY, and the site's Stripe key connected) so Checkbox reads it and compares its percent / amount off and duration with the declared intro.` : `The intro price is declared in checkbox.json, not verified: Checkbox could not read ${what} through the site's connected Stripe key (is the site known, via --site or CHECKBOX_SITE, and does the key have Coupons: Read?). Until it is read, its percent / amount off and duration are not compared with the declared intro.`
|
|
28769
29015
|
);
|
|
28770
29016
|
}
|
|
28771
|
-
const
|
|
29017
|
+
const sharedReview = !unread && !declaredCoupon && status === "pass" && job.type === "stripe_checkout" ? stripeParamsVerdict(report) : void 0;
|
|
29018
|
+
const unreadCoupon = sharedReview?.verdict === "needs_review" && sharedReview.reason ? sharedReview.reason : void 0;
|
|
29019
|
+
if (unreadCoupon) {
|
|
29020
|
+
const id = /discounts use (coupon|promotion code) (\S+?),/.exec(unreadCoupon);
|
|
29021
|
+
const what = id ? `${id[1]} ${id[2]}` : "the coupon";
|
|
29022
|
+
fixes.unshift(
|
|
29023
|
+
engine.mode === "offline" ? `The params apply ${what}, which Checkbox cannot read offline, so its discount and duration (what the shopper is charged first) are not checked. Declare the intro on the plan in checkbox.json (intro_percent_off, intro_amount_off or intro_price, with intro_periods) and state it in custom_text.submit, or run with --online so Checkbox reads the coupon.` : `The params apply ${what}, which Checkbox could not read through the site's connected Stripe key (is the site known, via --site or CHECKBOX_SITE, and does the key have Coupons: Read?), so its discount and duration are not checked. Declare the intro on the plan in checkbox.json and state it in custom_text.submit, or let Checkbox read the coupon.`
|
|
29024
|
+
);
|
|
29025
|
+
}
|
|
29026
|
+
const finalStatus = unread ? literal.length ? "fail" : "needs_review" : declaredCoupon || unreadCoupon ? "needs_review" : status;
|
|
28772
29027
|
const corrected = unread ? void 0 : correctedOf(job.type, report, job.partial, job.placeholders);
|
|
28773
29028
|
if (corrected?.format === "json" && job.placeholders) {
|
|
28774
29029
|
const unset = job.warnings.filter((w) => /(?:was|were) not set when Checkbox ran/.test(w));
|
|
28775
29030
|
if (unset.length) Object.assign(base, { alerts: [...unset, ...base.alerts ?? []] });
|
|
28776
29031
|
}
|
|
28777
|
-
const blocking = unread || status === "needs_review" && (job.type === "page" || job.type === "disclosure") && (report.compliance_status === "incomplete" || report.compliance_status === "not_checked") ||
|
|
29032
|
+
const blocking = unread || status === "needs_review" && (job.type === "page" || job.type === "disclosure") && (report.compliance_status === "incomplete" || report.compliance_status === "not_checked") || // Round 31 (pilot 30 item 2, headless-agent-pr false pass: a "strict" entry with a declared-but-unverified coupon exited
|
|
29033
|
+
// 0): the entry's strictness applies to the status the check reports (finalStatus), not the engine's raw verdict.
|
|
29034
|
+
finalStatus === "needs_review" && job.type === "stripe_checkout" && (!!job.strict || !!offer);
|
|
28778
29035
|
return {
|
|
28779
29036
|
...base,
|
|
28780
29037
|
status: finalStatus,
|
|
28781
29038
|
...blocking ? { blocking: true } : {},
|
|
28782
|
-
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary)) : void 0),
|
|
29039
|
+
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : unreadCoupon ? "Needs review \xB7 coupon not read" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary)) : void 0),
|
|
28783
29040
|
...fixes[0] ? { fix: fixes[0] } : {},
|
|
28784
29041
|
fixes,
|
|
28785
29042
|
...corrected ? { corrected } : {},
|
|
@@ -28850,7 +29107,7 @@ async function runCi(jobs, engine, opts) {
|
|
|
28850
29107
|
disclaimer: CI_DISCLAIMER
|
|
28851
29108
|
};
|
|
28852
29109
|
}
|
|
28853
|
-
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL, ALERT_CODES;
|
|
29110
|
+
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES, STAND_OUT_FIX, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL, ALERT_CODES;
|
|
28854
29111
|
var init_runner = __esm({
|
|
28855
29112
|
"src/ci/runner.ts"() {
|
|
28856
29113
|
"use strict";
|
|
@@ -28858,6 +29115,7 @@ var init_runner = __esm({
|
|
|
28858
29115
|
init_http();
|
|
28859
29116
|
init_src();
|
|
28860
29117
|
init_types2();
|
|
29118
|
+
init_stripe_verdict();
|
|
28861
29119
|
init_config3();
|
|
28862
29120
|
ENGINE_KIND = {
|
|
28863
29121
|
stripe_checkout: "stripe_checkout_session",
|
|
@@ -28869,6 +29127,7 @@ var init_runner = __esm({
|
|
|
28869
29127
|
CI_DISCLAIMER = "Automated check against Checkbox's catalog of automatic-renewal laws; not legal advice.";
|
|
28870
29128
|
RANK2 = { fail: 0, needs_review: 1 };
|
|
28871
29129
|
OK_STATUSES = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
29130
|
+
STAND_OUT_FIX = "Make its renewal line stand out from the text around it: larger type, bold (weight 600 or more), a contrasting colour, or a border.";
|
|
28872
29131
|
NOTICE_TO_EMAIL = {
|
|
28873
29132
|
acknowledgment: "acknowledgment",
|
|
28874
29133
|
trial_ending: "trial_reminder",
|
|
@@ -29884,6 +30143,16 @@ var init_device = __esm({
|
|
|
29884
30143
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
29885
30144
|
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
29886
30145
|
import { dirname as dirname8, join as join10 } from "node:path";
|
|
30146
|
+
function pinnedRelease(body) {
|
|
30147
|
+
const b = body;
|
|
30148
|
+
if (!b || typeof b.version !== "string" || !/^\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(b.version)) return void 0;
|
|
30149
|
+
if (typeof b.integrity !== "string" || !/^sha384-[A-Za-z0-9+/]{64}$/.test(b.integrity)) return void 0;
|
|
30150
|
+
return { version: b.version, integrity: b.integrity };
|
|
30151
|
+
}
|
|
30152
|
+
function snippetScriptTag(s2, jsx = false, attrs = "") {
|
|
30153
|
+
const pin = s2.pinned ? ` integrity="${s2.pinned.integrity}" ${jsx ? "crossOrigin" : "crossorigin"}="anonymous"` : "";
|
|
30154
|
+
return `<script${attrs} async src="${srcOf(s2)}"${pin} data-key="${s2.snippetKey}" data-api="${s2.apiBase.replace(/\/+$/, "")}"></script>`;
|
|
30155
|
+
}
|
|
29887
30156
|
function read(root, rel2) {
|
|
29888
30157
|
try {
|
|
29889
30158
|
return readFileSync11(join10(root, rel2), "utf8");
|
|
@@ -29892,19 +30161,20 @@ function read(root, rel2) {
|
|
|
29892
30161
|
}
|
|
29893
30162
|
}
|
|
29894
30163
|
function srcOf(s2) {
|
|
29895
|
-
|
|
30164
|
+
const api = s2.apiBase.replace(/\/+$/, "");
|
|
30165
|
+
return s2.pinned ? `${api}/s/${s2.pinned.version}.js` : `${api}/s.js`;
|
|
29896
30166
|
}
|
|
29897
30167
|
function jsxBlock(s2, indent) {
|
|
29898
30168
|
return [
|
|
29899
30169
|
`${indent}{/* ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet) */}`,
|
|
29900
|
-
`${indent}
|
|
30170
|
+
`${indent}${snippetScriptTag(s2, true)}`,
|
|
29901
30171
|
`${indent}{/* ${MARK_END} */}`
|
|
29902
30172
|
];
|
|
29903
30173
|
}
|
|
29904
30174
|
function htmlBlock(s2, indent, attrs = "") {
|
|
29905
30175
|
return [
|
|
29906
30176
|
`${indent}<!-- ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet) -->`,
|
|
29907
|
-
`${indent}
|
|
30177
|
+
`${indent}${snippetScriptTag(s2, false, attrs)}`,
|
|
29908
30178
|
`${indent}<!-- ${MARK_END} -->`
|
|
29909
30179
|
];
|
|
29910
30180
|
}
|
|
@@ -29963,7 +30233,7 @@ function planSnippet(d, s2) {
|
|
|
29963
30233
|
const root = d.root;
|
|
29964
30234
|
const file = d.layoutFile;
|
|
29965
30235
|
const manual = `Add this tag inside <head> on every page (your root layout / template):
|
|
29966
|
-
${s2.scriptTag}`;
|
|
30236
|
+
${s2.pinned ? snippetScriptTag(s2) : s2.scriptTag}`;
|
|
29967
30237
|
if (!file) return { change: null, manual };
|
|
29968
30238
|
const existing = read(root, file);
|
|
29969
30239
|
const jsx = (f) => f === "nextjs-app" || f === "nextjs-pages" || f === "remix" || f === "react-router";
|
|
@@ -29979,15 +30249,15 @@ function planSnippet(d, s2) {
|
|
|
29979
30249
|
}
|
|
29980
30250
|
return { change: null, manual };
|
|
29981
30251
|
}
|
|
29982
|
-
if (existing.includes(`data-key="${s2.snippetKey}"`) && existing.includes(MARK_START)) {
|
|
29983
|
-
return { change: { file, action: "unchanged", summary: "Snippet already installed", added: [] } };
|
|
30252
|
+
if (existing.includes(`data-key="${s2.snippetKey}"`) && existing.includes(MARK_START) && existing.includes(`src="${srcOf(s2)}"`) && (!s2.pinned || existing.includes(s2.pinned.integrity))) {
|
|
30253
|
+
return { change: { file, action: "unchanged", summary: s2.pinned ? `Snippet already installed (pinned release ${s2.pinned.version} with SRI)` : "Snippet already installed", added: [] } };
|
|
29984
30254
|
}
|
|
29985
30255
|
const isJsx = jsx(d.framework);
|
|
29986
30256
|
const indentRe = isJsx ? d.framework === "nextjs-pages" ? /<Head\b[^>]*>/ : /<head\b[^>]*>/ : /<\/head>/i;
|
|
29987
30257
|
const indent = isJsx ? indentOf2(existing, indentRe) : indentOf2(existing, /<\/head>/i, " ");
|
|
29988
30258
|
const block = isJsx ? jsxBlock(s2, indent) : htmlBlock(s2, indent, d.framework === "astro" ? " is:inline" : "");
|
|
29989
30259
|
const replaced = replaceMarked(existing, block);
|
|
29990
|
-
if (replaced !== null) return { change: { file, action: "modify", summary: "Snippet tag updated (new site key)", contents: replaced, added: block } };
|
|
30260
|
+
if (replaced !== null) return { change: { file, action: "modify", summary: existing.includes(`data-key="${s2.snippetKey}"`) ? s2.pinned ? `Snippet tag pinned to release ${s2.pinned.version} with SRI` : "Snippet tag updated" : "Snippet tag updated (new site key)", contents: replaced, added: block } };
|
|
29991
30261
|
let next = null;
|
|
29992
30262
|
switch (d.framework) {
|
|
29993
30263
|
case "nextjs-app": {
|
|
@@ -30018,7 +30288,7 @@ ${indentOf2(existing, /<Head\s*\/>/, "")}</Head>`) : null);
|
|
|
30018
30288
|
}
|
|
30019
30289
|
if (!next) return { change: null, manual: `Could not find <head> in ${file}. ${manual}` };
|
|
30020
30290
|
if (!snippetPlacedSafely(next, isJsx)) return { change: null, manual: `Could not place the snippet safely in ${file} (its <head> is not where Checkbox can patch it without breaking the file), so ${file} was NOT changed. ${manual}` };
|
|
30021
|
-
return { change: { file, action: "modify", summary: `Snippet added to <head> in ${file}`, contents: next, added: block } };
|
|
30291
|
+
return { change: { file, action: "modify", summary: `Snippet added to <head> in ${file}${s2.pinned ? ` (pinned release ${s2.pinned.version} with SRI)` : ""}`, contents: next, added: block } };
|
|
30022
30292
|
}
|
|
30023
30293
|
function envVarNames(d) {
|
|
30024
30294
|
return {
|
|
@@ -30126,7 +30396,7 @@ ${jsxBlock(s2, " ").join("\n")}
|
|
|
30126
30396
|
NUXT_PLUGIN = (s2) => `// ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet)
|
|
30127
30397
|
export default defineNuxtPlugin(() => {
|
|
30128
30398
|
useHead({
|
|
30129
|
-
script: [{ src: '${srcOf(s2)}', async: true, 'data-key': '${s2.snippetKey}', 'data-api': '${s2.apiBase.replace(/\/+$/, "")}' }],
|
|
30399
|
+
script: [{ src: '${srcOf(s2)}', async: true,${s2.pinned ? ` integrity: '${s2.pinned.integrity}', crossorigin: 'anonymous',` : ""} 'data-key': '${s2.snippetKey}', 'data-api': '${s2.apiBase.replace(/\/+$/, "")}' }],
|
|
30130
30400
|
});
|
|
30131
30401
|
});
|
|
30132
30402
|
// ${MARK_END}
|
|
@@ -30227,7 +30497,7 @@ async function syncCheckoutConfigs(client, site, sessions, root) {
|
|
|
30227
30497
|
const call = (perFile.get(s2.file) ?? 0) + 1;
|
|
30228
30498
|
perFile.set(s2.file, call);
|
|
30229
30499
|
if (!registrable(s2.params, s2.unresolved)) {
|
|
30230
|
-
out.push({ file: s2.file, line: s2.line, skipped: s2.unresolved.length ? `values not readable statically: ${s2.unresolved.slice(0, 5).join(", ")}` : "not a subscription-mode Checkout Session" });
|
|
30500
|
+
out.push({ file: s2.file, line: s2.line, skipped: s2.unresolved.length ? `values not readable statically: ${s2.unresolved.slice(0, 5).join(", ")} (a variable, call or template string). Next: register the exact params your server sends with createStripeCheckoutConfig (POST /v1/sites/{site}/stripe_checkout_configs), or, once Stripe is connected, create one Checkout Session in Stripe test mode so Checkbox reads it` : "not a subscription-mode Checkout Session" });
|
|
30231
30501
|
continue;
|
|
30232
30502
|
}
|
|
30233
30503
|
try {
|
|
@@ -30265,6 +30535,13 @@ function configLines(configs, now = Date.now()) {
|
|
|
30265
30535
|
};
|
|
30266
30536
|
return ["Checkout params registered:", ...configs.map((c) => c.id ? ` ${c.file}:${c.line} \u2192 ${c.id}${age(c.registered_at)}${c.replaced?.length ? ` (replaced ${c.replaced.length} older config${c.replaced.length === 1 ? "" : "s"})` : ""}` : ` ${c.file}${c.line ? `:${c.line}` : ""}: ${c.skipped}`)];
|
|
30267
30537
|
}
|
|
30538
|
+
async function snippetRelease(client) {
|
|
30539
|
+
try {
|
|
30540
|
+
return pinnedRelease(await client.get("/s/latest.json"));
|
|
30541
|
+
} catch {
|
|
30542
|
+
return void 0;
|
|
30543
|
+
}
|
|
30544
|
+
}
|
|
30268
30545
|
function registrable(params, unresolved) {
|
|
30269
30546
|
if (!params) return false;
|
|
30270
30547
|
if (unresolved.some((u) => /^(mode|line_items|subscription_data|custom_text|consent_collection)\b/.test(u))) return false;
|
|
@@ -30364,7 +30641,7 @@ var init_init = __esm({
|
|
|
30364
30641
|
init_patch2();
|
|
30365
30642
|
VERSION_FLAGS = {
|
|
30366
30643
|
string: ["site-url", "checkout-url", "account-url", "terms-url", "privacy-url", "refund-url", "cancellation-url", "wait-seconds", "agent", "support-email", "merchant-name", "plan"],
|
|
30367
|
-
boolean: ["yes", "dry-run", "open", "wait", "verify", "complete", "live", "stripe", "snippet", "relogin"],
|
|
30644
|
+
boolean: ["yes", "dry-run", "open", "wait", "verify", "complete", "live", "stripe", "snippet", "pin", "relogin"],
|
|
30368
30645
|
alias: { y: "yes" }
|
|
30369
30646
|
};
|
|
30370
30647
|
interactive = (ctx) => !ctx.flags.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
@@ -30376,8 +30653,11 @@ var init_init = __esm({
|
|
|
30376
30653
|
"Run it in the project root. What it does (idempotent; re-run any time):",
|
|
30377
30654
|
" 1. detects the framework, billing platform, checkout code and pay buttons",
|
|
30378
30655
|
' 2. signs you in: prints ONE link ("Open \u2026 and click Approve") and opens the browser; new accounts are free',
|
|
30379
|
-
" 3. writes CHECKBOX_SECRET_KEY and the publishable key to .env.local / .env (and git-ignores it)",
|
|
30380
|
-
"
|
|
30656
|
+
" 3. writes CHECKBOX_SECRET_KEY and the publishable key to .env.local / .env (and git-ignores it); when that file is",
|
|
30657
|
+
" tracked by git, or the folder is deployed as it is, the key is not written to it and stays only in the CLI config",
|
|
30658
|
+
" (~/.config/checkbox/config.json), which the output says",
|
|
30659
|
+
" 4. installs the Checkbox snippet in the root layout (marked with checkbox:snippet comments): the pinned release",
|
|
30660
|
+
" (/s/<version>.js with integrity= and crossorigin=, Subresource Integrity); --no-pin uses the unpinned /s.js",
|
|
30381
30661
|
" 5. registers the site, Terms / refund / cancellation policy URLs (fetched and checked by Checkbox)",
|
|
30382
30662
|
" 6. with your permission, uses the project's Stripe restricted TEST key (rk_test_\u2026; sk_test_ and live keys are refused) for a synthetic test subscription",
|
|
30383
30663
|
" 7. runs the first checks and prints the next step (`checkbox plan --json`); the human gets the summary email",
|
|
@@ -30623,7 +30903,9 @@ var init_init = __esm({
|
|
|
30623
30903
|
if (session.install.script_tag && session.install.snippet_key) {
|
|
30624
30904
|
if (bool(flags, "snippet") === false) manualSnippet = session.install.script_tag;
|
|
30625
30905
|
else {
|
|
30626
|
-
const
|
|
30906
|
+
const pinned = bool(flags, "pin") === false ? void 0 : await snippetRelease(client);
|
|
30907
|
+
if (!pinned && bool(flags, "pin") !== false) warnings.push("The pinned snippet release (/s/latest.json) could not be read, so the unpinned /s.js tag was used. Run `checkbox init` again to pin it with Subresource Integrity.");
|
|
30908
|
+
const plan2 = planSnippet(d, { scriptTag: session.install.script_tag, snippetKey: session.install.snippet_key, apiBase, ...pinned ? { pinned } : {} });
|
|
30627
30909
|
if (plan2.change) changes.push(plan2.change);
|
|
30628
30910
|
if (plan2.manual) manualSnippet = plan2.manual;
|
|
30629
30911
|
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkbox-site/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "The checkbox CLI: auto-renewal law and subscription compliance checks (FTC ROSCA, California ARL, New York and other states) for Stripe Checkout params, pages, disclosures and emails, from a terminal, CI or an AI coding agent. Runs offline.",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"checkbox",
|
|
6
7
|
"subscription",
|
|
7
8
|
"subscription-compliance",
|
|
8
|
-
"subscription compliance",
|
|
9
9
|
"auto-renewal",
|
|
10
|
-
"auto renewal",
|
|
11
|
-
"autorenewal",
|
|
12
10
|
"automatic-renewal",
|
|
11
|
+
"auto-renewal-law",
|
|
13
12
|
"compliance",
|
|
14
13
|
"rosca",
|
|
15
|
-
"click-to-cancel",
|
|
16
14
|
"california-arl",
|
|
17
|
-
"
|
|
15
|
+
"click-to-cancel",
|
|
16
|
+
"stripe",
|
|
17
|
+
"stripe-checkout",
|
|
18
|
+
"ci",
|
|
19
|
+
"github-actions",
|
|
20
|
+
"cli",
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"ai-agent",
|
|
24
|
+
"subscription compliance",
|
|
25
|
+
"auto renewal",
|
|
26
|
+
"autorenewal",
|
|
18
27
|
"auto-renewal-compliance",
|
|
19
28
|
"subscription-cancellation",
|
|
20
29
|
"renewal-reminder",
|
|
21
30
|
"stripe-subscriptions",
|
|
22
31
|
"ftc",
|
|
23
|
-
"negative-option"
|
|
24
|
-
"stripe",
|
|
25
|
-
"stripe-checkout",
|
|
26
|
-
"mcp",
|
|
27
|
-
"ci",
|
|
28
|
-
"github-actions",
|
|
29
|
-
"cli"
|
|
32
|
+
"negative-option"
|
|
30
33
|
],
|
|
31
34
|
"license": "MIT",
|
|
32
35
|
"author": "Checkbox <support@checkbox.site>",
|