@checkbox-site/cli 0.4.3 → 0.4.4
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/checkbox.js +520 -166
- package/package.json +2 -1
package/dist/checkbox.js
CHANGED
|
@@ -2444,7 +2444,7 @@ var init_operations = __esm({
|
|
|
2444
2444
|
/**
|
|
2445
2445
|
* Connect a billing platform.
|
|
2446
2446
|
*
|
|
2447
|
-
* Connects the site's billing platform with the one restricted key (a Stripe restricted key, a Recharge token, a Shopify Admin API token, a Chargebee read-only API key, a Recurly private API key or a Paddle API key with read permissions), so Checkbox reads the customer-portal and reminder settings. Stripe: One Stripe restricted key per site: Read permissions for the checks, and Write permissions that Checkbox uses only after you click Approve on a fix, or in test mode (the one exception: Webhook Endpoints, used once to add Checkbox's webhook endpoint when you connect the key) (checklist: https://checkbox.site/docs/guides/stripe-key). A full secret key (sk_test_ / sk_live_) is refused on every path with test_key_not_restricted: use a restricted key. API keys and AI agents may connect TEST-mode keys (rk_test_…) only; a live key (rk_live_ / sk_live_) is refused with live_key_requires_dashboard, because only a person signed in to the Checkbox dashboard can connect one. The credentials are validated by reading the settings once and stored encrypted. When the key's Stripe account looks like another site's (the site's prices or Payment Links are not in it, or it is already connected in another workspace), nothing is connected and the error billing_account_mismatch asks the human to confirm; retry with confirm_account_mismatch: true only after they said yes. The account owner is emailed about every key connected.
|
|
2447
|
+
* Connects the site's billing platform with the one restricted key (a Stripe restricted key, a Recharge token, a Shopify Admin API token, a Chargebee read-only API key, a Recurly private API key or a Paddle API key with read permissions), so Checkbox reads the customer-portal and reminder settings. Stripe: One Stripe restricted key per site: Read permissions for the checks, and Write permissions that Checkbox uses only after you click Approve on a fix, or in test mode (the one exception: Webhook Endpoints, used once to add Checkbox's webhook endpoint when you connect the key) (checklist: https://checkbox.site/docs/guides/stripe-key). A full secret key (sk_test_ / sk_live_) is refused on every path with test_key_not_restricted: use a restricted key. API keys and AI agents may connect TEST-mode keys (rk_test_…) only; a live key (rk_live_ / sk_live_) is refused with live_key_requires_dashboard, because only a person signed in to the Checkbox dashboard can connect one. The credentials are validated by reading the settings once and stored encrypted. When the key's Stripe account looks like another site's (the site's prices or Payment Links are not in it, or it is already connected in another workspace), nothing is connected and the error billing_account_mismatch asks the human to confirm; retry with confirm_account_mismatch: true only after they said yes; the key is then held until a person confirms the account in the dashboard (an agent cannot confirm it). The account owner is emailed about every key connected.
|
|
2448
2448
|
*
|
|
2449
2449
|
* @param site Unique identifier of the site (site_…).
|
|
2450
2450
|
* @see POST /v1/sites/{site}/integrations (operation `connectBillingPlatform`, scope `sites:write`)
|
|
@@ -2874,7 +2874,7 @@ var init_operations = __esm({
|
|
|
2874
2874
|
/**
|
|
2875
2875
|
* List due notices.
|
|
2876
2876
|
*
|
|
2877
|
-
* The legally required emails you owe subscribers (sites in "You send" mode, the default): kind, the subscriber (billing-platform ids), the legal window for the subscriber's state, the recommended send date, the
|
|
2877
|
+
* The legally required emails you owe subscribers (sites in "You send" mode, the default): kind, the subscriber (billing-platform ids), the legal window for the subscriber's state, the recommended send date, the email (ready to send once email.missing_fields is empty) and the BCC address. Default: everything not yet sent (upcoming, due, overdue), soonest first. Live keys list live subscribers' notices, test keys test-mode ones. Send each one on its send_date (inside its window), then call markNoticeSent (or BCC the notice's bcc address). Subscribe to the notice.due webhook to get each one on its send date.
|
|
2878
2878
|
*
|
|
2879
2879
|
* @see GET /v1/notices/due (operation `listDueNotices`, scope `notices:read`)
|
|
2880
2880
|
*/
|
|
@@ -2904,7 +2904,7 @@ var init_operations = __esm({
|
|
|
2904
2904
|
/**
|
|
2905
2905
|
* Retrieve a due notice.
|
|
2906
2906
|
*
|
|
2907
|
-
* One notice you owe a subscriber, with its window, recommended send date,
|
|
2907
|
+
* One notice you owe a subscriber, with its window, recommended send date, email (ready to send once email.missing_fields is empty), BCC address and send status.
|
|
2908
2908
|
*
|
|
2909
2909
|
* @param notice Unique identifier of the due notice (ntd_…).
|
|
2910
2910
|
* @see GET /v1/notices/{notice} (operation `retrieveDueNotice`, scope `notices:read`)
|
|
@@ -3257,7 +3257,7 @@ var init_operations = __esm({
|
|
|
3257
3257
|
/**
|
|
3258
3258
|
* Store a billing test-mode key.
|
|
3259
3259
|
*
|
|
3260
|
-
* Stores the site's Stripe TEST-mode restricted key (rk_test_…), after checking it with Stripe the way the dashboard's Connect Stripe does. An agent may call this itself once the owner approved the setup; every store is logged on the site's setup timeline and Billing connection line, and the human can revoke it in one click. Live keys are refused (test_key_not_test_mode: a human adds live keys in the dashboard); a key whose Stripe account looks like another site's is refused with billing_account_mismatch
|
|
3260
|
+
* Stores the site's Stripe TEST-mode restricted key (rk_test_…), after checking it with Stripe the way the dashboard's Connect Stripe does. An agent may call this itself once the owner approved the setup; every store is logged on the site's setup timeline and Billing connection line, and the human can revoke it in one click. Live keys are refused (test_key_not_test_mode: a human adds live keys in the dashboard); the owner must have ticked "Let your agent store a Stripe test-mode key" on the approval page of the agent's sign-in or connection (else test_key_declined); a key whose Stripe account looks like another site's is refused with billing_account_mismatch (with confirm_account_mismatch: true the key is connected on hold until a person confirms the account in the dashboard; only a person can confirm it); secret keys (sk_test_…) are refused (test_key_not_restricted). On a test-mode site without a Stripe connection the key also becomes the site's Stripe connection: one key for checks, fixes (only after the human approves each) and test runs. The key's checklist: Customers: Write, Products: Read, Prices: Read, Coupons: Read, Subscriptions: Write, Test clocks: Write, Customer portal: Write, Checkout Sessions: Write, Payment Links: Write, Webhook Endpoints: Write. Replaces an existing key. Next: createTestRun.
|
|
3261
3261
|
*
|
|
3262
3262
|
* @param site Unique identifier of the site (site_…).
|
|
3263
3263
|
* @see POST /v1/sites/{site}/test_credentials (operation `createTestCredential`, scope `test_runs:write`)
|
|
@@ -3579,7 +3579,7 @@ var SDK_VERSION;
|
|
|
3579
3579
|
var init_version = __esm({
|
|
3580
3580
|
"../sdk-node/src/version.ts"() {
|
|
3581
3581
|
"use strict";
|
|
3582
|
-
SDK_VERSION = "0.2.
|
|
3582
|
+
SDK_VERSION = "0.2.2";
|
|
3583
3583
|
}
|
|
3584
3584
|
});
|
|
3585
3585
|
|
|
@@ -13103,6 +13103,14 @@ function offerSlice(c, g) {
|
|
|
13103
13103
|
links: keep(c.links)
|
|
13104
13104
|
};
|
|
13105
13105
|
}
|
|
13106
|
+
function offerCardName(text, cta, index) {
|
|
13107
|
+
const t = normalizeText(text).replace(/\s+/g, " ").trim();
|
|
13108
|
+
const money3 = /(?:[$€£]\s?\d[\d,]*(?:\.\d{1,2})?|\d[\d,]*(?:\.\d{1,2})?\s?(?:USD|EUR|GBP))(?:\s*(?:\/|per|a|an|every)\s*(?:month|mo|year|yr|week|day|quarter)\b)?/i.exec(t);
|
|
13109
|
+
const before = (money3 ? t.slice(0, money3.index) : "").replace(new RegExp(`^${cta.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "i"), "").replace(/[\s:·|–—-]+$/, "").trim();
|
|
13110
|
+
const heading = before && before.length <= 40 ? before : before ? before.split(" ").slice(0, 4).join(" ") : "";
|
|
13111
|
+
const price3 = money3?.[0].trim();
|
|
13112
|
+
return heading && price3 ? `${heading}, ${price3}` : heading || price3 || `card ${index + 1}`;
|
|
13113
|
+
}
|
|
13106
13114
|
function buildSignupView(capture, offer) {
|
|
13107
13115
|
const c = offer === void 0 ? capture : offerSlice(capture, offer);
|
|
13108
13116
|
const device = c.device === "mobile" ? "mobile" : "desktop";
|
|
@@ -13127,9 +13135,11 @@ function buildSignupView(capture, offer) {
|
|
|
13127
13135
|
[...c.disclosures ?? [], ...checkboxes, ...allLinks].filter(isVisible).map((e) => e.style?.fontSizePx ?? NaN)
|
|
13128
13136
|
);
|
|
13129
13137
|
const offerCta = offer === void 0 ? void 0 : capture.offers?.[offer]?.cta ?? primaryCta2?.text;
|
|
13138
|
+
const sameCta = offer !== void 0 && offerCta ? (capture.offers ?? []).filter((o) => normalizeText(o.cta) === normalizeText(offerCta)).length > 1 : false;
|
|
13139
|
+
const card = sameCta ? offerCardName(capture.offers[offer].text, offerCta, offer) : void 0;
|
|
13130
13140
|
return {
|
|
13131
13141
|
capture,
|
|
13132
|
-
label: offerCta ? `${captureLabel(c)} \u2014 plan button \u201C${normalizeText(offerCta).slice(0, 60)}\u201D` : captureLabel(c),
|
|
13142
|
+
label: offerCta ? `${captureLabel(c)} \u2014 plan button \u201C${normalizeText(offerCta).slice(0, 60)}\u201D${card ? ` in the card \u201C${card}\u201D` : ""}` : captureLabel(c),
|
|
13133
13143
|
...offer !== void 0 ? { offer: { index: offer, cta: normalizeText(offerCta ?? "").slice(0, 120) } } : {},
|
|
13134
13144
|
device,
|
|
13135
13145
|
viewport,
|
|
@@ -15056,6 +15066,11 @@ function offerQuestionStop(flow) {
|
|
|
15056
15066
|
const last = [...flow.steps ?? []].reverse().find((s2) => s2.pageKind !== "account");
|
|
15057
15067
|
return last && OFFER_PERMISSION_RE.test(normalizeText(last.text)) ? last : void 0;
|
|
15058
15068
|
}
|
|
15069
|
+
function stuckHint(flow) {
|
|
15070
|
+
const blockers = flow.blockers ?? [];
|
|
15071
|
+
if (flow.completed || blockers.some((b) => /answered the cancel request with an error/i.test(b))) return "";
|
|
15072
|
+
return blockers.some((b) => /no further cancel option|not completed within \d+ steps/i.test(b)) ? `. ${CANCEL_STEPS_HINT}` : "";
|
|
15073
|
+
}
|
|
15059
15074
|
function offerQuestionStopResult(id, stop, refs2, data) {
|
|
15060
15075
|
return makeResult(
|
|
15061
15076
|
id,
|
|
@@ -15345,7 +15360,7 @@ function notRefreshedUnits(stale, fresh) {
|
|
|
15345
15360
|
};
|
|
15346
15361
|
});
|
|
15347
15362
|
}
|
|
15348
|
-
var CANCEL_MAX_STEPS, CANCEL_MAX_RETENTION_OFFERS, CANCEL_LINK_MIN_FONT_PX, CANCEL_LINK_MIN_CONTRAST, CANCEL_FLOW_MAX_AGE_DAYS, CLICK_TO_CANCEL_JURISDICTIONS, OFFER_PERMISSION_JURISDICTIONS, RETENTION_PAGE_RE, OFFER_PERMISSION_RE, ONLINE_ALTERNATIVE_RE, NO_CANCEL_ATTEMPT_MESSAGE, OBSTACLE_BLOCKER_RE, CONTACT_BLOCKER_RE, NOT_FOUND_BLOCKER_RE, RETENTION_BLOCKER_RE, TECHNICAL_BLOCKER_RE, CONTACT_TO_CANCEL_RE, CANCEL_CONTROL_RE, ORDER_ONLY_RE, MANAGE_RE, HOSTED_FLOW_NOTE, onlineAvailable, SUBSCRIPTION_CANCEL_LINK_RE, BILLING_ITEM_RE, NO_ACTIVE_SUBSCRIPTION_RE, FIND_RANK, mechanismFindable, flowFriction, CANCEL_CONFIRM_EMAIL_RE, confirmation, CANCELLATION_CHECKS;
|
|
15363
|
+
var CANCEL_MAX_STEPS, CANCEL_MAX_RETENTION_OFFERS, CANCEL_LINK_MIN_FONT_PX, CANCEL_LINK_MIN_CONTRAST, CANCEL_FLOW_MAX_AGE_DAYS, CLICK_TO_CANCEL_JURISDICTIONS, OFFER_PERMISSION_JURISDICTIONS, RETENTION_PAGE_RE, OFFER_PERMISSION_RE, ONLINE_ALTERNATIVE_RE, NO_CANCEL_ATTEMPT_MESSAGE, OBSTACLE_BLOCKER_RE, CONTACT_BLOCKER_RE, NOT_FOUND_BLOCKER_RE, RETENTION_BLOCKER_RE, TECHNICAL_BLOCKER_RE, CONTACT_TO_CANCEL_RE, CANCEL_CONTROL_RE, ORDER_ONLY_RE, MANAGE_RE, HOSTED_FLOW_NOTE, CANCEL_STEPS_HINT, onlineAvailable, SUBSCRIPTION_CANCEL_LINK_RE, BILLING_ITEM_RE, NO_ACTIVE_SUBSCRIPTION_RE, FIND_RANK, mechanismFindable, flowFriction, CANCEL_CONFIRM_EMAIL_RE, confirmation, CANCELLATION_CHECKS;
|
|
15349
15364
|
var init_cancellation = __esm({
|
|
15350
15365
|
"../rules/src/checks/cancellation.ts"() {
|
|
15351
15366
|
"use strict";
|
|
@@ -15379,6 +15394,7 @@ var init_cancellation = __esm({
|
|
|
15379
15394
|
ORDER_ONLY_RE = /\bcancel\s+(?:this\s+|my\s+)?order\b/i;
|
|
15380
15395
|
MANAGE_RE = /\b(?:manage\s+(?:my\s+|your\s+)?(?:subscriptions?|membership|plan|billing)|billing\s+portal|customer\s+portal|subscription\s+settings|my\s+subscriptions?|membership\s+settings|billing\s+(?:&|and)\s+plans?)\b/i;
|
|
15381
15396
|
HOSTED_FLOW_NOTE = "Checkbox-hosted cancellation page: subscribers confirm their email through a one-time link, then cancel in two clicks (choose the subscription, confirm), with at most one save offer shown behind a continuously displayed cancel button and an on-screen and emailed confirmation";
|
|
15397
|
+
CANCEL_STEPS_HINT = `If Checkbox took a wrong turn on your pages, list the clicks in the site's scan settings as cancel_steps (for example: click "Cancel membership", then click "No thanks, cancel") and re-check`;
|
|
15382
15398
|
onlineAvailable = {
|
|
15383
15399
|
id: "cancel.online_available",
|
|
15384
15400
|
usesText: false,
|
|
@@ -15485,10 +15501,10 @@ var init_cancellation = __esm({
|
|
|
15485
15501
|
return makeResult(
|
|
15486
15502
|
id,
|
|
15487
15503
|
"needs_review",
|
|
15488
|
-
`The test cancellation did not complete${flow.blockers?.length ? ` (${flow.blockers.join("; ")})` : ""}, so online cancellation could not be confirmed`,
|
|
15504
|
+
`The test cancellation did not complete${flow.blockers?.length ? ` (${flow.blockers.join("; ")})` : ""}, so online cancellation could not be confirmed${stuckHint(flow)}`,
|
|
15489
15505
|
refs2,
|
|
15490
15506
|
0.4,
|
|
15491
|
-
data
|
|
15507
|
+
stuckHint(flow) ? { ...data, suggestCancelSteps: true } : data
|
|
15492
15508
|
);
|
|
15493
15509
|
}
|
|
15494
15510
|
const offline = offlineOnlyCancellation(input);
|
|
@@ -15734,13 +15750,13 @@ var init_cancellation = __esm({
|
|
|
15734
15750
|
const offerStop = offerQuestionStop(flow);
|
|
15735
15751
|
if (offerStop) return offerQuestionStopResult(id, offerStop, refs2, data);
|
|
15736
15752
|
if (b.other.length) {
|
|
15737
|
-
return makeResult(id, "needs_review", `The cancellation flow reported: ${b.other.join("; ")}`, refs2, 0.6, data);
|
|
15753
|
+
return makeResult(id, "needs_review", `The cancellation flow reported: ${b.other.join("; ")}${stuckHint(flow)}`, refs2, 0.6, stuckHint(flow) ? { ...data, suggestCancelSteps: true } : data);
|
|
15738
15754
|
}
|
|
15739
15755
|
if (!flow.completed) {
|
|
15740
15756
|
return makeResult(
|
|
15741
15757
|
id,
|
|
15742
15758
|
"needs_review",
|
|
15743
|
-
`The test cancellation did not complete${flow.blockers?.length ? ` (${flow.blockers.join("; ")})` : ""}; friction could not be fully assessed`,
|
|
15759
|
+
`The test cancellation did not complete${flow.blockers?.length ? ` (${flow.blockers.join("; ")})` : ""}; friction could not be fully assessed${stuckHint(flow)}`,
|
|
15744
15760
|
refs2,
|
|
15745
15761
|
0.4,
|
|
15746
15762
|
data
|
|
@@ -15999,6 +16015,19 @@ var init_config2 = __esm({
|
|
|
15999
16015
|
}
|
|
16000
16016
|
return makeResult(id, "fail", `Self-serve cancellation is turned off in ${name}'s customer portal`, refs(input), 0.9, { ...data, cancelPath: "portal_only" });
|
|
16001
16017
|
}
|
|
16018
|
+
if (model.otherOnlineWorks) {
|
|
16019
|
+
const flow = input.ctx.cancellation;
|
|
16020
|
+
const works = inApp?.state === "works" && !!flow;
|
|
16021
|
+
const via = works ? "subscribers cancel online through your own page (a test cancellation completed; see \u201Ccancel online\u201D)" : model.hosted?.state === "offered" ? `subscribers cancel through the Checkbox hosted cancellation page (${model.hosted.url}); see \u201Ccancel online\u201D` : `your own page offers a cancel option (${inApp?.url}); online cancellation is judged on that page (see \u201Ccancel online\u201D), so the portal setting is not required`;
|
|
16022
|
+
return makeResult(
|
|
16023
|
+
id,
|
|
16024
|
+
"not_applicable",
|
|
16025
|
+
`Checkbox could not read whether self-serve cancellation is on in ${name}'s portal, but ${via}`,
|
|
16026
|
+
[...refs(input), ...works ? [flow.id] : []],
|
|
16027
|
+
works ? 0.75 : 0.6,
|
|
16028
|
+
{ ...data, verifiedBy: "cancel.online_available", ...works ? { cancellationId: flow.id } : { ownCancelPage: true }, ...inApp ? { cancelsInApp: true } : {} }
|
|
16029
|
+
);
|
|
16030
|
+
}
|
|
16002
16031
|
return makeResult(id, "needs_review", `Could not read whether self-serve cancellation is enabled in ${name}`, refs(input), 0.4, data);
|
|
16003
16032
|
}
|
|
16004
16033
|
};
|
|
@@ -18529,7 +18558,12 @@ async function overViews(checkId, input, headlines, evaluateView) {
|
|
|
18529
18558
|
for (const view of views) outcomes.push({ view, unit: await evaluateView(view) });
|
|
18530
18559
|
const failing = outcomes.filter((o) => o.unit.status === "fail");
|
|
18531
18560
|
const introOnly = checkId === "signup.disclosure.price" && failing.length > 0 && failing.every((o) => typeof o.unit.meta?.introFix === "string");
|
|
18532
|
-
|
|
18561
|
+
const mismatchOnly = checkId === "signup.disclosure.price" && !introOnly && failing.length > 0 && failing.every((o) => o.unit.meta?.priceMismatch === true);
|
|
18562
|
+
return aggregateViews(
|
|
18563
|
+
checkId,
|
|
18564
|
+
outcomes,
|
|
18565
|
+
introOnly ? { ...headlines, fail: "The introductory offer next to the purchase button is missing or misstated" } : mismatchOnly ? { ...headlines, fail: "The recurring charge stated next to the purchase button is not what the plan charges" } : headlines
|
|
18566
|
+
);
|
|
18533
18567
|
}
|
|
18534
18568
|
function pageAnalysis(view) {
|
|
18535
18569
|
const entry = heuristicCache.get(view) ?? {};
|
|
@@ -18821,6 +18855,12 @@ function introPriceUnit(u, view, plans, promosDeclared = false) {
|
|
|
18821
18855
|
{ quote: said, meta: { ...u.meta ?? {}, priceMismatch: true, introPriceMismatch: true, introFix: introFixLine(p.plan) } }
|
|
18822
18856
|
);
|
|
18823
18857
|
}
|
|
18858
|
+
function setOffAdvice(sizePx, contrast, weight, ref) {
|
|
18859
|
+
const refWeight = ref.fontWeight ?? 400;
|
|
18860
|
+
const larger = Math.ceil(ref.fontSizePx * SET_OFF_MIN_SIZE_RATIO);
|
|
18861
|
+
const ways = [`${larger}px or larger (the surrounding text is about ${ref.fontSizePx}px)`, ...refWeight < 600 ? ["semibold or bold (weight 600+; the surrounding text is weight " + refWeight + ")"] : [], "a contrasting colour", "a border or shaded box (confirm by eye: borders and symbols cannot be measured from a capture)"];
|
|
18862
|
+
return `the renewal terms are legible (${sizePx}px, weight ${weight}${contrast !== null ? `, ${contrast}:1 contrast` : ""}) but look like the surrounding text (about ${ref.fontSizePx}px, weight ${refWeight}${refWeight >= 600 ? ": already bold, so a bolder weight cannot set them apart" : ""}, similar colour) \u2014 California requires larger type, contrasting type / font / color, or setting them off with symbols or a border. Any one of these would: ${ways.join("; ")}`;
|
|
18863
|
+
}
|
|
18824
18864
|
function trialObservedOutsideCheckout(input) {
|
|
18825
18865
|
const ctx = input.ctx;
|
|
18826
18866
|
return [...ctx.config?.plans ?? [], ...ctx.referencePlans ?? []].some((p) => (p.trialDays ?? 0) > 0) || (ctx.events ?? []).some((e) => e.type === "trial_will_end" || (e.trialDays ?? 0) > 0) || input.data.emails.some((e) => e.kind === "trial_ending");
|
|
@@ -19767,7 +19807,7 @@ ${view.pageText}`;
|
|
|
19767
19807
|
if (setOff && !setOff.differentiators.length) {
|
|
19768
19808
|
return unit(
|
|
19769
19809
|
"needs_review",
|
|
19770
|
-
setOff.referenceUnknown ? `the renewal terms are legible (${best.a.fontSizePx}px, ${best.a.contrastRatio}:1 contrast), but there is no surrounding text to show they are set apart from it \u2014 California requires larger type, contrasting type / font / color, or setting them off with symbols or a border` :
|
|
19810
|
+
setOff.referenceUnknown ? `the renewal terms are legible (${best.a.fontSizePx}px, ${best.a.contrastRatio}:1 contrast), but there is no surrounding text to show they are set apart from it \u2014 California requires larger type, contrasting type / font / color, or setting them off with symbols or a border` : setOffAdvice(best.a.fontSizePx, best.a.contrastRatio, best.el.style?.fontWeight ?? 400, setOff.reference),
|
|
19771
19811
|
0.55,
|
|
19772
19812
|
{ meta: { ...meta, ...setOffMeta }, quote: q2 }
|
|
19773
19813
|
);
|
|
@@ -20472,7 +20512,7 @@ var NOTICES_TEST_MODE_NOTE, NOTICE_KIND_CHECKS, NOT_SEEN_RE;
|
|
|
20472
20512
|
var init_notices_on = __esm({
|
|
20473
20513
|
"../rules/src/checks/notices-on.ts"() {
|
|
20474
20514
|
"use strict";
|
|
20475
|
-
NOTICES_TEST_MODE_NOTE = "Test mode:
|
|
20515
|
+
NOTICES_TEST_MODE_NOTE = "Test mode: emails for a Stripe test customer go to you as a marked \u201CTest mode copy\u201D (and to the test-run inbox during a test run), never to the test customer. Use 'Send me a test' or a test run to see it work.";
|
|
20476
20516
|
NOTICE_KIND_CHECKS = {
|
|
20477
20517
|
acknowledgment: ["confirmation.email.sent", "confirmation.email.terms", "confirmation.email.cancellation_instructions", "confirmation.email.trial_terms"],
|
|
20478
20518
|
trial_ending: ["reminder.trial_ending", "config.trial_reminder_enabled"],
|
|
@@ -22016,6 +22056,9 @@ function planFrequencyFix(c) {
|
|
|
22016
22056
|
const every = /\bplans? (?:is|are) billed (every [a-z0-9 ]+?)(?= \/ | —|[.;,]|$)/i.exec(c.detail)?.[1];
|
|
22017
22057
|
return every ? `State how often it is charged ("${every}").` : void 0;
|
|
22018
22058
|
}
|
|
22059
|
+
function billingUnread(findings) {
|
|
22060
|
+
return findings.some((f) => String(f.checkId).startsWith("config.") && /^No billing platform (?:connected|settings to read)/i.test(f.detail ?? ""));
|
|
22061
|
+
}
|
|
22019
22062
|
function rollup(items, opts = {}) {
|
|
22020
22063
|
const counts = { needs_fix: 0, your_call: 0, pending_live_check: 0, verified_live: 0, compliant: 0, not_applicable: 0, not_set_up: 0, not_checked: 0 };
|
|
22021
22064
|
const pending = [];
|
|
@@ -22059,7 +22102,8 @@ function rollup(items, opts = {}) {
|
|
|
22059
22102
|
label = `${counts.not_checked} not checked yet${counts.pending_live_check ? ` \xB7 ${plural2(counts.pending_live_check, "live check", "live checks")} pending` : ""}`;
|
|
22060
22103
|
} else {
|
|
22061
22104
|
status = "compliant";
|
|
22062
|
-
|
|
22105
|
+
const pass = opts.billingUnread ? CHECKS_PASS_UNREAD_LABEL : "Checks pass";
|
|
22106
|
+
label = counts.pending_live_check ? `${pass} \xB7 ${plural2(counts.pending_live_check, "live check", "live checks")} pending` : counts.verified_live ? `${pass} \xB7 verified live` : pass;
|
|
22063
22107
|
}
|
|
22064
22108
|
return { status, label, counts, pending, suggestions, ...opts.groupOf ? { byTask: Object.fromEntries(groups) } : {} };
|
|
22065
22109
|
}
|
|
@@ -22092,7 +22136,7 @@ function classifyFindings(findings) {
|
|
|
22092
22136
|
info: classify({ checkId: f.checkId, status: f.status, detail: f.detail, missing: f.evidence?.missing, data: f.data, origin: "evaluation", siblings })
|
|
22093
22137
|
}));
|
|
22094
22138
|
}
|
|
22095
|
-
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, STATUS_RANK;
|
|
22139
|
+
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;
|
|
22096
22140
|
var init_status = __esm({
|
|
22097
22141
|
"../rules/src/status.ts"() {
|
|
22098
22142
|
"use strict";
|
|
@@ -22781,6 +22825,7 @@ var init_status = __esm({
|
|
|
22781
22825
|
"terms.refund_policy_consistent": "Make the refund policy match the money-back promise on your site.",
|
|
22782
22826
|
"terms.renewal_terms_present": "Add a short automatic-renewal section to your Terms."
|
|
22783
22827
|
};
|
|
22828
|
+
CHECKS_PASS_UNREAD_LABEL = "Checks pass on what we can see";
|
|
22784
22829
|
STATUS_RANK = { needs_fix: 0, not_set_up: 1, your_call: 2, not_checked: 3, pending_live_check: 4, verified_live: 5, compliant: 6, not_applicable: 7 };
|
|
22785
22830
|
}
|
|
22786
22831
|
});
|
|
@@ -23140,7 +23185,7 @@ async function runEvaluationInScope(input, opts) {
|
|
|
23140
23185
|
}
|
|
23141
23186
|
const classified = classifyFindings(findings);
|
|
23142
23187
|
for (const { finding, info } of classified) finding.compliance = info;
|
|
23143
|
-
const complianceRollup = rollup(classified.map(({ finding, info }) => ({ checkId: finding.checkId, info })), { groupOf: taskGroupOf });
|
|
23188
|
+
const complianceRollup = rollup(classified.map(({ finding, info }) => ({ checkId: finding.checkId, info })), { groupOf: taskGroupOf, billingUnread: billingUnread(findings) });
|
|
23144
23189
|
const { score, insurable } = scoreFindings(findings);
|
|
23145
23190
|
const summary = { pass: 0, fail: 0, needs_review: 0, not_applicable: 0 };
|
|
23146
23191
|
for (const f of findings) summary[f.status]++;
|
|
@@ -24488,16 +24533,24 @@ function readParams(params, merchant, kind, asOf) {
|
|
|
24488
24533
|
throw new PreflightInputError("The billing interval is unknown: pass line_items[].price_data.recurring.interval or merchant.interval", "merchant.interval");
|
|
24489
24534
|
}
|
|
24490
24535
|
const plan2 = resolvePlan(merged, "merchant.");
|
|
24491
|
-
if (coupon && merchantIntro &&
|
|
24536
|
+
if (coupon && merchantIntro && plan2.introAmount !== void 0) {
|
|
24492
24537
|
const pct = typeof coupon.percent_off === "number" ? coupon.percent_off : void 0;
|
|
24493
24538
|
const off = typeof coupon.amount_off === "number" ? coupon.amount_off : void 0;
|
|
24494
24539
|
const fromCoupon = pct !== void 0 ? Math.round(plan2.amount * (1 - pct / 100)) : off !== void 0 ? plan2.amount - off : void 0;
|
|
24495
|
-
|
|
24540
|
+
const months = plan2.interval === "year" ? 12 * plan2.intervalCount : plan2.interval === "month" ? plan2.intervalCount : void 0;
|
|
24541
|
+
const couponPeriods = coupon.duration === "forever" ? Infinity : coupon.duration === "once" ? 1 : coupon.duration === "repeating" && typeof coupon.duration_in_months === "number" && months ? Math.max(1, Math.ceil(coupon.duration_in_months / months)) : void 0;
|
|
24542
|
+
const declaredPeriods = plan2.introPeriods ?? 1;
|
|
24543
|
+
const amountDiffers = fromCoupon !== void 0 && Math.abs(fromCoupon - plan2.introAmount) >= 1;
|
|
24544
|
+
const durationDiffers = couponPeriods !== void 0 && couponPeriods !== declaredPeriods;
|
|
24545
|
+
if (amountDiffers || durationDiffers) {
|
|
24496
24546
|
const id = typeof coupon.id === "string" ? `coupon ${coupon.id}` : "the coupon";
|
|
24547
|
+
const per = (n) => n === Infinity ? "every billing period (forever)" : n === 1 ? "the first billing period" : `the first ${n} billing periods`;
|
|
24548
|
+
const couponSays = `${pct !== void 0 ? `${pct}% off` : off !== void 0 ? `${formatMoney(off, plan2.currency)} off` : "a discount"}${fromCoupon !== void 0 ? ` (${formatMoney(fromCoupon, plan2.currency)})` : ""} for ${couponPeriods !== void 0 ? per(couponPeriods) : `an unknown duration (${String(coupon.duration ?? "not given")})`}`;
|
|
24549
|
+
const declaredSays = `${formatMoney(plan2.introAmount, plan2.currency)} for ${per(declaredPeriods)}`;
|
|
24497
24550
|
caveats.push({
|
|
24498
24551
|
checks: ["signup.disclosure.price"],
|
|
24499
24552
|
status: "needs_review",
|
|
24500
|
-
reason: `discounts apply ${id}
|
|
24553
|
+
reason: `discounts apply ${id}, which gives ${couponSays}, but the plan's declared intro is ${declaredSays}: the coupon or the declared intro is wrong \u2014 make them the same, then state that promo`,
|
|
24501
24554
|
param: "params.discounts"
|
|
24502
24555
|
});
|
|
24503
24556
|
}
|
|
@@ -26460,7 +26513,7 @@ function parseObjectLiteral(source, env2 = {}) {
|
|
|
26460
26513
|
const p = new Parser(source, env2);
|
|
26461
26514
|
if (p.peek() !== "{") return { value: null, unresolved: ["<not an object literal>"] };
|
|
26462
26515
|
const v = p.object("");
|
|
26463
|
-
return { value: v === UNKNOWN ? null : v, unresolved: [...new Set(p.unresolved)] };
|
|
26516
|
+
return { value: v === UNKNOWN ? null : v, unresolved: [...new Set(p.unresolved)], ...Object.keys(p.fromEnv).length ? { fromEnv: p.fromEnv } : {} };
|
|
26464
26517
|
}
|
|
26465
26518
|
var UNKNOWN, Parser;
|
|
26466
26519
|
var init_literal = __esm({
|
|
@@ -26474,6 +26527,7 @@ var init_literal = __esm({
|
|
|
26474
26527
|
}
|
|
26475
26528
|
i = 0;
|
|
26476
26529
|
unresolved = [];
|
|
26530
|
+
fromEnv = {};
|
|
26477
26531
|
ws() {
|
|
26478
26532
|
for (; ; ) {
|
|
26479
26533
|
while (this.i < this.s.length && /\s/.test(this.s[this.i])) this.i++;
|
|
@@ -26560,7 +26614,10 @@ var init_literal = __esm({
|
|
|
26560
26614
|
if (name === "null") return null;
|
|
26561
26615
|
if (name === "undefined") return void 0;
|
|
26562
26616
|
const env2 = /^(?:process\.env|import\.meta\.env)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(name);
|
|
26563
|
-
if (env2)
|
|
26617
|
+
if (env2) {
|
|
26618
|
+
if (this.env[env2[1]] !== void 0 && path) this.fromEnv[path] = name;
|
|
26619
|
+
return this.env[env2[1]] ?? UNKNOWN;
|
|
26620
|
+
}
|
|
26564
26621
|
return UNKNOWN;
|
|
26565
26622
|
}
|
|
26566
26623
|
this.skipExpr();
|
|
@@ -27001,8 +27058,8 @@ var init_detect = __esm({
|
|
|
27001
27058
|
});
|
|
27002
27059
|
|
|
27003
27060
|
// src/ci/scaffold.ts
|
|
27004
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
27005
|
-
import { basename, join as join5, relative as relative2 } from "node:path";
|
|
27061
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
27062
|
+
import { basename, dirname as dirname3, join as join5, relative as relative2 } from "node:path";
|
|
27006
27063
|
function walk3(root, visit) {
|
|
27007
27064
|
let n = 0;
|
|
27008
27065
|
const go = (dir, depth) => {
|
|
@@ -27035,20 +27092,70 @@ function emailKindOf(file) {
|
|
|
27035
27092
|
const name = basename(file).replace(/\.[^.]+$/, "");
|
|
27036
27093
|
return EMAIL_NAMES.find(([re]) => re.test(name))?.[1];
|
|
27037
27094
|
}
|
|
27038
|
-
function
|
|
27095
|
+
function plansFromLiterals(text, file) {
|
|
27096
|
+
const out = [];
|
|
27097
|
+
for (const m of text.matchAll(/(?:(?:['"]?)([A-Za-z_$][\w$ -]*)['"]?\s*:\s*)?\{([^{}]{0,600})\}/g)) {
|
|
27098
|
+
const body = m[2];
|
|
27099
|
+
const interval = /\binterval\s*:\s*['"](day|week|month|year)['"]/.exec(body)?.[1];
|
|
27100
|
+
if (!interval) continue;
|
|
27101
|
+
const price3 = /\bprice\s*:\s*(\d+(?:\.\d{1,2})?)\b/.exec(body)?.[1];
|
|
27102
|
+
const minor = /\b(?:amount|unit_amount|price_cents|cents)\s*:\s*(\d+)\b/.exec(body)?.[1];
|
|
27103
|
+
const major = price3 !== void 0 ? Number(price3) : minor !== void 0 ? Number(minor) / 100 : void 0;
|
|
27104
|
+
if (major === void 0 || !(major > 0)) continue;
|
|
27105
|
+
const name = /\b(?:name|title|label)\s*:\s*['"]([^'"]{1,80})['"]/.exec(body)?.[1] ?? m[1]?.trim();
|
|
27106
|
+
if (!name) continue;
|
|
27107
|
+
const currency = /\bcurrency\s*:\s*['"]([a-z]{3})['"]/i.exec(body)?.[1]?.toLowerCase() ?? "usd";
|
|
27108
|
+
const count = /\binterval_?count\s*:\s*(\d+)/i.exec(body)?.[1];
|
|
27109
|
+
const trial = /\btrial(?:_period)?_?days\s*:\s*(\d+)/i.exec(body)?.[1];
|
|
27110
|
+
if (out.some((p) => p.name === name)) continue;
|
|
27111
|
+
const key = m[1]?.trim() && /^[A-Za-z_$][\w$]*$/.test(m[1].trim()) ? m[1].trim() : void 0;
|
|
27112
|
+
out.push({ name, ...key ? { key } : {}, price: major, currency, interval, ...count && Number(count) > 1 ? { interval_count: Number(count) } : {}, ...trial && Number(trial) > 0 ? { trial_days: Number(trial) } : {}, source: file });
|
|
27113
|
+
}
|
|
27114
|
+
return out;
|
|
27115
|
+
}
|
|
27116
|
+
function paramsBuilder(root, file, line2) {
|
|
27117
|
+
let text;
|
|
27118
|
+
try {
|
|
27119
|
+
text = readFileSync6(join5(root, file), "utf8");
|
|
27120
|
+
} catch {
|
|
27121
|
+
return void 0;
|
|
27122
|
+
}
|
|
27123
|
+
const from = text.split("\n").slice(line2 - 1).join("\n");
|
|
27124
|
+
const call = /checkout\.sessions\.create\s*\(\s*(?:await\s+)?([A-Za-z_$][\w$]*)\s*\(([^)]*)\)/.exec(from);
|
|
27125
|
+
if (!call) return void 0;
|
|
27126
|
+
const fn = call[1];
|
|
27127
|
+
const takesArgs = call[2].trim().length > 0;
|
|
27128
|
+
const imp = new RegExp(`import\\s*\\{[^}]*\\b${fn}\\b[^}]*\\}\\s*from\\s*['"](\\.{1,2}/[^'"]+)['"]`).exec(text);
|
|
27129
|
+
let target;
|
|
27130
|
+
if (imp) {
|
|
27131
|
+
const base = join5(dirname3(file), imp[1]).split("\\").join("/");
|
|
27132
|
+
target = [base, ...[".ts", ".tsx", ".js", ".mjs", "/index.ts", "/index.js"].map((e) => `${base.replace(/\.(?:[cm]?js)$/, "")}${e}`)].find((f) => /\.[cm]?[jt]sx?$/.test(f) && existsSync5(join5(root, f)));
|
|
27133
|
+
} else if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fn}\\b|export\\s+const\\s+${fn}\\b`).test(text)) target = file;
|
|
27134
|
+
return target ? { module: `${target}#${fn}`, takesArgs } : void 0;
|
|
27135
|
+
}
|
|
27136
|
+
function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
27039
27137
|
const det = detectProject(root, env2);
|
|
27040
27138
|
const todos = [];
|
|
27041
27139
|
const found = [];
|
|
27042
27140
|
const names = /* @__PURE__ */ new Set();
|
|
27043
27141
|
const merchant = {};
|
|
27044
|
-
|
|
27045
|
-
|
|
27142
|
+
const fact = (k) => typeof facts[k] === "string" && facts[k].trim() ? facts[k].trim() : void 0;
|
|
27143
|
+
const brand = fact("merchant_name") ?? det.brand?.name;
|
|
27144
|
+
merchant.name = brand ?? "TODO: the brand subscribers see";
|
|
27145
|
+
if (!brand) todos.push("merchant.name: the brand subscribers see");
|
|
27146
|
+
if (fact("merchant_name")) found.push(`merchant name "${fact("merchant_name")}" (from checkbox init)`);
|
|
27046
27147
|
const cancelPath = det.policyPaths.cancellation;
|
|
27047
|
-
|
|
27148
|
+
const siteUrl = fact("site_url") ?? det.siteUrl;
|
|
27149
|
+
const cancelFact = fact("cancellation_url") ?? fact("account_url");
|
|
27150
|
+
if (cancelFact) {
|
|
27151
|
+
merchant.cancel_url = cancelFact;
|
|
27152
|
+
found.push(`cancel URL ${cancelFact} (from checkbox init)`);
|
|
27153
|
+
} else if (siteUrl && cancelPath) merchant.cancel_url = `${originOf(siteUrl)}${cancelPath}`;
|
|
27048
27154
|
else {
|
|
27049
27155
|
merchant.cancel_url = "TODO: https://\u2026 page where subscribers cancel online";
|
|
27050
27156
|
todos.push("merchant.cancel_url: the page where subscribers cancel online");
|
|
27051
27157
|
}
|
|
27158
|
+
if (fact("support_email")) merchant.support_contact = fact("support_email");
|
|
27052
27159
|
const plans = det.plans.map((p, i) => ({
|
|
27053
27160
|
name: p.name ?? `Plan ${i + 1}`,
|
|
27054
27161
|
amount: p.amount,
|
|
@@ -27058,6 +27165,32 @@ function scaffoldConfig(root, env2 = {}) {
|
|
|
27058
27165
|
...p.trial_days ? { trial_days: p.trial_days } : {}
|
|
27059
27166
|
}));
|
|
27060
27167
|
for (const p of det.plans) found.push(`plan ${p.name ?? ""} ${p.amount / 100} ${p.currency}/${p.interval} (${p.source})`.replace(/\s+/g, " "));
|
|
27168
|
+
const planKeys = /* @__PURE__ */ new Map();
|
|
27169
|
+
if (!plans.length && Array.isArray(facts.plans)) {
|
|
27170
|
+
for (const [i, p] of facts.plans.entries()) {
|
|
27171
|
+
if (typeof p.amount !== "number" || typeof p.interval !== "string") continue;
|
|
27172
|
+
plans.push({ name: typeof p.name === "string" ? p.name : `Plan ${i + 1}`, amount: p.amount, currency: typeof p.currency === "string" ? p.currency : "usd", interval: p.interval, ...typeof p.interval_count === "number" ? { interval_count: p.interval_count } : {}, ...typeof p.trial_days === "number" ? { trial_days: p.trial_days } : {} });
|
|
27173
|
+
found.push(`plan ${String(plans[plans.length - 1].name)} (from checkbox init --plan)`);
|
|
27174
|
+
}
|
|
27175
|
+
}
|
|
27176
|
+
if (!plans.length) {
|
|
27177
|
+
walk3(root, (rel2) => {
|
|
27178
|
+
if (!PLAN_FILE.test(rel2) || plans.length >= 8) return;
|
|
27179
|
+
let text = "";
|
|
27180
|
+
try {
|
|
27181
|
+
text = readFileSync6(join5(root, rel2), "utf8");
|
|
27182
|
+
} catch {
|
|
27183
|
+
return;
|
|
27184
|
+
}
|
|
27185
|
+
for (const p of plansFromLiterals(text, rel2)) {
|
|
27186
|
+
if (plans.some((x) => x.name === p.name)) continue;
|
|
27187
|
+
const { source, key, ...plan2 } = p;
|
|
27188
|
+
plans.push(plan2);
|
|
27189
|
+
if (key) planKeys.set(p.name, key);
|
|
27190
|
+
found.push(`plan ${p.name} ${p.price} ${p.currency}/${p.interval} (${source})`);
|
|
27191
|
+
}
|
|
27192
|
+
});
|
|
27193
|
+
}
|
|
27061
27194
|
if (!plans.length) {
|
|
27062
27195
|
plans.push({ name: "TODO: plan name", price: "TODO: e.g. 19.99", currency: "usd", interval: "month" });
|
|
27063
27196
|
todos.push("plans: the price, interval and trial of each plan (the checkout uses Price ids, which cannot be read from code)");
|
|
@@ -27071,6 +27204,22 @@ function scaffoldConfig(root, env2 = {}) {
|
|
|
27071
27204
|
const call = (perFile.get(s2.file) ?? 0) + 1;
|
|
27072
27205
|
perFile.set(s2.file, call);
|
|
27073
27206
|
const entry = { name: unique(names, `Stripe Checkout (${basename(s2.file)}${call > 1 ? ` #${call}` : ""})`), source: s2.file, ...call > 1 ? { call } : {} };
|
|
27207
|
+
const builder = !s2.params ? paramsBuilder(root, s2.file, s2.line) : void 0;
|
|
27208
|
+
if (builder) {
|
|
27209
|
+
found.push(`${s2.file}:${s2.line}: Checkout Session built by ${builder.module}`);
|
|
27210
|
+
const keyed = plans.length > 1 && plans.every((p) => planKeys.has(String(p.name)));
|
|
27211
|
+
if (builder.takesArgs && plans.length > 1) {
|
|
27212
|
+
for (const p of plans) {
|
|
27213
|
+
const n = String(p.name);
|
|
27214
|
+
stripe.push({ name: unique(names, `Checkout (${n})`), module: builder.module, args: [keyed ? planKeys.get(n) : `TODO: the argument for plan ${n}`], plan: n, strict: true });
|
|
27215
|
+
}
|
|
27216
|
+
if (!keyed) todos.push(`${builder.module}: the argument that builds each plan's params ("args")`);
|
|
27217
|
+
} else {
|
|
27218
|
+
stripe.push({ name: entry.name, module: builder.module, ...builder.takesArgs ? { args: [plans.length === 1 && planKeys.size ? [...planKeys.values()][0] : "TODO: the function argument"] } : {}, ...plans.length === 1 ? { plan: String(plans[0].name) } : {}, strict: true });
|
|
27219
|
+
if (builder.takesArgs && !(plans.length === 1 && planKeys.size)) todos.push(`${builder.module}: the argument that builds the params ("args")`);
|
|
27220
|
+
}
|
|
27221
|
+
continue;
|
|
27222
|
+
}
|
|
27074
27223
|
if (!s2.params) {
|
|
27075
27224
|
todos.push(`${s2.file}:${s2.line}: the params are not an object literal; use "module" (an exported function returning the params) instead of "source"`);
|
|
27076
27225
|
delete entry.source;
|
|
@@ -27134,7 +27283,7 @@ function scaffoldConfig(root, env2 = {}) {
|
|
|
27134
27283
|
found
|
|
27135
27284
|
};
|
|
27136
27285
|
}
|
|
27137
|
-
var IGNORE, MAX_FILES2, EMAIL_NAMES, IN_EMAIL_DIR, PAGE_FILE, DISCLOSURE_TEXT, unique;
|
|
27286
|
+
var IGNORE, MAX_FILES2, EMAIL_NAMES, IN_EMAIL_DIR, PAGE_FILE, DISCLOSURE_TEXT, unique, PLAN_FILE;
|
|
27138
27287
|
var init_scaffold = __esm({
|
|
27139
27288
|
"src/ci/scaffold.ts"() {
|
|
27140
27289
|
"use strict";
|
|
@@ -27160,6 +27309,7 @@ var init_scaffold = __esm({
|
|
|
27160
27309
|
names.add(n);
|
|
27161
27310
|
return n;
|
|
27162
27311
|
};
|
|
27312
|
+
PLAN_FILE = /(?:^|\/)(?:plans?|pricing|prices|tiers|subscriptions?|billing|products?)(?:\.config)?\.(?:[cm]?[jt]sx?|json)$/i;
|
|
27163
27313
|
}
|
|
27164
27314
|
});
|
|
27165
27315
|
|
|
@@ -27287,8 +27437,8 @@ var init_coverage = __esm({
|
|
|
27287
27437
|
|
|
27288
27438
|
// src/ci/inputs.ts
|
|
27289
27439
|
import { execFile } from "node:child_process";
|
|
27290
|
-
import { existsSync as
|
|
27291
|
-
import { dirname as
|
|
27440
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
27441
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, relative as relative4, resolve as resolve4 } from "node:path";
|
|
27292
27442
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
27293
27443
|
function readRel(dir, file) {
|
|
27294
27444
|
const abs = isAbsolute2(file) ? file : resolve4(dir, file);
|
|
@@ -27301,8 +27451,8 @@ function readRel(dir, file) {
|
|
|
27301
27451
|
}
|
|
27302
27452
|
function hasTsx(dir) {
|
|
27303
27453
|
for (let d = dir, i = 0; i < 8; i++) {
|
|
27304
|
-
if (
|
|
27305
|
-
const up =
|
|
27454
|
+
if (existsSync6(join7(d, "node_modules", "tsx", "package.json"))) return true;
|
|
27455
|
+
const up = dirname4(d);
|
|
27306
27456
|
if (up === d) break;
|
|
27307
27457
|
d = up;
|
|
27308
27458
|
}
|
|
@@ -27322,7 +27472,7 @@ function runModule(dir, ref, args = [], env2 = process.env, timeoutMs = 3e4) {
|
|
|
27322
27472
|
const file = hash > 0 ? ref.slice(0, hash) : ref;
|
|
27323
27473
|
const name = hash > 0 ? ref.slice(hash + 1) : "default";
|
|
27324
27474
|
const abs = resolve4(dir, file);
|
|
27325
|
-
if (!
|
|
27475
|
+
if (!existsSync6(abs)) return Promise.reject(new InputError(`${file} does not exist (paths are relative to checkbox.json)`));
|
|
27326
27476
|
const flags = loaderFlags(file, dir);
|
|
27327
27477
|
if (typeof flags === "string") return Promise.reject(new InputError(flags));
|
|
27328
27478
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -27341,12 +27491,39 @@ function runModule(dir, ref, args = [], env2 = process.env, timeoutMs = 3e4) {
|
|
|
27341
27491
|
return;
|
|
27342
27492
|
}
|
|
27343
27493
|
const own = /__CHECKBOX_ERR__ (.+)/.exec(stderr)?.[1];
|
|
27344
|
-
|
|
27345
|
-
reject(new InputError(`${ref}: ${own ?? (err && err.killed ? `did not finish within ${timeoutMs / 1e3}s` : tail || err?.message || "no output")}`));
|
|
27494
|
+
reject(new InputError(`${ref}: ${own ?? (err && err.killed ? `did not finish within ${timeoutMs / 1e3}s` : moduleErrorMessage(stderr, dir, flags) || err?.message || "no output")}`));
|
|
27346
27495
|
}
|
|
27347
27496
|
);
|
|
27348
27497
|
});
|
|
27349
27498
|
}
|
|
27499
|
+
function moduleErrorMessage(stderr, dir, flags = []) {
|
|
27500
|
+
const text = stderr.trim();
|
|
27501
|
+
if (!text) return "";
|
|
27502
|
+
const rel2 = (p) => {
|
|
27503
|
+
const r = relative4(dir, p.replace(/^file:\/\//, ""));
|
|
27504
|
+
return r && !r.startsWith("..") ? r.split("\\").join("/") : p;
|
|
27505
|
+
};
|
|
27506
|
+
const tsx = flags.includes("tsx");
|
|
27507
|
+
const missing = /Cannot find (module|package) '([^']+)' imported from (\S+)/.exec(text);
|
|
27508
|
+
if (missing) {
|
|
27509
|
+
const [, what, target, from] = missing;
|
|
27510
|
+
if (what === "package" || !/^[./]|^[A-Za-z]:[\\/]/.test(target) && !target.startsWith(dir)) {
|
|
27511
|
+
return `${rel2(from)} imports the package "${target}", which is not installed here. Install the project's dependencies before checkbox ci (e.g. npm ci in the workflow).`;
|
|
27512
|
+
}
|
|
27513
|
+
const noExt = !/\.[cm]?[jt]sx?$|\.json$/.test(target);
|
|
27514
|
+
if (noExt && !tsx) {
|
|
27515
|
+
return `${rel2(from)} imports ${rel2(target)} without a file extension, which Node cannot resolve on its own. Add tsx to the project (npm install -D tsx; checkbox ci then imports modules the way your bundler does), or write the import with its extension (e.g. './${rel2(target).split("/").pop()}.ts').`;
|
|
27516
|
+
}
|
|
27517
|
+
return `${rel2(from)} imports ${rel2(target)}, which does not exist${noExt ? " (tsx found no .ts / .js file of that name either)" : ""}.`;
|
|
27518
|
+
}
|
|
27519
|
+
if (/ERR_UNKNOWN_FILE_EXTENSION/.test(text)) {
|
|
27520
|
+
const file = /Unknown file extension "([^"]+)" for (\S+)/.exec(text);
|
|
27521
|
+
return `Node cannot import ${file ? `${rel2(file[2])} (${file[1]})` : "a file the module imports"}. Add tsx to the project (npm install -D tsx), or point the entry at a .js / .ts file that imports no ${file?.[1] ?? "such"} files.`;
|
|
27522
|
+
}
|
|
27523
|
+
const lines = text.split("\n").map((l) => l.trim()).filter((l) => l && !/^at /.test(l) && !/^Node\.js v\d/.test(l));
|
|
27524
|
+
const first2 = lines.find((l) => /^(?:[A-Z]\w*)?Error\b.*:/.test(l)) ?? lines.find((l) => /error/i.test(l)) ?? lines[lines.length - 1] ?? "";
|
|
27525
|
+
return first2.replace(/file:\/\/\S+/g, (u) => rel2(u)).replace(/\s+/g, " ").slice(0, 400);
|
|
27526
|
+
}
|
|
27350
27527
|
function paramsFromSource(text, file, call = 1, env2 = {}) {
|
|
27351
27528
|
const calls = [...text.matchAll(/checkout\.sessions\.create\s*\(/g)];
|
|
27352
27529
|
if (!calls.length) throw new InputError(`${file} has no stripe.checkout.sessions.create( call`);
|
|
@@ -27357,7 +27534,32 @@ function paramsFromSource(text, file, call = 1, env2 = {}) {
|
|
|
27357
27534
|
if (!lit) throw new InputError(`${file}:${line2}: the params are not an object literal; use "module" (a function returning the params) or "file" instead`);
|
|
27358
27535
|
const parsed = parseObjectLiteral(lit, env2);
|
|
27359
27536
|
if (!parsed.value) throw new InputError(`${file}:${line2}: could not read the params object`);
|
|
27360
|
-
return { params: parsed.value, line: line2, unresolved: parsed.unresolved };
|
|
27537
|
+
return { params: parsed.value, line: line2, unresolved: parsed.unresolved, ...parsed.fromEnv ? { fromEnv: parsed.fromEnv } : {} };
|
|
27538
|
+
}
|
|
27539
|
+
function lineOfAny(text, needles) {
|
|
27540
|
+
const lower2 = text.toLowerCase();
|
|
27541
|
+
for (const n of needles) {
|
|
27542
|
+
const k = n.trim().toLowerCase();
|
|
27543
|
+
if (k.length < 2) continue;
|
|
27544
|
+
const i = lower2.indexOf(k);
|
|
27545
|
+
if (i >= 0) return text.slice(0, i).split("\n").length;
|
|
27546
|
+
}
|
|
27547
|
+
return void 0;
|
|
27548
|
+
}
|
|
27549
|
+
function exportLine(dir, ref) {
|
|
27550
|
+
const hash = ref.lastIndexOf("#");
|
|
27551
|
+
const file = hash > 0 ? ref.slice(0, hash) : ref;
|
|
27552
|
+
const name = hash > 0 ? ref.slice(hash + 1) : "default";
|
|
27553
|
+
let text;
|
|
27554
|
+
try {
|
|
27555
|
+
text = readFileSync8(resolve4(dir, file), "utf8");
|
|
27556
|
+
} catch {
|
|
27557
|
+
return 1;
|
|
27558
|
+
}
|
|
27559
|
+
const esc3 = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27560
|
+
const re = name === "default" ? /^\s*export\s+default\b/m : new RegExp(`^\\s*export\\s+(?:async\\s+)?(?:function\\*?|const|let|var|class)\\s+${esc3}\\b|^\\s*export\\s*\\{[^}]*\\b${esc3}\\b`, "m");
|
|
27561
|
+
const m = re.exec(text);
|
|
27562
|
+
return m ? text.slice(0, m.index).split("\n").length + (m[0].startsWith("\n") ? 1 : 0) : 1;
|
|
27361
27563
|
}
|
|
27362
27564
|
function projectEnv(dir) {
|
|
27363
27565
|
const out = {};
|
|
@@ -27423,15 +27625,22 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27423
27625
|
}
|
|
27424
27626
|
} else if (e.module) {
|
|
27425
27627
|
const file = e.module.replace(/#.*$/, "");
|
|
27426
|
-
job.target = { file: rel(loaded, root, file), line:
|
|
27628
|
+
job.target = { file: rel(loaded, root, file), line: exportLine(dir, e.module) };
|
|
27427
27629
|
job.inputs.push(file);
|
|
27428
27630
|
params = await runModule(dir, e.module, e.args ?? [], o.env);
|
|
27631
|
+
const unset = (Array.isArray(params?.line_items) ? params.line_items : []).flatMap((li, i) => li && typeof li === "object" && !Array.isArray(li) && li.price === void 0 && li.price_data === void 0 ? [`line_items[${i}].price`] : []);
|
|
27632
|
+
if (unset.length) {
|
|
27633
|
+
job.placeholders = Object.fromEntries(unset.map((p) => [p, "{{your Stripe Price id: unset when checkbox ci ran the module}}"]));
|
|
27634
|
+
job.warnings.push(`${unset.join(", ")} ${unset.length === 1 ? "was" : "were"} not set when Checkbox ran ${e.module} (an environment variable this job does not set?). The checks use "plan" in checkbox.json for the price, and corrected params keep a placeholder there for your own value.`);
|
|
27635
|
+
}
|
|
27429
27636
|
} else {
|
|
27430
27637
|
const src = e.source;
|
|
27431
27638
|
job.inputs.push(src);
|
|
27432
27639
|
const r = paramsFromSource(readRel(dir, src), src, e.call ?? 1, projectEnv(dir));
|
|
27433
27640
|
job.target = { file: rel(loaded, root, src), line: r.line };
|
|
27641
|
+
job.fromSource = true;
|
|
27434
27642
|
params = r.params;
|
|
27643
|
+
if (r.fromEnv) job.placeholders = Object.fromEntries(Object.entries(r.fromEnv).map(([p, expr]) => [p, `{{${expr}}}`]));
|
|
27435
27644
|
if (r.unresolved.length) {
|
|
27436
27645
|
job.partial = true;
|
|
27437
27646
|
job.unresolved = r.unresolved;
|
|
@@ -27507,6 +27716,14 @@ function pageJob(loaded, e, o, root) {
|
|
|
27507
27716
|
if (e.source) {
|
|
27508
27717
|
job.target = { file: rel(loaded, root, e.source), line: 1 };
|
|
27509
27718
|
job.inputs.push(e.source);
|
|
27719
|
+
const src = e.source;
|
|
27720
|
+
job.locate = (needles) => {
|
|
27721
|
+
try {
|
|
27722
|
+
return lineOfAny(readRel(dir, src), needles);
|
|
27723
|
+
} catch {
|
|
27724
|
+
return void 0;
|
|
27725
|
+
}
|
|
27726
|
+
};
|
|
27510
27727
|
}
|
|
27511
27728
|
if (e.url) {
|
|
27512
27729
|
if (!o.online) job.skip = "URL pages are fetched by the API: run with --online (and CHECKBOX_SECRET_KEY)";
|
|
@@ -27516,7 +27733,9 @@ function pageJob(loaded, e, o, root) {
|
|
|
27516
27733
|
try {
|
|
27517
27734
|
if (!e.source) job.target = { file: rel(loaded, root, e.file), line: 1 };
|
|
27518
27735
|
job.inputs.push(e.file);
|
|
27519
|
-
|
|
27736
|
+
const html = readRel(dir, e.file);
|
|
27737
|
+
if (!e.source) job.locate = (needles) => lineOfAny(html, needles);
|
|
27738
|
+
job.body = { html, ...common3 };
|
|
27520
27739
|
} catch (err) {
|
|
27521
27740
|
job.error = err.message;
|
|
27522
27741
|
}
|
|
@@ -27533,7 +27752,7 @@ async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
|
27533
27752
|
msg2 = emailFromFile(readRel(dir, e.file), e.file, e.subject);
|
|
27534
27753
|
} else if (e.module) {
|
|
27535
27754
|
const file = e.module.replace(/#.*$/, "");
|
|
27536
|
-
job.target = { file: rel(loaded, root, file), line:
|
|
27755
|
+
job.target = { file: rel(loaded, root, file), line: exportLine(dir, e.module) };
|
|
27537
27756
|
job.inputs.push(file);
|
|
27538
27757
|
const v = await runModule(dir, e.module, e.args ?? [], o.env);
|
|
27539
27758
|
if (typeof v === "string") msg2 = emailFromFile(v, /^\s*</.test(v) ? "x.html" : "x.txt", e.subject);
|
|
@@ -27641,7 +27860,9 @@ function renderHuman(run2, c, opts = {}) {
|
|
|
27641
27860
|
if (x.message) lines.push(c.dim(` ${wrap(x.message, 110, " ")}`));
|
|
27642
27861
|
if (x.fix) lines.push(` fix: ${wrap(x.fix, 110, " ")}${c.dim(` (${where(x)})`)}`);
|
|
27643
27862
|
if (opts.verbose) for (const f of x.fixes.slice(1)) lines.push(` fix: ${wrap(f, 110, " ")}`);
|
|
27644
|
-
|
|
27863
|
+
const alerts = x.alerts ?? [];
|
|
27864
|
+
for (const a of alerts) lines.push(c.yellow(` warning: ${wrap(a, 110, " ")}`));
|
|
27865
|
+
for (const w of x.warnings.filter((w2) => !alerts.includes(w2)).slice(0, opts.verbose ? 10 : alerts.length ? 0 : 1)) lines.push(c.dim(` note: ${wrap(w, 110, " ")}`));
|
|
27645
27866
|
if (x.corrected && opts.verbose) lines.push(c.dim(` ${x.corrected.title}:`), ...x.corrected.body.split("\n").map((l) => c.dim(` ${l}`)));
|
|
27646
27867
|
}
|
|
27647
27868
|
const tone = run2.status === "pass" ? c.green : run2.status === "needs_review" && run2.exit_code === 0 ? c.yellow : c.red;
|
|
@@ -27653,11 +27874,12 @@ function renderHuman(run2, c, opts = {}) {
|
|
|
27653
27874
|
function annotations(run2) {
|
|
27654
27875
|
const out = [];
|
|
27655
27876
|
for (const x of run2.checks) {
|
|
27656
|
-
const
|
|
27877
|
+
const alerts = x.alerts ?? [];
|
|
27878
|
+
const level = x.status === "fail" || x.status === "error" ? "error" : x.status === "needs_review" ? run2.strict || x.blocking ? "error" : "warning" : alerts.length ? "warning" : void 0;
|
|
27657
27879
|
if (!level) continue;
|
|
27658
27880
|
const props = [`file=${escProp(x.target.file)}`, ...x.target.line ? [`line=${x.target.line}`] : [], `title=${escProp(`Checkbox: ${x.name} \u2014 ${x.label}`)}`];
|
|
27659
27881
|
const body = x.status === "error" ? x.fix ? `${x.message ?? ""}
|
|
27660
|
-
${x.fix}` : `Could not run: ${x.message ?? ""}` : [x.fix ?? x.label, ...x.fixes.slice(1, 4).map((f) => `\u2022 ${f}`), x.corrected ? `${x.corrected.title}: see the job summary.` : ""].filter(Boolean).join("\n");
|
|
27882
|
+
${x.fix}` : `Could not run: ${x.message ?? ""}` : [x.status === "pass" && !x.fix ? "" : x.fix ?? x.label, ...x.fixes.slice(1, 4).map((f) => `\u2022 ${f}`), ...alerts.map((a) => `Warning: ${a}`), x.corrected ? `${x.corrected.title}: see the job summary.` : ""].filter(Boolean).join("\n");
|
|
27661
27883
|
out.push(`::${level} ${props.join(",")}::${escData(body)}`);
|
|
27662
27884
|
}
|
|
27663
27885
|
return out;
|
|
@@ -27669,7 +27891,8 @@ function stepSummary(run2) {
|
|
|
27669
27891
|
lines.push(`**${overallLine(run2)}** \u2014 \`${run2.config}\` \xB7 ${run2.mode}${versions}`, "");
|
|
27670
27892
|
lines.push("| Check | Status | Fix | File |", "|---|---|---|---|");
|
|
27671
27893
|
for (const x of run2.checks) {
|
|
27672
|
-
const
|
|
27894
|
+
const fix0 = x.status === "error" || x.status === "skipped" ? [x.message, x.fix].filter(Boolean).join(" ") : x.fix ? `${x.fix}${x.fixes.length > 1 ? ` (+${x.fixes.length - 1} more)` : ""}` : x.status === "needs_review" && x.message ? x.message : "";
|
|
27895
|
+
const fix = [fix0, ...(x.alerts ?? []).map((a) => `\u26A0\uFE0F ${a}`)].filter(Boolean).join("\n");
|
|
27673
27896
|
lines.push(`| ${STATUS_MD[x.status]} ${cell(x.name)}<br><sub>${TYPE_LABEL[x.type]}</sub> | ${cell(x.label)} | ${cell(fix)} | \`${cell(where(x))}\` |`);
|
|
27674
27897
|
}
|
|
27675
27898
|
const detailed = run2.checks.filter((x) => x.corrected || x.fixes.length > 1);
|
|
@@ -27874,6 +28097,10 @@ function pickDaysBefore(kind, range) {
|
|
|
27874
28097
|
if (open2) return lo + OPEN_WINDOW_MARGIN_DAYS;
|
|
27875
28098
|
return lo + Math.floor((range.maxDays - lo) / 2);
|
|
27876
28099
|
}
|
|
28100
|
+
function sendLeadDays(range, days) {
|
|
28101
|
+
if (range.maxDays >= OPEN_ENDED_DAYS2 || !Number.isInteger(days) || days <= 0) return days;
|
|
28102
|
+
return days <= range.maxDays ? days + BOUNDED_SEND_MARGIN_DAYS : days;
|
|
28103
|
+
}
|
|
27877
28104
|
function stabWindows(windows) {
|
|
27878
28105
|
const sorted = [...windows].sort((a, b) => a.window.maxDays - b.window.maxDays || a.window.minDays - b.window.minDays);
|
|
27879
28106
|
const groups = [];
|
|
@@ -27891,11 +28118,15 @@ function stabWindows(windows) {
|
|
|
27891
28118
|
function slotOf(kind, anchorAt, range, members, daysBefore) {
|
|
27892
28119
|
const days = daysBefore ?? pickDaysBefore(kind, range);
|
|
27893
28120
|
const maxDays = Math.min(range.maxDays, OPEN_ENDED_DAYS2);
|
|
28121
|
+
const lead = sendLeadDays(range, days);
|
|
28122
|
+
const sendAt = new Date(anchorAt.getTime() - lead * DAY_MS5);
|
|
28123
|
+
const opensAt = new Date(anchorAt.getTime() - maxDays * DAY_MS5);
|
|
27894
28124
|
return {
|
|
27895
28125
|
slot: `d${days}`,
|
|
27896
28126
|
daysBefore: days,
|
|
27897
|
-
sendAt
|
|
27898
|
-
opensAt
|
|
28127
|
+
sendAt,
|
|
28128
|
+
// The send time is inside [opensAt, closesAt] (a bounded window's send sits inside its maximum's rounding).
|
|
28129
|
+
opensAt: sendAt.getTime() < opensAt.getTime() ? sendAt : opensAt,
|
|
27899
28130
|
closesAt: new Date(anchorAt.getTime() - range.minDays * DAY_MS5),
|
|
27900
28131
|
range: { minDays: range.minDays, maxDays },
|
|
27901
28132
|
jurisdictions: [...new Set(members.map((m) => m.jurisdiction))].sort(),
|
|
@@ -27976,7 +28207,7 @@ function renewalReminderApplies(termMonths2, jurisdictions, asOf, requirements)
|
|
|
27976
28207
|
(r) => r.appliesWhen?.minTermMonths !== void 0 && r.appliesWhen.minTermMonths <= termMonths2
|
|
27977
28208
|
);
|
|
27978
28209
|
}
|
|
27979
|
-
var DAY_MS5, HOUR_MS2, OPEN_ENDED_DAYS2, OPEN_WINDOW_MARGIN_DAYS, TRIAL_MIN_MARGIN_DAYS, TRIAL_SIGNUP_GAP_DAYS, PRICE_CHANGE_MAX_LEAD_DAYS, ANNUAL_TERMS_DEFAULT_WINDOW, NOTICE_CHECKS2, isImmediateKind, KIND_ANCHORS, FALLBACK_WINDOWS, BUSINESS_DAY_MINIMUMS;
|
|
28210
|
+
var DAY_MS5, HOUR_MS2, OPEN_ENDED_DAYS2, OPEN_WINDOW_MARGIN_DAYS, TRIAL_MIN_MARGIN_DAYS, TRIAL_SIGNUP_GAP_DAYS, BOUNDED_SEND_MARGIN_DAYS, PRICE_CHANGE_MAX_LEAD_DAYS, ANNUAL_TERMS_DEFAULT_WINDOW, NOTICE_CHECKS2, isImmediateKind, KIND_ANCHORS, FALLBACK_WINDOWS, BUSINESS_DAY_MINIMUMS;
|
|
27980
28211
|
var init_timing = __esm({
|
|
27981
28212
|
"../../apps/api/src/notices/timing.ts"() {
|
|
27982
28213
|
"use strict";
|
|
@@ -27987,6 +28218,7 @@ var init_timing = __esm({
|
|
|
27987
28218
|
OPEN_WINDOW_MARGIN_DAYS = 2;
|
|
27988
28219
|
TRIAL_MIN_MARGIN_DAYS = 0.5;
|
|
27989
28220
|
TRIAL_SIGNUP_GAP_DAYS = 1 / 24;
|
|
28221
|
+
BOUNDED_SEND_MARGIN_DAYS = 0.25;
|
|
27990
28222
|
PRICE_CHANGE_MAX_LEAD_DAYS = 60;
|
|
27991
28223
|
ANNUAL_TERMS_DEFAULT_WINDOW = { minDays: 15, maxDays: 45 };
|
|
27992
28224
|
NOTICE_CHECKS2 = {
|
|
@@ -28039,7 +28271,7 @@ function singleDayNote(requirementIds, days) {
|
|
|
28039
28271
|
const lower2 = [...new Set(windows.filter((w) => w.minDays === days).map((w) => jur(w.id)))];
|
|
28040
28272
|
const upper = [...new Set(windows.filter((w) => w.maxDays === days).map((w) => jur(w.id)))];
|
|
28041
28273
|
const why = lower2.length && upper.length ? `at least ${days} days (${lower2.slice(0, 4).join(", ")}) and at most ${days} days (${upper.slice(0, 4).join(", ")}) leave a single day` : `the states' windows meet on a single day`;
|
|
28042
|
-
return `Exactly ${days} days before: ${why}. Send at send_at
|
|
28274
|
+
return `Exactly ${days} days before: ${why}. Send at send_at, ${days} days and 6 hours before the anchor. The one rule: a minimum counts only full days, less a 2-hour allowance for delivery; a maximum is rounded to the nearest day. So a send counts as ${days} days when it goes out less than ${days + 0.5} days and at least ${days} days less 2 hours before the anchor; anything earlier or later does not.`;
|
|
28043
28275
|
}
|
|
28044
28276
|
function addInterval(d, interval, n) {
|
|
28045
28277
|
if (interval === "day") return new Date(d.getTime() + n * DAY_MS5);
|
|
@@ -28202,6 +28434,15 @@ function operationFor2(kind) {
|
|
|
28202
28434
|
function onlineEngine(client) {
|
|
28203
28435
|
return { mode: "online", run: (kind, body) => client.sdk.call(operationFor2(kind), body) };
|
|
28204
28436
|
}
|
|
28437
|
+
function ciVocabulary(text) {
|
|
28438
|
+
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");
|
|
28439
|
+
}
|
|
28440
|
+
function stripeConsentFix(text, params) {
|
|
28441
|
+
if (!/^Add one unchecked checkbox/.test(text)) return text;
|
|
28442
|
+
const cc = params?.consent_collection;
|
|
28443
|
+
if (cc?.terms_of_service === "required") return text;
|
|
28444
|
+
return 'Set consent_collection: { terms_of_service: "required" } in the params: Stripe then shows an unchecked consent checkbox, labelled with custom_text.terms_of_service_acceptance.message.';
|
|
28445
|
+
}
|
|
28205
28446
|
function ciStatusOf(r) {
|
|
28206
28447
|
const cs = r.compliance_status;
|
|
28207
28448
|
if (r.verdict === "not_applicable") return "not_applicable";
|
|
@@ -28210,9 +28451,9 @@ function ciStatusOf(r) {
|
|
|
28210
28451
|
if (cs && OK_STATUSES.has(cs)) return "pass";
|
|
28211
28452
|
return "needs_review";
|
|
28212
28453
|
}
|
|
28213
|
-
function fixesOf(r) {
|
|
28454
|
+
function fixesOf(r, onlyNeedsFix = false) {
|
|
28214
28455
|
const out = [];
|
|
28215
|
-
const failing = (r.checks ?? []).filter((c) => (c.status === "fail" || c.status === "needs_review") && !(c.compliance_status && OK_STATUSES.has(c.compliance_status)));
|
|
28456
|
+
const failing = (r.checks ?? []).filter((c) => (c.status === "fail" || c.status === "needs_review") && !(c.compliance_status && OK_STATUSES.has(c.compliance_status)) && (!onlyNeedsFix || c.compliance_status === "needs_fix"));
|
|
28216
28457
|
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));
|
|
28217
28458
|
for (const c of failing) {
|
|
28218
28459
|
const base = c.compliance?.fix || c.fix || c.compliance?.question?.text || c.explanation || c.title || c.check_id;
|
|
@@ -28225,14 +28466,25 @@ function fixesOf(r) {
|
|
|
28225
28466
|
function failingCards(explanation) {
|
|
28226
28467
|
if (!explanation) return [];
|
|
28227
28468
|
if (/\(\d+ checkout page views? checked\) — e\.g\./.test(explanation)) return [];
|
|
28228
|
-
return [...new Set([...explanation.matchAll(/plan button “([^”]{1,80})
|
|
28469
|
+
return [...new Set([...explanation.matchAll(/plan button “([^”]{1,80})”(?: in the card “([^”]{1,80})”)?/g)].map((m) => m[2] ?? m[1]))];
|
|
28229
28470
|
}
|
|
28230
|
-
function
|
|
28471
|
+
function withPlaceholders(params, placeholders) {
|
|
28472
|
+
if (!placeholders || !params || typeof params !== "object") return params;
|
|
28473
|
+
const copy = JSON.parse(JSON.stringify(params));
|
|
28474
|
+
for (const [path, value] of Object.entries(placeholders)) {
|
|
28475
|
+
const keys = path.split(/\.|\[(\d+)\]/).filter((k) => k !== void 0 && k !== "");
|
|
28476
|
+
let node = copy;
|
|
28477
|
+
for (const k of keys.slice(0, -1)) node = node && typeof node === "object" ? node[k] : void 0;
|
|
28478
|
+
if (node && typeof node === "object") node[keys[keys.length - 1]] = value;
|
|
28479
|
+
}
|
|
28480
|
+
return copy;
|
|
28481
|
+
}
|
|
28482
|
+
function correctedOf(type, r, partial = false, placeholders) {
|
|
28231
28483
|
if (type === "stripe_checkout" && r.correction?.patch?.length && partial) {
|
|
28232
28484
|
return { format: "json", title: `Changes to make to the params (JSON Patch; keep your other values)${r.correction.verification?.passes ? ", verified by the rules engine" : ""}`, body: JSON.stringify(r.correction.patch, null, 2) };
|
|
28233
28485
|
}
|
|
28234
28486
|
if (type === "stripe_checkout" && r.correction?.patch?.length) {
|
|
28235
|
-
return { format: "json", title: `Corrected params${r.correction.verification?.passes ? " (verified by the rules engine)" : ""}`, body: JSON.stringify(r.correction.corrected_params, null, 2) };
|
|
28487
|
+
return { format: "json", title: `Corrected params${r.correction.verification?.passes ? " (verified by the rules engine)" : ""}${placeholders ? "; keep your own values where it says {{\u2026}}" : ""}`, body: JSON.stringify(withPlaceholders(r.correction.corrected_params, placeholders), null, 2) };
|
|
28236
28488
|
}
|
|
28237
28489
|
const perPlan = (r.plan_replacements ?? []).filter((p) => p.disclosure_text);
|
|
28238
28490
|
if (type === "page" && perPlan.length >= 2 && ciStatusOf(r) !== "pass") {
|
|
@@ -28264,11 +28516,38 @@ ${r.template.text}` };
|
|
|
28264
28516
|
}
|
|
28265
28517
|
return void 0;
|
|
28266
28518
|
}
|
|
28267
|
-
function
|
|
28519
|
+
function declaredByCheckbox(config) {
|
|
28520
|
+
const sb = config.notices?.sent_by_checkbox;
|
|
28521
|
+
return sb === true ? Object.values(NOTICE_TO_EMAIL) : Array.isArray(sb) ? sb : [];
|
|
28522
|
+
}
|
|
28523
|
+
function sentByNotices(seen) {
|
|
28524
|
+
if ("error" in seen || seen.mode !== "checkbox" || seen.real_sends === "off") return /* @__PURE__ */ new Set();
|
|
28525
|
+
return new Set(seen.enabled);
|
|
28526
|
+
}
|
|
28527
|
+
function noticesCrossCheck(config, seen, target) {
|
|
28528
|
+
const declared = declaredByCheckbox(config);
|
|
28529
|
+
if (!declared.length) return void 0;
|
|
28530
|
+
const base = { name: "Checkbox Notices (site settings)", type: "notice_schedule", target, warnings: [] };
|
|
28531
|
+
if ("error" in seen) {
|
|
28532
|
+
return { ...base, status: "needs_review", label: "Not checked: could not read the site's Notice settings", message: seen.error, fixes: [] };
|
|
28533
|
+
}
|
|
28534
|
+
const real = sentByNotices(seen);
|
|
28535
|
+
const all = config.notices?.sent_by_checkbox === true;
|
|
28536
|
+
const missing = all ? real.size ? [] : declared : declared.filter((k) => !real.has(k));
|
|
28537
|
+
if (!missing.length) {
|
|
28538
|
+
const note = seen.real_sends === "needs_confirmation" ? " (a person still has to confirm sending to real subscribers once in the dashboard: Emails tab)" : seen.real_sends === "test_mode" ? " (test mode: they go to test-run inboxes only)" : "";
|
|
28539
|
+
return { ...base, status: "pass", label: `Checks pass \xB7 Checkbox Notices is on for ${seen.site}${all ? ` (${real.size} kind${real.size === 1 ? "" : "s"} turned on)` : ` and sends the ${declared.length} email${declared.length === 1 ? "" : "s"} checkbox.json declares`}${note}`, fixes: [] };
|
|
28540
|
+
}
|
|
28541
|
+
const why = seen.mode !== "checkbox" ? `${seen.site} is in "You send" mode (Checkbox Notices sends nothing)` : seen.real_sends === "off" || !real.size ? `Checkbox Notices is off for ${seen.site}` : `${seen.site} does not have ${missing.map((k) => `"${k}"`).join(", ")} turned on in Checkbox Notices`;
|
|
28542
|
+
const fix = `checkbox.json says Checkbox Notices sends ${all ? "the required emails" : missing.map((k) => `"${k}"`).join(", ")}, but ${why}. Turn ${missing.length === 1 ? "it" : "them"} on in the dashboard (Emails tab) or with updateNoticeSettings, or remove ${missing.length === 1 ? "it" : "them"} from "notices.sent_by_checkbox" and add the templates to checks.emails.`;
|
|
28543
|
+
return { ...base, status: "fail", label: all ? "Checkbox Notices sends nothing for this site" : `${missing.length} declared email${missing.length === 1 ? " is" : "s are"} not sent by Checkbox Notices`, fix, fixes: [fix] };
|
|
28544
|
+
}
|
|
28545
|
+
function noticeCoverage(config, planName, schedule, opts = {}) {
|
|
28268
28546
|
const plan2 = (config.plans ?? []).find((p) => p.name === planName);
|
|
28269
28547
|
const have = new Set((config.checks.emails ?? []).filter((e) => !e.plan || e.plan === planName || planList(config, e.plan)?.includes(planName)).map((e) => e.kind));
|
|
28270
|
-
const
|
|
28271
|
-
const
|
|
28548
|
+
const declared = declaredByCheckbox(config);
|
|
28549
|
+
const real = opts.notices && !("error" in opts.notices) ? sentByNotices(opts.notices) : void 0;
|
|
28550
|
+
const byCheckbox = new Set(real ? declared.filter((k) => real.has(k)) : declared);
|
|
28272
28551
|
for (const k of byCheckbox) have.add(k);
|
|
28273
28552
|
const yearly = plan2 ? (plan2.interval === "year" ? 12 * (plan2.interval_count ?? 1) : plan2.interval === "month" ? plan2.interval_count ?? 1 : 0) >= 12 : false;
|
|
28274
28553
|
if (yearly && have.has("renewal_reminder")) have.add("annual_terms");
|
|
@@ -28280,8 +28559,8 @@ function noticeCoverage(config, planName, schedule) {
|
|
|
28280
28559
|
const missing = [...required].filter(([k]) => !have.has(k));
|
|
28281
28560
|
if (!required.size) return { status: "not_applicable", label: "No reminder emails required", fixes: [] };
|
|
28282
28561
|
const sent2 = [...required.keys()].filter((k) => byCheckbox.has(k)).length;
|
|
28283
|
-
if (!missing.length) return { status: "pass", label: `Checks pass \xB7 ${required.size} required email${required.size === 1 ? "" : "s"} covered${sent2 ? ` (${sent2} sent by Checkbox Notices, as checkbox.json declares)` : ""}`, fixes: [] };
|
|
28284
|
-
const fixes = missing.map(([k, w]) => `Add a ${EMAIL_LABEL[k]} to checks.emails (kind "${k}"; ${w.days > 0 ? `${w.days} days before: ${w.occasion}` : w.occasion}), or have Checkbox Notices send it.`);
|
|
28562
|
+
if (!missing.length) return { status: "pass", label: `Checks pass \xB7 ${required.size} required email${required.size === 1 ? "" : "s"} covered${sent2 ? ` (${sent2} sent by Checkbox Notices, ${real ? "as the site's Notice settings confirm" : "as checkbox.json declares; --online confirms it against the site"})` : ""}`, fixes: [] };
|
|
28563
|
+
const fixes = missing.map(([k, w]) => `Add ${/^[aeiou]/i.test(EMAIL_LABEL[k]) ? "an" : "a"} ${EMAIL_LABEL[k]} to checks.emails (kind "${k}"; ${w.days > 0 ? `${w.days} days before: ${w.occasion}` : w.occasion}), or have Checkbox Notices send it${declared.includes(k) ? " (checkbox.json says it does, but the site's Notice settings do not send it)" : ""}.`);
|
|
28285
28564
|
return { status: "needs_review", label: `${missing.length} required email${missing.length === 1 ? "" : "s"} not in checkbox.json`, fix: fixes[0], fixes };
|
|
28286
28565
|
}
|
|
28287
28566
|
function unverifiedOffer(r) {
|
|
@@ -28295,8 +28574,8 @@ function unverifiedOffer(r) {
|
|
|
28295
28574
|
if (coupon && open2.length) return coupon.message;
|
|
28296
28575
|
return void 0;
|
|
28297
28576
|
}
|
|
28298
|
-
async function runJob(job, engine, config) {
|
|
28299
|
-
const base = { name: job.name, type: job.type, ...job.plan ? { plan: job.plan } : {}, target: job.target, warnings: [...job.warnings] };
|
|
28577
|
+
async function runJob(job, engine, config, opts = {}) {
|
|
28578
|
+
const base = { name: job.name, type: job.type, ...job.plan ? { plan: job.plan } : {}, target: { ...job.target }, ...job.fromSource ? { fromSource: true } : {}, warnings: [...job.warnings] };
|
|
28300
28579
|
if (job.error) return { ...base, status: "error", label: "Could not run", message: job.error, fixes: [] };
|
|
28301
28580
|
if (job.skip || !job.body) return { ...base, status: "skipped", label: "Skipped", message: job.skip ?? "nothing to check", fixes: [] };
|
|
28302
28581
|
let report;
|
|
@@ -28305,37 +28584,68 @@ async function runJob(job, engine, config) {
|
|
|
28305
28584
|
} catch (err) {
|
|
28306
28585
|
const e = err;
|
|
28307
28586
|
const failure = isAuthError(err) ? "auth" : err instanceof CheckboxError && (!e.statusCode || e.statusCode >= 500 || e.statusCode === 429) ? "api" : void 0;
|
|
28308
|
-
return { ...base, status: "error", label: "Could not run", message: `${e.message}${e.param ? ` (${e.param})` : ""}`, fixes: [], ...failure ? { failure } : {} };
|
|
28587
|
+
return { ...base, status: "error", label: "Could not run", message: `${ciVocabulary(e.message)}${e.param && !failure && /^merchant\./.test(e.param) ? "" : e.param ? ` (${e.param})` : ""}`, fixes: [], ...failure ? { failure } : {} };
|
|
28309
28588
|
}
|
|
28310
|
-
if (job.type === "notice_schedule") return { ...base, ...noticeCoverage(config, job.plan, report), report };
|
|
28311
|
-
|
|
28589
|
+
if (job.type === "notice_schedule") return { ...base, ...noticeCoverage(config, job.plan, report, opts), report };
|
|
28590
|
+
const alerts = job.warnings.filter((w) => /^The params and checkbox\.json disagree/.test(w));
|
|
28591
|
+
for (const w of report.warnings ?? []) {
|
|
28592
|
+
const text = ciVocabulary(w.message);
|
|
28593
|
+
base.warnings.push(text);
|
|
28594
|
+
if (ALERT_CODES.has(w.code)) alerts.push(text);
|
|
28595
|
+
}
|
|
28596
|
+
if (alerts.length) Object.assign(base, { alerts });
|
|
28312
28597
|
const status = ciStatusOf(report);
|
|
28313
|
-
const
|
|
28598
|
+
const params = job.type === "stripe_checkout" ? job.body?.params ?? void 0 : void 0;
|
|
28599
|
+
const fixes = (status === "pass" || status === "not_applicable" ? [] : fixesOf(report)).map((f) => ciVocabulary(job.type === "stripe_checkout" ? stripeConsentFix(f, params) : f));
|
|
28314
28600
|
const unreadText = (job.unresolved ?? []).filter((p) => /^(?:custom_text|consent_collection|discounts|subscription_data|allow_promotion_codes|payment_method_collection)\b/.test(p));
|
|
28315
28601
|
const unread = job.type === "stripe_checkout" && unreadText.length > 0 && (status === "fail" || status === "needs_review" || status === "pass" && unreadText.some((p) => /^(?:custom_text|consent_collection|discounts|subscription_data\.trial)/.test(p)));
|
|
28316
28602
|
if (unread) {
|
|
28317
28603
|
fixes.splice(0, fixes.length, `${unreadText.slice(0, 3).join(", ")} ${unreadText.length === 1 ? "is an expression" : "are expressions"} Checkbox cannot read statically (e.g. a ternary or PLANS.monthly.*), so the checks that depend on ${unreadText.length === 1 ? "it were" : "them were"} not checked (no fix is given for a value Checkbox did not read). Use "module" (a function returning the exact params for each plan) or "file" instead of "source".`);
|
|
28318
28604
|
}
|
|
28605
|
+
const literal = [];
|
|
28606
|
+
if (unread) {
|
|
28607
|
+
const consentRead = !(job.unresolved ?? []).some((p) => /^(?:consent_collection|\.\.\.)/.test(p));
|
|
28608
|
+
const consentFails = (report.checks ?? []).some((c) => c.check_id === "signup.consent.affirmative" && (c.status === "fail" || c.compliance_status === "needs_fix"));
|
|
28609
|
+
if (consentRead && consentFails && params?.consent_collection?.terms_of_service !== "required") literal.push(stripeConsentFix("Add one unchecked checkbox", params));
|
|
28610
|
+
fixes.unshift(...literal);
|
|
28611
|
+
}
|
|
28319
28612
|
const offer = !unread && status === "needs_review" && job.type === "stripe_checkout" ? unverifiedOffer(report) : void 0;
|
|
28320
28613
|
if (offer) {
|
|
28321
28614
|
fixes.unshift(
|
|
28322
28615
|
`What the shopper is charged first is not verified (${offer.replace(/\.$/, "")}). Declare the discount 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 remove the discount. This fails the run (exit 2) until it is resolved.`
|
|
28323
28616
|
);
|
|
28324
28617
|
}
|
|
28325
|
-
|
|
28326
|
-
|
|
28618
|
+
if (job.locate && (status === "fail" || status === "needs_review")) {
|
|
28619
|
+
const named = (report.checks ?? []).filter((c) => c.status === "fail" || c.status === "needs_review").flatMap((c) => failingCards(c.explanation));
|
|
28620
|
+
const needles = named.flatMap((n) => [n.split(", ")[0], ...n.split(", ").slice(1)]);
|
|
28621
|
+
const line2 = needles.length ? job.locate(needles) : void 0;
|
|
28622
|
+
if (line2) base.target = { ...base.target, line: line2 };
|
|
28623
|
+
}
|
|
28624
|
+
const finalStatus = unread ? literal.length ? "fail" : "needs_review" : status;
|
|
28625
|
+
const corrected = unread ? void 0 : correctedOf(job.type, report, job.partial, job.placeholders);
|
|
28626
|
+
if (corrected?.format === "json" && job.placeholders) {
|
|
28627
|
+
const unset = job.warnings.filter((w) => /(?:was|were) not set when Checkbox ran/.test(w));
|
|
28628
|
+
if (unset.length) Object.assign(base, { alerts: [...unset, ...base.alerts ?? []] });
|
|
28629
|
+
}
|
|
28327
28630
|
const blocking = unread || status === "needs_review" && (job.type === "page" || job.type === "disclosure") && (report.compliance_status === "incomplete" || report.compliance_status === "not_checked") || status === "needs_review" && job.type === "stripe_checkout" && (!!job.strict || !!offer);
|
|
28328
28631
|
return {
|
|
28329
28632
|
...base,
|
|
28330
28633
|
status: finalStatus,
|
|
28331
28634
|
...blocking ? { blocking: true } : {},
|
|
28332
|
-
label: unread ? `Not checked: ${unreadText[0]} is an expression` : report.compliance?.label || DEFAULT_LABEL[status],
|
|
28635
|
+
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report),
|
|
28333
28636
|
...fixes[0] ? { fix: fixes[0] } : {},
|
|
28334
28637
|
fixes,
|
|
28335
28638
|
...corrected ? { corrected } : {},
|
|
28336
28639
|
report
|
|
28337
28640
|
};
|
|
28338
28641
|
}
|
|
28642
|
+
function countedLabel(label, r) {
|
|
28643
|
+
const m = /^(\d+) fix(?:es)? needed/.exec(label);
|
|
28644
|
+
if (!m) return label;
|
|
28645
|
+
const n = fixesOf(r, true).length;
|
|
28646
|
+
if (!n || n === Number(m[1])) return label;
|
|
28647
|
+
return label.replace(m[0], `${n} fix${n === 1 ? "" : "es"} needed`);
|
|
28648
|
+
}
|
|
28339
28649
|
function exitCodeOf(checks, strict) {
|
|
28340
28650
|
if (checks.some((c) => c.failure === "auth")) return { status: "error", exit: EXIT.auth };
|
|
28341
28651
|
if (checks.some((c) => c.failure === "api")) return { status: "error", exit: EXIT.error };
|
|
@@ -28347,7 +28657,7 @@ function exitCodeOf(checks, strict) {
|
|
|
28347
28657
|
function mergeSourcePatches(checks) {
|
|
28348
28658
|
const groups = /* @__PURE__ */ new Map();
|
|
28349
28659
|
for (const c of checks) {
|
|
28350
|
-
if (c.type !== "stripe_checkout" || c.corrected?.format !== "json" || !c.target.line) continue;
|
|
28660
|
+
if (c.type !== "stripe_checkout" || c.corrected?.format !== "json" || !c.target.line || !c.fromSource) continue;
|
|
28351
28661
|
const key = `${c.target.file}:${c.target.line}`;
|
|
28352
28662
|
groups.set(key, [...groups.get(key) ?? [], c]);
|
|
28353
28663
|
}
|
|
@@ -28365,7 +28675,9 @@ function mergeSourcePatches(checks) {
|
|
|
28365
28675
|
}
|
|
28366
28676
|
async function runCi(jobs, engine, opts) {
|
|
28367
28677
|
const checks = [...opts.extra ?? []];
|
|
28368
|
-
for (const j of jobs) checks.push(await runJob(j, engine, opts.config));
|
|
28678
|
+
for (const j of jobs) checks.push(await runJob(j, engine, opts.config, { ...opts.notices ? { notices: opts.notices } : {} }));
|
|
28679
|
+
const cross = opts.notices ? noticesCrossCheck(opts.config, opts.notices, opts.noticesTarget ?? { file: opts.configPath }) : void 0;
|
|
28680
|
+
if (cross) checks.push(cross);
|
|
28369
28681
|
mergeSourcePatches(checks);
|
|
28370
28682
|
const summary = { pass: 0, fail: 0, needs_review: 0, not_applicable: 0, skipped: 0, error: 0 };
|
|
28371
28683
|
for (const c of checks) summary[c.status]++;
|
|
@@ -28385,7 +28697,7 @@ async function runCi(jobs, engine, opts) {
|
|
|
28385
28697
|
disclaimer: CI_DISCLAIMER
|
|
28386
28698
|
};
|
|
28387
28699
|
}
|
|
28388
|
-
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL;
|
|
28700
|
+
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL, ALERT_CODES;
|
|
28389
28701
|
var init_runner = __esm({
|
|
28390
28702
|
"src/ci/runner.ts"() {
|
|
28391
28703
|
"use strict";
|
|
@@ -28422,6 +28734,75 @@ var init_runner = __esm({
|
|
|
28422
28734
|
cancellation_confirmation: "cancellation confirmation"
|
|
28423
28735
|
};
|
|
28424
28736
|
DEFAULT_LABEL = { pass: "Checks pass", fail: "Fixes needed", needs_review: "Needs review", not_applicable: "Not required for this plan" };
|
|
28737
|
+
ALERT_CODES = /* @__PURE__ */ new Set(["coupon_unverified", "coupon_read", "merchant_trial_ignored", "merchant_price_ignored", "price_not_in_site_plans", "price_from_setup_plan", "plan_terms_conflict", "trial_mismatch", "intro_mismatch"]);
|
|
28738
|
+
}
|
|
28739
|
+
});
|
|
28740
|
+
|
|
28741
|
+
// src/commands/plan/lib.ts
|
|
28742
|
+
import { dirname as dirname5, resolve as resolve5 } from "node:path";
|
|
28743
|
+
async function resolveSite(ctx, flag, client) {
|
|
28744
|
+
const explicit = flag ?? ctx.env.CHECKBOX_SITE;
|
|
28745
|
+
if (explicit) {
|
|
28746
|
+
if (!SITE_RE.test(explicit) && !/^[0-9a-f-]{36}$/i.test(explicit)) throw new UsageError(`--site must be a site id (site_\u2026), got "${explicit}"`);
|
|
28747
|
+
return explicit;
|
|
28748
|
+
}
|
|
28749
|
+
const projects = ctx.config.load().init?.projects ?? {};
|
|
28750
|
+
for (let dir = resolve5(ctx.cwd); ; dir = dirname5(dir)) {
|
|
28751
|
+
const site = projects[dir]?.site;
|
|
28752
|
+
if (site) return site;
|
|
28753
|
+
if (dirname5(dir) === dir) break;
|
|
28754
|
+
}
|
|
28755
|
+
const list2 = await client.sdk.sites.list({ limit: 3 });
|
|
28756
|
+
const sites = list2?.data ?? [];
|
|
28757
|
+
if (sites.length === 1) return sites[0].id;
|
|
28758
|
+
throw new UsageError(
|
|
28759
|
+
sites.length ? `This account has several sites; pass --site <id> (e.g. ${sites.map((s2) => s2.id).join(", ")}) or set CHECKBOX_SITE.` : "No site yet: run `checkbox init` in your project first (it creates the site and remembers it here)."
|
|
28760
|
+
);
|
|
28761
|
+
}
|
|
28762
|
+
function nextLine(n) {
|
|
28763
|
+
const text = n.description ?? n.message ?? n.type;
|
|
28764
|
+
return `${text}${n.command ? `
|
|
28765
|
+
$ ${n.command}` : n.endpoint ? `
|
|
28766
|
+
${n.endpoint}` : ""}`;
|
|
28767
|
+
}
|
|
28768
|
+
function renderPlan(p, c, opts = {}) {
|
|
28769
|
+
const head = `${c.bold(`Compliance plan ${p.site}`)} \u2014 ${p.compliance?.label ? p.status === "compliant" ? c.green(p.compliance.label) : p.status === "action_required" ? c.red(p.compliance.label) : c.yellow(p.compliance.label) : statusColor(p.status, c)}`;
|
|
28770
|
+
const tasks = opts.all ? p.tasks : p.tasks.filter((t) => t.status !== "done" && t.status !== "wont_fix");
|
|
28771
|
+
const lines = [head, p.summary, ""];
|
|
28772
|
+
for (const t of tasks) {
|
|
28773
|
+
lines.push(`${STATUS_MARK[t.status] ?? "[ ]"} ${String(t.order).padStart(2)}. ${t.title} ${c.dim(`(${t.priority}, ${t.id})`)}`);
|
|
28774
|
+
if (t.status === "manual" && t.next_action?.type === "ask_human_to_confirm") lines.push(` ${c.yellow("waiting for the human to confirm:")} ${t.next_action.description}`);
|
|
28775
|
+
else if (t.status === "manual" && t.question) lines.push(` ${c.yellow("ask the human:")} ${t.question.text} ${c.dim(`\u2192 checkbox answer ${t.id} <${t.question.options?.join("|") ?? "yes|no"}>`)}`);
|
|
28776
|
+
else if ((t.status === "done" || t.status === "wont_fix") && t.closed_reason) lines.push(c.dim(` closed: ${t.closed_reason.replace(/_/g, " ")}`));
|
|
28777
|
+
else if (t.blocked_by.length) lines.push(c.dim(` after ${t.blocked_by.join(", ")}`));
|
|
28778
|
+
else if (t.status === "open" || t.status === "in_progress") lines.push(c.dim(` ${t.where.file_hint ?? t.where.url ?? t.where.stripe_object ?? t.where.kind} \xB7 ${t.change.type} \xB7 checkbox fix ${t.id}`));
|
|
28779
|
+
}
|
|
28780
|
+
const hidden = p.tasks.length - tasks.length;
|
|
28781
|
+
if (hidden) lines.push(c.dim(` \u2026 ${hidden} done / declined (--all to show)`));
|
|
28782
|
+
lines.push("", `${c.cyan("next")} ${nextLine(p.next_action)}`);
|
|
28783
|
+
return lines;
|
|
28784
|
+
}
|
|
28785
|
+
function statusColor(s2, c) {
|
|
28786
|
+
return s2 === "compliant" ? c.green("all observed checks passing") : s2 === "action_required" ? c.red(s2) : c.yellow(s2);
|
|
28787
|
+
}
|
|
28788
|
+
function renderVerification(v, c) {
|
|
28789
|
+
const mark = v.status === "passed" ? c.green("passed") : v.status === "failed" ? c.red("failed") : v.status === "passed_with_gaps" ? c.yellow("passed with gaps (not verified)") : v.status === "passed_with_notes" ? c.yellow("passed with notes (a check outside your control needs review)") : v.status === "not_checked" ? c.yellow("not checked yet (missing evidence, nothing to change)") : v.status === "pending_live_check" ? c.yellow("live check pending (content passes; waiting for a real send)") : c.yellow("pending");
|
|
28790
|
+
const firstCheck = !v.task && v.next_action?.type === "first_check_complete";
|
|
28791
|
+
const lines = [`${c.bold(`Verification ${v.id}`)}${v.task ? ` for ${v.task}` : ""}: ${firstCheck ? c.cyan("first check complete") : mark} (${v.method})`];
|
|
28792
|
+
if (v.precheck) lines.push(` pre-check (${v.precheck.source}): ${v.precheck.verdict} \u2014 ${v.precheck.note}`);
|
|
28793
|
+
if (v.scan) lines.push(c.dim(` confirming scan ${v.scan}`));
|
|
28794
|
+
for (const d of v.diff.slice(0, 12)) lines.push(` ${d.observed === "fail" ? c.red("\u2717") : c.yellow("?")} ${d.check_id}: expected ${d.expected}, observed ${d.observed} \u2014 ${d.detail}`);
|
|
28795
|
+
if (v.diff.length > 12) lines.push(c.dim(` \u2026 ${v.diff.length - 12} more`));
|
|
28796
|
+
lines.push(`${c.cyan("next")} ${nextLine(v.next_action)}`);
|
|
28797
|
+
return lines;
|
|
28798
|
+
}
|
|
28799
|
+
var SITE_RE, STATUS_MARK;
|
|
28800
|
+
var init_lib = __esm({
|
|
28801
|
+
"src/commands/plan/lib.ts"() {
|
|
28802
|
+
"use strict";
|
|
28803
|
+
init_args();
|
|
28804
|
+
SITE_RE = /^site_[0-9a-f]{32}$/;
|
|
28805
|
+
STATUS_MARK = { open: "[ ]", in_progress: "[~]", verifying: "[?]", done: "[x]", wont_fix: "[-]", manual: "[!]" };
|
|
28425
28806
|
}
|
|
28426
28807
|
});
|
|
28427
28808
|
|
|
@@ -28470,14 +28851,14 @@ __export(ci_exports, {
|
|
|
28470
28851
|
undiffableInputs: () => undiffableInputs
|
|
28471
28852
|
});
|
|
28472
28853
|
import { execFileSync } from "node:child_process";
|
|
28473
|
-
import { appendFileSync, existsSync as
|
|
28854
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
28474
28855
|
import { createInterface } from "node:readline";
|
|
28475
|
-
import { dirname as
|
|
28856
|
+
import { dirname as dirname6, join as join8, relative as relative5, resolve as resolve6 } from "node:path";
|
|
28476
28857
|
function gitChanged(cwd, base) {
|
|
28477
28858
|
try {
|
|
28478
28859
|
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
28479
28860
|
const out = execFileSync("git", ["diff", "--name-only", `${base}...HEAD`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
28480
|
-
return { root, files: out.split("\n").map((l) => l.trim()).filter(Boolean).map((f) =>
|
|
28861
|
+
return { root, files: out.split("\n").map((l) => l.trim()).filter(Boolean).map((f) => resolve6(root, f)) };
|
|
28481
28862
|
} catch (err) {
|
|
28482
28863
|
const msg2 = String(err.stderr || err.message).trim().split("\n")[0];
|
|
28483
28864
|
return `git diff ${base}...HEAD failed (${msg2}); fetch the base branch (actions/checkout with fetch-depth: 0)`;
|
|
@@ -28487,13 +28868,13 @@ function undiffableInputs(loaded, jobs, gitRoot) {
|
|
|
28487
28868
|
const need = [];
|
|
28488
28869
|
const out = [];
|
|
28489
28870
|
for (const e of loaded.config.checks.pages ?? []) {
|
|
28490
|
-
if (e.source) need.push({ path:
|
|
28871
|
+
if (e.source) need.push({ path: resolve6(loaded.dir, e.source), label: `pages "${e.name}" source ${e.source}` });
|
|
28491
28872
|
else if (e.url) out.push(`pages "${e.name}" is a URL with no "source" file to diff`);
|
|
28492
|
-
else if (e.file) need.push({ path:
|
|
28873
|
+
else if (e.file) need.push({ path: resolve6(loaded.dir, e.file), label: `pages "${e.name}" file ${e.file}` });
|
|
28493
28874
|
}
|
|
28494
28875
|
for (const j of jobs) {
|
|
28495
28876
|
if (j.type === "page") continue;
|
|
28496
|
-
for (const f of j.inputs) need.push({ path:
|
|
28877
|
+
for (const f of j.inputs) need.push({ path: resolve6(loaded.dir, f), label: `"${j.name}" reads ${f}` });
|
|
28497
28878
|
}
|
|
28498
28879
|
if (!need.length) return out;
|
|
28499
28880
|
const real = (p) => {
|
|
@@ -28509,7 +28890,7 @@ function undiffableInputs(loaded, jobs, gitRoot) {
|
|
|
28509
28890
|
try {
|
|
28510
28891
|
const rels = [...new Set(need.map((n) => relative5(gitRoot, n.path)))];
|
|
28511
28892
|
const listed = execFileSync("git", ["ls-files", "-z", "--", ...rels], { cwd: gitRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
28512
|
-
tracked = new Set(listed.split("\0").filter(Boolean).map((f) =>
|
|
28893
|
+
tracked = new Set(listed.split("\0").filter(Boolean).map((f) => resolve6(gitRoot, f)));
|
|
28513
28894
|
} catch (err) {
|
|
28514
28895
|
return `git ls-files failed (${String(err.stderr || err.message).trim().split("\n")[0]})`;
|
|
28515
28896
|
}
|
|
@@ -28521,7 +28902,7 @@ function touchesConfig(loaded, jobs, changed) {
|
|
|
28521
28902
|
if (set.has(loaded.path)) return true;
|
|
28522
28903
|
const codeChanged = changed.some((f) => /\.(?:[cm]?[jt]sx?)$/.test(f));
|
|
28523
28904
|
for (const j of jobs) {
|
|
28524
|
-
for (const f of j.inputs) if (set.has(
|
|
28905
|
+
for (const f of j.inputs) if (set.has(resolve6(loaded.dir, f))) return true;
|
|
28525
28906
|
}
|
|
28526
28907
|
const hasModules = [...loaded.config.checks.stripe_checkout ?? [], ...loaded.config.checks.emails ?? []].some((e) => "module" in e && e.module);
|
|
28527
28908
|
return hasModules && codeChanged;
|
|
@@ -28546,12 +28927,21 @@ async function runInit(ctx, argv) {
|
|
|
28546
28927
|
const p = parseArgs(argv, { boolean: ["yes", "force", "dry-run", "workflow"], string: ["config"], alias: { y: "yes" } });
|
|
28547
28928
|
if (p.positionals.length) throw new UsageError(`Unexpected argument "${p.positionals[0]}"`);
|
|
28548
28929
|
const explicit = str(p.flags, "config");
|
|
28549
|
-
const configPath = explicit ?
|
|
28930
|
+
const configPath = explicit ? resolve6(ctx.cwd, explicit) : CONFIG_FILES.map((f) => join8(ctx.cwd, f)).find((f) => existsSync7(f)) ?? join8(ctx.cwd, CONFIG_FILES[0]);
|
|
28550
28931
|
const workflowPath = join8(ctx.cwd, WORKFLOW_PATH);
|
|
28551
28932
|
const force = bool(p.flags, "force") === true;
|
|
28552
28933
|
const withWorkflow = bool(p.flags, "workflow") !== false;
|
|
28553
|
-
const
|
|
28554
|
-
|
|
28934
|
+
const projects = ctx.config.load().init?.projects ?? {};
|
|
28935
|
+
let facts = {};
|
|
28936
|
+
for (let dir = resolve6(ctx.cwd); ; dir = dirname6(dir)) {
|
|
28937
|
+
if (projects[dir]?.facts) {
|
|
28938
|
+
facts = projects[dir].facts;
|
|
28939
|
+
break;
|
|
28940
|
+
}
|
|
28941
|
+
if (dirname6(dir) === dir) break;
|
|
28942
|
+
}
|
|
28943
|
+
const s2 = scaffoldConfig(ctx.cwd, ctx.env, facts);
|
|
28944
|
+
const actionFor = (f) => existsSync7(f) ? force ? "overwrite" : "keep" : "create";
|
|
28555
28945
|
const files = [{ path: configPath, content: `${JSON.stringify(s2.config, null, 2)}
|
|
28556
28946
|
`, action: actionFor(configPath) }];
|
|
28557
28947
|
if (withWorkflow) files.push({ path: workflowPath, content: WORKFLOW_YAML, action: actionFor(workflowPath) });
|
|
@@ -28566,7 +28956,7 @@ async function runInit(ctx, argv) {
|
|
|
28566
28956
|
}
|
|
28567
28957
|
if (write) {
|
|
28568
28958
|
for (const f of toWrite) {
|
|
28569
|
-
mkdirSync2(
|
|
28959
|
+
mkdirSync2(dirname6(f.path), { recursive: true });
|
|
28570
28960
|
writeFileSync2(f.path, f.content);
|
|
28571
28961
|
}
|
|
28572
28962
|
}
|
|
@@ -28593,7 +28983,21 @@ async function runInit(ctx, argv) {
|
|
|
28593
28983
|
});
|
|
28594
28984
|
return EXIT.ok;
|
|
28595
28985
|
}
|
|
28596
|
-
|
|
28986
|
+
async function readNotices(ctx, client, siteFlag) {
|
|
28987
|
+
let site;
|
|
28988
|
+
try {
|
|
28989
|
+
site = await resolveSite(ctx, siteFlag, client);
|
|
28990
|
+
} catch (err) {
|
|
28991
|
+
return { error: `${err.message} checkbox.json says Checkbox Notices sends emails; --online checks that against the site's Notice settings once it knows the site (--site or CHECKBOX_SITE).` };
|
|
28992
|
+
}
|
|
28993
|
+
try {
|
|
28994
|
+
const s2 = await client.sdk.noticeSettings.retrieve(site);
|
|
28995
|
+
return { site, mode: s2.mode, enabled: (s2.enabled_kinds ?? []).map((k) => NOTICE_KIND_TO_EMAIL[k] ?? k), real_sends: s2.real_sends };
|
|
28996
|
+
} catch (err) {
|
|
28997
|
+
return { error: `Could not read the Notice settings of ${site} (${err.message}).` };
|
|
28998
|
+
}
|
|
28999
|
+
}
|
|
29000
|
+
var HELP2, NOTICE_KIND_TO_EMAIL, ci_default;
|
|
28597
29001
|
var init_ci = __esm({
|
|
28598
29002
|
"src/commands/ci/index.ts"() {
|
|
28599
29003
|
"use strict";
|
|
@@ -28603,6 +29007,8 @@ var init_ci = __esm({
|
|
|
28603
29007
|
init_inputs();
|
|
28604
29008
|
init_report();
|
|
28605
29009
|
init_runner();
|
|
29010
|
+
init_run();
|
|
29011
|
+
init_lib();
|
|
28606
29012
|
init_scaffold();
|
|
28607
29013
|
init_workflow();
|
|
28608
29014
|
init_types2();
|
|
@@ -28611,7 +29017,9 @@ var init_ci = __esm({
|
|
|
28611
29017
|
"",
|
|
28612
29018
|
"Flags:",
|
|
28613
29019
|
" --config <path> Config file (default: checkbox.json, then .checkbox.json)",
|
|
28614
|
-
" --online Use the API (CHECKBOX_SECRET_KEY / CHECKBOX_API_KEY): URL pages and site-aware checks",
|
|
29020
|
+
" --online Use the API (CHECKBOX_SECRET_KEY / CHECKBOX_API_KEY): URL pages and site-aware checks;",
|
|
29021
|
+
` with "notices" in checkbox.json, also checks that the site's Checkbox Notices really send them`,
|
|
29022
|
+
" --site <site_\u2026> The site for --online (default: $CHECKBOX_SITE, the site `checkbox init` saved, or the only one)",
|
|
28615
29023
|
' --strict "Needs review" fails too (exit 2)',
|
|
28616
29024
|
" --changed-only Skip when no file the config reads changed (git diff against --base, or",
|
|
28617
29025
|
" origin/$GITHUB_BASE_REF in a pull request). Never skips while a page or file it reads",
|
|
@@ -28634,6 +29042,7 @@ var init_ci = __esm({
|
|
|
28634
29042
|
"",
|
|
28635
29043
|
"Docs: https://checkbox.site/docs/guides/ci"
|
|
28636
29044
|
].join("\n");
|
|
29045
|
+
NOTICE_KIND_TO_EMAIL = { trial_ending: "trial_reminder" };
|
|
28637
29046
|
ci_default = defineCommand({
|
|
28638
29047
|
name: "ci",
|
|
28639
29048
|
summary: "Run the checks in checkbox.json (CI: annotations + job summary); `ci init` scaffolds the config and workflow",
|
|
@@ -28641,7 +29050,7 @@ var init_ci = __esm({
|
|
|
28641
29050
|
help: HELP2,
|
|
28642
29051
|
async run(ctx) {
|
|
28643
29052
|
if (ctx.argv[0] === "init") return runInit(ctx, ctx.argv.slice(1));
|
|
28644
|
-
const p = parseArgs(ctx.argv, { boolean: ["online", "offline", "strict", "verbose", "changed-only"], string: ["config", "base"] });
|
|
29053
|
+
const p = parseArgs(ctx.argv, { boolean: ["online", "offline", "strict", "verbose", "changed-only"], string: ["config", "base", "site"] });
|
|
28645
29054
|
if (p.positionals.length) throw new UsageError(`Unexpected argument "${p.positionals[0]}" (did you mean \`checkbox ci init\`?)`);
|
|
28646
29055
|
const gha = ctx.env.GITHUB_ACTIONS === "true";
|
|
28647
29056
|
const strict = bool(p.flags, "strict") === true;
|
|
@@ -28698,8 +29107,21 @@ ${message}
|
|
|
28698
29107
|
}
|
|
28699
29108
|
}
|
|
28700
29109
|
}
|
|
28701
|
-
|
|
28702
|
-
|
|
29110
|
+
let client;
|
|
29111
|
+
if (online) {
|
|
29112
|
+
try {
|
|
29113
|
+
client = ctx.client();
|
|
29114
|
+
} catch (err) {
|
|
29115
|
+
if (!(err instanceof NotLoggedInError)) throw err;
|
|
29116
|
+
if (gha) emit("::error title=Checkbox::--online needs CHECKBOX_SECRET_KEY. Add it as a repository secret, or drop --online (checkbox ci runs offline without a key).");
|
|
29117
|
+
ctx.out.error({ message: `${err.message}`, code: "not_logged_in", type: "authentication_error", next_action: { type: "set_key", message: "--online needs an API key: set CHECKBOX_SECRET_KEY (a repository secret in GitHub Actions), or drop --online: checkbox ci runs offline with no key." } });
|
|
29118
|
+
return EXIT.auth;
|
|
29119
|
+
}
|
|
29120
|
+
}
|
|
29121
|
+
const engine = client ? onlineEngine(client) : offlineEngine();
|
|
29122
|
+
const notices = client && declaredByCheckbox(loaded.config).length ? await readNotices(ctx, client, str(p.flags, "site")) : void 0;
|
|
29123
|
+
const noticesLine = Math.max(1, loaded.raw.split("\n").findIndex((l) => l.includes('"sent_by_checkbox"')) + 1);
|
|
29124
|
+
const run2 = await runCi(jobs, engine, { config: loaded.config, configPath: configName, strict, extra, ...notices ? { notices, noticesTarget: { file: relative5(workspace, path).split("\\").join("/") || configName, line: noticesLine } } : {} });
|
|
28703
29125
|
ctx.out.result(run2, () => renderHuman(run2, ctx.out.c, { verbose: bool(p.flags, "verbose") === true }));
|
|
28704
29126
|
if (gha) {
|
|
28705
29127
|
for (const a of annotations(run2)) emit(a);
|
|
@@ -28931,8 +29353,8 @@ var init_patch = __esm({
|
|
|
28931
29353
|
});
|
|
28932
29354
|
|
|
28933
29355
|
// src/commands/plan/fix.ts
|
|
28934
|
-
import { existsSync as
|
|
28935
|
-
import { relative as relative6, resolve as
|
|
29356
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "node:fs";
|
|
29357
|
+
import { relative as relative6, resolve as resolve7 } from "node:path";
|
|
28936
29358
|
function fixForTask(task, cwd, file) {
|
|
28937
29359
|
const base = {
|
|
28938
29360
|
object: "task_fix",
|
|
@@ -28944,9 +29366,9 @@ function fixForTask(task, cwd, file) {
|
|
|
28944
29366
|
verify: task.verify
|
|
28945
29367
|
};
|
|
28946
29368
|
const verifyCmd = `checkbox verify ${task.id} --wait`;
|
|
28947
|
-
const candidate = file ?? (task.where.file_hint &&
|
|
29369
|
+
const candidate = file ?? (task.where.file_hint && existsSync8(resolve7(cwd, task.where.file_hint)) ? task.where.file_hint : void 0);
|
|
28948
29370
|
if (task.change.type === "patch" && task.change.edits.length && candidate) {
|
|
28949
|
-
const path =
|
|
29371
|
+
const path = resolve7(cwd, candidate);
|
|
28950
29372
|
let src;
|
|
28951
29373
|
try {
|
|
28952
29374
|
src = readFileSync9(path, "utf8");
|
|
@@ -29009,7 +29431,7 @@ var init_fix = __esm({
|
|
|
29009
29431
|
const task = await ctx.client().sdk.tasks.retrieve(id);
|
|
29010
29432
|
const result = fixForTask(task, ctx.cwd, str(flags, "file"));
|
|
29011
29433
|
const out = str(flags, "out");
|
|
29012
|
-
if (out && result.diff) writeFileSync3(
|
|
29434
|
+
if (out && result.diff) writeFileSync3(resolve7(ctx.cwd, out), result.diff);
|
|
29013
29435
|
ctx.out.result({ ...result, task_object: task }, () => {
|
|
29014
29436
|
const c = ctx.out.c;
|
|
29015
29437
|
const lines = [`${c.bold(task.title)} ${c.dim(`(${task.priority}, ${task.id})`)}`, c.dim(`${task.requirement.citation}`), task.why, ""];
|
|
@@ -29112,8 +29534,8 @@ var init_device = __esm({
|
|
|
29112
29534
|
|
|
29113
29535
|
// src/commands/init/patch.ts
|
|
29114
29536
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
29115
|
-
import { existsSync as
|
|
29116
|
-
import { dirname as
|
|
29537
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "node:fs";
|
|
29538
|
+
import { dirname as dirname7, join as join9 } from "node:path";
|
|
29117
29539
|
function read(root, rel2) {
|
|
29118
29540
|
try {
|
|
29119
29541
|
return readFileSync10(join9(root, rel2), "utf8");
|
|
@@ -29311,7 +29733,7 @@ function planGitignore(d) {
|
|
|
29311
29733
|
const covered = lines.some((l) => l === env2 || l === `/${env2}` || l === ".env*" || l === "*.env" || env2 === ".env.local" && (l === ".env*.local" || l === "*.local" || l === ".env.local*") || env2 === ".env" && l === ".env*");
|
|
29312
29734
|
if (covered) return null;
|
|
29313
29735
|
}
|
|
29314
|
-
if (existing === null && !
|
|
29736
|
+
if (existing === null && !existsSync9(join9(d.root, ".git"))) return null;
|
|
29315
29737
|
const text = `${existing ?? ""}${existing && !existing.endsWith("\n") ? "\n" : ""}# Checkbox keys (checkbox init)
|
|
29316
29738
|
${env2}
|
|
29317
29739
|
`;
|
|
@@ -29328,7 +29750,7 @@ function planWellKnown(d, token) {
|
|
|
29328
29750
|
function applyChange(root, c) {
|
|
29329
29751
|
if (c.action === "unchanged" || c.contents === void 0) return;
|
|
29330
29752
|
const abs = join9(root, c.file);
|
|
29331
|
-
mkdirSync3(
|
|
29753
|
+
mkdirSync3(dirname7(abs), { recursive: true });
|
|
29332
29754
|
writeFileSync4(abs, c.contents, c.file.startsWith(".env") ? { mode: 384 } : void 0);
|
|
29333
29755
|
}
|
|
29334
29756
|
var MARK_START, MARK_END, NEXT_DOCUMENT, NUXT_PLUGIN;
|
|
@@ -29375,7 +29797,7 @@ __export(init_exports, {
|
|
|
29375
29797
|
parsePlanFlag: () => parsePlanFlag,
|
|
29376
29798
|
syncCheckoutConfigs: () => syncCheckoutConfigs
|
|
29377
29799
|
});
|
|
29378
|
-
import { existsSync as
|
|
29800
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
29379
29801
|
import { join as join10 } from "node:path";
|
|
29380
29802
|
import { createInterface as createInterface2 } from "node:readline";
|
|
29381
29803
|
function loadState(ctx, root) {
|
|
@@ -29474,7 +29896,7 @@ async function syncCheckoutConfigs(client, site, sessions, root) {
|
|
|
29474
29896
|
const from = configFileOf(c.name);
|
|
29475
29897
|
if (!from) continue;
|
|
29476
29898
|
const sameFile = perFile.has(from.file);
|
|
29477
|
-
const gone = !sameFile && !
|
|
29899
|
+
const gone = !sameFile && !existsSync10(join10(root, from.file));
|
|
29478
29900
|
const current = sameFile && from.call !== void 0 && from.call <= perFile.get(from.file) && c.name === configNameOf(from.file, from.call);
|
|
29479
29901
|
if (!sameFile && !gone || current) continue;
|
|
29480
29902
|
if (await client.sdk.stripeCheckoutConfigs.del(c.id).then(() => true, () => false)) {
|
|
@@ -30093,74 +30515,6 @@ var init_logout = __esm({
|
|
|
30093
30515
|
}
|
|
30094
30516
|
});
|
|
30095
30517
|
|
|
30096
|
-
// src/commands/plan/lib.ts
|
|
30097
|
-
import { dirname as dirname6, resolve as resolve7 } from "node:path";
|
|
30098
|
-
async function resolveSite(ctx, flag, client) {
|
|
30099
|
-
const explicit = flag ?? ctx.env.CHECKBOX_SITE;
|
|
30100
|
-
if (explicit) {
|
|
30101
|
-
if (!SITE_RE.test(explicit) && !/^[0-9a-f-]{36}$/i.test(explicit)) throw new UsageError(`--site must be a site id (site_\u2026), got "${explicit}"`);
|
|
30102
|
-
return explicit;
|
|
30103
|
-
}
|
|
30104
|
-
const projects = ctx.config.load().init?.projects ?? {};
|
|
30105
|
-
for (let dir = resolve7(ctx.cwd); ; dir = dirname6(dir)) {
|
|
30106
|
-
const site = projects[dir]?.site;
|
|
30107
|
-
if (site) return site;
|
|
30108
|
-
if (dirname6(dir) === dir) break;
|
|
30109
|
-
}
|
|
30110
|
-
const list2 = await client.sdk.sites.list({ limit: 3 });
|
|
30111
|
-
const sites = list2?.data ?? [];
|
|
30112
|
-
if (sites.length === 1) return sites[0].id;
|
|
30113
|
-
throw new UsageError(
|
|
30114
|
-
sites.length ? `This account has several sites; pass --site <id> (e.g. ${sites.map((s2) => s2.id).join(", ")}) or set CHECKBOX_SITE.` : "No site yet: run `checkbox init` in your project first (it creates the site and remembers it here)."
|
|
30115
|
-
);
|
|
30116
|
-
}
|
|
30117
|
-
function nextLine(n) {
|
|
30118
|
-
const text = n.description ?? n.message ?? n.type;
|
|
30119
|
-
return `${text}${n.command ? `
|
|
30120
|
-
$ ${n.command}` : n.endpoint ? `
|
|
30121
|
-
${n.endpoint}` : ""}`;
|
|
30122
|
-
}
|
|
30123
|
-
function renderPlan(p, c, opts = {}) {
|
|
30124
|
-
const head = `${c.bold(`Compliance plan ${p.site}`)} \u2014 ${p.compliance?.label ? p.status === "compliant" ? c.green(p.compliance.label) : p.status === "action_required" ? c.red(p.compliance.label) : c.yellow(p.compliance.label) : statusColor(p.status, c)}`;
|
|
30125
|
-
const tasks = opts.all ? p.tasks : p.tasks.filter((t) => t.status !== "done" && t.status !== "wont_fix");
|
|
30126
|
-
const lines = [head, p.summary, ""];
|
|
30127
|
-
for (const t of tasks) {
|
|
30128
|
-
lines.push(`${STATUS_MARK[t.status] ?? "[ ]"} ${String(t.order).padStart(2)}. ${t.title} ${c.dim(`(${t.priority}, ${t.id})`)}`);
|
|
30129
|
-
if (t.status === "manual" && t.next_action?.type === "ask_human_to_confirm") lines.push(` ${c.yellow("waiting for the human to confirm:")} ${t.next_action.description}`);
|
|
30130
|
-
else if (t.status === "manual" && t.question) lines.push(` ${c.yellow("ask the human:")} ${t.question.text} ${c.dim(`\u2192 checkbox answer ${t.id} <${t.question.options?.join("|") ?? "yes|no"}>`)}`);
|
|
30131
|
-
else if ((t.status === "done" || t.status === "wont_fix") && t.closed_reason) lines.push(c.dim(` closed: ${t.closed_reason.replace(/_/g, " ")}`));
|
|
30132
|
-
else if (t.blocked_by.length) lines.push(c.dim(` after ${t.blocked_by.join(", ")}`));
|
|
30133
|
-
else if (t.status === "open" || t.status === "in_progress") lines.push(c.dim(` ${t.where.file_hint ?? t.where.url ?? t.where.stripe_object ?? t.where.kind} \xB7 ${t.change.type} \xB7 checkbox fix ${t.id}`));
|
|
30134
|
-
}
|
|
30135
|
-
const hidden = p.tasks.length - tasks.length;
|
|
30136
|
-
if (hidden) lines.push(c.dim(` \u2026 ${hidden} done / declined (--all to show)`));
|
|
30137
|
-
lines.push("", `${c.cyan("next")} ${nextLine(p.next_action)}`);
|
|
30138
|
-
return lines;
|
|
30139
|
-
}
|
|
30140
|
-
function statusColor(s2, c) {
|
|
30141
|
-
return s2 === "compliant" ? c.green("all observed checks passing") : s2 === "action_required" ? c.red(s2) : c.yellow(s2);
|
|
30142
|
-
}
|
|
30143
|
-
function renderVerification(v, c) {
|
|
30144
|
-
const mark = v.status === "passed" ? c.green("passed") : v.status === "failed" ? c.red("failed") : v.status === "passed_with_gaps" ? c.yellow("passed with gaps (not verified)") : v.status === "passed_with_notes" ? c.yellow("passed with notes (a check outside your control needs review)") : v.status === "not_checked" ? c.yellow("not checked yet (missing evidence, nothing to change)") : v.status === "pending_live_check" ? c.yellow("live check pending (content passes; waiting for a real send)") : c.yellow("pending");
|
|
30145
|
-
const firstCheck = !v.task && v.next_action?.type === "first_check_complete";
|
|
30146
|
-
const lines = [`${c.bold(`Verification ${v.id}`)}${v.task ? ` for ${v.task}` : ""}: ${firstCheck ? c.cyan("first check complete") : mark} (${v.method})`];
|
|
30147
|
-
if (v.precheck) lines.push(` pre-check (${v.precheck.source}): ${v.precheck.verdict} \u2014 ${v.precheck.note}`);
|
|
30148
|
-
if (v.scan) lines.push(c.dim(` confirming scan ${v.scan}`));
|
|
30149
|
-
for (const d of v.diff.slice(0, 12)) lines.push(` ${d.observed === "fail" ? c.red("\u2717") : c.yellow("?")} ${d.check_id}: expected ${d.expected}, observed ${d.observed} \u2014 ${d.detail}`);
|
|
30150
|
-
if (v.diff.length > 12) lines.push(c.dim(` \u2026 ${v.diff.length - 12} more`));
|
|
30151
|
-
lines.push(`${c.cyan("next")} ${nextLine(v.next_action)}`);
|
|
30152
|
-
return lines;
|
|
30153
|
-
}
|
|
30154
|
-
var SITE_RE, STATUS_MARK;
|
|
30155
|
-
var init_lib = __esm({
|
|
30156
|
-
"src/commands/plan/lib.ts"() {
|
|
30157
|
-
"use strict";
|
|
30158
|
-
init_args();
|
|
30159
|
-
SITE_RE = /^site_[0-9a-f]{32}$/;
|
|
30160
|
-
STATUS_MARK = { open: "[ ]", in_progress: "[~]", verifying: "[?]", done: "[x]", wont_fix: "[-]", manual: "[!]" };
|
|
30161
|
-
}
|
|
30162
|
-
});
|
|
30163
|
-
|
|
30164
30518
|
// src/commands/plan/index.ts
|
|
30165
30519
|
var plan_exports = {};
|
|
30166
30520
|
__export(plan_exports, {
|