@checkbox-site/cli 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/checkbox.js +949 -301
- package/package.json +17 -12
package/dist/checkbox.js
CHANGED
|
@@ -123,7 +123,37 @@ var init_args = __esm({
|
|
|
123
123
|
// src/config.ts
|
|
124
124
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
125
125
|
import { homedir } from "node:os";
|
|
126
|
-
import { join } from "node:path";
|
|
126
|
+
import { dirname, join, resolve } from "node:path";
|
|
127
|
+
function readEnvValue(text, name) {
|
|
128
|
+
const re = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(?:"([^"\\r\\n]*)"|'([^'\\r\\n]*)'|([^\\s#]*))[ \\t]*(?:#.*)?$`, "gm");
|
|
129
|
+
let found;
|
|
130
|
+
for (let m = re.exec(text); m; m = re.exec(text)) found = (m[1] ?? m[2] ?? m[3] ?? "").trim();
|
|
131
|
+
return found || void 0;
|
|
132
|
+
}
|
|
133
|
+
function findProjectKey(cwd, env2 = {}) {
|
|
134
|
+
const home = resolve(env2.HOME || homedir());
|
|
135
|
+
let dir = resolve(cwd);
|
|
136
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
137
|
+
for (const f of PROJECT_KEY_FILES) {
|
|
138
|
+
let text;
|
|
139
|
+
try {
|
|
140
|
+
text = readFileSync(join(dir, f), "utf8");
|
|
141
|
+
} catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const v = readEnvValue(text, "CHECKBOX_SECRET_KEY");
|
|
145
|
+
if (v && looksLikeApiKey(v)) {
|
|
146
|
+
const up = depth === 0 ? "" : "../".repeat(depth);
|
|
147
|
+
return { apiKey: v, file: `${up}${f}` };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (existsSync(join(dir, ".git")) || dir === home) break;
|
|
151
|
+
const parent = dirname(dir);
|
|
152
|
+
if (parent === dir) break;
|
|
153
|
+
dir = parent;
|
|
154
|
+
}
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
127
157
|
function configDir(env2) {
|
|
128
158
|
if (env2.CHECKBOX_CONFIG_DIR) return env2.CHECKBOX_CONFIG_DIR;
|
|
129
159
|
const base = env2.XDG_CONFIG_HOME || join(env2.HOME || homedir(), ".config");
|
|
@@ -167,27 +197,35 @@ function createConfigStore(env2) {
|
|
|
167
197
|
}
|
|
168
198
|
};
|
|
169
199
|
}
|
|
170
|
-
function resolveCredentials(env2, stored, flags) {
|
|
200
|
+
function resolveCredentials(env2, stored, flags, project) {
|
|
171
201
|
const apiBase = (flags.apiBase || env2.CHECKBOX_API_BASE || stored.api_base || DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
172
202
|
if (flags.apiKey) return { apiKey: flags.apiKey, apiBase, source: "flag" };
|
|
173
203
|
if (env2.CHECKBOX_API_KEY) return { apiKey: env2.CHECKBOX_API_KEY, apiBase, source: "env" };
|
|
174
204
|
if (env2.CHECKBOX_SECRET_KEY) return { apiKey: env2.CHECKBOX_SECRET_KEY, apiBase, source: "env" };
|
|
205
|
+
if (project) return { apiKey: project.apiKey, apiBase, source: "project", file: project.file };
|
|
175
206
|
if (stored.api_key) return { apiKey: stored.api_key, apiBase, source: "config" };
|
|
176
207
|
return void 0;
|
|
177
208
|
}
|
|
209
|
+
function describeKeySource(c, env2 = {}) {
|
|
210
|
+
if (c.source === "flag") return "--api-key";
|
|
211
|
+
if (c.source === "env") return env2.CHECKBOX_API_KEY ? "CHECKBOX_API_KEY" : "CHECKBOX_SECRET_KEY in the environment";
|
|
212
|
+
if (c.source === "project") return `this project's ${c.file ?? ".env.local"}`;
|
|
213
|
+
return "your saved login (checkbox login)";
|
|
214
|
+
}
|
|
178
215
|
function maskKey(key) {
|
|
179
|
-
const m = /^([a-z]{2}_(?:live|test)_)/.exec(key);
|
|
216
|
+
const m = /^([a-z]{2,3}_(?:live|test)_)/.exec(key);
|
|
180
217
|
const prefix = m?.[1] ?? key.slice(0, 3);
|
|
181
218
|
return `${prefix}\u2026${key.slice(-4)}`;
|
|
182
219
|
}
|
|
183
220
|
function looksLikeApiKey(key) {
|
|
184
221
|
return /^(?:ckr|ck|rk|sk)_(?:live|test)_[A-Za-z0-9_-]{8,}$/.test(key.trim());
|
|
185
222
|
}
|
|
186
|
-
var DEFAULT_API_BASE;
|
|
223
|
+
var DEFAULT_API_BASE, PROJECT_KEY_FILES;
|
|
187
224
|
var init_config = __esm({
|
|
188
225
|
"src/config.ts"() {
|
|
189
226
|
"use strict";
|
|
190
227
|
DEFAULT_API_BASE = "https://app.checkbox.site";
|
|
228
|
+
PROJECT_KEY_FILES = [".env.local", ".env", ".checkbox/keys.env"];
|
|
191
229
|
}
|
|
192
230
|
});
|
|
193
231
|
|
|
@@ -2972,7 +3010,7 @@ var init_operations = __esm({
|
|
|
2972
3010
|
/**
|
|
2973
3011
|
* Check Stripe Checkout Session params.
|
|
2974
3012
|
*
|
|
2975
|
-
*
|
|
3013
|
+
* Body: `{ params, merchant }` (or `{ params, site }`). `params` is exactly the object your code gives stripe.checkout.sessions.create; `merchant` adds merchant_name, cancel_url, and price/interval when line_items use a Price id. Checkbox models the Stripe-hosted page those params render and returns findings plus `correction`: a JSON Patch (RFC 6902) and the full corrected params (custom_text.submit with the renewal terms, consent_collection.terms_of_service = "required", custom_text.terms_of_service_acceptance as the consent label), verified by the engine. Stripe's own small grey styling keeps "clear and conspicuous" at needs_review on any Stripe-hosted page; also show the terms on your page before redirecting. Stateless and deterministic: nothing is stored, the same input on the same as_of date gives the same verdicts. Every verdict comes from the Checkbox rules engine over the legal catalog (federal ROSCA / EFTA plus the state automatic-renewal laws); the response carries rules_version, catalog_version and a disclaimer (not legal advice). Read `verdict`, then `next_action`: it tells you exactly what to change and to call the check again until verdict is pass.
|
|
2976
3014
|
*
|
|
2977
3015
|
* @see POST /v1/checks/stripe_checkout_session (operation `checkStripeCheckoutSession`, scope `checks:read`)
|
|
2978
3016
|
*/
|
|
@@ -3297,7 +3335,7 @@ var init_operations = __esm({
|
|
|
3297
3335
|
/**
|
|
3298
3336
|
* Register the Checkout Session params your code uses.
|
|
3299
3337
|
*
|
|
3300
|
-
* Registers exactly the object your code passes to stripe.checkout.sessions.create (customer, URLs and secrets are not needed and are ignored; `warnings` says what was not stored). It is judged exactly like checkStripeCheckoutSession, discounts included. A config with the same name, or for the same Price ids, replaces the older one (`replaced`). Test runs mirror it into Stripe test mode; when it matches your latest live checkout, the scanned test page counts as an observation of your live checkout. Re-register after changing your checkout code, then call createTestRun.
|
|
3338
|
+
* Registers exactly the object your code passes to stripe.checkout.sessions.create, as `params` (that object sent alone as the body is accepted too; customer, URLs and secrets are not needed and are ignored; `warnings` says what was not stored). It is judged exactly like checkStripeCheckoutSession, discounts included. A config with the same name, or for the same Price ids, replaces the older one (`replaced`). Test runs mirror it into Stripe test mode; when it matches your latest live checkout, the scanned test page counts as an observation of your live checkout. Re-register after changing your checkout code, then call createTestRun.
|
|
3301
3339
|
*
|
|
3302
3340
|
* @param site Unique identifier of the site (site_…).
|
|
3303
3341
|
* @see POST /v1/sites/{site}/stripe_checkout_configs (operation `createStripeCheckoutConfig`, scope `test_runs:write`)
|
|
@@ -3365,7 +3403,7 @@ var init_operations = __esm({
|
|
|
3365
3403
|
/**
|
|
3366
3404
|
* Start a lifecycle test run.
|
|
3367
3405
|
*
|
|
3368
|
-
* Starts a synthetic test subscription in TEST mode and returns it queued. Stripe runs mirror your checkout into a test-mode Checkout Session, subscribe a test customer (card 4242) whose email is a Checkbox inbox, wait for your acknowledgment, advance a test clock through every legal notice window before trial end and renewal, cancel online in the customer portal and wait for the confirmation; then delete the test clock. Usually takes 2
|
|
3406
|
+
* Starts a synthetic test subscription in TEST mode and returns it queued. Stripe runs mirror your checkout into a test-mode Checkout Session, subscribe a test customer (card 4242) whose email is a Checkbox inbox, wait for your acknowledgment, advance a test clock through every legal notice window before trial end and renewal, cancel online in the customer portal and wait for the confirmation; then delete the test clock. Usually takes 1-2 minutes; a real_checkout run a few minutes (longer when your own emails are slow to arrive): poll retrieveTestRun (next_action says when) or listen for test_run.completed. Each step reports observations, check results and agent-readable fixes; apply them and run again to verify.
|
|
3369
3407
|
*
|
|
3370
3408
|
* @param site Unique identifier of the site (site_…).
|
|
3371
3409
|
* @see POST /v1/sites/{site}/test_runs (operation `createTestRun`, scope `test_runs:write`, live mode needs an activated account)
|
|
@@ -3579,7 +3617,7 @@ var SDK_VERSION;
|
|
|
3579
3617
|
var init_version = __esm({
|
|
3580
3618
|
"../sdk-node/src/version.ts"() {
|
|
3581
3619
|
"use strict";
|
|
3582
|
-
SDK_VERSION = "0.2.
|
|
3620
|
+
SDK_VERSION = "0.2.4";
|
|
3583
3621
|
}
|
|
3584
3622
|
});
|
|
3585
3623
|
|
|
@@ -3768,9 +3806,9 @@ var init_client = __esm({
|
|
|
3768
3806
|
const p = globalThis.process;
|
|
3769
3807
|
return p?.env?.[name] || void 0;
|
|
3770
3808
|
};
|
|
3771
|
-
sleep = (ms, signal) => new Promise((
|
|
3809
|
+
sleep = (ms, signal) => new Promise((resolve10, reject) => {
|
|
3772
3810
|
if (signal?.aborted) return reject(signal.reason);
|
|
3773
|
-
const t = setTimeout(
|
|
3811
|
+
const t = setTimeout(resolve10, ms);
|
|
3774
3812
|
signal?.addEventListener("abort", () => (clearTimeout(t), reject(signal.reason)), { once: true });
|
|
3775
3813
|
});
|
|
3776
3814
|
Checkbox = class {
|
|
@@ -4164,7 +4202,7 @@ var init_types2 = __esm({
|
|
|
4164
4202
|
|
|
4165
4203
|
// src/run.ts
|
|
4166
4204
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
4167
|
-
import { dirname, join as join2 } from "node:path";
|
|
4205
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
4168
4206
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4169
4207
|
function registerCommands(registry) {
|
|
4170
4208
|
REGISTRY = registry;
|
|
@@ -4208,11 +4246,11 @@ async function listCommands(dir = COMMANDS_DIR) {
|
|
|
4208
4246
|
}
|
|
4209
4247
|
function readAll(stream) {
|
|
4210
4248
|
if (!stream || stream.isTTY) return Promise.resolve("");
|
|
4211
|
-
return new Promise((
|
|
4249
|
+
return new Promise((resolve10, reject) => {
|
|
4212
4250
|
let data = "";
|
|
4213
4251
|
stream.setEncoding?.("utf8");
|
|
4214
4252
|
stream.on("data", (c) => data += c.toString());
|
|
4215
|
-
stream.on("end", () =>
|
|
4253
|
+
stream.on("end", () => resolve10(data));
|
|
4216
4254
|
stream.on("error", reject);
|
|
4217
4255
|
});
|
|
4218
4256
|
}
|
|
@@ -4230,7 +4268,8 @@ async function helpText(dir) {
|
|
|
4230
4268
|
"",
|
|
4231
4269
|
"Global flags:",
|
|
4232
4270
|
" --json One JSON document on stdout (for agents and scripts)",
|
|
4233
|
-
" --api-key <key> Use this key (default: $CHECKBOX_API_KEY, then
|
|
4271
|
+
" --api-key <key> Use this key (default: $CHECKBOX_API_KEY, then CHECKBOX_SECRET_KEY from the environment or",
|
|
4272
|
+
" this project's .env.local / .env, then the key saved by `checkbox login`)",
|
|
4234
4273
|
" --api-base <url> API base URL (default: $CHECKBOX_API_BASE or https://app.checkbox.site)",
|
|
4235
4274
|
" -q, --quiet Only print errors",
|
|
4236
4275
|
" --no-color Plain output",
|
|
@@ -4293,7 +4332,18 @@ async function run(argv, io) {
|
|
|
4293
4332
|
}
|
|
4294
4333
|
const flags = { json: g.json, quiet: g.quiet, color, help: false, ...g.apiKey ? { apiKey: g.apiKey } : {}, ...g.apiBase ? { apiBase: g.apiBase } : {} };
|
|
4295
4334
|
const config = createConfigStore(io.env);
|
|
4296
|
-
|
|
4335
|
+
let projectKey = null;
|
|
4336
|
+
const project = () => projectKey === null ? projectKey = findProjectKey(io.cwd, io.env) : projectKey;
|
|
4337
|
+
const credentials = () => resolveCredentials(io.env, config.load(), flags, project());
|
|
4338
|
+
let saidWhichKey = false;
|
|
4339
|
+
const sayWhichKey = (c) => {
|
|
4340
|
+
if (saidWhichKey || !c) return;
|
|
4341
|
+
saidWhichKey = true;
|
|
4342
|
+
const stored = config.load().api_key;
|
|
4343
|
+
if (c.source !== "config" && stored && stored !== c.apiKey) {
|
|
4344
|
+
out.info(out.c.dim(`Using the key from ${describeKeySource(c, io.env)} (${maskKey(c.apiKey)}), not your saved login (${maskKey(stored)}).`));
|
|
4345
|
+
}
|
|
4346
|
+
};
|
|
4297
4347
|
const ctx = {
|
|
4298
4348
|
argv: rest,
|
|
4299
4349
|
flags,
|
|
@@ -4307,6 +4357,7 @@ async function run(argv, io) {
|
|
|
4307
4357
|
const c = credentials();
|
|
4308
4358
|
const apiKey = o.apiKey ?? c?.apiKey;
|
|
4309
4359
|
if (!apiKey) throw new NotLoggedInError();
|
|
4360
|
+
if (!o.apiKey) sayWhichKey(c);
|
|
4310
4361
|
return createApiClient({ apiKey, apiBase: o.apiBase ?? c?.apiBase ?? resolveCredentials(io.env, {}, flags)?.apiBase ?? "https://app.checkbox.site", userAgent: `checkbox-cli/${CLI_VERSION}`, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
4311
4362
|
},
|
|
4312
4363
|
readStdin: () => readAll(io.stdin),
|
|
@@ -4315,10 +4366,21 @@ async function run(argv, io) {
|
|
|
4315
4366
|
try {
|
|
4316
4367
|
return await cmd.run(ctx);
|
|
4317
4368
|
} catch (err) {
|
|
4318
|
-
return reportError(out, err);
|
|
4369
|
+
return reportError(out, err, { credentials: credentials(), project: project() ?? void 0, env: io.env });
|
|
4319
4370
|
}
|
|
4320
4371
|
}
|
|
4321
|
-
function
|
|
4372
|
+
function authNextAction(ctxInfo) {
|
|
4373
|
+
const used = ctxInfo?.credentials;
|
|
4374
|
+
const proj = ctxInfo?.project;
|
|
4375
|
+
if (proj && used && proj.apiKey !== used.apiKey) {
|
|
4376
|
+
return {
|
|
4377
|
+
type: "use_project_key",
|
|
4378
|
+
message: `The key from ${describeKeySource(used, ctxInfo?.env)} (${maskKey(used.apiKey)}) was rejected, but this project's ${proj.file} has another key (${maskKey(proj.apiKey)}). Unset ${used.source === "env" ? "that environment variable" : "the --api-key flag"} so the CLI uses it, or check that the right key is in ${proj.file}.`
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4381
|
+
return void 0;
|
|
4382
|
+
}
|
|
4383
|
+
function reportError(out, err, auth) {
|
|
4322
4384
|
if (err instanceof UsageError) {
|
|
4323
4385
|
out.error({ message: err.message, code: "usage_error", type: "usage_error" });
|
|
4324
4386
|
return EXIT.usage;
|
|
@@ -4341,7 +4403,10 @@ function reportError(out, err) {
|
|
|
4341
4403
|
...err.requestId ? { request_id: err.requestId } : {},
|
|
4342
4404
|
...err.docUrl ? { doc_url: err.docUrl } : {},
|
|
4343
4405
|
status: apiStatus(err),
|
|
4344
|
-
...apiStatus(err) === 0 ? { next_action: { type: "retry", message: "Check your network (and --api-base / CHECKBOX_API_BASE), or add --offline to run a check locally." } } : err
|
|
4406
|
+
...apiStatus(err) === 0 ? { next_action: { type: "retry", message: "Check your network (and --api-base / CHECKBOX_API_BASE), or add --offline to run a check locally." } } : isAuthError(err) && authNextAction(auth) ? (
|
|
4407
|
+
// The server's 401 next step sends agents to device approval; a key this CLI can see locally comes first.
|
|
4408
|
+
{ next_action: authNextAction(auth), ...auth?.credentials ? { key_source: auth.credentials.source, ...auth.credentials.file ? { key_file: auth.credentials.file } : {} } : {} }
|
|
4409
|
+
) : err.nextAction ? { next_action: err.nextAction } : isAuthError(err) ? { next_action: { type: "login", message: "The API key was rejected. Run `checkbox login` with a valid key, or set CHECKBOX_API_KEY." } } : {}
|
|
4345
4410
|
});
|
|
4346
4411
|
return isAuthError(err) ? EXIT.auth : apiStatus(err) === 400 ? EXIT.usage : EXIT.error;
|
|
4347
4412
|
}
|
|
@@ -4357,7 +4422,7 @@ var init_run = __esm({
|
|
|
4357
4422
|
init_http();
|
|
4358
4423
|
init_output();
|
|
4359
4424
|
init_types2();
|
|
4360
|
-
HERE =
|
|
4425
|
+
HERE = dirname2(fileURLToPath(import.meta.url));
|
|
4361
4426
|
COMMANDS_DIR = join2(HERE, "commands");
|
|
4362
4427
|
CLI_VERSION = (() => {
|
|
4363
4428
|
try {
|
|
@@ -4368,7 +4433,7 @@ var init_run = __esm({
|
|
|
4368
4433
|
})();
|
|
4369
4434
|
NotLoggedInError = class extends Error {
|
|
4370
4435
|
constructor() {
|
|
4371
|
-
super("Not logged in: no API key found (flag --api-key, env CHECKBOX_API_KEY, or `checkbox login`).");
|
|
4436
|
+
super("Not logged in: no API key found (flag --api-key, env CHECKBOX_API_KEY, CHECKBOX_SECRET_KEY in .env.local, or `checkbox login`).");
|
|
4372
4437
|
this.name = "NotLoggedInError";
|
|
4373
4438
|
}
|
|
4374
4439
|
};
|
|
@@ -4707,7 +4772,8 @@ var init_secrets = __esm({
|
|
|
4707
4772
|
{ name: "stripe_webhook_secret", re: /\bwhsec_[A-Za-z0-9+/=_-]{4,}/g, replace: keepPrefix(() => 6) },
|
|
4708
4773
|
{ name: "stripe_publishable_live", re: /\bpk_live_[A-Za-z0-9_-]{4,}/g, replace: keepPrefix(() => 8) },
|
|
4709
4774
|
// Checkbox API keys (ck_test_ / ck_live_) and scoped tokens.
|
|
4710
|
-
|
|
4775
|
+
// ckr_ (restricted) keys too: round 29 found they were left whole.
|
|
4776
|
+
{ name: "checkbox_key", re: /\bckr?_(?:live|test)_[A-Za-z0-9_-]{4,}/g, replace: keepPrefix((m) => m.indexOf("_", 4) + 1) },
|
|
4711
4777
|
// AWS access key ids (and the secret that usually follows them on the same line).
|
|
4712
4778
|
{ name: "aws_access_key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, replace: () => "AKIA[redacted]" },
|
|
4713
4779
|
{ name: "aws_secret", re: /(aws_secret_access_key|AWS_SECRET_ACCESS_KEY|secretAccessKey)(\s*[:=]\s*["']?)[A-Za-z0-9/+=]{30,}/g, replace: (m) => m.replace(/[A-Za-z0-9/+=]{30,}$/, "[redacted]") },
|
|
@@ -5126,7 +5192,7 @@ var init_california = __esm({
|
|
|
5126
5192
|
sourceUrl: URL_ARTICLE,
|
|
5127
5193
|
effectiveDate: ORIGINAL,
|
|
5128
5194
|
lastVerified: LAST_VERIFIED,
|
|
5129
|
-
summary: 'Clear and conspicuous offer terms next to the consent request; affirmative (and since July 2025, express affirmative) consent; retainable acknowledgment; online "click to cancel" for online sign-ups; a notice 3-21 days before a free trial or promotional price lasting more than 31 days ends, and 15-45 days before a term of one year or longer renews; 7-30 day fee-change notice; annual reminder; same-medium cancellation; limits on save offers; consent records kept 3 years, or 1 year after termination if longer.
|
|
5195
|
+
summary: 'Clear and conspicuous offer terms next to the consent request; affirmative (and since July 2025, express affirmative) consent; retainable acknowledgment; online "click to cancel" for online sign-ups; a notice 3-21 days before a free trial or promotional price lasting more than 31 days ends, and 15-45 days before a term of one year or longer renews; 7-30 day fee-change notice; annual reminder; same-medium cancellation; limits on save offers; consent records kept 3 years, or 1 year after termination if longer. No criminal penalties; civil remedies apply (\xA7 17604), in practice through the UCL, FAL and CLRA; unconsented goods are gifts (\xA7 17603).',
|
|
5130
5196
|
status: "in_force",
|
|
5131
5197
|
confidence: "high",
|
|
5132
5198
|
verification: "primary",
|
|
@@ -5250,14 +5316,14 @@ var init_federal = __esm({
|
|
|
5250
5316
|
effectiveDate: EFTA_EFFECTIVE,
|
|
5251
5317
|
stage: "reminder",
|
|
5252
5318
|
title: "Advance notice when a recurring debit will change in amount",
|
|
5253
|
-
summary: "When a preauthorized debit from a consumer's account will vary in amount from the previous transfer or from the authorized amount (e.g. a subscription price increase billed to a debit card or by ACH), the payee must send written notice of the amount and date of the transfer at least 10 days before the scheduled transfer date.",
|
|
5319
|
+
summary: "When a preauthorized debit from a consumer's account will vary in amount from the previous transfer or from the authorized amount (e.g. a subscription price increase billed to a debit card or by ACH), the payee must send written notice of the amount and date of the transfer at least 10 days before the scheduled transfer date (\xA7 1005.10(d)(1)). The payee must tell the consumer of the right to notice of every varying transfer, but may let the consumer choose notice only when a transfer falls outside a specified range of amounts, or differs from the most recent transfer by more than an agreed amount (\xA7 1005.10(d)(2)).",
|
|
5254
5320
|
appliesWhen: { paymentMethods: ["debit", "ach"] },
|
|
5255
5321
|
checkIds: ["reminder.price_change"],
|
|
5256
5322
|
severity: "medium",
|
|
5257
5323
|
noticeWindow: noticeWindowAtLeast(10, "charge"),
|
|
5258
5324
|
notes: notes(
|
|
5259
5325
|
windowNote(noticeWindowAtLeast(10, "charge"), "the changed debit"),
|
|
5260
|
-
"Applies only to debits from consumer asset accounts (debit, prepaid, ACH), not credit cards.
|
|
5326
|
+
"Applies only to debits from consumer asset accounts (debit, prepaid, ACH), not credit cards. A consumer who chose range-only notice under \xA7 1005.10(d)(2) gets no notice for a change inside that range; Checkbox cannot see that choice, so the checks expect the notice for every price change."
|
|
5261
5327
|
)
|
|
5262
5328
|
})
|
|
5263
5329
|
];
|
|
@@ -5406,7 +5472,7 @@ var init_exposure = __esm({
|
|
|
5406
5472
|
fees: NO_PRIVATE
|
|
5407
5473
|
},
|
|
5408
5474
|
"US-FED.1005.": {
|
|
5409
|
-
privateSuit: "Yes: the Electronic Fund Transfer Act lets consumers sue (15 U.S.C. \xA7 1693m). Applies when the subscription is paid by debit card or ACH.",
|
|
5475
|
+
privateSuit: "Yes, in the law itself: the Electronic Fund Transfer Act lets consumers sue (15 U.S.C. \xA7 1693m). Applies when the subscription is paid by debit card or ACH.",
|
|
5410
5476
|
damages: "Actual damages, plus statutory damages of $100 to $1,000 in an individual action, or in a class action up to the lesser of $500,000 or 1% of the defendant's net worth (15 U.S.C. \xA7 1693m(a)(1), (2)).",
|
|
5411
5477
|
penalties: "None in private suits: \xA7 1693m provides damages, costs and fees, not civil penalties. The CFPB enforces the Act (15 U.S.C. \xA7 1693o(a)(5)) and can seek civil money penalties for a violation of federal consumer financial law, which includes the Act (12 U.S.C. \xA7\xA7 5481(12)(C), 5565(a)(2)(H)): up to $5,000 a day, $25,000 a day if reckless and $1,000,000 a day if knowing, before inflation adjustment (12 U.S.C. \xA7 5565(c)(2)).",
|
|
5412
5478
|
fees: "Costs and reasonable attorney's fees for a successful action (15 U.S.C. \xA7 1693m(a)(3))."
|
|
@@ -5418,7 +5484,7 @@ var init_exposure = __esm({
|
|
|
5418
5484
|
fees: NO_PRIVATE
|
|
5419
5485
|
},
|
|
5420
5486
|
"US-CA": {
|
|
5421
|
-
privateSuit: "
|
|
5487
|
+
privateSuit: "Yes, through general consumer law: the ARL has no private right of its own (Mayron v. Google, 2020); plaintiffs sue under the UCL, FAL and CLRA and must show economic injury. Before suing for damages under the CLRA, the consumer must send a written notice and demand by certified or registered mail at least 30 days ahead, and no damages action lies if an appropriate correction is given or agreed within 30 days (Cal. Civ. Code \xA7 1782(a), (b)); a CLRA claim for an injunction needs no prior notice (\xA7 1782(d)).",
|
|
5422
5488
|
damages: "Goods or services provided without the required consent are an unconditional gift (Cal. Bus. & Prof. Code \xA7 17603); all civil remedies are available (\xA7 17604, with a good-faith-compliance safe harbor). Under the CLRA: actual damages (in a class action, at least $1,000 in total), an injunction, restitution of property, punitive damages and other relief the court deems proper (Cal. Civ. Code \xA7 1780(a)). Under the UCL and the FAL: an injunction and orders restoring money or property acquired by the unlawful practice (restitution) (Cal. Bus. & Prof. Code \xA7\xA7 17203, 17535); a private plaintiff must have suffered injury in fact and lost money or property (\xA7 17535, for the FAL). Neither section provides damages.",
|
|
5423
5489
|
penalties: "In public actions only (the Attorney General, district attorneys and some county counsel and city attorneys): up to $2,500 per violation under the UCL (Cal. Bus. & Prof. Code \xA7 17206(a)) and up to $2,500 per violation under the FAL (\xA7 17536(a)).",
|
|
5424
5490
|
fees: "Under the CLRA: the court shall award costs and attorney's fees to a prevailing plaintiff; a prevailing defendant may get reasonable fees only if the suit was not brought in good faith (Cal. Civ. Code \xA7 1780(e)). The UCL and FAL sections provide no attorney's fees (Cal. Bus. & Prof. Code \xA7\xA7 17203, 17535). Plaintiffs sometimes seek fees under the private attorney general statute, which lets a court award fees to a successful party in an action that enforced an important right affecting the public interest, when its conditions are met (Cal. Code Civ. Proc. \xA7 1021.5).",
|
|
@@ -5551,7 +5617,7 @@ var init_exposure = __esm({
|
|
|
5551
5617
|
fees: OPEN
|
|
5552
5618
|
},
|
|
5553
5619
|
"US-ND": {
|
|
5554
|
-
privateSuit: "Yes: a person aggrieved may sue to enjoin the violation or for restitution, or both (N.D. Cent. Code \xA7 51-37-06).",
|
|
5620
|
+
privateSuit: "Yes, in the law itself: a person aggrieved may sue to enjoin the violation or for restitution, or both (N.D. Cent. Code \xA7 51-37-06).",
|
|
5555
5621
|
damages: "Restitution (N.D. Cent. Code \xA7 51-37-06); goods or services provided under a non-compliant renewal, or after an attempt to cancel, are an unconditional gift (\xA7 51-37-04).",
|
|
5556
5622
|
penalties: "Each act is a separate violation of N.D. Cent. Code ch. 51-15, enforced by the attorney general with that chapter's remedies (\xA7 51-37-05). Unsettled: the figure is not yet confirmed from the statute text.",
|
|
5557
5623
|
fees: "Costs, expenses and reasonable attorney's fees (N.D. Cent. Code \xA7 51-37-06)."
|
|
@@ -9824,7 +9890,7 @@ var init_catalog = __esm({
|
|
|
9824
9890
|
init_states();
|
|
9825
9891
|
init_terms();
|
|
9826
9892
|
init_types3();
|
|
9827
|
-
CATALOG_VERSION = "2026.11.09.
|
|
9893
|
+
CATALOG_VERSION = "2026.11.09.3";
|
|
9828
9894
|
CATALOG = [
|
|
9829
9895
|
...FEDERAL_REQUIREMENTS,
|
|
9830
9896
|
...CALIFORNIA_REQUIREMENTS,
|
|
@@ -10645,7 +10711,7 @@ function analyzePolicyText(input) {
|
|
|
10645
10711
|
function clausesOf(clauses, code2) {
|
|
10646
10712
|
return clauses.filter((c) => c.code === code2);
|
|
10647
10713
|
}
|
|
10648
|
-
var POLICY_KINDS, POLICY_FRAGMENT, POLICY_FETCHER_USER_AGENT, SUBSCRIPTION_WORDS, NO_CANCEL, OFFLINE_ONLY, CONTACT_TO_CANCEL, ACCOUNT_NOT_PLACE, CANCEL_ONLINE_STRONG, NOTICE_PERIOD, ALL_SALES_FINAL, CANCEL_ONLINE, AUTO_RENEWAL, HOW_TO_CANCEL, NO_REFUNDS, REFUND_WINDOW, GUARANTEE, REFUND_EXCEPTION;
|
|
10714
|
+
var POLICY_KINDS, POLICY_FRAGMENT, POLICY_FETCHER_USER_AGENT, SUBSCRIPTION_WORDS, NO_CANCEL, OFFLINE_ONLY, STOP_SUBSCRIPTION, CONTACT_TO_CANCEL, ACCOUNT_NOT_PLACE, CANCEL_ONLINE_STRONG, NOTICE_PERIOD, ALL_SALES_FINAL, CANCEL_ONLINE, AUTO_RENEWAL, HOW_TO_CANCEL, NO_REFUNDS, REFUND_WINDOW, GUARANTEE, REFUND_EXCEPTION;
|
|
10649
10715
|
var init_policy_docs = __esm({
|
|
10650
10716
|
"../rules/src/policy-docs.ts"() {
|
|
10651
10717
|
"use strict";
|
|
@@ -10666,9 +10732,12 @@ var init_policy_docs = __esm({
|
|
|
10666
10732
|
/\bcancell?(?:ations?|ing)?\b[^.]{0,80}\b(?:only|solely|exclusively)\b[^.]{0,60}\b(?:phone|telephone|mail|post|in writing|in person|fax)\b/i,
|
|
10667
10733
|
/\bcancell?ation requests?\b[^.]{0,60}\b(?:by|via) (?:certified |registered )?(?:mail|post|letter|fax|phone|telephone)\b/i
|
|
10668
10734
|
];
|
|
10735
|
+
STOP_SUBSCRIPTION = "(?:stop|end|terminate|discontinue|turn off)\\s+(?:your |the |a |my |this )?(?:paid |monthly |annual |yearly )?(?:subscription|membership|plan|auto-?renewal|automatic renewal|recurring (?:billing|payments?|charges?))";
|
|
10669
10736
|
CONTACT_TO_CANCEL = [
|
|
10670
10737
|
/\bto cancel\b[^.]{0,80}\b(?:contact|email|e-mail|reach out to|message)\b/i,
|
|
10671
10738
|
/\b(?:contact|email|e-mail)\b[^.]{0,60}\bto cancel\b/i,
|
|
10739
|
+
new RegExp(`\\bto ${STOP_SUBSCRIPTION}\\b[^.]{0,80}(?:\\b(?:contact|email|e-mail|reach out to|message|write to)\\b|\\[redacted\\])`, "i"),
|
|
10740
|
+
new RegExp(`(?:\\b(?:contact|email|e-mail|write to)\\b|\\[redacted\\])[^.]{0,60}\\bto ${STOP_SUBSCRIPTION}\\b`, "i"),
|
|
10672
10741
|
// Round 2 pilot: "You may cancel by contacting support" / "cancellations are handled by emailing us".
|
|
10673
10742
|
/\bcancel\w*\b[^.]{0,60}\b(?:by|through|via)\s+(?:contacting|emailing|e-mailing|messaging|writing to|reaching out to|calling|phoning|sending (?:us )?an? (?:email|e-mail|message))\b/i,
|
|
10674
10743
|
// Normal round 2 (nextjs): the scanner redacts e-mail addresses before the rules see the text, so
|
|
@@ -11252,6 +11321,9 @@ function introAmountOf(amount, o) {
|
|
|
11252
11321
|
return void 0;
|
|
11253
11322
|
}
|
|
11254
11323
|
function unverifiedReason(ctx) {
|
|
11324
|
+
const known = ctx.referencePlans ?? [];
|
|
11325
|
+
if (known.length) return `none of your plans (${known.slice(0, 4).map((p) => p.label ?? p.name ?? p.id).join(", ")}) was matched to this checkout's price (the Price id it sells is not one Checkbox has for them)`;
|
|
11326
|
+
if ((ctx.checkoutConfigs?.length ?? 0) > 0) return "the price this checkout sells is not in your registered Checkout configs or your billing platform's prices";
|
|
11255
11327
|
const chosen = ctx.facts?.planPriceId;
|
|
11256
11328
|
if (chosen && ctx.config) return `the chosen price ${chosen} is not among the active recurring prices read from your billing platform`;
|
|
11257
11329
|
if (chosen) return `the chosen price ${chosen} could not be read (no billing platform connected)`;
|
|
@@ -11309,7 +11381,7 @@ function noticePlanOptions(ctx, priceChange = false) {
|
|
|
11309
11381
|
const newPrices = (ctx.events ?? []).filter((e) => e.type === "price_change_scheduled").map((e) => e.amount).filter((n) => typeof n === "number" && n > 0).map((n) => n / 10 ** currencyDigitsOf(currency));
|
|
11310
11382
|
if (priceChange && newPrices.length) return { plans: plans.map((p) => ({ ...p, amount: -1 })), extraAmounts: newPrices };
|
|
11311
11383
|
if (!plans.length) return { plans: [], extraAmounts: [] };
|
|
11312
|
-
return { plans, extraAmounts: newPrices };
|
|
11384
|
+
return { plans, extraAmounts: newPrices, ...ctx.events?.length ? { events: ctx.events } : {} };
|
|
11313
11385
|
}
|
|
11314
11386
|
function declaredPlansFrom(plans) {
|
|
11315
11387
|
const out = [];
|
|
@@ -11392,8 +11464,8 @@ function sameProductName(a, b) {
|
|
|
11392
11464
|
function declaredPriceIds(x) {
|
|
11393
11465
|
return [x.price_id, x.stripe_price, x.priceId, x.id].filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
11394
11466
|
}
|
|
11395
|
-
function reconcileDeclaredPlans(raw, billing, platform = "Stripe") {
|
|
11396
|
-
const prices = billing.filter((p) => p && p.amount > 0 && p.interval);
|
|
11467
|
+
function reconcileDeclaredPlans(raw, billing, platform = "Stripe", checkoutTrials) {
|
|
11468
|
+
const prices = billing.filter((p) => p && p.amount > 0 && p.interval).map((p) => checkoutTrials?.has(p.id) ? { ...p, trialDays: checkoutTrials.get(p.id) } : p);
|
|
11397
11469
|
const out = [...raw];
|
|
11398
11470
|
const conflicts = [];
|
|
11399
11471
|
if (!prices.length || !raw.length) return { plans: out, conflicts };
|
|
@@ -11551,11 +11623,11 @@ function siteOffers(input) {
|
|
|
11551
11623
|
};
|
|
11552
11624
|
for (const d of declared) {
|
|
11553
11625
|
const price3 = stripe.find((s2) => sameTerms(s2, d));
|
|
11554
|
-
const trial = d.trialDays && d.trialDays > 0 ? d.trialDays :
|
|
11626
|
+
const trial = d.trialDays && d.trialDays > 0 ? d.trialDays : soldTrial({ ...d, ...price3 ? { priceId: price3.id } : {} }) || (price3?.trialDays && price3.trialDays > 0 ? price3.trialDays : 0);
|
|
11555
11627
|
push(toOffer(d, "declared", { trialDays: trial, ...price3 ? { priceId: price3.id } : {} }));
|
|
11556
11628
|
}
|
|
11557
11629
|
const chosen = input.facts?.planPriceId ? stripe.find((p) => p.id === input.facts.planPriceId) : void 0;
|
|
11558
|
-
if (chosen) push(toOffer(chosen, "chosen", chosen
|
|
11630
|
+
if (chosen) push(toOffer(chosen, "chosen", soldTrial(chosen) > 0 ? { trialDays: soldTrial(chosen) } : {}));
|
|
11559
11631
|
if (!declared.length) {
|
|
11560
11632
|
for (const s2 of sold) {
|
|
11561
11633
|
const price3 = stripe.find((p) => sameTerms(p, s2));
|
|
@@ -11570,7 +11642,7 @@ function siteOffers(input) {
|
|
|
11570
11642
|
const anchors = stripe.filter((s2) => out.some((o) => o.priceId === s2.id));
|
|
11571
11643
|
for (const s2 of stripe) {
|
|
11572
11644
|
if (out.some((o) => o.priceId === s2.id) || !anchors.some((a) => sameProductName(a.name, s2.name))) continue;
|
|
11573
|
-
push(toOffer(s2, "stripe", s2
|
|
11645
|
+
push(toOffer(s2, "stripe", soldTrial(s2) > 0 ? { trialDays: soldTrial(s2) } : {}));
|
|
11574
11646
|
}
|
|
11575
11647
|
}
|
|
11576
11648
|
const facts = input.facts;
|
|
@@ -12937,14 +13009,14 @@ var init_visible_html = __esm({
|
|
|
12937
13009
|
|
|
12938
13010
|
// ../rules/src/checks/context.ts
|
|
12939
13011
|
function heuristicReader() {
|
|
12940
|
-
const
|
|
13012
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
12941
13013
|
return {
|
|
12942
13014
|
mode: "heuristic",
|
|
12943
13015
|
async analyze(text) {
|
|
12944
|
-
let a =
|
|
13016
|
+
let a = cache2.get(text);
|
|
12945
13017
|
if (!a) {
|
|
12946
13018
|
a = analyzeTerms(text);
|
|
12947
|
-
|
|
13019
|
+
cache2.set(text, a);
|
|
12948
13020
|
}
|
|
12949
13021
|
return a;
|
|
12950
13022
|
}
|
|
@@ -13793,7 +13865,7 @@ function remediationFor(checkId, platform) {
|
|
|
13793
13865
|
if (guide?.snippet) rem.snippet = { ...guide.snippet };
|
|
13794
13866
|
return rem;
|
|
13795
13867
|
}
|
|
13796
|
-
var DISCLOSURE_COPY_TEMPLATE, CONSENT_COPY_TEMPLATE, ACKNOWLEDGMENT_COPY_TEMPLATE, TRIAL_ACKNOWLEDGMENT_COPY_TEMPLATE, TRIAL_REMINDER_COPY_TEMPLATE, RENEWAL_REMINDER_COPY_TEMPLATE, PRICE_CHANGE_COPY_TEMPLATE, ANNUAL_TERMS_COPY_TEMPLATE, CHARGE_NOTICE_COPY_TEMPLATE, CANCELLATION_CONFIRMATION_COPY_TEMPLATE, TERMS_CANCELLATION_COPY_TEMPLATE, TERMS_REFUND_COPY_TEMPLATE, TERMS_RENEWAL_COPY_TEMPLATE, PLACEHOLDER_STEP, CUSTOM_DISCLOSURE_HTML, CUSTOM_CONSENT_HTML, STRIPE_CHECKOUT_TS, STRIPE_ANNUAL_REMINDER_TS, STRIPE_CHARGE_NOTICE_TS, STRIPE_PORTAL_TS, STRIPE_ACK_WEBHOOK_TS, STRIPE_CANCEL_CONFIRMATION_TS, SHOPIFY_LIQUID, SHOPIFY_CHECKOUT_EXTENSION_TSX, CUSTOM_ACCOUNT_CANCEL_HTML, CUSTOM_ACK_EMAIL_HTML, VERIFY, VERIFY_TERMS, VERIFY_EMAIL, CB_CHECKOUT, CB_PORTAL, CB_EMAILS, CHARGEBEE_DISCLOSURE, CHARGEBEE_CONSENT, CHARGEBEE_ACK, CHARGEBEE_PORTAL, CHARGEBEE_TRIAL_REMINDER, CHARGEBEE_RENEWAL_REMINDER, CHARGEBEE_ANNUAL_TERMS, CHARGEBEE_CHARGE_NOTICE, RC_ACCOUNT_PAGES, RC_EMAILS, RECURLY_DISCLOSURE, RECURLY_CONSENT, RECURLY_ACK, RECURLY_PORTAL, RECURLY_TRIAL_REMINDER, RECURLY_RENEWAL_REMINDER, RECURLY_ANNUAL_TERMS, RECURLY_CHARGE_NOTICE, PD_RETAIN, PADDLE_DISCLOSURE, PADDLE_CONSENT, PADDLE_ACK, PADDLE_PORTAL, PADDLE_TRIAL_REMINDER, PADDLE_RENEWAL_REMINDER, PADDLE_ANNUAL_TERMS, PADDLE_CHARGE_NOTICE, STRIPE_DISCLOSURE, SHOPIFY_DISCLOSURE, RECHARGE_DISCLOSURE, CUSTOM_DISCLOSURE, DISCLOSURE_PLATFORMS, STRIPE_ACK, SHOPIFY_ACK, RECHARGE_ACK, CUSTOM_ACK, ACK_PLATFORMS, STRIPE_PORTAL, SHOPIFY_PORTAL, RECHARGE_PORTAL, CUSTOM_PORTAL, PORTAL_PLATFORMS, STRIPE_TRIAL_REMINDER, STRIPE_RENEWAL_REMINDER, STRIPE_ANNUAL_TERMS, STRIPE_CHARGE_NOTICE, SHOPIFY_ANNUAL_TERMS, SHOPIFY_CHARGE_NOTICE, RECHARGE_ANNUAL_TERMS, RECHARGE_CHARGE_NOTICE, CUSTOM_ANNUAL_TERMS, CUSTOM_CHARGE_NOTICE, SHOPIFY_REMINDER, RECHARGE_REMINDER, CUSTOM_REMINDER, SPECS, PLATFORM_LABELS, OFFER_PERMISSION_FIX, OFFER_QUESTION_STOP_FIX, OFFER_PERMISSION_SNIPPET, CANCEL_HANDLER_REMEDIATION, OFFER_PERMISSION_REMEDIATION;
|
|
13868
|
+
var DISCLOSURE_COPY_TEMPLATE, CONSENT_COPY_TEMPLATE, ACKNOWLEDGMENT_COPY_TEMPLATE, TRIAL_ACKNOWLEDGMENT_COPY_TEMPLATE, TRIAL_REMINDER_COPY_TEMPLATE, RENEWAL_REMINDER_COPY_TEMPLATE, PRICE_CHANGE_COPY_TEMPLATE, ANNUAL_TERMS_COPY_TEMPLATE, CHARGE_NOTICE_COPY_TEMPLATE, CANCELLATION_CONFIRMATION_COPY_TEMPLATE, TERMS_CANCELLATION_COPY_TEMPLATE, TERMS_REFUND_COPY_TEMPLATE, TERMS_RENEWAL_COPY_TEMPLATE, PLACEHOLDER_STEP, CUSTOM_DISCLOSURE_HTML, CUSTOM_CONSENT_HTML, STRIPE_CHECKOUT_TS, STRIPE_ANNUAL_REMINDER_TS, STRIPE_CHARGE_NOTICE_TS, STRIPE_PORTAL_TS, STRIPE_ACK_WEBHOOK_TS, STRIPE_CANCEL_CONFIRMATION_TS, SHOPIFY_LIQUID, SHOPIFY_CHECKOUT_EXTENSION_TSX, CUSTOM_ACCOUNT_CANCEL_HTML, CUSTOM_ACK_EMAIL_HTML, VERIFY, VERIFY_TERMS, VERIFY_EMAIL, CB_CHECKOUT, CB_PORTAL, CB_EMAILS, CHARGEBEE_DISCLOSURE, CHARGEBEE_CONSENT, CHARGEBEE_ACK, CHARGEBEE_PORTAL, CHARGEBEE_TRIAL_REMINDER, CHARGEBEE_RENEWAL_REMINDER, CHARGEBEE_ANNUAL_TERMS, CHARGEBEE_CHARGE_NOTICE, RC_ACCOUNT_PAGES, RC_EMAILS, RECURLY_DISCLOSURE, RECURLY_CONSENT, RECURLY_ACK, RECURLY_PORTAL, RECURLY_TRIAL_REMINDER, RECURLY_RENEWAL_REMINDER, RECURLY_ANNUAL_TERMS, RECURLY_CHARGE_NOTICE, PD_RETAIN, PADDLE_DISCLOSURE, PADDLE_CONSENT_HTML, PADDLE_CONSENT, PADDLE_ACK, PADDLE_PORTAL, PADDLE_TRIAL_REMINDER, PADDLE_RENEWAL_REMINDER, PADDLE_ANNUAL_TERMS, PADDLE_CHARGE_NOTICE, STRIPE_DISCLOSURE, SHOPIFY_DISCLOSURE, RECHARGE_DISCLOSURE, CUSTOM_DISCLOSURE, DISCLOSURE_PLATFORMS, STRIPE_ACK, SHOPIFY_ACK, RECHARGE_ACK, CUSTOM_ACK, ACK_PLATFORMS, STRIPE_PORTAL, SHOPIFY_PORTAL, RECHARGE_PORTAL, CUSTOM_PORTAL, PORTAL_PLATFORMS, STRIPE_TRIAL_REMINDER, STRIPE_RENEWAL_REMINDER, STRIPE_ANNUAL_TERMS, STRIPE_CHARGE_NOTICE, SHOPIFY_ANNUAL_TERMS, SHOPIFY_CHARGE_NOTICE, RECHARGE_ANNUAL_TERMS, RECHARGE_CHARGE_NOTICE, CUSTOM_ANNUAL_TERMS, CUSTOM_CHARGE_NOTICE, SHOPIFY_REMINDER, RECHARGE_REMINDER, CUSTOM_REMINDER, SPECS, PLATFORM_LABELS, OFFER_PERMISSION_FIX, OFFER_QUESTION_STOP_FIX, OFFER_PERMISSION_SNIPPET, CANCEL_HANDLER_REMEDIATION, OFFER_PERMISSION_REMEDIATION;
|
|
13797
13869
|
var init_remediation = __esm({
|
|
13798
13870
|
"../rules/src/remediation.ts"() {
|
|
13799
13871
|
"use strict";
|
|
@@ -14109,13 +14181,32 @@ and click \u201CCancel subscription\u201D. You can cancel anytime online.</p>`;
|
|
|
14109
14181
|
],
|
|
14110
14182
|
snippet: { language: "html", code: CUSTOM_DISCLOSURE_HTML }
|
|
14111
14183
|
};
|
|
14184
|
+
PADDLE_CONSENT_HTML = `<!-- Replace your button that opens Paddle Checkout with this form (Paddle.js must be loaded and initialized). -->
|
|
14185
|
+
<form id="checkbox-paddle-checkout">
|
|
14186
|
+
<label style="display:flex;gap:8px;align-items:flex-start;font-size:14px;color:#1f2937;">
|
|
14187
|
+
<!-- Never add the "checked" attribute: consent must be an affirmative act -->
|
|
14188
|
+
<input type="checkbox" name="auto_renew_consent" value="yes" required>
|
|
14189
|
+
<span>I agree that my subscription will automatically renew every {interval} at {price} until I cancel.</span>
|
|
14190
|
+
</label>
|
|
14191
|
+
<button type="submit">Subscribe</button>
|
|
14192
|
+
</form>
|
|
14193
|
+
<script>
|
|
14194
|
+
document.getElementById('checkbox-paddle-checkout').addEventListener('submit', function (event) {
|
|
14195
|
+
event.preventDefault(); // the browser only gets here once the required box is ticked
|
|
14196
|
+
var label = this.querySelector('label span').textContent.trim();
|
|
14197
|
+
Paddle.Checkout.open({
|
|
14198
|
+
items: [{ priceId: 'pri_REPLACE_ME', quantity: 1 }],
|
|
14199
|
+
customData: { auto_renew_consent: { at: new Date().toISOString(), label: label } },
|
|
14200
|
+
});
|
|
14201
|
+
});
|
|
14202
|
+
</script>`;
|
|
14112
14203
|
PADDLE_CONSENT = {
|
|
14113
14204
|
steps: [
|
|
14114
14205
|
"Add a required, unchecked renewal-consent checkbox (the wording below) on your page, directly above the button that opens Paddle Checkout, and only open checkout once it is ticked.",
|
|
14115
|
-
"
|
|
14116
|
-
"
|
|
14206
|
+
'Paddle opens checkout from a button, not a form, so a "required" box alone blocks nothing: put the box and the button in one form (the snippet does) or keep the button disabled until the box is ticked.',
|
|
14207
|
+
"Save when it was ticked and the terms that were shown, so you can prove consent later: the snippet passes them in customData, which Paddle stores on the transaction. On Lemon Squeezy, pass them as checkout[custom][\u2026] on the checkout URL."
|
|
14117
14208
|
],
|
|
14118
|
-
snippet: { language: "html", code:
|
|
14209
|
+
snippet: { language: "html", code: PADDLE_CONSENT_HTML }
|
|
14119
14210
|
};
|
|
14120
14211
|
PADDLE_ACK = {
|
|
14121
14212
|
steps: [
|
|
@@ -14829,7 +14920,8 @@ function inAppPath(input) {
|
|
|
14829
14920
|
return { kind: "in_app", state: "works", detail: "a test cancellation completed online" };
|
|
14830
14921
|
}
|
|
14831
14922
|
if (hostedStep2) return void 0;
|
|
14832
|
-
const
|
|
14923
|
+
const onHosted = (flow.steps ?? []).some((s2) => s2.source !== "snippet" && isHostedCancelUrl(s2.url));
|
|
14924
|
+
const kinds = (flow.blockers ?? []).map(classifyBlocker).filter((k) => !(onHosted && k === "contact"));
|
|
14833
14925
|
if (kinds.some((k) => k === "contact" || k === "not_found" || k === "obstacle")) {
|
|
14834
14926
|
return { kind: "in_app", state: "broken", detail: `the test cancellation did not complete (${flow.blockers.join("; ")})` };
|
|
14835
14927
|
}
|
|
@@ -14906,7 +14998,7 @@ function offlineOnlyCancellation(input) {
|
|
|
14906
14998
|
const hosted = input.ctx.hostedCancel;
|
|
14907
14999
|
const helpHits = [];
|
|
14908
15000
|
for (const c of captures) {
|
|
14909
|
-
if (isFetchedDocument(c) || c.pageProblem || c.pageKind === "checkout") continue;
|
|
15001
|
+
if (isFetchedDocument(c) || c.pageProblem || c.pageKind === "checkout" || isHostedCancelUrl(c.url)) continue;
|
|
14910
15002
|
const sentence = contactOnlySentence(c.text);
|
|
14911
15003
|
if (!sentence) continue;
|
|
14912
15004
|
if (ownOnlineCancelControls(c, hosted).length) continue;
|
|
@@ -14917,7 +15009,7 @@ function offlineOnlyCancellation(input) {
|
|
|
14917
15009
|
}
|
|
14918
15010
|
return { where: `${c.pageKind === "account" ? "account page" : c.pageKind === "cancel" ? "cancel page" : "page"} ${c.url.split("#")[0]}`, quote: sentence, capture: c };
|
|
14919
15011
|
}
|
|
14920
|
-
const contactOnlyUrls = new Set(captures.filter((c) => !isFetchedDocument(c) && contactOnlySentence(c.text)).map((c) => stripUrl(c.url)));
|
|
15012
|
+
const contactOnlyUrls = new Set(captures.filter((c) => !isFetchedDocument(c) && !isHostedCancelUrl(c.url) && contactOnlySentence(c.text)).map((c) => stripUrl(c.url)));
|
|
14921
15013
|
const onlineControl = captures.some(
|
|
14922
15014
|
(c) => !isFetchedDocument(c) && ownOnlineCancelControls(c, hosted).some((el) => !contactOnlyUrls.has(stripUrl(hrefOf(el) ?? "", c.url)))
|
|
14923
15015
|
);
|
|
@@ -16291,15 +16383,34 @@ function offlineOnlyCancelIn(text, cancelMethods) {
|
|
|
16291
16383
|
const online = cancelMethods.some((m) => !CONTACT_TO_CANCEL_RE.test(m.text) && !/@|\d{3}[\s.-]\d{4}|\b(?:call|phone|chat|e-?mail|contact|mail|reply)\b/i.test(m.text));
|
|
16292
16384
|
return online ? null : offline;
|
|
16293
16385
|
}
|
|
16386
|
+
function inForceAmount(e, events, fallbackCurrency = "usd") {
|
|
16387
|
+
const own = e.noticeTerms;
|
|
16388
|
+
if (own && typeof own.amount === "number" && own.amount > 0) return own.amount / 10 ** currencyDigitsOf(typeof own.currency === "string" ? own.currency : fallbackCurrency);
|
|
16389
|
+
const ref = typeof e.subscriptionRef === "string" ? e.subscriptionRef.trim() : "";
|
|
16390
|
+
const sent2 = toDate(e.receivedAt)?.getTime();
|
|
16391
|
+
if (!ref || sent2 === void 0) return void 0;
|
|
16392
|
+
let best;
|
|
16393
|
+
let bestAt = -Infinity;
|
|
16394
|
+
for (const ev of events) {
|
|
16395
|
+
if (ev.subscriptionRef !== ref || !CHARGE_AMOUNT_EVENTS.has(ev.type) || !(typeof ev.amount === "number" && ev.amount > 0)) continue;
|
|
16396
|
+
const at = toDate(ev.occurredAt)?.getTime();
|
|
16397
|
+
if (at === void 0 || at > sent2 + 36e5 || at < bestAt) continue;
|
|
16398
|
+
best = ev;
|
|
16399
|
+
bestAt = at;
|
|
16400
|
+
}
|
|
16401
|
+
return best ? best.amount / 10 ** currencyDigitsOf(best.currency ?? fallbackCurrency) : void 0;
|
|
16402
|
+
}
|
|
16294
16403
|
function noticeContent(e, opts = {}) {
|
|
16295
16404
|
const body = emailBody(e);
|
|
16296
16405
|
const a = analyzeTerms(body);
|
|
16297
16406
|
const quotes = {};
|
|
16298
16407
|
const wrong = {};
|
|
16299
16408
|
const plans = opts.plans ?? [];
|
|
16409
|
+
const inForce = opts.events ? inForceAmount(e, opts.events, plans[0]?.currency) : void 0;
|
|
16300
16410
|
const expected = [
|
|
16301
16411
|
...plans.map((p) => p.amount / 10 ** currencyDigitsOf(p.currency)),
|
|
16302
|
-
...opts.extraAmounts ?? []
|
|
16412
|
+
...opts.extraAmounts ?? [],
|
|
16413
|
+
...inForce !== void 0 ? [inForce] : []
|
|
16303
16414
|
];
|
|
16304
16415
|
const amount = a.recurringPrices[0]?.money ?? positiveAmounts(a)[0];
|
|
16305
16416
|
const stated = positiveAmounts(a).filter((m) => m.amount !== null);
|
|
@@ -16364,7 +16475,7 @@ function contentFixFromText(checkId, text) {
|
|
|
16364
16475
|
const m = new RegExp(`^the ${noun} \\(.*?\\) .*? does not state (.+?)(?: correctly \\(| \u2014 )`, "i").exec(text.trim());
|
|
16365
16476
|
return m ? `Add ${m[1]} to the ${noun}; Checkbox's template has every required line.` : void 0;
|
|
16366
16477
|
}
|
|
16367
|
-
var ELEMENT_LABELS, CHARGE_FOLLOWS_RE, DATE_TAG_RE, TRIAL_END_WHEN_RE, PRODUCT_RE, CONTACT_RE, CANCEL_URL_RE, NOTICE_NOUN;
|
|
16478
|
+
var ELEMENT_LABELS, CHARGE_FOLLOWS_RE, DATE_TAG_RE, TRIAL_END_WHEN_RE, PRODUCT_RE, CONTACT_RE, CANCEL_URL_RE, CHARGE_AMOUNT_EVENTS, NOTICE_NOUN;
|
|
16368
16479
|
var init_notice_content = __esm({
|
|
16369
16480
|
"../rules/src/checks/notice-content.ts"() {
|
|
16370
16481
|
"use strict";
|
|
@@ -16390,6 +16501,7 @@ var init_notice_content = __esm({
|
|
|
16390
16501
|
PRODUCT_RE = /\b(?:[Yy]our|[Tt]he|[Tt]his)\s+(?:[A-Z0-9][\w&'’.+-]*\s+){1,5}(?:[Ss]ubscription|[Mm]embership|[Pp]lan|[Bb]ox|[Ss]ervice|[Cc]lub|[Pp]ackage)\b|\b(?:[Pp]lan|[Pp]roduct|[Ss]ubscription|[Mm]embership)\s*:\s*\S[^\n]{1,60}/;
|
|
16391
16502
|
CONTACT_RE = /[\w.+-]+@[\w-]+(?:\.[\w-]+)+|(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b|\bcontact\s+(?:us|support|our\s+(?:support|customer|help))\b[^\n]{0,80}(?:https?:\/\/\S+|@)|\b(?:support|help)\s+(?:center|centre)\s*:?\s*https?:\/\/\S+/i;
|
|
16392
16503
|
CANCEL_URL_RE = /https?:\/\/\S*(?:cancel|billing\.stripe\.com|customer[-_]?portal|\/portal|manage|subscription|account)\S*/i;
|
|
16504
|
+
CHARGE_AMOUNT_EVENTS = /* @__PURE__ */ new Set(["subscription_created", "subscription_renewed", "upcoming_renewal", "trial_will_end"]);
|
|
16393
16505
|
NOTICE_NOUN = {
|
|
16394
16506
|
"reminder.trial_ending": "trial-ending reminder",
|
|
16395
16507
|
"reminder.annual_renewal": "renewal reminder",
|
|
@@ -16459,6 +16571,64 @@ var init_email_plan = __esm({
|
|
|
16459
16571
|
}
|
|
16460
16572
|
});
|
|
16461
16573
|
|
|
16574
|
+
// ../rules/src/checks/forwarded.ts
|
|
16575
|
+
function headerAddress(block, name) {
|
|
16576
|
+
const m = new RegExp(String.raw`^\s*[*>]*\s*${name}\s*:\s*\*?\s*(.+)$`, "im").exec(block);
|
|
16577
|
+
if (!m) return void 0;
|
|
16578
|
+
const a = ADDRESS_RE.exec(m[1]);
|
|
16579
|
+
return a ? a[0].toLowerCase().replace(/[.>]+$/, "") : void 0;
|
|
16580
|
+
}
|
|
16581
|
+
function forwardedInfo(e) {
|
|
16582
|
+
const hit = cache.get(e);
|
|
16583
|
+
if (hit) return hit;
|
|
16584
|
+
const text = e.text ?? "";
|
|
16585
|
+
const marker = FWD_MARKER_RE.exec(text);
|
|
16586
|
+
const forwarded = !!marker || FWD_SUBJECT_RE.test(e.subject ?? "");
|
|
16587
|
+
const info = { forwarded };
|
|
16588
|
+
if (forwarded) {
|
|
16589
|
+
const start = marker ? marker.index : 0;
|
|
16590
|
+
const block = text.slice(start, start + 1200);
|
|
16591
|
+
const from = headerAddress(block, "From");
|
|
16592
|
+
const to = headerAddress(block, "To");
|
|
16593
|
+
if (from) info.originalFrom = from;
|
|
16594
|
+
if (to) info.originalTo = to;
|
|
16595
|
+
}
|
|
16596
|
+
cache.set(e, info);
|
|
16597
|
+
return info;
|
|
16598
|
+
}
|
|
16599
|
+
function domainOf(address) {
|
|
16600
|
+
const m = /@([^\s<>"]+)/.exec(address ?? "");
|
|
16601
|
+
return m ? m[1].toLowerCase().replace(/[>.]+$/, "") : "";
|
|
16602
|
+
}
|
|
16603
|
+
function isMorDomain(domain) {
|
|
16604
|
+
return !!domain && MERCHANT_OF_RECORD_DOMAINS.some((d) => domain === d || domain.endsWith(`.${d}`));
|
|
16605
|
+
}
|
|
16606
|
+
function isMerchantOfRecordEmail(e) {
|
|
16607
|
+
return isMorDomain(domainOf(e.from)) || isMorDomain(domainOf(forwardedInfo(e).originalFrom));
|
|
16608
|
+
}
|
|
16609
|
+
function inboundDomain() {
|
|
16610
|
+
const env2 = typeof process !== "undefined" ? process.env.INBOUND_EMAIL_DOMAIN : void 0;
|
|
16611
|
+
return (env2 ?? "").trim().replace(/^@/, "").replace(/\.$/, "").toLowerCase() || "inbound.subcomply.localhost";
|
|
16612
|
+
}
|
|
16613
|
+
function sentStraightToCheckbox(e) {
|
|
16614
|
+
if (forwardedInfo(e).originalTo) return false;
|
|
16615
|
+
if (e.scanId || e.relatedSignupAt || e.subscriptionRef || e.testRun) return false;
|
|
16616
|
+
const addresses = (e.to ?? "").match(/[^\s<>,"]+@[^\s<>,"]+/g) ?? [];
|
|
16617
|
+
const domain = inboundDomain();
|
|
16618
|
+
return addresses.length > 0 && addresses.every((a) => domainOf(a) === domain);
|
|
16619
|
+
}
|
|
16620
|
+
var FWD_SUBJECT_RE, FWD_MARKER_RE, ADDRESS_RE, cache, MERCHANT_OF_RECORD_DOMAINS;
|
|
16621
|
+
var init_forwarded = __esm({
|
|
16622
|
+
"../rules/src/checks/forwarded.ts"() {
|
|
16623
|
+
"use strict";
|
|
16624
|
+
FWD_SUBJECT_RE = /^\s*(?:(?:re|aw|sv)\s*:\s*)*(?:fwd?|fw|wg|tr)\s*:/i;
|
|
16625
|
+
FWD_MARKER_RE = /(?:-{2,}\s*Forwarded message\s*-{2,}|Begin forwarded message\s*:|-{2,}\s*Original Message\s*-{2,}|-{2,}\s*Forwarded Message\s*-{2,}|^\s*_{8,}\s*$\s*^\s*From\s*:)/im;
|
|
16626
|
+
ADDRESS_RE = /[^\s<>,"':;()[\]]+@[^\s<>,"':;()[\]]+\.[a-z]{2,}/i;
|
|
16627
|
+
cache = /* @__PURE__ */ new WeakMap();
|
|
16628
|
+
MERCHANT_OF_RECORD_DOMAINS = ["paddle.com", "paddle.net", "lemonsqueezy.com", "fastspring.com"];
|
|
16629
|
+
}
|
|
16630
|
+
});
|
|
16631
|
+
|
|
16462
16632
|
// ../rules/src/checks/confirmation.ts
|
|
16463
16633
|
function isPostSignupEmail(e) {
|
|
16464
16634
|
if (e.kind !== "other" || !e.scanId || !e.relatedSignupAt) return false;
|
|
@@ -16471,10 +16641,12 @@ function findAcknowledgments(emails) {
|
|
|
16471
16641
|
if (cached) return cached;
|
|
16472
16642
|
const acks = [];
|
|
16473
16643
|
const receipts = [];
|
|
16474
|
-
const
|
|
16644
|
+
const explicit = emails.filter((e) => e.kind === "acknowledgment");
|
|
16645
|
+
const acknowledged = new Set(explicit.map((e) => recipientOf(e)).filter((r) => r !== ""));
|
|
16475
16646
|
for (const e of emails) {
|
|
16476
16647
|
if (e.kind === "acknowledgment") acks.push(e);
|
|
16477
16648
|
else if (acknowledged.has(recipientOf(e))) continue;
|
|
16649
|
+
else if (explicit.length && e.kind === "receipt" && (forwardedInfo(e).forwarded || isMerchantOfRecordEmail(e) || sentStraightToCheckbox(e))) continue;
|
|
16478
16650
|
else if (e.kind === "receipt" || isPostSignupEmail(e)) {
|
|
16479
16651
|
(looksLikeSubscriptionTerms(analyzeTerms(emailBody(e))) ? acks : receipts).push(e);
|
|
16480
16652
|
}
|
|
@@ -16484,6 +16656,8 @@ function findAcknowledgments(emails) {
|
|
|
16484
16656
|
return set;
|
|
16485
16657
|
}
|
|
16486
16658
|
function recipientOf(e) {
|
|
16659
|
+
const original = forwardedInfo(e).originalTo;
|
|
16660
|
+
if (original) return original;
|
|
16487
16661
|
const m = /[^\s<>,"]+@[^\s<>,"]+/.exec(e.to ?? "");
|
|
16488
16662
|
return m ? m[0].toLowerCase() : "";
|
|
16489
16663
|
}
|
|
@@ -16681,6 +16855,17 @@ function cancelBeforeTrialMatch(text) {
|
|
|
16681
16855
|
if (byDate && TRIAL_ENDS_DATE_RE.test(text)) return byDate[0];
|
|
16682
16856
|
return null;
|
|
16683
16857
|
}
|
|
16858
|
+
function statedTrialMismatch(a, p, offers) {
|
|
16859
|
+
const stated = a.trialLengths.filter((t) => typeof t.days === "number" && t.days > 0);
|
|
16860
|
+
if (!stated.length) return void 0;
|
|
16861
|
+
const own = p?.offer && p.offer.trialDays > 0 ? [p.offer] : p?.offer ? [] : offers.filter((o) => o.trialDays > 0);
|
|
16862
|
+
if (!own.length) return void 0;
|
|
16863
|
+
const fits = (days, trial) => days === trial || days % 30 === 0 && Math.abs(days - trial) <= Math.max(1, days / 30) * 2;
|
|
16864
|
+
if (stated.some((t) => own.some((o) => fits(t.days, o.trialDays)))) return void 0;
|
|
16865
|
+
const lengths = [...new Set(own.map((o) => o.trialDays))].sort((x, y) => x - y);
|
|
16866
|
+
const plan2 = p?.offer ? `${offerLabel(p.offer)} has a ${p.offer.trialDays}-day free trial` : `the plan${lengths.length > 1 ? "s give" : " gives"} a ${lengths.join("- or ")}-day free trial`;
|
|
16867
|
+
return { stated: `\u201C${stated[0].text}\u201D`, plan: plan2 };
|
|
16868
|
+
}
|
|
16684
16869
|
var ACK_MAX_DELAY_HOURS, NO_ACK_EMAIL_MESSAGE, ackCache, SIGNUP_LINK_TOLERANCE_MS, sent, terms, cancellationInstructions, CANCELLING_RE, CANCEL_BEFORE_TRIAL_RE, MONTH, DATE, CANCEL_BEFORE_NO_CHARGE_RE, CANCEL_BEFORE_DATE_RE, TRIAL_ENDS_DATE_RE, trialTerms, CONFIRMATION_CHECKS;
|
|
16685
16870
|
var init_confirmation = __esm({
|
|
16686
16871
|
"../rules/src/checks/confirmation.ts"() {
|
|
@@ -16693,6 +16878,7 @@ var init_confirmation = __esm({
|
|
|
16693
16878
|
init_intro_terms();
|
|
16694
16879
|
init_offer();
|
|
16695
16880
|
init_email_plan();
|
|
16881
|
+
init_forwarded();
|
|
16696
16882
|
ACK_MAX_DELAY_HOURS = 24;
|
|
16697
16883
|
NO_ACK_EMAIL_MESSAGE = "No acknowledgment email captured yet \u2014 BCC your sign-up confirmation email to your Checkbox inbound address (see Install), or add a test purchase to your scan settings so our shopper inbox receives it.";
|
|
16698
16884
|
ackCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -16989,6 +17175,15 @@ ${v.pageText}`);
|
|
|
16989
17175
|
);
|
|
16990
17176
|
const units = trialDocs.map((doc) => {
|
|
16991
17177
|
const a = doc.analysis;
|
|
17178
|
+
const wrongTrial = statedTrialMismatch(a, ackPlan(doc.email, input, offers), offers);
|
|
17179
|
+
if (wrongTrial) {
|
|
17180
|
+
return docUnit(doc, {
|
|
17181
|
+
status: "fail",
|
|
17182
|
+
detail: `says ${wrongTrial.stated}, but ${wrongTrial.plan}: state the trial the subscriber really gets`,
|
|
17183
|
+
confidence: 0.8,
|
|
17184
|
+
meta: { trialLengthMismatch: true }
|
|
17185
|
+
});
|
|
17186
|
+
}
|
|
16992
17187
|
const method = a.cancelMethod[0];
|
|
16993
17188
|
const timing = a.trialLengths[0] ?? a.postTrialCharge[0];
|
|
16994
17189
|
const beforeMatch = cancelBeforeTrialMatch(a.text);
|
|
@@ -18309,6 +18504,17 @@ function requiredFor(reqs) {
|
|
|
18309
18504
|
if (!out.size) for (const el of CHARGE_NOTICE_REQUIRED) out.add(el);
|
|
18310
18505
|
return [...out];
|
|
18311
18506
|
}
|
|
18507
|
+
function chargeGapPhrase(content, gaps, expected) {
|
|
18508
|
+
const wrong = gaps.filter((g) => content.wrong?.[g]);
|
|
18509
|
+
const absent = gaps.filter((g) => !content.wrong?.[g]);
|
|
18510
|
+
const parts = [];
|
|
18511
|
+
if (absent.length) parts.push(`do not state ${describeMissing(absent)}`);
|
|
18512
|
+
for (const g of wrong) {
|
|
18513
|
+
const said = content.wrong[g];
|
|
18514
|
+
parts.push(g === "amount" ? `states ${ELEMENT_LABELS[g]} as \u201C${said}\u201D${expected ? `, but ${expected}` : ""}` : `states ${ELEMENT_LABELS[g]} as \u201C${said}\u201D, which does not count`);
|
|
18515
|
+
}
|
|
18516
|
+
return parts.join("; ");
|
|
18517
|
+
}
|
|
18312
18518
|
async function runChargeNotice(input) {
|
|
18313
18519
|
const id = "reminder.charge_notice";
|
|
18314
18520
|
const configRefs = input.data.configRef ? [input.data.configRef] : [];
|
|
@@ -18336,6 +18542,12 @@ async function runChargeNotice(input) {
|
|
|
18336
18542
|
}
|
|
18337
18543
|
return c;
|
|
18338
18544
|
};
|
|
18545
|
+
const chargeExpected = (e) => {
|
|
18546
|
+
const own = inForceAmount(e, input.ctx.events ?? [], chargePlanOpts.plans[0]?.currency);
|
|
18547
|
+
const cur = (chargePlanOpts.plans[0]?.currency ?? "usd").toUpperCase();
|
|
18548
|
+
if (own !== void 0) return `this subscription\u2019s charge is ${own.toFixed(2)} ${cur}`;
|
|
18549
|
+
return chargePlanOpts.plans.length ? `the plan is ${chargePlanOpts.plans.map((p) => `${planAmount(p).toFixed(2)} ${p.currency.toUpperCase()} ${planEvery(p)}`).join(", ")}` : "";
|
|
18550
|
+
};
|
|
18339
18551
|
const counted = judged.filter((j) => j.state === "matched" || j.state === "partial" || j.state === "outside_window" || j.state === "missing");
|
|
18340
18552
|
const matched = counted.filter((j) => j.state === "matched");
|
|
18341
18553
|
const partial = counted.filter((j) => j.state === "partial");
|
|
@@ -18367,10 +18579,12 @@ async function runChargeNotice(input) {
|
|
|
18367
18579
|
return makeResult(
|
|
18368
18580
|
id,
|
|
18369
18581
|
"fail",
|
|
18370
|
-
`${incompleteMatched.length} of ${matched.length} charge notice(s)
|
|
18582
|
+
`${incompleteMatched.length} of ${matched.length} charge notice(s) ${chargeGapPhrase(content(x.j.email), x.missing, chargeExpected(x.j.email))} (e.g. ${emailLabel(x.j.email)})`,
|
|
18371
18583
|
[...refs2(incompleteMatched.map((i) => i.j)), ...refs2(missing), ...refs2(outside)],
|
|
18372
18584
|
0.75,
|
|
18373
|
-
|
|
18585
|
+
// Round 29 (pilot 28 item 11): when every failing notice is Checkbox Notices' own send, the plan fixes Checkbox's
|
|
18586
|
+
// template and settings; it never asks the merchant's agent to change an email it does not send.
|
|
18587
|
+
incompleteMatched.every((x2) => isCheckboxSentEmail(x2.j.email)) && !missing.length && !outside.length ? { ...data, contentByCheckbox: true } : data
|
|
18374
18588
|
);
|
|
18375
18589
|
}
|
|
18376
18590
|
if (outside.length) {
|
|
@@ -20663,9 +20877,9 @@ function limiter(max) {
|
|
|
20663
20877
|
run2();
|
|
20664
20878
|
}
|
|
20665
20879
|
};
|
|
20666
|
-
return async (fn) => new Promise((
|
|
20880
|
+
return async (fn) => new Promise((resolve10, reject) => {
|
|
20667
20881
|
queue.push(() => {
|
|
20668
|
-
fn().then(
|
|
20882
|
+
fn().then(resolve10, reject).finally(() => {
|
|
20669
20883
|
active--;
|
|
20670
20884
|
next();
|
|
20671
20885
|
});
|
|
@@ -22031,7 +22245,7 @@ function classifyRaw(input) {
|
|
|
22031
22245
|
}
|
|
22032
22246
|
}
|
|
22033
22247
|
function fixFromFail(c) {
|
|
22034
|
-
return withPlanTerms(contentFixLine(String(c.checkId), c.data) ?? contentFixFromText(String(c.checkId), c.detail) ?? introFixFromText(c) ?? planFrequencyFix(c) ?? FAIL_FIX[String(c.checkId)] ?? "Apply the change in the task for this check.", c.data);
|
|
22248
|
+
return withPlanTerms(contentFixLine(String(c.checkId), c.data) ?? contentFixFromText(String(c.checkId), c.detail) ?? introFixFromText(c) ?? trialLengthFixFromText(c) ?? planFrequencyFix(c) ?? FAIL_FIX[String(c.checkId)] ?? "Apply the change in the task for this check.", c.data);
|
|
22035
22249
|
}
|
|
22036
22250
|
function withPlanTerms(fix, data) {
|
|
22037
22251
|
if (!/<(?:price|interval)>/.test(fix)) return fix;
|
|
@@ -22051,6 +22265,11 @@ function introFixFromText(c) {
|
|
|
22051
22265
|
const m = /introductory price of (\S+?)(?: \(\d+% off\))? for (the first [^—,]+?)\s*(?:,|—)/i.exec(c.detail);
|
|
22052
22266
|
return m ? `State the introductory price (${m[1]} for ${m[2]}) and when it ends, then the regular price from the next charge, in the renewal line.` : "State the introductory price and how long it lasts, then the regular price from the next charge, in the renewal line.";
|
|
22053
22267
|
}
|
|
22268
|
+
function trialLengthFixFromText(c) {
|
|
22269
|
+
if (c.checkId !== "confirmation.email.trial_terms") return void 0;
|
|
22270
|
+
const m = /says “([^”]+)”, but .*?\b(?:has|gives?) a ([\d -]+?or [\d]+|\d+)-day free trial/.exec(c.detail);
|
|
22271
|
+
return m ? `The acknowledgment says \u201C${m[1]}\u201D, but the plan has a ${m[2]}-day free trial: state the trial the subscriber really gets, and how to cancel before it ends.` : void 0;
|
|
22272
|
+
}
|
|
22054
22273
|
function planFrequencyFix(c) {
|
|
22055
22274
|
if (c.checkId !== "signup.disclosure.frequency") return void 0;
|
|
22056
22275
|
const every = /\bplans? (?:is|are) billed (every [a-z0-9 ]+?)(?= \/ | —|[.;,]|$)/i.exec(c.detail)?.[1];
|
|
@@ -22136,7 +22355,7 @@ function classifyFindings(findings) {
|
|
|
22136
22355
|
info: classify({ checkId: f.checkId, status: f.status, detail: f.detail, missing: f.evidence?.missing, data: f.data, origin: "evaluation", siblings })
|
|
22137
22356
|
}));
|
|
22138
22357
|
}
|
|
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;
|
|
22358
|
+
var STATUS_LABELS, CHECK_TIERS, LIVE_CHECKS, has, isStripeInput, HOSTED_BEFORE_LINE_NOTE, HOSTED_CHECKOUT_NOTE, family, isEmailCheck, ask, PLAN_FACTS_FIX, STRIPE_CANCEL_UNREAD_FIX, BILLING_CONTRADICTS_FIX, VERIFIED_FROM_STRIPE_NOTE, VERIFIED_FROM_HOSTED_NOTE, HOSTED_UNLINKED_FIX, NOT_CHECKED_CANCEL_FIX, EMAIL_EXISTS, RULES, STATUS_RULES, NY_EMAIL_ONLY_LABEL, NY_EMAIL_ONLY_NOTE, STRIPE_HOSTED_UNITS, PLATFORM_NAMES2, PORTAL_REQUIRED_FIX, PORTAL_IN_APP_BROKEN_FIX, PORTAL_OFFLINE_FIX, PAYMENT_LINK_PORTAL_FIX, PAYMENT_LINK_OFFLINE_FIX, RENEWAL_LINE_EXAMPLE, FAIL_FIX, CHECKS_PASS_UNREAD_LABEL, STATUS_RANK;
|
|
22140
22359
|
var init_status = __esm({
|
|
22141
22360
|
"../rules/src/status.ts"() {
|
|
22142
22361
|
"use strict";
|
|
@@ -22332,7 +22551,7 @@ var init_status = __esm({
|
|
|
22332
22551
|
},
|
|
22333
22552
|
{ code: "receipt_without_terms", category: "content", when: has(/only a receipt without subscription terms/), build: () => ({ status: "needs_fix", fix: "Send an acknowledgment that restates the renewal terms (price, frequency, renews until cancelled) and how to cancel; a plain receipt is not enough." }) },
|
|
22334
22553
|
// Audit P2: Stripe wording (custom_text) only for Stripe inputs; any other checkout gets the same line, platform-neutral.
|
|
22335
|
-
{ code: "processor_default_line", category: "content", when: has(/payment processor.s default line|authorization line is not the renewal terms|no automatic-renewal statement was found/), build: (c) => ({ status: "needs_fix", fix: withPlanTerms(`Add your own renewal line next to the pay button${isStripeInput(c) ? " (Stripe custom_text.submit)" : ""}: "
|
|
22554
|
+
{ code: "processor_default_line", category: "content", when: has(/payment processor.s default line|authorization line is not the renewal terms|no automatic-renewal statement was found/), build: (c) => ({ status: "needs_fix", fix: withPlanTerms(`Add your own renewal line next to the pay button${isStripeInput(c) ? " (Stripe custom_text.submit)" : ""}: "${RENEWAL_LINE_EXAMPLE} Cancel anytime with your cancel link."`, c.data) }) },
|
|
22336
22555
|
{ code: "renewal_without_until_cancel", category: "content", when: has(/but not that it continues until the customer cancels/), build: () => ({ status: "needs_fix", fix: 'Add "until you cancel" to the renewal line.' }) },
|
|
22337
22556
|
{ code: "terms_hidden", category: "content", when: has(/collapsed <details>|\(nearly\) the colour of its background|nearly the colour of the background|hidden by a stylesheet|sr-only/), build: () => ({ status: "needs_fix", fix: "Show the renewal line as plain visible text next to the button (not collapsed, not faded into the background)." }) },
|
|
22338
22557
|
// Pilot round 11 (change-guard): an intro / discounted price the Stripe session does not apply.
|
|
@@ -22361,7 +22580,7 @@ var init_status = __esm({
|
|
|
22361
22580
|
)
|
|
22362
22581
|
})
|
|
22363
22582
|
},
|
|
22364
|
-
{ code: "consent_implied", category: "content", when: has(/consent is implied by clicking/), build: () => ({ status: "needs_fix", fix: '
|
|
22583
|
+
{ code: "consent_implied", category: "content", when: has(/consent is implied by clicking/), build: () => ({ status: "needs_fix", fix: 'Ask for a separate yes to the renewal: usually one unchecked box, "I agree to the automatic renewal terms above".' }) },
|
|
22365
22584
|
{ code: "consent_general_terms", category: "content", when: has(/covers general terms, not the/), build: () => ({ status: "needs_fix", fix: 'Make the checkbox about the renewal: "I agree to the automatic renewal terms above."' }) },
|
|
22366
22585
|
// Round 23 (pilot 22 nontech-founder done blocker): "does not appear to be required" means the capture could not see
|
|
22367
22586
|
// whether the box is enforced (a script may block the purchase, as Stripe's own page does). A person who can try it
|
|
@@ -22795,8 +23014,9 @@ var init_status = __esm({
|
|
|
22795
23014
|
PORTAL_OFFLINE_FIX = 'Turn on "Cancel subscriptions" in the Stripe customer portal and link it from your account page, then replace the "email / call us to cancel" line with "Cancel anytime online in your account".';
|
|
22796
23015
|
PAYMENT_LINK_PORTAL_FIX = `Turn on "Cancel subscriptions" in the Stripe customer portal (Stripe Dashboard \u2192 Settings \u2192 Billing \u2192 Customer portal), copy its billing.stripe.com login link and add it to your site's footer as a "Cancel subscription" link.`;
|
|
22797
23016
|
PAYMENT_LINK_OFFLINE_FIX = `${PAYMENT_LINK_PORTAL_FIX} Then replace the "email / call us to cancel" line with "Cancel anytime online" and that link.`;
|
|
23017
|
+
RENEWAL_LINE_EXAMPLE = "<price> every <interval> (plus applicable taxes). Renews automatically until you cancel.";
|
|
22798
23018
|
FAIL_FIX = {
|
|
22799
|
-
"signup.disclosure.autorenew_language":
|
|
23019
|
+
"signup.disclosure.autorenew_language": `Add one line under the price, before the card and email fields: "${RENEWAL_LINE_EXAMPLE}"`,
|
|
22800
23020
|
"signup.disclosure.price": "State the recurring price in the renewal line (the amount actually charged each time).",
|
|
22801
23021
|
"signup.disclosure.frequency": `State how often it is charged, in the plan's own interval ("every month", "every year").`,
|
|
22802
23022
|
"signup.disclosure.cancellation_policy": 'Add "Cancel anytime online" to the renewal line, with your cancel link.',
|
|
@@ -22804,7 +23024,7 @@ var init_status = __esm({
|
|
|
22804
23024
|
"signup.disclosure.term_length": 'Say how often it renews ("renews every month until you cancel").',
|
|
22805
23025
|
"signup.disclosure.proximity": "Put the renewal line under the price, before the card and email fields and the pay button.",
|
|
22806
23026
|
"signup.disclosure.conspicuous": "Show the renewal line at body size, semibold, in your normal text colour.",
|
|
22807
|
-
"signup.consent.affirmative": '
|
|
23027
|
+
"signup.consent.affirmative": 'Ask for a separate yes to the renewal: usually one unchecked box, "I agree to the automatic renewal terms above".',
|
|
22808
23028
|
"signup.consent.not_prechecked": "Leave the consent checkbox unchecked by default.",
|
|
22809
23029
|
"confirmation.email.sent": "Send an acknowledgment email right after sign-up (Checkbox has a template).",
|
|
22810
23030
|
"confirmation.email.terms": "Restate the renewal terms (price, frequency, until cancelled) in the acknowledgment.",
|
|
@@ -25061,6 +25281,24 @@ var init_stripe2 = __esm({
|
|
|
25061
25281
|
}
|
|
25062
25282
|
});
|
|
25063
25283
|
|
|
25284
|
+
// ../rules/src/preflight/stripe-verdict.ts
|
|
25285
|
+
function stripeParamsVerdict(r) {
|
|
25286
|
+
if (r.verdict === "not_applicable") return { verdict: "not_applicable", open: [] };
|
|
25287
|
+
const open2 = r.checks.filter((c) => c.status === "fail" || c.status === "needs_review" && !(c.compliance_status && SATISFIED_COMPLIANCE.has(c.compliance_status))).map((c) => ({ check_id: c.check_id, status: c.status, title: c.title ?? c.check_id, detail: c.explanation ?? "" })).sort((a, b) => (a.status === "fail" ? 0 : 1) - (b.status === "fail" ? 0 : 1));
|
|
25288
|
+
if (r.verdict === "fail" || r.compliance_status === "needs_fix" || open2.some((c) => c.status === "fail")) return { verdict: "fail", open: open2 };
|
|
25289
|
+
const coupon = (r.warnings ?? []).find((w) => w.code === "coupon_unverified");
|
|
25290
|
+
if (open2.length) return { verdict: "needs_review", open: open2, ...coupon ? { reason: coupon.message } : {} };
|
|
25291
|
+
if (coupon) return { verdict: "needs_review", open: [], reason: coupon.message };
|
|
25292
|
+
return { verdict: "pass", open: [] };
|
|
25293
|
+
}
|
|
25294
|
+
var SATISFIED_COMPLIANCE;
|
|
25295
|
+
var init_stripe_verdict = __esm({
|
|
25296
|
+
"../rules/src/preflight/stripe-verdict.ts"() {
|
|
25297
|
+
"use strict";
|
|
25298
|
+
SATISFIED_COMPLIANCE = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
25299
|
+
}
|
|
25300
|
+
});
|
|
25301
|
+
|
|
25064
25302
|
// ../rules/src/preflight/email.ts
|
|
25065
25303
|
function linkify(escaped) {
|
|
25066
25304
|
return escaped.replace(/https?:\/\/[^\s<]+/g, (raw) => {
|
|
@@ -25244,6 +25482,10 @@ function contextFor(kind, occ, plan2, jurisdictions, asOf, email) {
|
|
|
25244
25482
|
function longDate2(d) {
|
|
25245
25483
|
return d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
25246
25484
|
}
|
|
25485
|
+
function supportContactLine(contact, opts = {}) {
|
|
25486
|
+
if (contact || opts.required) return `Questions? Reply to this email or contact us at ${contact || SUPPORT_CONTACT_PLACEHOLDER}.`;
|
|
25487
|
+
return "Questions? Reply to this email.";
|
|
25488
|
+
}
|
|
25247
25489
|
function emailTemplate(kind, plan2, opts = {}) {
|
|
25248
25490
|
const brand = plan2.merchantName ?? "";
|
|
25249
25491
|
const sub = `${brand ? `${brand} ` : ""}subscription`;
|
|
@@ -25251,7 +25493,7 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25251
25493
|
const every = intervalPhrase(plan2.interval, plan2.intervalCount);
|
|
25252
25494
|
const cancel = plan2.cancelUrl ?? CANCEL_URL_PLACEHOLDER;
|
|
25253
25495
|
const effect = plan2.cancelTiming === "immediately" ? "cancellation takes effect immediately" : "cancellation takes effect at the end of your current billing period";
|
|
25254
|
-
const contact = opts.supportContact
|
|
25496
|
+
const contact = supportContactLine(opts.supportContact);
|
|
25255
25497
|
const lines = (...l) => l.filter(Boolean).join("\n\n");
|
|
25256
25498
|
const intro = plan2.introAmount !== void 0 && plan2.introAmount < plan2.amount ? `${formatMoney(plan2.introAmount, plan2.currency)} for ${plan2.introPeriods && plan2.introPeriods > 1 ? `your first ${plan2.introPeriods} billing periods` : `your first ${every}`}` : "";
|
|
25257
25499
|
const introLine = intro ? `Introductory price: ${intro}, then ${money3} every ${every}.` : "";
|
|
@@ -25259,11 +25501,13 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25259
25501
|
case "acknowledgment":
|
|
25260
25502
|
return {
|
|
25261
25503
|
subject: `Your ${sub} is confirmed`,
|
|
25504
|
+
// Round 31 (pilot 30 item 22): the same sentences, in the same order, as the "You send" acknowledgment template
|
|
25505
|
+
// (apps/api notices/merchant-templates.ts), so the plan, checkEmail and the due notice give one email.
|
|
25262
25506
|
text: lines(
|
|
25263
|
-
`Thanks for subscribing${brand ? ` to ${brand}` : ""}
|
|
25264
|
-
introLine,
|
|
25507
|
+
`Thanks for subscribing${brand ? ` to ${brand}` : ""}. This email confirms your subscription terms.`,
|
|
25265
25508
|
plan2.trialDays ? `Your free trial lasts ${plan2.trialDays} days and ends on {{trial_end_date}}. To avoid being charged, cancel before your trial ends at ${cancel} \u2014 it takes less than a minute. If you do nothing, your subscription starts automatically on {{trial_end_date}} and you will be charged ${money3} every ${every}.` : "",
|
|
25266
|
-
`Your ${sub} automatically renews every ${every} at ${money3} until you cancel. Your next charge is on {{next_charge_date}}.`,
|
|
25509
|
+
`Your ${sub} automatically renews every ${every} at ${money3} until you cancel. Your ${plan2.trialDays ? "first" : "next"} charge is on {{next_charge_date}}.`,
|
|
25510
|
+
introLine,
|
|
25267
25511
|
`How to cancel: go to ${cancel} \u2014 you can cancel anytime online; ${effect}.`,
|
|
25268
25512
|
contact
|
|
25269
25513
|
),
|
|
@@ -25319,7 +25563,7 @@ function emailTemplate(kind, plan2, opts = {}) {
|
|
|
25319
25563
|
`To stop future charges, cancel online anytime at ${cancel}.`,
|
|
25320
25564
|
// Pilot round 11 (invariant test): Georgia's charge notice must give the business's contact information;
|
|
25321
25565
|
// "reply to this email" is not one, so without a support contact the template asks for it.
|
|
25322
|
-
opts.supportContact
|
|
25566
|
+
supportContactLine(opts.supportContact, { required: true })
|
|
25323
25567
|
),
|
|
25324
25568
|
// Round 12: the template used {{support_contact}} without listing it as a merge tag to fill.
|
|
25325
25569
|
tags: ["{{charge_date}}", "{{next_charge_date}}", ...opts.supportContact ? [] : [SUPPORT_CONTACT_PLACEHOLDER]]
|
|
@@ -25860,6 +26104,7 @@ __export(preflight_exports, {
|
|
|
25860
26104
|
PREFLIGHT_DISCLAIMER: () => PREFLIGHT_DISCLAIMER,
|
|
25861
26105
|
PREFLIGHT_DOCS_URL: () => PREFLIGHT_DOCS_URL,
|
|
25862
26106
|
PreflightInputError: () => PreflightInputError,
|
|
26107
|
+
SATISFIED_COMPLIANCE: () => SATISFIED_COMPLIANCE,
|
|
25863
26108
|
STRIPE_HOSTED_STYLING_NOTE: () => STRIPE_HOSTED_STYLING_NOTE,
|
|
25864
26109
|
checkDisclosure: () => checkDisclosure,
|
|
25865
26110
|
checkEmail: () => checkEmail,
|
|
@@ -25881,6 +26126,8 @@ __export(preflight_exports, {
|
|
|
25881
26126
|
runChecks: () => runChecks,
|
|
25882
26127
|
shapeChecks: () => shapeChecks,
|
|
25883
26128
|
signupStripeObjects: () => signupStripeObjects,
|
|
26129
|
+
stripeParamsVerdict: () => stripeParamsVerdict,
|
|
26130
|
+
supportContactLine: () => supportContactLine,
|
|
25884
26131
|
withIntroOffers: () => withIntroOffers
|
|
25885
26132
|
});
|
|
25886
26133
|
var init_preflight = __esm({
|
|
@@ -25889,6 +26136,7 @@ var init_preflight = __esm({
|
|
|
25889
26136
|
init_common();
|
|
25890
26137
|
init_disclosure();
|
|
25891
26138
|
init_stripe2();
|
|
26139
|
+
init_stripe_verdict();
|
|
25892
26140
|
init_email();
|
|
25893
26141
|
init_html_page();
|
|
25894
26142
|
init_page();
|
|
@@ -25904,7 +26152,7 @@ __export(check_exports, {
|
|
|
25904
26152
|
default: () => check_default
|
|
25905
26153
|
});
|
|
25906
26154
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
25907
|
-
import { resolve } from "node:path";
|
|
26155
|
+
import { resolve as resolve2 } from "node:path";
|
|
25908
26156
|
function operationFor(endpoint) {
|
|
25909
26157
|
const op = Object.values(OPERATIONS).find((o) => o.httpMethod === "POST" && o.path === endpoint);
|
|
25910
26158
|
if (!op) throw new Error(`No API operation for POST ${endpoint}`);
|
|
@@ -25913,7 +26161,7 @@ function operationFor(endpoint) {
|
|
|
25913
26161
|
async function readInput(ctx, file) {
|
|
25914
26162
|
if (file === "-") return ctx.readStdin();
|
|
25915
26163
|
try {
|
|
25916
|
-
return readFileSync3(
|
|
26164
|
+
return readFileSync3(resolve2(ctx.cwd, file), "utf8");
|
|
25917
26165
|
} catch (err) {
|
|
25918
26166
|
throw new UsageError(`Cannot read ${file}: ${err.code ?? err.message}`);
|
|
25919
26167
|
}
|
|
@@ -26201,7 +26449,7 @@ var init_check = __esm({
|
|
|
26201
26449
|
|
|
26202
26450
|
// src/ci/config.ts
|
|
26203
26451
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
26204
|
-
import { dirname as
|
|
26452
|
+
import { dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
|
|
26205
26453
|
function planList(config, ref) {
|
|
26206
26454
|
if (Array.isArray(ref)) return ref;
|
|
26207
26455
|
if (ref === "all" && !(config.plans ?? []).some((p) => p.name === "all")) return (config.plans ?? []).map((p) => p.name);
|
|
@@ -26209,7 +26457,7 @@ function planList(config, ref) {
|
|
|
26209
26457
|
}
|
|
26210
26458
|
function findConfig(cwd, explicit) {
|
|
26211
26459
|
if (explicit) {
|
|
26212
|
-
const p = isAbsolute(explicit) ? explicit :
|
|
26460
|
+
const p = isAbsolute(explicit) ? explicit : resolve3(cwd, explicit);
|
|
26213
26461
|
return existsSync3(p) ? p : void 0;
|
|
26214
26462
|
}
|
|
26215
26463
|
for (const f of CONFIG_FILES) if (existsSync3(join3(cwd, f))) return join3(cwd, f);
|
|
@@ -26230,7 +26478,7 @@ function loadConfig(path) {
|
|
|
26230
26478
|
}
|
|
26231
26479
|
const issues = validateConfig(value);
|
|
26232
26480
|
if (issues.length) throw new ConfigError(path, issues);
|
|
26233
|
-
return { config: value, path, dir:
|
|
26481
|
+
return { config: value, path, dir: dirname3(path), raw };
|
|
26234
26482
|
}
|
|
26235
26483
|
function validateConfig(value) {
|
|
26236
26484
|
const is = new Issues();
|
|
@@ -26509,8 +26757,8 @@ var init_config3 = __esm({
|
|
|
26509
26757
|
});
|
|
26510
26758
|
|
|
26511
26759
|
// src/commands/init/literal.ts
|
|
26512
|
-
function parseObjectLiteral(source, env2 = {}) {
|
|
26513
|
-
const p = new Parser(source, env2);
|
|
26760
|
+
function parseObjectLiteral(source, env2 = {}, consts = {}) {
|
|
26761
|
+
const p = new Parser(source, env2, consts);
|
|
26514
26762
|
if (p.peek() !== "{") return { value: null, unresolved: ["<not an object literal>"] };
|
|
26515
26763
|
const v = p.object("");
|
|
26516
26764
|
return { value: v === UNKNOWN ? null : v, unresolved: [...new Set(p.unresolved)], ...Object.keys(p.fromEnv).length ? { fromEnv: p.fromEnv } : {} };
|
|
@@ -26520,10 +26768,11 @@ var init_literal = __esm({
|
|
|
26520
26768
|
"src/commands/init/literal.ts"() {
|
|
26521
26769
|
"use strict";
|
|
26522
26770
|
UNKNOWN = Symbol("unknown");
|
|
26523
|
-
Parser = class {
|
|
26524
|
-
constructor(s2, env2) {
|
|
26771
|
+
Parser = class _Parser {
|
|
26772
|
+
constructor(s2, env2, consts = {}) {
|
|
26525
26773
|
this.s = s2;
|
|
26526
26774
|
this.env = env2;
|
|
26775
|
+
this.consts = consts;
|
|
26527
26776
|
}
|
|
26528
26777
|
i = 0;
|
|
26529
26778
|
unresolved = [];
|
|
@@ -26613,6 +26862,12 @@ var init_literal = __esm({
|
|
|
26613
26862
|
if (name === "false") return false;
|
|
26614
26863
|
if (name === "null") return null;
|
|
26615
26864
|
if (name === "undefined") return void 0;
|
|
26865
|
+
if (Object.prototype.hasOwnProperty.call(this.consts, name)) {
|
|
26866
|
+
const sub = new _Parser(this.consts[name], this.env);
|
|
26867
|
+
const v = sub.value("");
|
|
26868
|
+
if (v !== UNKNOWN && v !== void 0 && path) this.fromEnv[path] = name;
|
|
26869
|
+
return v;
|
|
26870
|
+
}
|
|
26616
26871
|
const env2 = /^(?:process\.env|import\.meta\.env)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(name);
|
|
26617
26872
|
if (env2) {
|
|
26618
26873
|
if (this.env[env2[1]] !== void 0 && path) this.fromEnv[path] = name;
|
|
@@ -26932,11 +27187,17 @@ function findStripeKey(env2, processEnv) {
|
|
|
26932
27187
|
sources.push([`${k} (environment)`, processEnv[k]]);
|
|
26933
27188
|
sources.push([`${k} (.env)`, env2[k]]);
|
|
26934
27189
|
}
|
|
26935
|
-
const found = sources.filter((s2) => typeof s2[1] === "string" && /^(sk|rk)
|
|
26936
|
-
const
|
|
26937
|
-
|
|
26938
|
-
|
|
26939
|
-
|
|
27190
|
+
const found = sources.filter((s2) => typeof s2[1] === "string" && /^(sk|rk)_(test|live)_[A-Za-z0-9]{10,}$/.test(s2[1].trim())).map(([src, v]) => [src, v.trim()]);
|
|
27191
|
+
const hint = (key) => `${key.slice(0, 8)}\u2026${key.slice(-4)}`;
|
|
27192
|
+
const pick = found.find((s2) => s2[1].startsWith("rk_test_"));
|
|
27193
|
+
if (pick) return { key: { key: pick[1], hint: hint(pick[1]), source: pick[0], restricted: true } };
|
|
27194
|
+
const other = found.find((s2) => s2[1].startsWith("sk_test_")) ?? found[0];
|
|
27195
|
+
if (!other) return {};
|
|
27196
|
+
return { refused: { hint: hint(other[1]), source: other[0], kind: /_live_/.test(other[1]) ? "live" : "secret" } };
|
|
27197
|
+
}
|
|
27198
|
+
function stripeKeyRefusedMessage(r) {
|
|
27199
|
+
const what = r.kind === "live" ? `${r.hint} in ${r.source} is a LIVE Stripe key. Live keys are never stored by the CLI or an agent: a person adds them in the Checkbox dashboard.` : `${r.hint} in ${r.source} is the Stripe secret key, which can do everything in your Stripe test account, so Checkbox refuses it.`;
|
|
27200
|
+
return `${what} For test runs, make a restricted TEST key instead: in Stripe, switch to Test mode, open Developers \u2192 API keys \u2192 Create restricted key, tick the permissions on https://checkbox.site/docs/guides/stripe-key, and put the key (rk_test_\u2026) in STRIPE_RESTRICTED_KEY in .env.local (never in a chat). Then run \`checkbox init\` again.`;
|
|
26940
27201
|
}
|
|
26941
27202
|
function sessionLiteral(text, index) {
|
|
26942
27203
|
const open2 = text.indexOf("{", index);
|
|
@@ -26951,7 +27212,13 @@ function sessionLiteral(text, index) {
|
|
|
26951
27212
|
else if (ch === quote2) quote2 = null;
|
|
26952
27213
|
continue;
|
|
26953
27214
|
}
|
|
26954
|
-
if (ch ===
|
|
27215
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
27216
|
+
const nl = text.indexOf("\n", i);
|
|
27217
|
+
i = nl === -1 ? text.length : nl;
|
|
27218
|
+
} else if (ch === "/" && text[i + 1] === "*") {
|
|
27219
|
+
const end = text.indexOf("*/", i + 2);
|
|
27220
|
+
i = end === -1 ? text.length : end + 1;
|
|
27221
|
+
} else if (ch === '"' || ch === "'" || ch === "`") quote2 = ch;
|
|
26955
27222
|
else if (ch === "{") depth++;
|
|
26956
27223
|
else if (ch === "}") {
|
|
26957
27224
|
depth--;
|
|
@@ -26960,6 +27227,58 @@ function sessionLiteral(text, index) {
|
|
|
26960
27227
|
}
|
|
26961
27228
|
return null;
|
|
26962
27229
|
}
|
|
27230
|
+
function declaredExpr(text, from) {
|
|
27231
|
+
let depth = 0;
|
|
27232
|
+
let quote2 = null;
|
|
27233
|
+
for (let i = from; i < text.length && i - from < 2e3; i++) {
|
|
27234
|
+
const ch = text[i];
|
|
27235
|
+
if (quote2) {
|
|
27236
|
+
if (ch === "\\") i++;
|
|
27237
|
+
else if (ch === quote2) quote2 = null;
|
|
27238
|
+
continue;
|
|
27239
|
+
}
|
|
27240
|
+
if (ch === '"' || ch === "'" || ch === "`") quote2 = ch;
|
|
27241
|
+
else if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
27242
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
27243
|
+
else if ((ch === ";" || ch === "\n") && depth <= 0) return text.slice(from, i).trim();
|
|
27244
|
+
}
|
|
27245
|
+
return text.slice(from, from + 2e3).trim();
|
|
27246
|
+
}
|
|
27247
|
+
function constDeclarations(text, exportedOnly) {
|
|
27248
|
+
const out = {};
|
|
27249
|
+
const re = exportedOnly ? /(?:^|\n)[ \t]*export\s+const\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*/g : /(?:^|\n)[ \t]*(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*/g;
|
|
27250
|
+
for (const m of text.matchAll(re)) if (!(m[1] in out)) out[m[1]] = declaredExpr(text, m.index + m[0].length);
|
|
27251
|
+
return out;
|
|
27252
|
+
}
|
|
27253
|
+
function resolveImport(root, fromFile, spec) {
|
|
27254
|
+
const dir = fromFile.includes("/") ? fromFile.slice(0, fromFile.lastIndexOf("/")) : "";
|
|
27255
|
+
const bases = spec.startsWith(".") ? [join4(dir, spec)] : /^[@~]\//.test(spec) ? [spec.slice(2), join4("src", spec.slice(2))] : [];
|
|
27256
|
+
for (const b of bases) {
|
|
27257
|
+
const base = b.split("\\").join("/").replace(/\.(?:[cm]?js|jsx)$/, "");
|
|
27258
|
+
for (const f of [b, ...[".ts", ".tsx", ".js", ".mjs", ".jsx", "/index.ts", "/index.js"].map((e) => `${base}${e}`)]) {
|
|
27259
|
+
if (/\.[cm]?[jt]sx?$/.test(f) && existsSync4(join4(root, f))) return f.split("\\").join("/");
|
|
27260
|
+
}
|
|
27261
|
+
}
|
|
27262
|
+
return void 0;
|
|
27263
|
+
}
|
|
27264
|
+
function constantsFor(root, relFile, text) {
|
|
27265
|
+
const out = {};
|
|
27266
|
+
for (const m of text.matchAll(/import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
27267
|
+
const target = resolveImport(root, relFile, m[2]);
|
|
27268
|
+
if (!target) continue;
|
|
27269
|
+
let exported;
|
|
27270
|
+
try {
|
|
27271
|
+
exported = constDeclarations(readFileSync5(join4(root, target), "utf8"), true);
|
|
27272
|
+
} catch {
|
|
27273
|
+
continue;
|
|
27274
|
+
}
|
|
27275
|
+
for (const part of m[1].split(",")) {
|
|
27276
|
+
const [name, alias] = part.trim().split(/\s+as\s+/).map((x) => x.trim());
|
|
27277
|
+
if (name && exported[name] !== void 0) out[alias || name] = exported[name];
|
|
27278
|
+
}
|
|
27279
|
+
}
|
|
27280
|
+
return { ...out, ...constDeclarations(text, false) };
|
|
27281
|
+
}
|
|
26963
27282
|
function detectProject(root, processEnv = {}) {
|
|
26964
27283
|
const pkg = readJson(join4(root, "package.json"));
|
|
26965
27284
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
@@ -26996,7 +27315,7 @@ function detectProject(root, processEnv = {}) {
|
|
|
26996
27315
|
checkoutSessions.push({ file: rel2, line: line2, unresolved: ["<params are not an object literal>"] });
|
|
26997
27316
|
continue;
|
|
26998
27317
|
}
|
|
26999
|
-
const parsed = parseObjectLiteral(lit, { ...env2, ...Object.fromEntries(Object.entries(processEnv).filter((e) => typeof e[1] === "string" && /^(STRIPE_|NEXT_PUBLIC_STRIPE_|PRICE_)/.test(e[0]))) });
|
|
27318
|
+
const parsed = parseObjectLiteral(lit, { ...env2, ...Object.fromEntries(Object.entries(processEnv).filter((e) => typeof e[1] === "string" && /^(STRIPE_|NEXT_PUBLIC_STRIPE_|PRICE_)/.test(e[0]))) }, constantsFor(root, rel2, text));
|
|
27000
27319
|
checkoutSessions.push({ file: rel2, line: line2, ...parsed.value ? { params: redactSecretsDeep(parsed.value) } : {}, unresolved: parsed.unresolved });
|
|
27001
27320
|
}
|
|
27002
27321
|
});
|
|
@@ -27018,7 +27337,8 @@ function detectProject(root, processEnv = {}) {
|
|
|
27018
27337
|
checkoutCode,
|
|
27019
27338
|
payButtons,
|
|
27020
27339
|
checkoutSessions,
|
|
27021
|
-
...stripeKey ? { stripeTestKey: stripeKey } : {},
|
|
27340
|
+
...stripeKey.key ? { stripeTestKey: stripeKey.key } : {},
|
|
27341
|
+
...stripeKey.refused ? { stripeKeyRefused: stripeKey.refused } : {},
|
|
27022
27342
|
...brand ? { brand } : {},
|
|
27023
27343
|
plans: plansFromSessions(checkoutSessions),
|
|
27024
27344
|
env: env2
|
|
@@ -27059,7 +27379,7 @@ var init_detect = __esm({
|
|
|
27059
27379
|
|
|
27060
27380
|
// src/ci/scaffold.ts
|
|
27061
27381
|
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
27062
|
-
import { basename, dirname as
|
|
27382
|
+
import { basename, dirname as dirname4, join as join5, relative as relative2 } from "node:path";
|
|
27063
27383
|
function walk3(root, visit) {
|
|
27064
27384
|
let n = 0;
|
|
27065
27385
|
const go = (dir, depth) => {
|
|
@@ -27128,11 +27448,38 @@ function paramsBuilder(root, file, line2) {
|
|
|
27128
27448
|
const imp = new RegExp(`import\\s*\\{[^}]*\\b${fn}\\b[^}]*\\}\\s*from\\s*['"](\\.{1,2}/[^'"]+)['"]`).exec(text);
|
|
27129
27449
|
let target;
|
|
27130
27450
|
if (imp) {
|
|
27131
|
-
const base = join5(
|
|
27451
|
+
const base = join5(dirname4(file), imp[1]).split("\\").join("/");
|
|
27132
27452
|
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
27453
|
} else if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fn}\\b|export\\s+const\\s+${fn}\\b`).test(text)) target = file;
|
|
27134
27454
|
return target ? { module: `${target}#${fn}`, takesArgs } : void 0;
|
|
27135
27455
|
}
|
|
27456
|
+
function plansSoldBy(params, plans) {
|
|
27457
|
+
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
27458
|
+
const out = [];
|
|
27459
|
+
for (const li of items) {
|
|
27460
|
+
const pd = li?.price_data;
|
|
27461
|
+
if (!pd || typeof pd.unit_amount !== "number") continue;
|
|
27462
|
+
const match = plans.find((p) => (typeof p.amount === "number" ? p.amount : typeof p.price === "number" ? Math.round(p.price * 100) : NaN) === pd.unit_amount && p.interval === pd.recurring?.interval);
|
|
27463
|
+
if (match && !out.includes(match)) out.push(match);
|
|
27464
|
+
}
|
|
27465
|
+
return out;
|
|
27466
|
+
}
|
|
27467
|
+
function cancelUrlInCode(sessions) {
|
|
27468
|
+
const urls = /* @__PURE__ */ new Set();
|
|
27469
|
+
for (const s2 of sessions) {
|
|
27470
|
+
const text = JSON.stringify(s2.params?.custom_text ?? "");
|
|
27471
|
+
for (const m of text.matchAll(/cancel[^"]{0,80}?(https:\/\/[^\s"'<>)]+?)(?=[.,;:!?]?(?:[\s"'<>)]|$))/gi)) urls.add(m[1]);
|
|
27472
|
+
}
|
|
27473
|
+
return urls.size === 1 ? [...urls][0] : void 0;
|
|
27474
|
+
}
|
|
27475
|
+
function pageSource(root, built) {
|
|
27476
|
+
const route = /(?:^|\/)(?:public|dist|out|build|static|_site|www)\/(?:(.*)\/)?([^/]+?)(?:\/index)?\.html?$/i.exec(built);
|
|
27477
|
+
if (!route) return void 0;
|
|
27478
|
+
const path = [route[1], route[2]].filter(Boolean).join("/");
|
|
27479
|
+
const bases = [`app/${path}/page`, `src/app/${path}/page`, `app/(marketing)/${path}/page`, `pages/${path}`, `src/pages/${path}`, `src/routes/${path}/+page`, `pages/${path}/index`, `src/pages/${path}/index`];
|
|
27480
|
+
const hits = bases.flatMap((b) => [".tsx", ".jsx", ".ts", ".js", ".svelte", ".vue", ".astro", ".mdx"].map((e) => `${b}${e}`)).filter((f) => existsSync5(join5(root, f)));
|
|
27481
|
+
return hits.length === 1 ? hits[0] : void 0;
|
|
27482
|
+
}
|
|
27136
27483
|
function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
27137
27484
|
const det = detectProject(root, env2);
|
|
27138
27485
|
const todos = [];
|
|
@@ -27151,13 +27498,21 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27151
27498
|
merchant.cancel_url = cancelFact;
|
|
27152
27499
|
found.push(`cancel URL ${cancelFact} (from checkbox init)`);
|
|
27153
27500
|
} else if (siteUrl && cancelPath) merchant.cancel_url = `${originOf(siteUrl)}${cancelPath}`;
|
|
27154
|
-
else {
|
|
27501
|
+
else if (cancelUrlInCode(det.checkoutSessions)) {
|
|
27502
|
+
merchant.cancel_url = cancelUrlInCode(det.checkoutSessions);
|
|
27503
|
+
found.push(`cancel URL ${String(merchant.cancel_url)} (from the Checkout custom_text)`);
|
|
27504
|
+
} else {
|
|
27155
27505
|
merchant.cancel_url = "TODO: https://\u2026 page where subscribers cancel online";
|
|
27156
27506
|
todos.push("merchant.cancel_url: the page where subscribers cancel online");
|
|
27157
27507
|
}
|
|
27158
27508
|
if (fact("support_email")) merchant.support_contact = fact("support_email");
|
|
27509
|
+
const initPlans = Array.isArray(facts.plans) ? facts.plans : [];
|
|
27510
|
+
const initName = (p) => {
|
|
27511
|
+
const m = initPlans.find((x) => typeof x.name === "string" && x.amount === p.amount && x.interval === p.interval && (Number(x.interval_count ?? 1) || 1) === (p.interval_count ?? 1));
|
|
27512
|
+
return m ? String(m.name) : void 0;
|
|
27513
|
+
};
|
|
27159
27514
|
const plans = det.plans.map((p, i) => ({
|
|
27160
|
-
name: p.name ?? `Plan ${i + 1}`,
|
|
27515
|
+
name: p.name ?? initName(p) ?? `Plan ${i + 1}`,
|
|
27161
27516
|
amount: p.amount,
|
|
27162
27517
|
currency: p.currency,
|
|
27163
27518
|
interval: p.interval,
|
|
@@ -27196,8 +27551,13 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27196
27551
|
todos.push("plans: the price, interval and trial of each plan (the checkout uses Price ids, which cannot be read from code)");
|
|
27197
27552
|
}
|
|
27198
27553
|
const planNames = plans.map((p) => String(p.name));
|
|
27199
|
-
const
|
|
27200
|
-
|
|
27554
|
+
const oneOf = planNames.map((n) => JSON.stringify(n)).join(", ");
|
|
27555
|
+
let planTodo = false;
|
|
27556
|
+
const planFor = (kind = "entry") => {
|
|
27557
|
+
if (plans.length <= 1) return {};
|
|
27558
|
+
planTodo = true;
|
|
27559
|
+
return { plan: kind === "page" ? `TODO: "all" (the page shows every plan) or one of ${oneOf}` : `TODO: one of ${oneOf}` };
|
|
27560
|
+
};
|
|
27201
27561
|
const stripe = [];
|
|
27202
27562
|
const perFile = /* @__PURE__ */ new Map();
|
|
27203
27563
|
for (const s2 of det.checkoutSessions) {
|
|
@@ -27228,7 +27588,12 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27228
27588
|
} else if (s2.unresolved.length) found.push(`${s2.file}:${s2.line}: Checkout Session (${s2.unresolved.length} value(s) are expressions, read as-is)`);
|
|
27229
27589
|
else found.push(`${s2.file}:${s2.line}: Checkout Session`);
|
|
27230
27590
|
entry.strict = true;
|
|
27231
|
-
|
|
27591
|
+
const sold = s2.params ? plansSoldBy(s2.params, plans) : [];
|
|
27592
|
+
if (plans.length === 1 || sold.length === 1) stripe.push({ ...entry, plan: String((sold[0] ?? plans[0]).name) });
|
|
27593
|
+
else if (s2.params && plans.length > 1 && !plans.some((p) => String(p.name).startsWith("TODO"))) {
|
|
27594
|
+
for (const p of plans) stripe.push({ ...entry, name: unique(names, `${String(entry.name).replace(/\)$/, "")}: ${String(p.name)})`), plan: String(p.name) });
|
|
27595
|
+
names.delete(String(entry.name));
|
|
27596
|
+
} else stripe.push(entry);
|
|
27232
27597
|
}
|
|
27233
27598
|
const emails = [];
|
|
27234
27599
|
const pages = [];
|
|
@@ -27248,7 +27613,8 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27248
27613
|
}
|
|
27249
27614
|
if (PAGE_FILE.test(rel2)) {
|
|
27250
27615
|
const kind = /checkout|subscribe/i.test(rel2) ? "checkout" : "pricing";
|
|
27251
|
-
|
|
27616
|
+
const src = pageSource(root, rel2);
|
|
27617
|
+
pages.push({ name: unique(names, `Page: ${rel2}`), file: rel2, kind, ...planFor("page"), ...src ? { source: src } : {} });
|
|
27252
27618
|
found.push(`${rel2}: ${kind} page`);
|
|
27253
27619
|
return;
|
|
27254
27620
|
}
|
|
@@ -27268,6 +27634,7 @@ function scaffoldConfig(root, env2 = {}, facts = {}) {
|
|
|
27268
27634
|
if (!disclosures.length && det.payButtons.length) {
|
|
27269
27635
|
todos.push(`The renewal disclosure lives in code (${det.payButtons[0].file}); keep its text in a .txt file the component imports and add it under checks.disclosures, or check the rendered page under checks.pages`);
|
|
27270
27636
|
}
|
|
27637
|
+
if (planTodo) todos.push('plan: pick the plan of each disclosure, page and email entry marked TODO ("all" for a page that shows every plan)');
|
|
27271
27638
|
const checks = {};
|
|
27272
27639
|
if (stripe.length) checks.stripe_checkout = stripe;
|
|
27273
27640
|
if (disclosures.length) checks.disclosures = disclosures;
|
|
@@ -27315,7 +27682,7 @@ var init_scaffold = __esm({
|
|
|
27315
27682
|
|
|
27316
27683
|
// src/ci/coverage.ts
|
|
27317
27684
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
27318
|
-
import { join as join6, relative as relative3, resolve as
|
|
27685
|
+
import { join as join6, relative as relative3, resolve as resolve4 } from "node:path";
|
|
27319
27686
|
function callsIn(text, file) {
|
|
27320
27687
|
const hits = [];
|
|
27321
27688
|
for (const [re, kind] of PATTERNS) {
|
|
@@ -27337,7 +27704,7 @@ function callsIn(text, file) {
|
|
|
27337
27704
|
}
|
|
27338
27705
|
function coverageOf(loaded, root, files) {
|
|
27339
27706
|
const cfg = loaded.config;
|
|
27340
|
-
const toRoot = (p) => relative3(root,
|
|
27707
|
+
const toRoot = (p) => relative3(root, resolve4(loaded.dir, p)).split("\\").join("/");
|
|
27341
27708
|
const calls = [];
|
|
27342
27709
|
const visit = (rel2) => {
|
|
27343
27710
|
if (!CODE.test(rel2) || TESTISH.test(rel2)) return;
|
|
@@ -27438,10 +27805,10 @@ var init_coverage = __esm({
|
|
|
27438
27805
|
// src/ci/inputs.ts
|
|
27439
27806
|
import { execFile } from "node:child_process";
|
|
27440
27807
|
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
27441
|
-
import { dirname as
|
|
27808
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join7, relative as relative4, resolve as resolve5 } from "node:path";
|
|
27442
27809
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
27443
27810
|
function readRel(dir, file) {
|
|
27444
|
-
const abs = isAbsolute2(file) ? file :
|
|
27811
|
+
const abs = isAbsolute2(file) ? file : resolve5(dir, file);
|
|
27445
27812
|
try {
|
|
27446
27813
|
return readFileSync8(abs, "utf8");
|
|
27447
27814
|
} catch (err) {
|
|
@@ -27452,26 +27819,42 @@ function readRel(dir, file) {
|
|
|
27452
27819
|
function hasTsx(dir) {
|
|
27453
27820
|
for (let d = dir, i = 0; i < 8; i++) {
|
|
27454
27821
|
if (existsSync6(join7(d, "node_modules", "tsx", "package.json"))) return true;
|
|
27455
|
-
const up =
|
|
27822
|
+
const up = dirname5(d);
|
|
27456
27823
|
if (up === d) break;
|
|
27457
27824
|
d = up;
|
|
27458
27825
|
}
|
|
27459
27826
|
return false;
|
|
27460
27827
|
}
|
|
27828
|
+
function declaresTsx(dir) {
|
|
27829
|
+
for (let d = dir, i = 0; i < 8; i++) {
|
|
27830
|
+
try {
|
|
27831
|
+
const pkg = JSON.parse(readFileSync8(join7(d, "package.json"), "utf8"));
|
|
27832
|
+
if (pkg.dependencies?.tsx || pkg.devDependencies?.tsx) return true;
|
|
27833
|
+
} catch {
|
|
27834
|
+
}
|
|
27835
|
+
const up = dirname5(d);
|
|
27836
|
+
if (up === d) break;
|
|
27837
|
+
d = up;
|
|
27838
|
+
}
|
|
27839
|
+
return false;
|
|
27840
|
+
}
|
|
27841
|
+
function addTsx(dir, detail = "") {
|
|
27842
|
+
return declaresTsx(dir) ? NOT_INSTALLED : `Add tsx to the project (npm install -D tsx${detail})`;
|
|
27843
|
+
}
|
|
27461
27844
|
function loaderFlags(file, dir, nodeVersion = process.versions.node, features = process.features) {
|
|
27462
27845
|
if (/\.[cm]?js$/.test(file)) return [];
|
|
27463
27846
|
if (hasTsx(dir)) return ["--import", "tsx"];
|
|
27464
|
-
if (/\.[jt]sx$/.test(file)) return `${file}: JSX modules need tsx (npm install -D tsx), or export the rendered { subject, html } from a .js / .ts file`;
|
|
27847
|
+
if (/\.[jt]sx$/.test(file)) return declaresTsx(dir) ? `${file}: ${NOT_INSTALLED}` : `${file}: JSX modules need tsx (npm install -D tsx), or export the rendered { subject, html } from a .js / .ts file`;
|
|
27465
27848
|
if (features.typescript) return [];
|
|
27466
27849
|
const [major, minor] = nodeVersion.split(".").map(Number);
|
|
27467
27850
|
if (major > 22 || major === 22 && minor >= 6) return ["--experimental-strip-types"];
|
|
27468
|
-
return `${file}: TypeScript modules need Node 22.6+ (type stripping) or tsx in the project (npm install -D tsx); or point "file" at a JSON export`;
|
|
27851
|
+
return declaresTsx(dir) ? `${file}: ${NOT_INSTALLED}` : `${file}: TypeScript modules need Node 22.6+ (type stripping) or tsx in the project (npm install -D tsx); or point "file" at a JSON export`;
|
|
27469
27852
|
}
|
|
27470
27853
|
function runModule(dir, ref, args = [], env2 = process.env, timeoutMs = 3e4) {
|
|
27471
27854
|
const hash = ref.lastIndexOf("#");
|
|
27472
27855
|
const file = hash > 0 ? ref.slice(0, hash) : ref;
|
|
27473
27856
|
const name = hash > 0 ? ref.slice(hash + 1) : "default";
|
|
27474
|
-
const abs =
|
|
27857
|
+
const abs = resolve5(dir, file);
|
|
27475
27858
|
if (!existsSync6(abs)) return Promise.reject(new InputError(`${file} does not exist (paths are relative to checkbox.json)`));
|
|
27476
27859
|
const flags = loaderFlags(file, dir);
|
|
27477
27860
|
if (typeof flags === "string") return Promise.reject(new InputError(flags));
|
|
@@ -27512,19 +27895,19 @@ function moduleErrorMessage(stderr, dir, flags = []) {
|
|
|
27512
27895
|
}
|
|
27513
27896
|
const noExt = !/\.[cm]?[jt]sx?$|\.json$/.test(target);
|
|
27514
27897
|
if (noExt && !tsx) {
|
|
27515
|
-
return `${rel2(from)} imports ${rel2(target)} without a file extension, which Node cannot resolve on its own.
|
|
27898
|
+
return `${rel2(from)} imports ${rel2(target)} without a file extension, which Node cannot resolve on its own. ${addTsx(dir, "; 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
27899
|
}
|
|
27517
27900
|
return `${rel2(from)} imports ${rel2(target)}, which does not exist${noExt ? " (tsx found no .ts / .js file of that name either)" : ""}.`;
|
|
27518
27901
|
}
|
|
27519
27902
|
if (/ERR_UNKNOWN_FILE_EXTENSION/.test(text)) {
|
|
27520
27903
|
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"}.
|
|
27904
|
+
return `Node cannot import ${file ? `${rel2(file[2])} (${file[1]})` : "a file the module imports"}. ${addTsx(dir)}, or point the entry at a .js / .ts file that imports no ${file?.[1] ?? "such"} files.`;
|
|
27522
27905
|
}
|
|
27523
27906
|
const lines = text.split("\n").map((l) => l.trim()).filter((l) => l && !/^at /.test(l) && !/^Node\.js v\d/.test(l));
|
|
27524
27907
|
const first2 = lines.find((l) => /^(?:[A-Z]\w*)?Error\b.*:/.test(l)) ?? lines.find((l) => /error/i.test(l)) ?? lines[lines.length - 1] ?? "";
|
|
27525
27908
|
return first2.replace(/file:\/\/\S+/g, (u) => rel2(u)).replace(/\s+/g, " ").slice(0, 400);
|
|
27526
27909
|
}
|
|
27527
|
-
function paramsFromSource(text, file, call = 1, env2 = {}) {
|
|
27910
|
+
function paramsFromSource(text, file, call = 1, env2 = {}, consts = {}) {
|
|
27528
27911
|
const calls = [...text.matchAll(/checkout\.sessions\.create\s*\(/g)];
|
|
27529
27912
|
if (!calls.length) throw new InputError(`${file} has no stripe.checkout.sessions.create( call`);
|
|
27530
27913
|
const m = calls[call - 1];
|
|
@@ -27532,7 +27915,7 @@ function paramsFromSource(text, file, call = 1, env2 = {}) {
|
|
|
27532
27915
|
const line2 = text.slice(0, m.index).split("\n").length;
|
|
27533
27916
|
const lit = sessionLiteral(text, m.index + m[0].length);
|
|
27534
27917
|
if (!lit) throw new InputError(`${file}:${line2}: the params are not an object literal; use "module" (a function returning the params) or "file" instead`);
|
|
27535
|
-
const parsed = parseObjectLiteral(lit, env2);
|
|
27918
|
+
const parsed = parseObjectLiteral(lit, env2, consts);
|
|
27536
27919
|
if (!parsed.value) throw new InputError(`${file}:${line2}: could not read the params object`);
|
|
27537
27920
|
return { params: parsed.value, line: line2, unresolved: parsed.unresolved, ...parsed.fromEnv ? { fromEnv: parsed.fromEnv } : {} };
|
|
27538
27921
|
}
|
|
@@ -27552,7 +27935,7 @@ function exportLine(dir, ref) {
|
|
|
27552
27935
|
const name = hash > 0 ? ref.slice(hash + 1) : "default";
|
|
27553
27936
|
let text;
|
|
27554
27937
|
try {
|
|
27555
|
-
text = readFileSync8(
|
|
27938
|
+
text = readFileSync8(resolve5(dir, file), "utf8");
|
|
27556
27939
|
} catch {
|
|
27557
27940
|
return 1;
|
|
27558
27941
|
}
|
|
@@ -27591,7 +27974,7 @@ function planTerms(config, plan2) {
|
|
|
27591
27974
|
return t;
|
|
27592
27975
|
}
|
|
27593
27976
|
function rel(loaded, root, file) {
|
|
27594
|
-
return relative4(root,
|
|
27977
|
+
return relative4(root, resolve5(loaded.dir, file)).split("\\").join("/") || file;
|
|
27595
27978
|
}
|
|
27596
27979
|
function emailFromFile(content, file, subject) {
|
|
27597
27980
|
let body = content;
|
|
@@ -27636,7 +28019,8 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27636
28019
|
} else {
|
|
27637
28020
|
const src = e.source;
|
|
27638
28021
|
job.inputs.push(src);
|
|
27639
|
-
const
|
|
28022
|
+
const srcText = readRel(dir, src);
|
|
28023
|
+
const r = paramsFromSource(srcText, src, e.call ?? 1, projectEnv(dir), constantsFor(dir, src, srcText));
|
|
27640
28024
|
job.target = { file: rel(loaded, root, src), line: r.line };
|
|
27641
28025
|
job.fromSource = true;
|
|
27642
28026
|
params = r.params;
|
|
@@ -27648,9 +28032,16 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27648
28032
|
}
|
|
27649
28033
|
}
|
|
27650
28034
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new InputError("the Checkout Session params must be a JSON object");
|
|
28035
|
+
const items0 = Array.isArray(params.line_items) ? params.line_items : [];
|
|
28036
|
+
if (!e.plan && (config.plans?.length ?? 0) > 1 && !items0.some((li) => li && typeof li === "object" && li.price_data)) {
|
|
28037
|
+
throw new InputError(`"${e.name}" does not say which plan it sells, and its price is a Price id or an expression Checkbox cannot read: add "plan": one of ${(config.plans ?? []).map((p) => JSON.stringify(p.name)).join(", ")} to this entry (one entry per plan when one call serves several plans).`);
|
|
28038
|
+
}
|
|
27651
28039
|
const merchant = planTerms(config, pickPlan(config, e.plan));
|
|
27652
28040
|
const drift = planDrift(params, pickPlan(config, e.plan));
|
|
27653
28041
|
if (drift) job.warnings.push(drift);
|
|
28042
|
+
const soldPlan = pickPlan(config, e.plan);
|
|
28043
|
+
const overrides = soldPlan ? planAsSold(params, soldPlan) : void 0;
|
|
28044
|
+
if (soldPlan && overrides) job.sold = { plan: soldPlan.name, overrides };
|
|
27654
28045
|
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
27655
28046
|
if (items.length && items.every((li) => li && typeof li === "object" && li.price_data)) for (const k of ["price", "amount", "currency", "interval", "interval_count"]) delete merchant[k];
|
|
27656
28047
|
job.body = { params, ...Object.keys(merchant).length ? { merchant } : {}, ...jurisdictionsOf(config) };
|
|
@@ -27659,6 +28050,29 @@ async function stripeJob(loaded, e, o, root) {
|
|
|
27659
28050
|
}
|
|
27660
28051
|
return job;
|
|
27661
28052
|
}
|
|
28053
|
+
function planAsSold(params, plan2) {
|
|
28054
|
+
const sd = params.subscription_data ?? {};
|
|
28055
|
+
const trial = typeof sd.trial_period_days === "number" ? sd.trial_period_days : void 0;
|
|
28056
|
+
const out = {};
|
|
28057
|
+
if (trial !== void 0 && trial !== (plan2.trial_days ?? 0)) out.trial_days = trial;
|
|
28058
|
+
if (trial === void 0 && plan2.trial_days && plan2.trial_days > 0 && sd.trial_end === void 0) out.trial_days = 0;
|
|
28059
|
+
const items = Array.isArray(params.line_items) ? params.line_items : [];
|
|
28060
|
+
const pd = items.map((li) => li && typeof li === "object" ? li.price_data : void 0).find((x) => x && typeof x === "object");
|
|
28061
|
+
const rec = pd && typeof pd.recurring === "object" ? pd.recurring : void 0;
|
|
28062
|
+
if (rec && typeof rec.interval === "string" && rec.interval !== plan2.interval && ["day", "week", "month", "year"].includes(rec.interval)) out.interval = rec.interval;
|
|
28063
|
+
const price3 = typeof plan2.price === "number" ? plan2.price : typeof plan2.price === "string" && /^[\d.]+$/.test(plan2.price) ? Number(plan2.price) : void 0;
|
|
28064
|
+
if (pd && typeof pd.unit_amount === "number" && price3 !== void 0 && /^(?:usd|eur|gbp|cad|aud|nzd)$/i.test(String(pd.currency ?? plan2.currency ?? "usd")) && (!pd.currency || pd.currency === (plan2.currency ?? "usd")) && Math.abs(pd.unit_amount - Math.round(price3 * 100)) >= 1) {
|
|
28065
|
+
out.price = pd.unit_amount / 100;
|
|
28066
|
+
}
|
|
28067
|
+
return Object.keys(out).length ? out : void 0;
|
|
28068
|
+
}
|
|
28069
|
+
function describeSold(o) {
|
|
28070
|
+
const parts = [];
|
|
28071
|
+
if (o.trial_days !== void 0) parts.push(o.trial_days ? `a ${o.trial_days}-day trial` : "no trial");
|
|
28072
|
+
const charge = [o.price !== void 0 ? Number(o.price).toFixed(2) : "", o.interval ? `every ${o.interval}` : ""].filter(Boolean).join(" ");
|
|
28073
|
+
if (charge) parts.push(charge);
|
|
28074
|
+
return parts.join(", ");
|
|
28075
|
+
}
|
|
27662
28076
|
function planDrift(params, plan2) {
|
|
27663
28077
|
if (!plan2) return void 0;
|
|
27664
28078
|
const sd = params.subscription_data ?? {};
|
|
@@ -27674,7 +28088,7 @@ function planDrift(params, plan2) {
|
|
|
27674
28088
|
if (pd && typeof pd.unit_amount === "number" && price3 !== void 0 && /^(?:usd|eur|gbp|cad|aud|nzd)$/i.test(String(pd.currency ?? plan2.currency ?? "usd")) && (!pd.currency || pd.currency === (plan2.currency ?? "usd")) && Math.abs(pd.unit_amount - Math.round(price3 * 100)) >= 1) {
|
|
27675
28089
|
out.push(`the params charge ${(pd.unit_amount / 100).toFixed(2)}, plan "${plan2.name}" ${price3.toFixed(2)}`);
|
|
27676
28090
|
}
|
|
27677
|
-
return out.length ? `The params and checkbox.json disagree: ${out.join("; ")}. The
|
|
28091
|
+
return out.length ? `The params and checkbox.json disagree: ${out.join("; ")}. The Checkout check uses the params, and the emails, pages and disclosures of plan "${plan2.name}" are checked against both until they agree: update "plans" in checkbox.json to the plan you really sell.` : void 0;
|
|
27678
28092
|
}
|
|
27679
28093
|
function disclosureJob(loaded, e, root) {
|
|
27680
28094
|
const { config, dir } = loaded;
|
|
@@ -27726,14 +28140,20 @@ function pageJob(loaded, e, o, root) {
|
|
|
27726
28140
|
};
|
|
27727
28141
|
}
|
|
27728
28142
|
if (e.url) {
|
|
27729
|
-
if (!o.online) job.skip =
|
|
28143
|
+
if (!o.online) job.skip = 'URL pages are fetched by the API: run with --online and a live-mode key (ck_live_\u2026 on an activated account), or list the built page under "file" to check it offline';
|
|
27730
28144
|
else job.body = { url: e.url, ...common3 };
|
|
27731
28145
|
return job;
|
|
27732
28146
|
}
|
|
27733
28147
|
try {
|
|
27734
28148
|
if (!e.source) job.target = { file: rel(loaded, root, e.file), line: 1 };
|
|
27735
28149
|
job.inputs.push(e.file);
|
|
27736
|
-
|
|
28150
|
+
let html;
|
|
28151
|
+
try {
|
|
28152
|
+
html = readRel(dir, e.file);
|
|
28153
|
+
} catch (err) {
|
|
28154
|
+
if (!existsSync6(resolve5(dir, e.file))) throw new InputError(`${e.file} does not exist (paths are relative to checkbox.json). If it is build output, build it before checkbox ci: in GitHub Actions, add a build step before the Checkbox step (\`checkbox ci init\` writes one when package.json has a build script).`);
|
|
28155
|
+
throw err;
|
|
28156
|
+
}
|
|
27737
28157
|
if (!e.source) job.locate = (needles) => lineOfAny(html, needles);
|
|
27738
28158
|
job.body = { html, ...common3 };
|
|
27739
28159
|
} catch (err) {
|
|
@@ -27774,9 +28194,35 @@ async function emailJob(loaded, e, o, root, planName = e.plan, name = e.name) {
|
|
|
27774
28194
|
}
|
|
27775
28195
|
async function resolveJobs(loaded, o) {
|
|
27776
28196
|
const root = o.root ?? loaded.dir;
|
|
28197
|
+
const jobs = [];
|
|
28198
|
+
for (const e of loaded.config.checks.stripe_checkout ?? []) jobs.push(await stripeJob(loaded, e, o, root));
|
|
28199
|
+
const dependent = await planJobs(loaded, o, root);
|
|
28200
|
+
jobs.push(...dependent);
|
|
28201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28202
|
+
for (const sj of jobs.filter((j) => j.type === "stripe_checkout" && j.sold)) {
|
|
28203
|
+
const key = JSON.stringify(sj.sold);
|
|
28204
|
+
if (seen.has(key)) continue;
|
|
28205
|
+
seen.add(key);
|
|
28206
|
+
const { plan: planName, overrides } = sj.sold;
|
|
28207
|
+
const config = { ...loaded.config, plans: (loaded.config.plans ?? []).map((p) => p.name === planName ? { ...p, ...overrides } : p) };
|
|
28208
|
+
const variant = await planJobs({ ...loaded, config }, o, root);
|
|
28209
|
+
const sold = describeSold(overrides);
|
|
28210
|
+
variant.forEach((v, i) => {
|
|
28211
|
+
const orig = dependent[i];
|
|
28212
|
+
if (orig.error || orig.skip || !v.body || JSON.stringify(v.body) === JSON.stringify(orig.body)) return;
|
|
28213
|
+
jobs.push({
|
|
28214
|
+
...v,
|
|
28215
|
+
name: `${orig.name} (as "${sj.name}" sells it: ${sold})`,
|
|
28216
|
+
inputs: [.../* @__PURE__ */ new Set([...v.inputs, ...sj.inputs])],
|
|
28217
|
+
warnings: [`The params and checkbox.json disagree: "${sj.name}" sells plan "${planName}" with ${sold}, which checkbox.json does not say. This check judges ${orig.name} against what the params sell; the same check against checkbox.json runs too, and both must pass. Update "plans" in checkbox.json (or the params) so they agree.`, ...v.warnings]
|
|
28218
|
+
});
|
|
28219
|
+
});
|
|
28220
|
+
}
|
|
28221
|
+
return jobs;
|
|
28222
|
+
}
|
|
28223
|
+
async function planJobs(loaded, o, root) {
|
|
27777
28224
|
const c = loaded.config.checks;
|
|
27778
28225
|
const jobs = [];
|
|
27779
|
-
for (const e of c.stripe_checkout ?? []) jobs.push(await stripeJob(loaded, e, o, root));
|
|
27780
28226
|
for (const e of c.disclosures ?? []) jobs.push(disclosureJob(loaded, e, root));
|
|
27781
28227
|
for (const e of c.pages ?? []) jobs.push(pageJob(loaded, e, o, root));
|
|
27782
28228
|
for (const e of c.emails ?? []) {
|
|
@@ -27800,7 +28246,7 @@ async function resolveJobs(loaded, o) {
|
|
|
27800
28246
|
}
|
|
27801
28247
|
return jobs;
|
|
27802
28248
|
}
|
|
27803
|
-
var InputError, CHILD, jurisdictionsOf;
|
|
28249
|
+
var InputError, NOT_INSTALLED, CHILD, jurisdictionsOf;
|
|
27804
28250
|
var init_inputs = __esm({
|
|
27805
28251
|
"src/ci/inputs.ts"() {
|
|
27806
28252
|
"use strict";
|
|
@@ -27813,6 +28259,7 @@ var init_inputs = __esm({
|
|
|
27813
28259
|
this.name = "InputError";
|
|
27814
28260
|
}
|
|
27815
28261
|
};
|
|
28262
|
+
NOT_INSTALLED = "tsx is in package.json but not installed: install the project's dependencies before checkbox ci (npm ci; in GitHub Actions a `- run: npm ci` step before the Checkbox step, which `checkbox ci init` writes)";
|
|
27816
28263
|
CHILD = `
|
|
27817
28264
|
const [url, name, argsJson] = process.argv.slice(1);
|
|
27818
28265
|
const mod = await import(url);
|
|
@@ -27831,106 +28278,6 @@ process.exit(0);
|
|
|
27831
28278
|
}
|
|
27832
28279
|
});
|
|
27833
28280
|
|
|
27834
|
-
// src/ci/report.ts
|
|
27835
|
-
function icon(s2, c) {
|
|
27836
|
-
if (s2 === "pass") return c.green("\u2713");
|
|
27837
|
-
if (s2 === "fail" || s2 === "error") return c.red("\u2717");
|
|
27838
|
-
if (s2 === "needs_review") return c.yellow("?");
|
|
27839
|
-
return c.dim("\u2013");
|
|
27840
|
-
}
|
|
27841
|
-
function overallLine(run2) {
|
|
27842
|
-
const s2 = run2.summary;
|
|
27843
|
-
const parts = [];
|
|
27844
|
-
if (s2.error) parts.push(`${s2.error} could not run`);
|
|
27845
|
-
if (s2.fail) parts.push(`${s2.fail} need${s2.fail === 1 ? "s" : ""} a fix`);
|
|
27846
|
-
if (s2.needs_review) parts.push(`${s2.needs_review} need${s2.needs_review === 1 ? "s" : ""} review${run2.strict ? " (strict)" : ""}`);
|
|
27847
|
-
if (s2.pass) parts.push(`${s2.pass} pass`);
|
|
27848
|
-
if (s2.not_applicable) parts.push(`${s2.not_applicable} not required`);
|
|
27849
|
-
if (s2.skipped) parts.push(`${s2.skipped} skipped`);
|
|
27850
|
-
return parts.join(" \xB7 ") || "nothing checked";
|
|
27851
|
-
}
|
|
27852
|
-
function renderHuman(run2, c, opts = {}) {
|
|
27853
|
-
const lines = [];
|
|
27854
|
-
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
27855
|
-
lines.push(`${c.bold("Checkbox CI")} ${run2.config} \xB7 ${run2.mode}${versions} \xB7 ${run2.checks.length} check${run2.checks.length === 1 ? "" : "s"}`);
|
|
27856
|
-
const width = Math.min(40, Math.max(...run2.checks.map((x) => x.name.length), 10));
|
|
27857
|
-
for (const x of run2.checks) {
|
|
27858
|
-
const label = x.status === "fail" || x.status === "error" ? c.red(x.label) : x.status === "needs_review" ? c.yellow(x.label) : x.status === "pass" ? c.green(x.label) : c.dim(x.label);
|
|
27859
|
-
lines.push(` ${icon(x.status, c)} ${x.name.padEnd(width)} \u2192 ${label}`);
|
|
27860
|
-
if (x.message) lines.push(c.dim(` ${wrap(x.message, 110, " ")}`));
|
|
27861
|
-
if (x.fix) lines.push(` fix: ${wrap(x.fix, 110, " ")}${c.dim(` (${where(x)})`)}`);
|
|
27862
|
-
if (opts.verbose) for (const f of x.fixes.slice(1)) lines.push(` fix: ${wrap(f, 110, " ")}`);
|
|
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, " ")}`));
|
|
27866
|
-
if (x.corrected && opts.verbose) lines.push(c.dim(` ${x.corrected.title}:`), ...x.corrected.body.split("\n").map((l) => c.dim(` ${l}`)));
|
|
27867
|
-
}
|
|
27868
|
-
const tone = run2.status === "pass" ? c.green : run2.status === "needs_review" && run2.exit_code === 0 ? c.yellow : c.red;
|
|
27869
|
-
lines.push("", `${tone(overallLine(run2))} \xB7 exit ${run2.exit_code}`);
|
|
27870
|
-
if (run2.checks.some((x) => x.corrected) && !opts.verbose) lines.push(c.dim("Corrected params / text: --verbose, --json, or the job summary in GitHub Actions."));
|
|
27871
|
-
lines.push(c.dim(run2.disclaimer));
|
|
27872
|
-
return lines;
|
|
27873
|
-
}
|
|
27874
|
-
function annotations(run2) {
|
|
27875
|
-
const out = [];
|
|
27876
|
-
for (const x of run2.checks) {
|
|
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;
|
|
27879
|
-
if (!level) continue;
|
|
27880
|
-
const props = [`file=${escProp(x.target.file)}`, ...x.target.line ? [`line=${x.target.line}`] : [], `title=${escProp(`Checkbox: ${x.name} \u2014 ${x.label}`)}`];
|
|
27881
|
-
const body = x.status === "error" ? x.fix ? `${x.message ?? ""}
|
|
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");
|
|
27883
|
-
out.push(`::${level} ${props.join(",")}::${escData(body)}`);
|
|
27884
|
-
}
|
|
27885
|
-
return out;
|
|
27886
|
-
}
|
|
27887
|
-
function stepSummary(run2) {
|
|
27888
|
-
const lines = [];
|
|
27889
|
-
lines.push("## Checkbox compliance checks", "");
|
|
27890
|
-
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
27891
|
-
lines.push(`**${overallLine(run2)}** \u2014 \`${run2.config}\` \xB7 ${run2.mode}${versions}`, "");
|
|
27892
|
-
lines.push("| Check | Status | Fix | File |", "|---|---|---|---|");
|
|
27893
|
-
for (const x of run2.checks) {
|
|
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");
|
|
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))}\` |`);
|
|
27897
|
-
}
|
|
27898
|
-
const detailed = run2.checks.filter((x) => x.corrected || x.fixes.length > 1);
|
|
27899
|
-
for (const x of detailed) {
|
|
27900
|
-
lines.push("", `<details><summary><b>${escapeHtml(x.name)}</b>: ${escapeHtml(x.corrected ? x.corrected.title.toLowerCase() : `${x.fixes.length} fixes`)}</summary>`, "");
|
|
27901
|
-
if (x.fixes.length) lines.push(...x.fixes.map((f) => `- ${f}`), "");
|
|
27902
|
-
if (x.corrected) {
|
|
27903
|
-
const fence = x.corrected.body.includes("```") ? "````" : "```";
|
|
27904
|
-
lines.push(`${fence}${x.corrected.format === "json" ? "json" : "text"}`, x.corrected.body, fence, "");
|
|
27905
|
-
}
|
|
27906
|
-
lines.push("</details>");
|
|
27907
|
-
}
|
|
27908
|
-
lines.push("", `<sub>${run2.disclaimer} Docs: https://checkbox.site/docs/guides/ci</sub>`, "");
|
|
27909
|
-
return lines.join("\n");
|
|
27910
|
-
}
|
|
27911
|
-
function escapeHtml(s2) {
|
|
27912
|
-
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
27913
|
-
}
|
|
27914
|
-
var TYPE_LABEL, where, escData, escProp, cell, STATUS_MD;
|
|
27915
|
-
var init_report = __esm({
|
|
27916
|
-
"src/ci/report.ts"() {
|
|
27917
|
-
"use strict";
|
|
27918
|
-
init_output();
|
|
27919
|
-
TYPE_LABEL = {
|
|
27920
|
-
stripe_checkout: "Stripe Checkout",
|
|
27921
|
-
disclosure: "Disclosure",
|
|
27922
|
-
page: "Page",
|
|
27923
|
-
email: "Email",
|
|
27924
|
-
notice_schedule: "Notice schedule"
|
|
27925
|
-
};
|
|
27926
|
-
where = (c) => `${c.target.file}${c.target.line && c.target.line > 1 ? `:${c.target.line}` : ""}`;
|
|
27927
|
-
escData = (s2) => s2.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
27928
|
-
escProp = (s2) => escData(s2).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
27929
|
-
cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
27930
|
-
STATUS_MD = { pass: "\u2705", fail: "\u274C", needs_review: "\u26A0\uFE0F", error: "\u274C", skipped: "\u23ED\uFE0F", not_applicable: "\u2796" };
|
|
27931
|
-
}
|
|
27932
|
-
});
|
|
27933
|
-
|
|
27934
28281
|
// ../rules/src/fixes/conflicts.ts
|
|
27935
28282
|
var init_conflicts = __esm({
|
|
27936
28283
|
"../rules/src/fixes/conflicts.ts"() {
|
|
@@ -28435,10 +28782,35 @@ function onlineEngine(client) {
|
|
|
28435
28782
|
return { mode: "online", run: (kind, body) => client.sdk.call(operationFor2(kind), body) };
|
|
28436
28783
|
}
|
|
28437
28784
|
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");
|
|
28785
|
+
return text.replace(/Pass every plan the page sells \(plans: \[\.\.\.\]\)/g, 'Set "plan": "all" on this page entry in checkbox.json (every plan the page sells)').replace(/Pass plans with every plan the page sells/g, 'Set "plan": "all" on this page entry in checkbox.json').replace(/\(page_kind "checkout"\)/g, '(a pages entry with "kind": "checkout")').replace(/page_kind "checkout"/g, '"kind": "checkout"').replace(/check the renewal (text|text itself) with check_disclosure/g, 'check the renewal $1 with a "disclosures" entry').replace(/\bcheck_disclosure\b/g, 'a "disclosures" entry').replace(/pass merchant\.price \(e\.g\. 19\.99\), merchant\.currency and merchant\.interval/g, 'add the plan to "plans" in checkbox.json (price, currency, interval) and name it in "plan" on this entry').replace(/merchant\.price \+ merchant\.interval/g, 'a plan in "plans" of checkbox.json (and "plan" on this entry)').replace(/pass merchant\.price and merchant\.interval/gi, 'add the plan to "plans" in checkbox.json and name it in "plan" on this entry').replace(/merchant\.intro_percent_off \(or intro_amount_off\) and merchant\.intro_periods/g, "intro_percent_off (or intro_amount_off) and intro_periods on the plan in checkbox.json").replace(/merchant\.intro_\*/g, "the plan's intro_* in checkbox.json").replace(/merchant\.trial_days \((\d+)\)/g, "trial_days ($1) of the plan in checkbox.json").replace(/merchant\.price was ignored/g, "the plan's price in checkbox.json was ignored").replace(/comes from merchant\.price and is not verified against the Price\. Connect Stripe on the site, or register the params with createStripeCheckoutConfig on a site whose Stripe account has this Price\./g, "comes from the plan's price in checkbox.json and is not verified against the Price. Connect Stripe on the site in Checkbox, then run checkbox ci --online (CHECKBOX_SECRET_KEY) so Checkbox reads the Price.").replace(/comes from merchant\.price\b/g, "comes from the plan's price in checkbox.json").replace(/\bmerchant\.price says\b/g, "The plan's price in checkbox.json says").replace(/\(or pass merchant\.cancel_url\)/g, "(or set merchant.cancel_url in checkbox.json)").replace(/In test mode, pass the page source as html\./g, 'With a test key, list the built page under "file" in checkbox.json instead of "url" (or run --online with a live key, ck_live_\u2026).');
|
|
28786
|
+
}
|
|
28787
|
+
function pricingPageFixes(fixes) {
|
|
28788
|
+
const reworded = fixes.map(
|
|
28789
|
+
(f) => f.replace(/,? before the card and email fields and the (?:pay|purchase) button/g, ", next to the plan's button").replace(/,? before the card and email fields/g, ", next to the plan's button")
|
|
28790
|
+
);
|
|
28791
|
+
const out = [];
|
|
28792
|
+
const renewalLineFor = /* @__PURE__ */ new Set();
|
|
28793
|
+
for (const f of reworded) {
|
|
28794
|
+
const card = /^(Plan card [^:]+): /.exec(f)?.[1] ?? "";
|
|
28795
|
+
const isRenewalLine = /\b(?:Add one line under the price|Put the renewal line under the price|Move the renewal line under the price|Add your own renewal line next to the (?:pay|plan's) button)/.test(f);
|
|
28796
|
+
if (isRenewalLine) {
|
|
28797
|
+
const cards = [...card.matchAll(/“([^”]+)”/g)].map((m) => m[1]);
|
|
28798
|
+
const fresh = cards.filter((c) => !renewalLineFor.has(c));
|
|
28799
|
+
if (cards.length ? !fresh.length : renewalLineFor.has(card)) continue;
|
|
28800
|
+
if (!cards.length) renewalLineFor.add(card);
|
|
28801
|
+
for (const c of fresh) renewalLineFor.add(c);
|
|
28802
|
+
if (cards.length && fresh.length < cards.length) {
|
|
28803
|
+
const g = `Plan card ${fresh.map((c) => `\u201C${c}\u201D`).join(", ")}: ${f.slice(card.length + 2)}`;
|
|
28804
|
+
if (!out.includes(g)) out.push(g);
|
|
28805
|
+
continue;
|
|
28806
|
+
}
|
|
28807
|
+
}
|
|
28808
|
+
if (!out.includes(f)) out.push(f);
|
|
28809
|
+
}
|
|
28810
|
+
return out;
|
|
28439
28811
|
}
|
|
28440
28812
|
function stripeConsentFix(text, params) {
|
|
28441
|
-
if (!/^Add one unchecked checkbox/.test(text)) return text;
|
|
28813
|
+
if (!/^(Ask for a separate yes to the renewal|Add one unchecked checkbox)/.test(text)) return text;
|
|
28442
28814
|
const cc = params?.consent_collection;
|
|
28443
28815
|
if (cc?.terms_of_service === "required") return text;
|
|
28444
28816
|
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.';
|
|
@@ -28457,12 +28829,23 @@ function fixesOf(r, onlyNeedsFix = false) {
|
|
|
28457
28829
|
failing.sort((a, b) => (a.compliance_status === "needs_fix" ? 0 : 1) - (b.compliance_status === "needs_fix" ? 0 : 1) || (RANK2[a.status] ?? 2) - (RANK2[b.status] ?? 2));
|
|
28458
28830
|
for (const c of failing) {
|
|
28459
28831
|
const base = c.compliance?.fix || c.fix || c.compliance?.question?.text || c.explanation || c.title || c.check_id;
|
|
28460
|
-
|
|
28832
|
+
let cards = failingCards(c.explanation);
|
|
28833
|
+
const plain = cards.length > 1 ? cardClauses(c.explanation).filter((x) => /look like the surrounding text/.test(x.clause)).map((x) => x.card) : [];
|
|
28834
|
+
if (plain.length && plain.length < cards.length && /renewal line/i.test(base)) {
|
|
28835
|
+
cards = cards.filter((x) => !plain.includes(x));
|
|
28836
|
+
const standOut = `Plan card ${plain.map((x) => `\u201C${x}\u201D`).join(", ")}: ${STAND_OUT_FIX}`;
|
|
28837
|
+
if (!out.includes(standOut)) out.push(standOut);
|
|
28838
|
+
}
|
|
28461
28839
|
const text = cards.length && base ? `Plan card ${cards.map((x) => `\u201C${x}\u201D`).join(", ")}: ${base}` : base;
|
|
28462
28840
|
if (text && !out.includes(text)) out.push(text);
|
|
28463
28841
|
}
|
|
28464
28842
|
return out;
|
|
28465
28843
|
}
|
|
28844
|
+
function cardClauses(explanation) {
|
|
28845
|
+
if (!explanation) return [];
|
|
28846
|
+
const ms = [...explanation.matchAll(/plan button “([^”]{1,80})”(?: in the card “([^”]{1,80})”)?/g)];
|
|
28847
|
+
return ms.map((m, i) => ({ card: m[2] ?? m[1], clause: explanation.slice(m.index + m[0].length, ms[i + 1]?.index ?? explanation.length) }));
|
|
28848
|
+
}
|
|
28466
28849
|
function failingCards(explanation) {
|
|
28467
28850
|
if (!explanation) return [];
|
|
28468
28851
|
if (/\(\d+ checkout page views? checked\) — e\.g\./.test(explanation)) return [];
|
|
@@ -28596,7 +28979,9 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28596
28979
|
if (alerts.length) Object.assign(base, { alerts });
|
|
28597
28980
|
const status = ciStatusOf(report);
|
|
28598
28981
|
const params = job.type === "stripe_checkout" ? job.body?.params ?? void 0 : void 0;
|
|
28599
|
-
|
|
28982
|
+
let fixes = (status === "pass" || status === "not_applicable" ? [] : fixesOf(report)).map((f) => ciVocabulary(job.type === "stripe_checkout" ? stripeConsentFix(f, params) : f));
|
|
28983
|
+
const pricingPage = job.type === "page" && job.body?.page_kind === "pricing";
|
|
28984
|
+
if (pricingPage) fixes = pricingPageFixes(fixes);
|
|
28600
28985
|
const unreadText = (job.unresolved ?? []).filter((p) => /^(?:custom_text|consent_collection|discounts|subscription_data|allow_promotion_codes|payment_method_collection)\b/.test(p));
|
|
28601
28986
|
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)));
|
|
28602
28987
|
if (unread) {
|
|
@@ -28621,28 +29006,47 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28621
29006
|
const line2 = needles.length ? job.locate(needles) : void 0;
|
|
28622
29007
|
if (line2) base.target = { ...base.target, line: line2 };
|
|
28623
29008
|
}
|
|
28624
|
-
const
|
|
29009
|
+
const declaredCoupon = !unread && status === "pass" && job.type === "stripe_checkout" ? (report.warnings ?? []).find((w) => w.code === "coupon_unverified" && /merchant\.intro_\* and are not verified/.test(w.message)) : void 0;
|
|
29010
|
+
if (declaredCoupon) {
|
|
29011
|
+
const id = /discounts use (coupon|promotion code) (\S+?),/.exec(declaredCoupon.message);
|
|
29012
|
+
const what = id ? `${id[1]} ${id[2]}` : "the coupon";
|
|
29013
|
+
fixes.unshift(
|
|
29014
|
+
engine.mode === "offline" ? `The intro price is declared in checkbox.json, not verified: offline, Checkbox cannot read ${what} from Stripe. Run with --online (CHECKBOX_SECRET_KEY, and the site's Stripe key connected) so Checkbox reads it and compares its percent / amount off and duration with the declared intro.` : `The intro price is declared in checkbox.json, not verified: Checkbox could not read ${what} through the site's connected Stripe key (is the site known, via --site or CHECKBOX_SITE, and does the key have Coupons: Read?). Until it is read, its percent / amount off and duration are not compared with the declared intro.`
|
|
29015
|
+
);
|
|
29016
|
+
}
|
|
29017
|
+
const sharedReview = !unread && !declaredCoupon && status === "pass" && job.type === "stripe_checkout" ? stripeParamsVerdict(report) : void 0;
|
|
29018
|
+
const unreadCoupon = sharedReview?.verdict === "needs_review" && sharedReview.reason ? sharedReview.reason : void 0;
|
|
29019
|
+
if (unreadCoupon) {
|
|
29020
|
+
const id = /discounts use (coupon|promotion code) (\S+?),/.exec(unreadCoupon);
|
|
29021
|
+
const what = id ? `${id[1]} ${id[2]}` : "the coupon";
|
|
29022
|
+
fixes.unshift(
|
|
29023
|
+
engine.mode === "offline" ? `The params apply ${what}, which Checkbox cannot read offline, so its discount and duration (what the shopper is charged first) are not checked. Declare the intro on the plan in checkbox.json (intro_percent_off, intro_amount_off or intro_price, with intro_periods) and state it in custom_text.submit, or run with --online so Checkbox reads the coupon.` : `The params apply ${what}, which Checkbox could not read through the site's connected Stripe key (is the site known, via --site or CHECKBOX_SITE, and does the key have Coupons: Read?), so its discount and duration are not checked. Declare the intro on the plan in checkbox.json and state it in custom_text.submit, or let Checkbox read the coupon.`
|
|
29024
|
+
);
|
|
29025
|
+
}
|
|
29026
|
+
const finalStatus = unread ? literal.length ? "fail" : "needs_review" : declaredCoupon || unreadCoupon ? "needs_review" : status;
|
|
28625
29027
|
const corrected = unread ? void 0 : correctedOf(job.type, report, job.partial, job.placeholders);
|
|
28626
29028
|
if (corrected?.format === "json" && job.placeholders) {
|
|
28627
29029
|
const unset = job.warnings.filter((w) => /(?:was|were) not set when Checkbox ran/.test(w));
|
|
28628
29030
|
if (unset.length) Object.assign(base, { alerts: [...unset, ...base.alerts ?? []] });
|
|
28629
29031
|
}
|
|
28630
|
-
const blocking = unread || status === "needs_review" && (job.type === "page" || job.type === "disclosure") && (report.compliance_status === "incomplete" || report.compliance_status === "not_checked") ||
|
|
29032
|
+
const blocking = unread || status === "needs_review" && (job.type === "page" || job.type === "disclosure") && (report.compliance_status === "incomplete" || report.compliance_status === "not_checked") || // Round 31 (pilot 30 item 2, headless-agent-pr false pass: a "strict" entry with a declared-but-unverified coupon exited
|
|
29033
|
+
// 0): the entry's strictness applies to the status the check reports (finalStatus), not the engine's raw verdict.
|
|
29034
|
+
finalStatus === "needs_review" && job.type === "stripe_checkout" && (!!job.strict || !!offer);
|
|
28631
29035
|
return {
|
|
28632
29036
|
...base,
|
|
28633
29037
|
status: finalStatus,
|
|
28634
29038
|
...blocking ? { blocking: true } : {},
|
|
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),
|
|
29039
|
+
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : unreadCoupon ? "Needs review \xB7 coupon not read" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary)) : void 0),
|
|
28636
29040
|
...fixes[0] ? { fix: fixes[0] } : {},
|
|
28637
29041
|
fixes,
|
|
28638
29042
|
...corrected ? { corrected } : {},
|
|
28639
29043
|
report
|
|
28640
29044
|
};
|
|
28641
29045
|
}
|
|
28642
|
-
function countedLabel(label, r) {
|
|
29046
|
+
function countedLabel(label, r, adjust) {
|
|
28643
29047
|
const m = /^(\d+) fix(?:es)? needed/.exec(label);
|
|
28644
29048
|
if (!m) return label;
|
|
28645
|
-
const n = fixesOf(r, true).length;
|
|
29049
|
+
const n = (adjust ? adjust(fixesOf(r, true)) : fixesOf(r, true)).length;
|
|
28646
29050
|
if (!n || n === Number(m[1])) return label;
|
|
28647
29051
|
return label.replace(m[0], `${n} fix${n === 1 ? "" : "es"} needed`);
|
|
28648
29052
|
}
|
|
@@ -28652,6 +29056,7 @@ function exitCodeOf(checks, strict) {
|
|
|
28652
29056
|
if (checks.some((c) => c.status === "error")) return { status: "error", exit: EXIT.usage };
|
|
28653
29057
|
if (checks.some((c) => c.status === "fail")) return { status: "fail", exit: EXIT.fail };
|
|
28654
29058
|
if (checks.some((c) => c.status === "needs_review")) return { status: "needs_review", exit: strict || checks.some((c) => c.blocking) ? EXIT.needs_review : EXIT.ok };
|
|
29059
|
+
if (!checks.some((c) => c.status === "pass")) return { status: "skipped", exit: EXIT.needs_review };
|
|
28655
29060
|
return { status: "pass", exit: EXIT.ok };
|
|
28656
29061
|
}
|
|
28657
29062
|
function mergeSourcePatches(checks) {
|
|
@@ -28673,6 +29078,11 @@ function mergeSourcePatches(checks) {
|
|
|
28673
29078
|
}
|
|
28674
29079
|
}
|
|
28675
29080
|
}
|
|
29081
|
+
function nothingCheckedMessage(run2) {
|
|
29082
|
+
const urlSkipped = run2.checks.some((c) => c.status === "skipped" && /^URL pages/.test(c.message ?? ""));
|
|
29083
|
+
const how = urlSkipped ? run2.mode === "offline" ? ' Page entries with a "url" are fetched only with --online and an API key (CHECKBOX_SECRET_KEY); offline, point them at the built HTML with "file" instead.' : ' Page entries with a "url" need a key that can read that site (a live key for a live site); or point them at the built HTML with "file".' : ' Add a check checkbox.json can run (a "file" page, a disclosure, Stripe Checkout params or an email).';
|
|
29084
|
+
return `Nothing was checked: every check was skipped or does not apply, so this run is not a pass (exit 2).${how}`;
|
|
29085
|
+
}
|
|
28676
29086
|
async function runCi(jobs, engine, opts) {
|
|
28677
29087
|
const checks = [...opts.extra ?? []];
|
|
28678
29088
|
for (const j of jobs) checks.push(await runJob(j, engine, opts.config, { ...opts.notices ? { notices: opts.notices } : {} }));
|
|
@@ -28697,7 +29107,7 @@ async function runCi(jobs, engine, opts) {
|
|
|
28697
29107
|
disclaimer: CI_DISCLAIMER
|
|
28698
29108
|
};
|
|
28699
29109
|
}
|
|
28700
|
-
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL, ALERT_CODES;
|
|
29110
|
+
var ENGINE_KIND, CI_DISCLAIMER, RANK2, OK_STATUSES, STAND_OUT_FIX, NOTICE_TO_EMAIL, EMAIL_LABEL, DEFAULT_LABEL, ALERT_CODES;
|
|
28701
29111
|
var init_runner = __esm({
|
|
28702
29112
|
"src/ci/runner.ts"() {
|
|
28703
29113
|
"use strict";
|
|
@@ -28705,6 +29115,7 @@ var init_runner = __esm({
|
|
|
28705
29115
|
init_http();
|
|
28706
29116
|
init_src();
|
|
28707
29117
|
init_types2();
|
|
29118
|
+
init_stripe_verdict();
|
|
28708
29119
|
init_config3();
|
|
28709
29120
|
ENGINE_KIND = {
|
|
28710
29121
|
stripe_checkout: "stripe_checkout_session",
|
|
@@ -28716,6 +29127,7 @@ var init_runner = __esm({
|
|
|
28716
29127
|
CI_DISCLAIMER = "Automated check against Checkbox's catalog of automatic-renewal laws; not legal advice.";
|
|
28717
29128
|
RANK2 = { fail: 0, needs_review: 1 };
|
|
28718
29129
|
OK_STATUSES = /* @__PURE__ */ new Set(["compliant", "pending_live_check", "verified_live", "not_applicable"]);
|
|
29130
|
+
STAND_OUT_FIX = "Make its renewal line stand out from the text around it: larger type, bold (weight 600 or more), a contrasting colour, or a border.";
|
|
28719
29131
|
NOTICE_TO_EMAIL = {
|
|
28720
29132
|
acknowledgment: "acknowledgment",
|
|
28721
29133
|
trial_ending: "trial_reminder",
|
|
@@ -28738,8 +29150,112 @@ var init_runner = __esm({
|
|
|
28738
29150
|
}
|
|
28739
29151
|
});
|
|
28740
29152
|
|
|
29153
|
+
// src/ci/report.ts
|
|
29154
|
+
function icon(s2, c) {
|
|
29155
|
+
if (s2 === "pass") return c.green("\u2713");
|
|
29156
|
+
if (s2 === "fail" || s2 === "error") return c.red("\u2717");
|
|
29157
|
+
if (s2 === "needs_review") return c.yellow("?");
|
|
29158
|
+
return c.dim("\u2013");
|
|
29159
|
+
}
|
|
29160
|
+
function overallLine(run2) {
|
|
29161
|
+
const s2 = run2.summary;
|
|
29162
|
+
const parts = [];
|
|
29163
|
+
if (s2.error) parts.push(`${s2.error} could not run`);
|
|
29164
|
+
if (s2.fail) parts.push(`${s2.fail} need${s2.fail === 1 ? "s" : ""} a fix`);
|
|
29165
|
+
if (s2.needs_review) parts.push(`${s2.needs_review} need${s2.needs_review === 1 ? "s" : ""} review${run2.strict ? " (strict)" : ""}`);
|
|
29166
|
+
if (s2.pass) parts.push(`${s2.pass} pass`);
|
|
29167
|
+
if (s2.not_applicable) parts.push(`${s2.not_applicable} not required`);
|
|
29168
|
+
if (s2.skipped) parts.push(`${s2.skipped} skipped`);
|
|
29169
|
+
return parts.join(" \xB7 ") || "nothing checked";
|
|
29170
|
+
}
|
|
29171
|
+
function renderHuman(run2, c, opts = {}) {
|
|
29172
|
+
const lines = [];
|
|
29173
|
+
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
29174
|
+
lines.push(`${c.bold("Checkbox CI")} ${run2.config} \xB7 ${run2.mode}${versions} \xB7 ${run2.checks.length} check${run2.checks.length === 1 ? "" : "s"}`);
|
|
29175
|
+
const width = Math.min(40, Math.max(...run2.checks.map((x) => x.name.length), 10));
|
|
29176
|
+
for (const x of run2.checks) {
|
|
29177
|
+
const label = x.status === "fail" || x.status === "error" ? c.red(x.label) : x.status === "needs_review" ? c.yellow(x.label) : x.status === "pass" ? c.green(x.label) : c.dim(x.label);
|
|
29178
|
+
lines.push(` ${icon(x.status, c)} ${x.name.padEnd(width)} \u2192 ${label}`);
|
|
29179
|
+
if (x.message) lines.push(c.dim(` ${wrap(x.message, 110, " ")}`));
|
|
29180
|
+
if (x.fix) lines.push(` fix: ${wrap(x.fix, 110, " ")}${c.dim(` (${where(x)})`)}`);
|
|
29181
|
+
if (opts.verbose) for (const f of x.fixes.slice(1)) lines.push(` fix: ${wrap(f, 110, " ")}`);
|
|
29182
|
+
const alerts = x.alerts ?? [];
|
|
29183
|
+
for (const a of alerts) lines.push(c.yellow(` warning: ${wrap(a, 110, " ")}`));
|
|
29184
|
+
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, " ")}`));
|
|
29185
|
+
if (x.corrected && opts.verbose) lines.push(c.dim(` ${x.corrected.title}:`), ...x.corrected.body.split("\n").map((l) => c.dim(` ${l}`)));
|
|
29186
|
+
}
|
|
29187
|
+
const tone = run2.status === "pass" ? c.green : run2.status === "needs_review" && run2.exit_code === 0 ? c.yellow : c.red;
|
|
29188
|
+
if (run2.status === "skipped") lines.push("", c.red(wrap(nothingCheckedMessage(run2), 110, "")));
|
|
29189
|
+
lines.push("", `${tone(overallLine(run2))} \xB7 exit ${run2.exit_code}`);
|
|
29190
|
+
if (run2.checks.some((x) => x.corrected) && !opts.verbose) lines.push(c.dim("Corrected params / text: --verbose, --json, or the job summary in GitHub Actions."));
|
|
29191
|
+
lines.push(c.dim(run2.disclaimer));
|
|
29192
|
+
return lines;
|
|
29193
|
+
}
|
|
29194
|
+
function annotations(run2) {
|
|
29195
|
+
const out = [];
|
|
29196
|
+
for (const x of run2.checks) {
|
|
29197
|
+
const alerts = x.alerts ?? [];
|
|
29198
|
+
const level = x.status === "fail" || x.status === "error" ? "error" : x.status === "needs_review" ? run2.strict || x.blocking ? "error" : "warning" : alerts.length ? "warning" : void 0;
|
|
29199
|
+
if (!level) continue;
|
|
29200
|
+
const props = [`file=${escProp(x.target.file)}`, ...x.target.line ? [`line=${x.target.line}`] : [], `title=${escProp(`Checkbox: ${x.name} \u2014 ${x.label}`)}`];
|
|
29201
|
+
const body = x.status === "error" ? x.fix ? `${x.message ?? ""}
|
|
29202
|
+
${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");
|
|
29203
|
+
out.push(`::${level} ${props.join(",")}::${escData(body)}`);
|
|
29204
|
+
}
|
|
29205
|
+
if (run2.status === "skipped") out.push(`::error file=${escProp(run2.config)},title=${escProp("Checkbox: nothing was checked")}::${escData(nothingCheckedMessage(run2))}`);
|
|
29206
|
+
return out;
|
|
29207
|
+
}
|
|
29208
|
+
function stepSummary(run2) {
|
|
29209
|
+
const lines = [];
|
|
29210
|
+
lines.push("## Checkbox compliance checks", "");
|
|
29211
|
+
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
29212
|
+
lines.push(`**${overallLine(run2)}** \u2014 \`${run2.config}\` \xB7 ${run2.mode}${versions}`, "");
|
|
29213
|
+
if (run2.status === "skipped") lines.push(`\u274C ${nothingCheckedMessage(run2)}`, "");
|
|
29214
|
+
lines.push("| Check | Status | Fix | File |", "|---|---|---|---|");
|
|
29215
|
+
for (const x of run2.checks) {
|
|
29216
|
+
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 : "";
|
|
29217
|
+
const fix = [fix0, ...(x.alerts ?? []).map((a) => `\u26A0\uFE0F ${a}`)].filter(Boolean).join("\n");
|
|
29218
|
+
lines.push(`| ${STATUS_MD[x.status]} ${cell(x.name)}<br><sub>${TYPE_LABEL[x.type]}</sub> | ${cell(x.label)} | ${cell(fix)} | \`${cell(where(x))}\` |`);
|
|
29219
|
+
}
|
|
29220
|
+
const detailed = run2.checks.filter((x) => x.corrected || x.fixes.length > 1);
|
|
29221
|
+
for (const x of detailed) {
|
|
29222
|
+
lines.push("", `<details><summary><b>${escapeHtml(x.name)}</b>: ${escapeHtml(x.corrected ? x.corrected.title.toLowerCase() : `${x.fixes.length} fixes`)}</summary>`, "");
|
|
29223
|
+
if (x.fixes.length) lines.push(...x.fixes.map((f) => `- ${f}`), "");
|
|
29224
|
+
if (x.corrected) {
|
|
29225
|
+
const fence = x.corrected.body.includes("```") ? "````" : "```";
|
|
29226
|
+
lines.push(`${fence}${x.corrected.format === "json" ? "json" : "text"}`, x.corrected.body, fence, "");
|
|
29227
|
+
}
|
|
29228
|
+
lines.push("</details>");
|
|
29229
|
+
}
|
|
29230
|
+
lines.push("", `<sub>${run2.disclaimer} Docs: https://checkbox.site/docs/guides/ci</sub>`, "");
|
|
29231
|
+
return lines.join("\n");
|
|
29232
|
+
}
|
|
29233
|
+
function escapeHtml(s2) {
|
|
29234
|
+
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
29235
|
+
}
|
|
29236
|
+
var TYPE_LABEL, where, escData, escProp, cell, STATUS_MD;
|
|
29237
|
+
var init_report = __esm({
|
|
29238
|
+
"src/ci/report.ts"() {
|
|
29239
|
+
"use strict";
|
|
29240
|
+
init_output();
|
|
29241
|
+
init_runner();
|
|
29242
|
+
TYPE_LABEL = {
|
|
29243
|
+
stripe_checkout: "Stripe Checkout",
|
|
29244
|
+
disclosure: "Disclosure",
|
|
29245
|
+
page: "Page",
|
|
29246
|
+
email: "Email",
|
|
29247
|
+
notice_schedule: "Notice schedule"
|
|
29248
|
+
};
|
|
29249
|
+
where = (c) => `${c.target.file}${c.target.line && c.target.line > 1 ? `:${c.target.line}` : ""}`;
|
|
29250
|
+
escData = (s2) => s2.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
29251
|
+
escProp = (s2) => escData(s2).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
29252
|
+
cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
29253
|
+
STATUS_MD = { pass: "\u2705", fail: "\u274C", needs_review: "\u26A0\uFE0F", error: "\u274C", skipped: "\u23ED\uFE0F", not_applicable: "\u2796" };
|
|
29254
|
+
}
|
|
29255
|
+
});
|
|
29256
|
+
|
|
28741
29257
|
// src/commands/plan/lib.ts
|
|
28742
|
-
import { dirname as
|
|
29258
|
+
import { dirname as dirname6, resolve as resolve6 } from "node:path";
|
|
28743
29259
|
async function resolveSite(ctx, flag, client) {
|
|
28744
29260
|
const explicit = flag ?? ctx.env.CHECKBOX_SITE;
|
|
28745
29261
|
if (explicit) {
|
|
@@ -28747,10 +29263,10 @@ async function resolveSite(ctx, flag, client) {
|
|
|
28747
29263
|
return explicit;
|
|
28748
29264
|
}
|
|
28749
29265
|
const projects = ctx.config.load().init?.projects ?? {};
|
|
28750
|
-
for (let dir =
|
|
29266
|
+
for (let dir = resolve6(ctx.cwd); ; dir = dirname6(dir)) {
|
|
28751
29267
|
const site = projects[dir]?.site;
|
|
28752
29268
|
if (site) return site;
|
|
28753
|
-
if (
|
|
29269
|
+
if (dirname6(dir) === dir) break;
|
|
28754
29270
|
}
|
|
28755
29271
|
const list2 = await client.sdk.sites.list({ limit: 3 });
|
|
28756
29272
|
const sites = list2?.data ?? [];
|
|
@@ -28807,12 +29323,28 @@ var init_lib = __esm({
|
|
|
28807
29323
|
});
|
|
28808
29324
|
|
|
28809
29325
|
// src/ci/workflow.ts
|
|
28810
|
-
|
|
28811
|
-
|
|
28812
|
-
|
|
28813
|
-
|
|
28814
|
-
|
|
28815
|
-
|
|
29326
|
+
import { execFileSync } from "node:child_process";
|
|
29327
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
29328
|
+
import { join as join8 } from "node:path";
|
|
29329
|
+
function workflowYaml(needs = {}) {
|
|
29330
|
+
const node = !!(needs.install || needs.build);
|
|
29331
|
+
const steps = [" - uses: actions/checkout@v4"];
|
|
29332
|
+
if (node) {
|
|
29333
|
+
steps.push(" - uses: actions/setup-node@v4", " with:", " node-version: 20");
|
|
29334
|
+
if (needs.install) steps.push(` # ${needs.why?.install ?? 'checkbox.json "module" entries import your code and its dependencies.'}`, ` - run: ${needs.install}`);
|
|
29335
|
+
if (needs.build) steps.push(` # ${needs.why?.build ?? 'checkbox.json "pages" read built HTML, which a fresh clone does not have.'}`, ` - run: ${needs.build}`);
|
|
29336
|
+
} else {
|
|
29337
|
+
steps.push(' # Only needed when checkbox.json uses "module" entries that import your dependencies:', " # - run: npm ci");
|
|
29338
|
+
}
|
|
29339
|
+
steps.push(" - uses: checkbox-site/checkbox@v1", " # with:", " # strict: true # also fail when a check needs review");
|
|
29340
|
+
if (needs.urlPages) {
|
|
29341
|
+
steps.push(
|
|
29342
|
+
' # args: --online # "url" pages are fetched by the API: needs a LIVE key (ck_live_\u2026) on an',
|
|
29343
|
+
' # # activated account. Without one, list the built page as "file" instead.'
|
|
29344
|
+
);
|
|
29345
|
+
} else steps.push(" # args: --online # URL pages via the API; needs the env below");
|
|
29346
|
+
steps.push(" # env:", " # CHECKBOX_SECRET_KEY: ${{ secrets.CHECKBOX_SECRET_KEY }}");
|
|
29347
|
+
return `# Checkbox: automatic-renewal law checks on every pull request (https://checkbox.site/docs/guides/ci).
|
|
28816
29348
|
# Runs the checks listed in checkbox.json with the Checkbox rules engine, offline (no key, no network), annotates the
|
|
28817
29349
|
# lines that need a fix and writes a summary with the corrected params / text to the job page.
|
|
28818
29350
|
name: Checkbox
|
|
@@ -28830,35 +29362,85 @@ jobs:
|
|
|
28830
29362
|
runs-on: ubuntu-latest
|
|
28831
29363
|
timeout-minutes: 10
|
|
28832
29364
|
steps:
|
|
28833
|
-
|
|
28834
|
-
# Only needed when checkbox.json uses "module" entries that import your dependencies:
|
|
28835
|
-
# - run: npm ci
|
|
28836
|
-
- uses: checkbox-site/checkbox@v1
|
|
28837
|
-
# with:
|
|
28838
|
-
# strict: true # also fail when a check needs review
|
|
28839
|
-
# args: --online # URL pages via the API; needs the env below
|
|
28840
|
-
# env:
|
|
28841
|
-
# CHECKBOX_SECRET_KEY: \${{ secrets.CHECKBOX_SECRET_KEY }}
|
|
29365
|
+
${steps.join("\n")}
|
|
28842
29366
|
`;
|
|
29367
|
+
}
|
|
29368
|
+
function installCommand(root) {
|
|
29369
|
+
if (existsSync7(join8(root, "pnpm-lock.yaml"))) return "corepack enable && pnpm install --frozen-lockfile";
|
|
29370
|
+
if (existsSync7(join8(root, "yarn.lock"))) return "corepack enable && yarn install --immutable";
|
|
29371
|
+
if (existsSync7(join8(root, "bun.lockb")) || existsSync7(join8(root, "bun.lock"))) return "npx --yes bun install --frozen-lockfile";
|
|
29372
|
+
if (existsSync7(join8(root, "package-lock.json")) || existsSync7(join8(root, "npm-shrinkwrap.json"))) return "npm ci";
|
|
29373
|
+
return "npm install";
|
|
29374
|
+
}
|
|
29375
|
+
function runScript(root, script) {
|
|
29376
|
+
if (existsSync7(join8(root, "pnpm-lock.yaml"))) return `pnpm run ${script}`;
|
|
29377
|
+
if (existsSync7(join8(root, "yarn.lock"))) return `yarn run ${script}`;
|
|
29378
|
+
return `npm run ${script}`;
|
|
29379
|
+
}
|
|
29380
|
+
function notInClone(root, files) {
|
|
29381
|
+
if (!files.length) return [];
|
|
29382
|
+
let tracked = null;
|
|
29383
|
+
try {
|
|
29384
|
+
const out = execFileSync("git", ["ls-files", "-z", "--", ...files], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
|
|
29385
|
+
tracked = new Set(out.split("\0").filter(Boolean));
|
|
29386
|
+
} catch {
|
|
29387
|
+
tracked = null;
|
|
29388
|
+
}
|
|
29389
|
+
return files.filter((f) => tracked ? !tracked.has(f.replace(/^\.\//, "")) : !existsSync7(join8(root, f)));
|
|
29390
|
+
}
|
|
29391
|
+
function workflowNeeds(root, config) {
|
|
29392
|
+
const checks = config.checks ?? {};
|
|
29393
|
+
const list2 = (k) => Array.isArray(checks[k]) ? checks[k] : [];
|
|
29394
|
+
const modules = [...list2("stripe_checkout"), ...list2("emails"), ...list2("disclosures")].some((e) => typeof e.module === "string" && !!e.module);
|
|
29395
|
+
const pageFiles = list2("pages").flatMap((e) => typeof e.file === "string" && !e.file.startsWith("TODO") ? [e.file] : []);
|
|
29396
|
+
const urlPages = list2("pages").some((e) => typeof e.url === "string");
|
|
29397
|
+
let pkg = null;
|
|
29398
|
+
try {
|
|
29399
|
+
pkg = JSON.parse(readFileSync9(join8(root, "package.json"), "utf8"));
|
|
29400
|
+
} catch {
|
|
29401
|
+
pkg = null;
|
|
29402
|
+
}
|
|
29403
|
+
const built = notInClone(root, pageFiles);
|
|
29404
|
+
const buildScript = pkg?.scripts?.build ? "build" : void 0;
|
|
29405
|
+
const needs = { why: {} };
|
|
29406
|
+
if (built.length && buildScript) {
|
|
29407
|
+
needs.build = runScript(root, buildScript);
|
|
29408
|
+
needs.why.build = `checkbox.json "pages" read ${built.slice(0, 2).join(", ")}${built.length > 2 ? ", \u2026" : ""}, built here (not in git).`;
|
|
29409
|
+
}
|
|
29410
|
+
if (pkg && (modules || needs.build)) {
|
|
29411
|
+
needs.install = installCommand(root);
|
|
29412
|
+
needs.why.install = modules ? 'checkbox.json "module" entries import your code and its dependencies.' : "The build needs your dependencies.";
|
|
29413
|
+
}
|
|
29414
|
+
if (urlPages) needs.urlPages = true;
|
|
29415
|
+
return needs;
|
|
29416
|
+
}
|
|
29417
|
+
var WORKFLOW_PATH, WORKFLOW_YAML;
|
|
29418
|
+
var init_workflow = __esm({
|
|
29419
|
+
"src/ci/workflow.ts"() {
|
|
29420
|
+
"use strict";
|
|
29421
|
+
WORKFLOW_PATH = ".github/workflows/checkbox.yml";
|
|
29422
|
+
WORKFLOW_YAML = workflowYaml();
|
|
28843
29423
|
}
|
|
28844
29424
|
});
|
|
28845
29425
|
|
|
28846
29426
|
// src/commands/ci/index.ts
|
|
28847
29427
|
var ci_exports = {};
|
|
28848
29428
|
__export(ci_exports, {
|
|
29429
|
+
attachSiteForCoupons: () => attachSiteForCoupons,
|
|
29430
|
+
couponIdsIn: () => couponIdsIn,
|
|
28849
29431
|
default: () => ci_default,
|
|
28850
29432
|
touchesConfig: () => touchesConfig,
|
|
28851
29433
|
undiffableInputs: () => undiffableInputs
|
|
28852
29434
|
});
|
|
28853
|
-
import { execFileSync } from "node:child_process";
|
|
28854
|
-
import { appendFileSync, existsSync as
|
|
29435
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
29436
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
28855
29437
|
import { createInterface } from "node:readline";
|
|
28856
|
-
import { dirname as
|
|
29438
|
+
import { dirname as dirname7, join as join9, relative as relative5, resolve as resolve7 } from "node:path";
|
|
28857
29439
|
function gitChanged(cwd, base) {
|
|
28858
29440
|
try {
|
|
28859
|
-
const root =
|
|
28860
|
-
const out =
|
|
28861
|
-
return { root, files: out.split("\n").map((l) => l.trim()).filter(Boolean).map((f) =>
|
|
29441
|
+
const root = execFileSync2("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
29442
|
+
const out = execFileSync2("git", ["diff", "--name-only", `${base}...HEAD`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
29443
|
+
return { root, files: out.split("\n").map((l) => l.trim()).filter(Boolean).map((f) => resolve7(root, f)) };
|
|
28862
29444
|
} catch (err) {
|
|
28863
29445
|
const msg2 = String(err.stderr || err.message).trim().split("\n")[0];
|
|
28864
29446
|
return `git diff ${base}...HEAD failed (${msg2}); fetch the base branch (actions/checkout with fetch-depth: 0)`;
|
|
@@ -28868,13 +29450,13 @@ function undiffableInputs(loaded, jobs, gitRoot) {
|
|
|
28868
29450
|
const need = [];
|
|
28869
29451
|
const out = [];
|
|
28870
29452
|
for (const e of loaded.config.checks.pages ?? []) {
|
|
28871
|
-
if (e.source) need.push({ path:
|
|
29453
|
+
if (e.source) need.push({ path: resolve7(loaded.dir, e.source), label: `pages "${e.name}" source ${e.source}` });
|
|
28872
29454
|
else if (e.url) out.push(`pages "${e.name}" is a URL with no "source" file to diff`);
|
|
28873
|
-
else if (e.file) need.push({ path:
|
|
29455
|
+
else if (e.file) need.push({ path: resolve7(loaded.dir, e.file), label: `pages "${e.name}" file ${e.file}` });
|
|
28874
29456
|
}
|
|
28875
29457
|
for (const j of jobs) {
|
|
28876
29458
|
if (j.type === "page") continue;
|
|
28877
|
-
for (const f of j.inputs) need.push({ path:
|
|
29459
|
+
for (const f of j.inputs) need.push({ path: resolve7(loaded.dir, f), label: `"${j.name}" reads ${f}` });
|
|
28878
29460
|
}
|
|
28879
29461
|
if (!need.length) return out;
|
|
28880
29462
|
const real = (p) => {
|
|
@@ -28889,8 +29471,8 @@ function undiffableInputs(loaded, jobs, gitRoot) {
|
|
|
28889
29471
|
let tracked;
|
|
28890
29472
|
try {
|
|
28891
29473
|
const rels = [...new Set(need.map((n) => relative5(gitRoot, n.path)))];
|
|
28892
|
-
const listed =
|
|
28893
|
-
tracked = new Set(listed.split("\0").filter(Boolean).map((f) =>
|
|
29474
|
+
const listed = execFileSync2("git", ["ls-files", "-z", "--", ...rels], { cwd: gitRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
29475
|
+
tracked = new Set(listed.split("\0").filter(Boolean).map((f) => resolve7(gitRoot, f)));
|
|
28894
29476
|
} catch (err) {
|
|
28895
29477
|
return `git ls-files failed (${String(err.stderr || err.message).trim().split("\n")[0]})`;
|
|
28896
29478
|
}
|
|
@@ -28902,7 +29484,7 @@ function touchesConfig(loaded, jobs, changed) {
|
|
|
28902
29484
|
if (set.has(loaded.path)) return true;
|
|
28903
29485
|
const codeChanged = changed.some((f) => /\.(?:[cm]?[jt]sx?)$/.test(f));
|
|
28904
29486
|
for (const j of jobs) {
|
|
28905
|
-
for (const f of j.inputs) if (set.has(
|
|
29487
|
+
for (const f of j.inputs) if (set.has(resolve7(loaded.dir, f))) return true;
|
|
28906
29488
|
}
|
|
28907
29489
|
const hasModules = [...loaded.config.checks.stripe_checkout ?? [], ...loaded.config.checks.emails ?? []].some((e) => "module" in e && e.module);
|
|
28908
29490
|
return hasModules && codeChanged;
|
|
@@ -28927,24 +29509,26 @@ async function runInit(ctx, argv) {
|
|
|
28927
29509
|
const p = parseArgs(argv, { boolean: ["yes", "force", "dry-run", "workflow"], string: ["config"], alias: { y: "yes" } });
|
|
28928
29510
|
if (p.positionals.length) throw new UsageError(`Unexpected argument "${p.positionals[0]}"`);
|
|
28929
29511
|
const explicit = str(p.flags, "config");
|
|
28930
|
-
const configPath = explicit ?
|
|
28931
|
-
const workflowPath =
|
|
29512
|
+
const configPath = explicit ? resolve7(ctx.cwd, explicit) : CONFIG_FILES.map((f) => join9(ctx.cwd, f)).find((f) => existsSync8(f)) ?? join9(ctx.cwd, CONFIG_FILES[0]);
|
|
29513
|
+
const workflowPath = join9(ctx.cwd, WORKFLOW_PATH);
|
|
28932
29514
|
const force = bool(p.flags, "force") === true;
|
|
28933
29515
|
const withWorkflow = bool(p.flags, "workflow") !== false;
|
|
28934
29516
|
const projects = ctx.config.load().init?.projects ?? {};
|
|
28935
29517
|
let facts = {};
|
|
28936
|
-
for (let dir =
|
|
29518
|
+
for (let dir = resolve7(ctx.cwd); ; dir = dirname7(dir)) {
|
|
28937
29519
|
if (projects[dir]?.facts) {
|
|
28938
29520
|
facts = projects[dir].facts;
|
|
28939
29521
|
break;
|
|
28940
29522
|
}
|
|
28941
|
-
if (
|
|
29523
|
+
if (dirname7(dir) === dir) break;
|
|
28942
29524
|
}
|
|
28943
29525
|
const s2 = scaffoldConfig(ctx.cwd, ctx.env, facts);
|
|
28944
|
-
const actionFor = (f) =>
|
|
29526
|
+
const actionFor = (f) => existsSync8(f) ? force ? "overwrite" : "keep" : "create";
|
|
28945
29527
|
const files = [{ path: configPath, content: `${JSON.stringify(s2.config, null, 2)}
|
|
28946
29528
|
`, action: actionFor(configPath) }];
|
|
28947
|
-
|
|
29529
|
+
const needs = workflowNeeds(ctx.cwd, s2.config);
|
|
29530
|
+
const workflowSteps = [...needs.install ? [needs.install] : [], ...needs.build ? [needs.build] : []];
|
|
29531
|
+
if (withWorkflow) files.push({ path: workflowPath, content: workflowYaml(needs), action: actionFor(workflowPath) });
|
|
28948
29532
|
const toWrite = files.filter((f) => f.action !== "keep");
|
|
28949
29533
|
const dry = bool(p.flags, "dry-run") === true;
|
|
28950
29534
|
let write = !dry && toWrite.length > 0;
|
|
@@ -28956,7 +29540,7 @@ async function runInit(ctx, argv) {
|
|
|
28956
29540
|
}
|
|
28957
29541
|
if (write) {
|
|
28958
29542
|
for (const f of toWrite) {
|
|
28959
|
-
mkdirSync2(
|
|
29543
|
+
mkdirSync2(dirname7(f.path), { recursive: true });
|
|
28960
29544
|
writeFileSync2(f.path, f.content);
|
|
28961
29545
|
}
|
|
28962
29546
|
}
|
|
@@ -28967,6 +29551,7 @@ async function runInit(ctx, argv) {
|
|
|
28967
29551
|
kept: files.filter((f) => f.action === "keep").map((f) => relName(f.path)),
|
|
28968
29552
|
...write ? {} : { would_write: toWrite.map((f) => relName(f.path)) },
|
|
28969
29553
|
found: s2.found,
|
|
29554
|
+
...withWorkflow ? { workflow_steps: workflowSteps } : {},
|
|
28970
29555
|
todos: s2.todos,
|
|
28971
29556
|
config: s2.config,
|
|
28972
29557
|
next_action: s2.todos.length ? { type: "edit_config", message: `Fill in the ${s2.todos.length} TODO item(s) in ${relName(configPath)} (checkbox ci refuses TODO values), then run \`checkbox ci\` and fix what it reports.` } : { type: "run_ci", message: "Run `checkbox ci` locally, fix what it reports, then commit checkbox.json and the workflow." }
|
|
@@ -28976,6 +29561,8 @@ async function runInit(ctx, argv) {
|
|
|
28976
29561
|
for (const f of files) lines.push(`${f.action === "keep" ? ctx.out.c.dim("kept ") : write ? ctx.out.c.green(f.action === "create" ? "created" : "wrote ") : ctx.out.c.yellow("would write")} ${relName(f.path)}${f.action === "keep" ? ctx.out.c.dim(" (exists; --force overwrites)") : ""}`);
|
|
28977
29562
|
if (!write && toWrite.length && !dry) lines.push(ctx.out.c.dim("Nothing written: add --yes to write without asking."));
|
|
28978
29563
|
if (s2.found.length) lines.push("", "Found:", ...s2.found.map((f) => ` - ${f}`));
|
|
29564
|
+
if (withWorkflow && workflowSteps.length) lines.push("", `The workflow runs ${workflowSteps.map((c) => `\`${c}\``).join(" and ")} before the checks, so they work on a fresh clone.`);
|
|
29565
|
+
if (needs.urlPages) lines.push("", 'Note: "url" pages are fetched by the API (checkbox ci --online), which needs a LIVE key (ck_live_\u2026) on an activated account. Without one, list the built page as "file".');
|
|
28979
29566
|
if (s2.todos.length) lines.push("", "To do:", ...s2.todos.map((t) => ` - ${t}`));
|
|
28980
29567
|
lines.push("", `${ctx.out.c.cyan("Next:")} ${result.next_action.message}`);
|
|
28981
29568
|
if (!write) lines.push("", ctx.out.c.dim(`${relName(configPath)}:`), ...JSON.stringify(s2.config, null, 2).split("\n").map((l) => ctx.out.c.dim(` ${l}`)));
|
|
@@ -28997,6 +29584,22 @@ async function readNotices(ctx, client, siteFlag) {
|
|
|
28997
29584
|
return { error: `Could not read the Notice settings of ${site} (${err.message}).` };
|
|
28998
29585
|
}
|
|
28999
29586
|
}
|
|
29587
|
+
async function attachSiteForCoupons(ctx, client, jobs, siteFlag) {
|
|
29588
|
+
const withCoupon = jobs.filter((j) => j.type === "stripe_checkout" && j.body && couponIdsIn(j.body.params).length);
|
|
29589
|
+
if (!withCoupon.length) return;
|
|
29590
|
+
let site;
|
|
29591
|
+
try {
|
|
29592
|
+
site = await resolveSite(ctx, siteFlag, client);
|
|
29593
|
+
} catch (err) {
|
|
29594
|
+
for (const j of withCoupon) j.warnings.push(`The coupon was not read from Stripe: ${err.message} --online reads it through the site's connected Stripe key once it knows the site (--site or CHECKBOX_SITE).`);
|
|
29595
|
+
return;
|
|
29596
|
+
}
|
|
29597
|
+
for (const j of withCoupon) j.body = { ...j.body, site };
|
|
29598
|
+
}
|
|
29599
|
+
function couponIdsIn(params) {
|
|
29600
|
+
const discounts = params?.discounts;
|
|
29601
|
+
return (Array.isArray(discounts) ? discounts : []).flatMap((d) => d && typeof d === "object" && typeof d.coupon === "string" ? [d.coupon] : []);
|
|
29602
|
+
}
|
|
29000
29603
|
var HELP2, NOTICE_KIND_TO_EMAIL, ci_default;
|
|
29001
29604
|
var init_ci = __esm({
|
|
29002
29605
|
"src/commands/ci/index.ts"() {
|
|
@@ -29017,7 +29620,8 @@ var init_ci = __esm({
|
|
|
29017
29620
|
"",
|
|
29018
29621
|
"Flags:",
|
|
29019
29622
|
" --config <path> Config file (default: checkbox.json, then .checkbox.json)",
|
|
29020
|
-
" --online Use the API (CHECKBOX_SECRET_KEY / CHECKBOX_API_KEY): URL pages
|
|
29623
|
+
" --online Use the API (CHECKBOX_SECRET_KEY / CHECKBOX_API_KEY): URL pages (these need a live-mode",
|
|
29624
|
+
' key on an activated account; with a test key list the built page as "file") and site-aware checks;',
|
|
29021
29625
|
` with "notices" in checkbox.json, also checks that the site's Checkbox Notices really send them`,
|
|
29022
29626
|
" --site <site_\u2026> The site for --online (default: $CHECKBOX_SITE, the site `checkbox init` saved, or the only one)",
|
|
29023
29627
|
' --strict "Needs review" fails too (exit 2)',
|
|
@@ -29033,12 +29637,14 @@ var init_ci = __esm({
|
|
|
29033
29637
|
"",
|
|
29034
29638
|
"Exit: 0 pass (or needs review without --strict) \xB7 1 a check needs a fix \xB7 2 needs review with --strict, or a",
|
|
29035
29639
|
" page / disclosure with no renewal terms to judge, a checkout whose coupon / trial is not verified, or a",
|
|
29036
|
-
' checkout entry with "strict": true that needs review
|
|
29640
|
+
' checkout entry with "strict": true that needs review, or nothing was checked (every check skipped or',
|
|
29641
|
+
" not applicable, e.g. url pages without --online) \xB7",
|
|
29037
29642
|
" 3 config / usage error \xB7 4 auth (--online) \xB7 5 API or network error",
|
|
29038
29643
|
"",
|
|
29039
29644
|
"checkbox ci init [--yes] [--force] [--dry-run] [--no-workflow]",
|
|
29040
29645
|
' Writes checkbox.json from the project (Stripe Checkout calls, plans, email templates, pages; "TODO" where it',
|
|
29041
|
-
|
|
29646
|
+
' cannot tell) and .github/workflows/checkbox.yml (with the install and build steps a fresh clone needs for "module"',
|
|
29647
|
+
" entries and built pages). Asks first on a terminal; --yes writes without asking (agents).",
|
|
29042
29648
|
"",
|
|
29043
29649
|
"Docs: https://checkbox.site/docs/guides/ci"
|
|
29044
29650
|
].join("\n");
|
|
@@ -29081,7 +29687,7 @@ var init_ci = __esm({
|
|
|
29081
29687
|
const workspace = ctx.env.GITHUB_WORKSPACE ?? ctx.cwd;
|
|
29082
29688
|
const extra = (loaded.config.coverage === false ? [] : coverageResults(coverageOf(loaded, loaded.dir))).map((r) => ({
|
|
29083
29689
|
...r,
|
|
29084
|
-
target: { ...r.target, file: relative5(workspace,
|
|
29690
|
+
target: { ...r.target, file: relative5(workspace, join9(loaded.dir, r.target.file)).split("\\").join("/") || r.target.file }
|
|
29085
29691
|
}));
|
|
29086
29692
|
if (bool(p.flags, "changed-only")) {
|
|
29087
29693
|
const base = str(p.flags, "base") ?? (ctx.env.GITHUB_BASE_REF ? `origin/${ctx.env.GITHUB_BASE_REF}` : void 0);
|
|
@@ -29118,6 +29724,7 @@ ${message}
|
|
|
29118
29724
|
return EXIT.auth;
|
|
29119
29725
|
}
|
|
29120
29726
|
}
|
|
29727
|
+
if (client) await attachSiteForCoupons(ctx, client, jobs, str(p.flags, "site"));
|
|
29121
29728
|
const engine = client ? onlineEngine(client) : offlineEngine();
|
|
29122
29729
|
const notices = client && declaredByCheckbox(loaded.config).length ? await readNotices(ctx, client, str(p.flags, "site")) : void 0;
|
|
29123
29730
|
const noticesLine = Math.max(1, loaded.raw.split("\n").findIndex((l) => l.includes('"sent_by_checkbox"')) + 1);
|
|
@@ -29353,8 +29960,8 @@ var init_patch = __esm({
|
|
|
29353
29960
|
});
|
|
29354
29961
|
|
|
29355
29962
|
// src/commands/plan/fix.ts
|
|
29356
|
-
import { existsSync as
|
|
29357
|
-
import { relative as relative6, resolve as
|
|
29963
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
|
|
29964
|
+
import { relative as relative6, resolve as resolve8 } from "node:path";
|
|
29358
29965
|
function fixForTask(task, cwd, file) {
|
|
29359
29966
|
const base = {
|
|
29360
29967
|
object: "task_fix",
|
|
@@ -29366,12 +29973,12 @@ function fixForTask(task, cwd, file) {
|
|
|
29366
29973
|
verify: task.verify
|
|
29367
29974
|
};
|
|
29368
29975
|
const verifyCmd = `checkbox verify ${task.id} --wait`;
|
|
29369
|
-
const candidate = file ?? (task.where.file_hint &&
|
|
29976
|
+
const candidate = file ?? (task.where.file_hint && existsSync9(resolve8(cwd, task.where.file_hint)) ? task.where.file_hint : void 0);
|
|
29370
29977
|
if (task.change.type === "patch" && task.change.edits.length && candidate) {
|
|
29371
|
-
const path =
|
|
29978
|
+
const path = resolve8(cwd, candidate);
|
|
29372
29979
|
let src;
|
|
29373
29980
|
try {
|
|
29374
|
-
src =
|
|
29981
|
+
src = readFileSync10(path, "utf8");
|
|
29375
29982
|
} catch (err) {
|
|
29376
29983
|
throw new UsageError(`Cannot read ${candidate}: ${err.code ?? err.message}`);
|
|
29377
29984
|
}
|
|
@@ -29431,7 +30038,7 @@ var init_fix = __esm({
|
|
|
29431
30038
|
const task = await ctx.client().sdk.tasks.retrieve(id);
|
|
29432
30039
|
const result = fixForTask(task, ctx.cwd, str(flags, "file"));
|
|
29433
30040
|
const out = str(flags, "out");
|
|
29434
|
-
if (out && result.diff) writeFileSync3(
|
|
30041
|
+
if (out && result.diff) writeFileSync3(resolve8(ctx.cwd, out), result.diff);
|
|
29435
30042
|
ctx.out.result({ ...result, task_object: task }, () => {
|
|
29436
30043
|
const c = ctx.out.c;
|
|
29437
30044
|
const lines = [`${c.bold(task.title)} ${c.dim(`(${task.priority}, ${task.id})`)}`, c.dim(`${task.requirement.citation}`), task.why, ""];
|
|
@@ -29533,30 +30140,41 @@ var init_device = __esm({
|
|
|
29533
30140
|
});
|
|
29534
30141
|
|
|
29535
30142
|
// src/commands/init/patch.ts
|
|
29536
|
-
import { execFileSync as
|
|
29537
|
-
import { existsSync as
|
|
29538
|
-
import { dirname as
|
|
30143
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
30144
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
30145
|
+
import { dirname as dirname8, join as join10 } from "node:path";
|
|
30146
|
+
function pinnedRelease(body) {
|
|
30147
|
+
const b = body;
|
|
30148
|
+
if (!b || typeof b.version !== "string" || !/^\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(b.version)) return void 0;
|
|
30149
|
+
if (typeof b.integrity !== "string" || !/^sha384-[A-Za-z0-9+/]{64}$/.test(b.integrity)) return void 0;
|
|
30150
|
+
return { version: b.version, integrity: b.integrity };
|
|
30151
|
+
}
|
|
30152
|
+
function snippetScriptTag(s2, jsx = false, attrs = "") {
|
|
30153
|
+
const pin = s2.pinned ? ` integrity="${s2.pinned.integrity}" ${jsx ? "crossOrigin" : "crossorigin"}="anonymous"` : "";
|
|
30154
|
+
return `<script${attrs} async src="${srcOf(s2)}"${pin} data-key="${s2.snippetKey}" data-api="${s2.apiBase.replace(/\/+$/, "")}"></script>`;
|
|
30155
|
+
}
|
|
29539
30156
|
function read(root, rel2) {
|
|
29540
30157
|
try {
|
|
29541
|
-
return
|
|
30158
|
+
return readFileSync11(join10(root, rel2), "utf8");
|
|
29542
30159
|
} catch {
|
|
29543
30160
|
return null;
|
|
29544
30161
|
}
|
|
29545
30162
|
}
|
|
29546
30163
|
function srcOf(s2) {
|
|
29547
|
-
|
|
30164
|
+
const api = s2.apiBase.replace(/\/+$/, "");
|
|
30165
|
+
return s2.pinned ? `${api}/s/${s2.pinned.version}.js` : `${api}/s.js`;
|
|
29548
30166
|
}
|
|
29549
30167
|
function jsxBlock(s2, indent) {
|
|
29550
30168
|
return [
|
|
29551
30169
|
`${indent}{/* ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet) */}`,
|
|
29552
|
-
`${indent}
|
|
30170
|
+
`${indent}${snippetScriptTag(s2, true)}`,
|
|
29553
30171
|
`${indent}{/* ${MARK_END} */}`
|
|
29554
30172
|
];
|
|
29555
30173
|
}
|
|
29556
30174
|
function htmlBlock(s2, indent, attrs = "") {
|
|
29557
30175
|
return [
|
|
29558
30176
|
`${indent}<!-- ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet) -->`,
|
|
29559
|
-
`${indent}
|
|
30177
|
+
`${indent}${snippetScriptTag(s2, false, attrs)}`,
|
|
29560
30178
|
`${indent}<!-- ${MARK_END} -->`
|
|
29561
30179
|
];
|
|
29562
30180
|
}
|
|
@@ -29615,7 +30233,7 @@ function planSnippet(d, s2) {
|
|
|
29615
30233
|
const root = d.root;
|
|
29616
30234
|
const file = d.layoutFile;
|
|
29617
30235
|
const manual = `Add this tag inside <head> on every page (your root layout / template):
|
|
29618
|
-
${s2.scriptTag}`;
|
|
30236
|
+
${s2.pinned ? snippetScriptTag(s2) : s2.scriptTag}`;
|
|
29619
30237
|
if (!file) return { change: null, manual };
|
|
29620
30238
|
const existing = read(root, file);
|
|
29621
30239
|
const jsx = (f) => f === "nextjs-app" || f === "nextjs-pages" || f === "remix" || f === "react-router";
|
|
@@ -29631,15 +30249,15 @@ function planSnippet(d, s2) {
|
|
|
29631
30249
|
}
|
|
29632
30250
|
return { change: null, manual };
|
|
29633
30251
|
}
|
|
29634
|
-
if (existing.includes(`data-key="${s2.snippetKey}"`) && existing.includes(MARK_START)) {
|
|
29635
|
-
return { change: { file, action: "unchanged", summary: "Snippet already installed", added: [] } };
|
|
30252
|
+
if (existing.includes(`data-key="${s2.snippetKey}"`) && existing.includes(MARK_START) && existing.includes(`src="${srcOf(s2)}"`) && (!s2.pinned || existing.includes(s2.pinned.integrity))) {
|
|
30253
|
+
return { change: { file, action: "unchanged", summary: s2.pinned ? `Snippet already installed (pinned release ${s2.pinned.version} with SRI)` : "Snippet already installed", added: [] } };
|
|
29636
30254
|
}
|
|
29637
30255
|
const isJsx = jsx(d.framework);
|
|
29638
30256
|
const indentRe = isJsx ? d.framework === "nextjs-pages" ? /<Head\b[^>]*>/ : /<head\b[^>]*>/ : /<\/head>/i;
|
|
29639
30257
|
const indent = isJsx ? indentOf2(existing, indentRe) : indentOf2(existing, /<\/head>/i, " ");
|
|
29640
30258
|
const block = isJsx ? jsxBlock(s2, indent) : htmlBlock(s2, indent, d.framework === "astro" ? " is:inline" : "");
|
|
29641
30259
|
const replaced = replaceMarked(existing, block);
|
|
29642
|
-
if (replaced !== null) return { change: { file, action: "modify", summary: "Snippet tag updated (new site key)", contents: replaced, added: block } };
|
|
30260
|
+
if (replaced !== null) return { change: { file, action: "modify", summary: existing.includes(`data-key="${s2.snippetKey}"`) ? s2.pinned ? `Snippet tag pinned to release ${s2.pinned.version} with SRI` : "Snippet tag updated" : "Snippet tag updated (new site key)", contents: replaced, added: block } };
|
|
29643
30261
|
let next = null;
|
|
29644
30262
|
switch (d.framework) {
|
|
29645
30263
|
case "nextjs-app": {
|
|
@@ -29670,7 +30288,7 @@ ${indentOf2(existing, /<Head\s*\/>/, "")}</Head>`) : null);
|
|
|
29670
30288
|
}
|
|
29671
30289
|
if (!next) return { change: null, manual: `Could not find <head> in ${file}. ${manual}` };
|
|
29672
30290
|
if (!snippetPlacedSafely(next, isJsx)) return { change: null, manual: `Could not place the snippet safely in ${file} (its <head> is not where Checkbox can patch it without breaking the file), so ${file} was NOT changed. ${manual}` };
|
|
29673
|
-
return { change: { file, action: "modify", summary: `Snippet added to <head> in ${file}`, contents: next, added: block } };
|
|
30291
|
+
return { change: { file, action: "modify", summary: `Snippet added to <head> in ${file}${s2.pinned ? ` (pinned release ${s2.pinned.version} with SRI)` : ""}`, contents: next, added: block } };
|
|
29674
30292
|
}
|
|
29675
30293
|
function envVarNames(d) {
|
|
29676
30294
|
return {
|
|
@@ -29716,7 +30334,7 @@ function mask(v) {
|
|
|
29716
30334
|
}
|
|
29717
30335
|
function isGitTracked(root, file) {
|
|
29718
30336
|
try {
|
|
29719
|
-
|
|
30337
|
+
execFileSync3("git", ["ls-files", "--error-unmatch", "--", file], { cwd: root, stdio: "ignore", timeout: 5e3 });
|
|
29720
30338
|
return true;
|
|
29721
30339
|
} catch {
|
|
29722
30340
|
return false;
|
|
@@ -29733,7 +30351,7 @@ function planGitignore(d) {
|
|
|
29733
30351
|
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*");
|
|
29734
30352
|
if (covered) return null;
|
|
29735
30353
|
}
|
|
29736
|
-
if (existing === null && !
|
|
30354
|
+
if (existing === null && !existsSync10(join10(d.root, ".git"))) return null;
|
|
29737
30355
|
const text = `${existing ?? ""}${existing && !existing.endsWith("\n") ? "\n" : ""}# Checkbox keys (checkbox init)
|
|
29738
30356
|
${env2}
|
|
29739
30357
|
`;
|
|
@@ -29749,8 +30367,8 @@ function planWellKnown(d, token) {
|
|
|
29749
30367
|
}
|
|
29750
30368
|
function applyChange(root, c) {
|
|
29751
30369
|
if (c.action === "unchanged" || c.contents === void 0) return;
|
|
29752
|
-
const abs =
|
|
29753
|
-
mkdirSync3(
|
|
30370
|
+
const abs = join10(root, c.file);
|
|
30371
|
+
mkdirSync3(dirname8(abs), { recursive: true });
|
|
29754
30372
|
writeFileSync4(abs, c.contents, c.file.startsWith(".env") ? { mode: 384 } : void 0);
|
|
29755
30373
|
}
|
|
29756
30374
|
var MARK_START, MARK_END, NEXT_DOCUMENT, NUXT_PLUGIN;
|
|
@@ -29778,7 +30396,7 @@ ${jsxBlock(s2, " ").join("\n")}
|
|
|
29778
30396
|
NUXT_PLUGIN = (s2) => `// ${MARK_START} \u2014 Checkbox subscription-compliance monitoring (https://checkbox.site/docs/snippet)
|
|
29779
30397
|
export default defineNuxtPlugin(() => {
|
|
29780
30398
|
useHead({
|
|
29781
|
-
script: [{ src: '${srcOf(s2)}', async: true, 'data-key': '${s2.snippetKey}', 'data-api': '${s2.apiBase.replace(/\/+$/, "")}' }],
|
|
30399
|
+
script: [{ src: '${srcOf(s2)}', async: true,${s2.pinned ? ` integrity: '${s2.pinned.integrity}', crossorigin: 'anonymous',` : ""} 'data-key': '${s2.snippetKey}', 'data-api': '${s2.apiBase.replace(/\/+$/, "")}' }],
|
|
29782
30400
|
});
|
|
29783
30401
|
});
|
|
29784
30402
|
// ${MARK_END}
|
|
@@ -29797,8 +30415,8 @@ __export(init_exports, {
|
|
|
29797
30415
|
parsePlanFlag: () => parsePlanFlag,
|
|
29798
30416
|
syncCheckoutConfigs: () => syncCheckoutConfigs
|
|
29799
30417
|
});
|
|
29800
|
-
import { existsSync as
|
|
29801
|
-
import { join as
|
|
30418
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
30419
|
+
import { join as join11 } from "node:path";
|
|
29802
30420
|
import { createInterface as createInterface2 } from "node:readline";
|
|
29803
30421
|
function loadState(ctx, root) {
|
|
29804
30422
|
const cfg = ctx.config.load().init ?? {};
|
|
@@ -29818,7 +30436,7 @@ function saveState(ctx, root, patch) {
|
|
|
29818
30436
|
async function ask2(question) {
|
|
29819
30437
|
const rl = createInterface2({ input: process.stdin, output: process.stderr, terminal: true });
|
|
29820
30438
|
try {
|
|
29821
|
-
return await new Promise((
|
|
30439
|
+
return await new Promise((resolve10) => rl.question(question, resolve10));
|
|
29822
30440
|
} finally {
|
|
29823
30441
|
rl.close();
|
|
29824
30442
|
}
|
|
@@ -29879,7 +30497,7 @@ async function syncCheckoutConfigs(client, site, sessions, root) {
|
|
|
29879
30497
|
const call = (perFile.get(s2.file) ?? 0) + 1;
|
|
29880
30498
|
perFile.set(s2.file, call);
|
|
29881
30499
|
if (!registrable(s2.params, s2.unresolved)) {
|
|
29882
|
-
out.push({ file: s2.file, line: s2.line, skipped: s2.unresolved.length ? `values not readable statically: ${s2.unresolved.slice(0, 5).join(", ")}` : "not a subscription-mode Checkout Session" });
|
|
30500
|
+
out.push({ file: s2.file, line: s2.line, skipped: s2.unresolved.length ? `values not readable statically: ${s2.unresolved.slice(0, 5).join(", ")} (a variable, call or template string). Next: register the exact params your server sends with createStripeCheckoutConfig (POST /v1/sites/{site}/stripe_checkout_configs), or, once Stripe is connected, create one Checkout Session in Stripe test mode so Checkbox reads it` : "not a subscription-mode Checkout Session" });
|
|
29883
30501
|
continue;
|
|
29884
30502
|
}
|
|
29885
30503
|
try {
|
|
@@ -29896,7 +30514,7 @@ async function syncCheckoutConfigs(client, site, sessions, root) {
|
|
|
29896
30514
|
const from = configFileOf(c.name);
|
|
29897
30515
|
if (!from) continue;
|
|
29898
30516
|
const sameFile = perFile.has(from.file);
|
|
29899
|
-
const gone = !sameFile && !
|
|
30517
|
+
const gone = !sameFile && !existsSync11(join11(root, from.file));
|
|
29900
30518
|
const current = sameFile && from.call !== void 0 && from.call <= perFile.get(from.file) && c.name === configNameOf(from.file, from.call);
|
|
29901
30519
|
if (!sameFile && !gone || current) continue;
|
|
29902
30520
|
if (await client.sdk.stripeCheckoutConfigs.del(c.id).then(() => true, () => false)) {
|
|
@@ -29917,6 +30535,13 @@ function configLines(configs, now = Date.now()) {
|
|
|
29917
30535
|
};
|
|
29918
30536
|
return ["Checkout params registered:", ...configs.map((c) => c.id ? ` ${c.file}:${c.line} \u2192 ${c.id}${age(c.registered_at)}${c.replaced?.length ? ` (replaced ${c.replaced.length} older config${c.replaced.length === 1 ? "" : "s"})` : ""}` : ` ${c.file}${c.line ? `:${c.line}` : ""}: ${c.skipped}`)];
|
|
29919
30537
|
}
|
|
30538
|
+
async function snippetRelease(client) {
|
|
30539
|
+
try {
|
|
30540
|
+
return pinnedRelease(await client.get("/s/latest.json"));
|
|
30541
|
+
} catch {
|
|
30542
|
+
return void 0;
|
|
30543
|
+
}
|
|
30544
|
+
}
|
|
29920
30545
|
function registrable(params, unresolved) {
|
|
29921
30546
|
if (!params) return false;
|
|
29922
30547
|
if (unresolved.some((u) => /^(mode|line_items|subscription_data|custom_text|consent_collection)\b/.test(u))) return false;
|
|
@@ -29936,6 +30561,7 @@ function detectionSummary(d, siteUrl) {
|
|
|
29936
30561
|
pay_buttons: d.payButtons.map((c) => ({ file: c.file, line: c.line })),
|
|
29937
30562
|
checkout_sessions: d.checkoutSessions.map((s2) => ({ file: s2.file, line: s2.line, readable: !!s2.params, unresolved: s2.unresolved })),
|
|
29938
30563
|
stripe_test_key: d.stripeTestKey ? { hint: d.stripeTestKey.hint, source: d.stripeTestKey.source, restricted: d.stripeTestKey.restricted } : null,
|
|
30564
|
+
...d.stripeKeyRefused ? { stripe_key_refused: { ...d.stripeKeyRefused, message: stripeKeyRefusedMessage(d.stripeKeyRefused) } } : {},
|
|
29939
30565
|
merchant_name: d.brand ?? null,
|
|
29940
30566
|
plans: d.plans
|
|
29941
30567
|
};
|
|
@@ -30015,7 +30641,7 @@ var init_init = __esm({
|
|
|
30015
30641
|
init_patch2();
|
|
30016
30642
|
VERSION_FLAGS = {
|
|
30017
30643
|
string: ["site-url", "checkout-url", "account-url", "terms-url", "privacy-url", "refund-url", "cancellation-url", "wait-seconds", "agent", "support-email", "merchant-name", "plan"],
|
|
30018
|
-
boolean: ["yes", "dry-run", "open", "wait", "verify", "complete", "live", "stripe", "snippet", "relogin"],
|
|
30644
|
+
boolean: ["yes", "dry-run", "open", "wait", "verify", "complete", "live", "stripe", "snippet", "pin", "relogin"],
|
|
30019
30645
|
alias: { y: "yes" }
|
|
30020
30646
|
};
|
|
30021
30647
|
interactive = (ctx) => !ctx.flags.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
@@ -30027,10 +30653,13 @@ var init_init = __esm({
|
|
|
30027
30653
|
"Run it in the project root. What it does (idempotent; re-run any time):",
|
|
30028
30654
|
" 1. detects the framework, billing platform, checkout code and pay buttons",
|
|
30029
30655
|
' 2. signs you in: prints ONE link ("Open \u2026 and click Approve") and opens the browser; new accounts are free',
|
|
30030
|
-
" 3. writes CHECKBOX_SECRET_KEY and the publishable key to .env.local / .env (and git-ignores it)",
|
|
30031
|
-
"
|
|
30656
|
+
" 3. writes CHECKBOX_SECRET_KEY and the publishable key to .env.local / .env (and git-ignores it); when that file is",
|
|
30657
|
+
" tracked by git, or the folder is deployed as it is, the key is not written to it and stays only in the CLI config",
|
|
30658
|
+
" (~/.config/checkbox/config.json), which the output says",
|
|
30659
|
+
" 4. installs the Checkbox snippet in the root layout (marked with checkbox:snippet comments): the pinned release",
|
|
30660
|
+
" (/s/<version>.js with integrity= and crossorigin=, Subresource Integrity); --no-pin uses the unpinned /s.js",
|
|
30032
30661
|
" 5. registers the site, Terms / refund / cancellation policy URLs (fetched and checked by Checkbox)",
|
|
30033
|
-
" 6. with your permission, uses the project's Stripe TEST key for a synthetic test subscription",
|
|
30662
|
+
" 6. with your permission, uses the project's Stripe restricted TEST key (rk_test_\u2026; sk_test_ and live keys are refused) for a synthetic test subscription",
|
|
30034
30663
|
" 7. runs the first checks and prints the next step (`checkbox plan --json`); the human gets the summary email",
|
|
30035
30664
|
" when the setup is finished (every check passes, or `checkbox init --complete`)",
|
|
30036
30665
|
"",
|
|
@@ -30226,7 +30855,20 @@ var init_init = __esm({
|
|
|
30226
30855
|
}
|
|
30227
30856
|
const policyFromPaths = (kind) => siteUrl && d.policyPaths[kind] ? `${siteUrl}${d.policyPaths[kind]}` : void 0;
|
|
30228
30857
|
const agent = detectAgent(ctx.env, agentName);
|
|
30229
|
-
|
|
30858
|
+
let plans = flagPlans ?? (d.plans.length ? d.plans.map(({ source: _source, ...p }) => p) : void 0);
|
|
30859
|
+
if (plans?.length && !planFlag) {
|
|
30860
|
+
let current = null;
|
|
30861
|
+
try {
|
|
30862
|
+
current = await client.sdk.setupSessions.retrieve(sessionId);
|
|
30863
|
+
} catch {
|
|
30864
|
+
current = null;
|
|
30865
|
+
}
|
|
30866
|
+
const saved = Array.isArray(current?.business?.plans) ? current.business.plans : [];
|
|
30867
|
+
if (saved.length) {
|
|
30868
|
+
plans = void 0;
|
|
30869
|
+
out.info(`Kept the ${saved.length} plan${saved.length === 1 ? "" : "s"} already saved for this site (pass --plan to replace ${saved.length === 1 ? "it" : "them"}).`);
|
|
30870
|
+
}
|
|
30871
|
+
}
|
|
30230
30872
|
const brand = merchantName ?? d.brand?.name;
|
|
30231
30873
|
const update = {
|
|
30232
30874
|
...siteUrl ? { site_url: siteUrl } : {},
|
|
@@ -30261,7 +30903,9 @@ var init_init = __esm({
|
|
|
30261
30903
|
if (session.install.script_tag && session.install.snippet_key) {
|
|
30262
30904
|
if (bool(flags, "snippet") === false) manualSnippet = session.install.script_tag;
|
|
30263
30905
|
else {
|
|
30264
|
-
const
|
|
30906
|
+
const pinned = bool(flags, "pin") === false ? void 0 : await snippetRelease(client);
|
|
30907
|
+
if (!pinned && bool(flags, "pin") !== false) warnings.push("The pinned snippet release (/s/latest.json) could not be read, so the unpinned /s.js tag was used. Run `checkbox init` again to pin it with Subresource Integrity.");
|
|
30908
|
+
const plan2 = planSnippet(d, { scriptTag: session.install.script_tag, snippetKey: session.install.snippet_key, apiBase, ...pinned ? { pinned } : {} });
|
|
30265
30909
|
if (plan2.change) changes.push(plan2.change);
|
|
30266
30910
|
if (plan2.manual) manualSnippet = plan2.manual;
|
|
30267
30911
|
}
|
|
@@ -30292,13 +30936,16 @@ var init_init = __esm({
|
|
|
30292
30936
|
if (consent && d.stripeTestKey && !session.stripe.test_credential_stored) {
|
|
30293
30937
|
try {
|
|
30294
30938
|
await client.sdk.testCredentials.create(site, { type: "stripe_test", secret_key: d.stripeTestKey.key });
|
|
30295
|
-
stripe.test_credential = { stored: true, key: d.stripeTestKey.hint, restricted:
|
|
30296
|
-
if (!d.stripeTestKey.restricted) warnings.push(`Stored your full Stripe TEST key (${d.stripeTestKey.hint}) because no restricted key was found. Recommended: create a restricted test key (${session.stripe.restricted_key.create_url}) with ${session.stripe.restricted_key.permissions.map((p) => `${p.resource} ${p.access}`).join(", ")}, put it in STRIPE_RESTRICTED_KEY and run \`checkbox init\` again.`);
|
|
30939
|
+
stripe.test_credential = { stored: true, key: d.stripeTestKey.hint, restricted: true };
|
|
30297
30940
|
} catch (err) {
|
|
30298
30941
|
stripe.test_credential = { stored: false, error: err instanceof ApiError ? `${err.code ?? ""} ${err.message}`.trim() : String(err) };
|
|
30299
30942
|
warnings.push(`Stripe test key not stored: ${stripe.test_credential.error}`);
|
|
30300
30943
|
}
|
|
30301
30944
|
}
|
|
30945
|
+
if (!d.stripeTestKey && d.stripeKeyRefused) {
|
|
30946
|
+
stripe.test_credential = { stored: false, refused: d.stripeKeyRefused.kind };
|
|
30947
|
+
warnings.push(stripeKeyRefusedMessage(d.stripeKeyRefused));
|
|
30948
|
+
}
|
|
30302
30949
|
const configs = await syncCheckoutConfigs(client, site, d.checkoutSessions, root);
|
|
30303
30950
|
if (configs.length) stripe.checkout_configs = configs;
|
|
30304
30951
|
const registered = configs.find((c) => c.id);
|
|
@@ -30401,10 +31048,11 @@ var init_whoami = __esm({
|
|
|
30401
31048
|
object: "whoami",
|
|
30402
31049
|
key: creds ? maskKey(creds.apiKey) : null,
|
|
30403
31050
|
key_source: creds?.source ?? null,
|
|
31051
|
+
...creds?.file ? { key_file: creds.file } : {},
|
|
30404
31052
|
api_base: client.apiBase,
|
|
30405
31053
|
account: account.raw
|
|
30406
31054
|
},
|
|
30407
|
-
() => [describeAccount(account), ctx.out.c.dim(`key ${creds ? maskKey(creds.apiKey) : "?"} from ${creds
|
|
31055
|
+
() => [describeAccount(account), ctx.out.c.dim(`key ${creds ? maskKey(creds.apiKey) : "?"} from ${creds ? describeKeySource(creds, ctx.env) : "?"} \xB7 ${client.apiBase}`)]
|
|
30408
31056
|
);
|
|
30409
31057
|
return EXIT.ok;
|
|
30410
31058
|
}
|
|
@@ -30421,7 +31069,7 @@ import { createInterface as createInterface3 } from "node:readline";
|
|
|
30421
31069
|
async function prompt(question) {
|
|
30422
31070
|
const rl = createInterface3({ input: process.stdin, output: process.stderr, terminal: true });
|
|
30423
31071
|
try {
|
|
30424
|
-
return await new Promise((
|
|
31072
|
+
return await new Promise((resolve10) => rl.question(question, resolve10));
|
|
30425
31073
|
} finally {
|
|
30426
31074
|
rl.close();
|
|
30427
31075
|
}
|
|
@@ -30555,11 +31203,11 @@ var init_plan = __esm({
|
|
|
30555
31203
|
});
|
|
30556
31204
|
|
|
30557
31205
|
// src/commands/plan/verify.ts
|
|
30558
|
-
import { readFileSync as
|
|
30559
|
-
import { resolve as
|
|
31206
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
31207
|
+
import { resolve as resolve9 } from "node:path";
|
|
30560
31208
|
function readJson2(ctx, file, what) {
|
|
30561
31209
|
try {
|
|
30562
|
-
const v = JSON.parse(
|
|
31210
|
+
const v = JSON.parse(readFileSync12(resolve9(ctx.cwd, file), "utf8"));
|
|
30563
31211
|
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("not a JSON object");
|
|
30564
31212
|
return v;
|
|
30565
31213
|
} catch (err) {
|
|
@@ -30605,7 +31253,7 @@ var init_verify2 = __esm({
|
|
|
30605
31253
|
const htmlFile = str(flags, "html-file");
|
|
30606
31254
|
if (htmlFile) {
|
|
30607
31255
|
try {
|
|
30608
|
-
body.html =
|
|
31256
|
+
body.html = readFileSync12(resolve9(ctx.cwd, htmlFile), "utf8");
|
|
30609
31257
|
} catch (err) {
|
|
30610
31258
|
throw new UsageError(`Cannot read ${htmlFile}: ${err.code ?? err.message}`);
|
|
30611
31259
|
}
|