@checkbox-site/cli 0.4.4 → 0.4.5
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 +618 -252
- package/package.json +4 -2
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
|
*/
|
|
@@ -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.3";
|
|
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 });
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
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
|
+
};
|
|
4319
4380
|
}
|
|
4381
|
+
return void 0;
|
|
4320
4382
|
}
|
|
4321
|
-
function reportError(out, err) {
|
|
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
|
|
@@ -11309,7 +11378,7 @@ function noticePlanOptions(ctx, priceChange = false) {
|
|
|
11309
11378
|
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
11379
|
if (priceChange && newPrices.length) return { plans: plans.map((p) => ({ ...p, amount: -1 })), extraAmounts: newPrices };
|
|
11311
11380
|
if (!plans.length) return { plans: [], extraAmounts: [] };
|
|
11312
|
-
return { plans, extraAmounts: newPrices };
|
|
11381
|
+
return { plans, extraAmounts: newPrices, ...ctx.events?.length ? { events: ctx.events } : {} };
|
|
11313
11382
|
}
|
|
11314
11383
|
function declaredPlansFrom(plans) {
|
|
11315
11384
|
const out = [];
|
|
@@ -11551,11 +11620,11 @@ function siteOffers(input) {
|
|
|
11551
11620
|
};
|
|
11552
11621
|
for (const d of declared) {
|
|
11553
11622
|
const price3 = stripe.find((s2) => sameTerms(s2, d));
|
|
11554
|
-
const trial = d.trialDays && d.trialDays > 0 ? d.trialDays :
|
|
11623
|
+
const trial = d.trialDays && d.trialDays > 0 ? d.trialDays : soldTrial({ ...d, ...price3 ? { priceId: price3.id } : {} }) || (price3?.trialDays && price3.trialDays > 0 ? price3.trialDays : 0);
|
|
11555
11624
|
push(toOffer(d, "declared", { trialDays: trial, ...price3 ? { priceId: price3.id } : {} }));
|
|
11556
11625
|
}
|
|
11557
11626
|
const chosen = input.facts?.planPriceId ? stripe.find((p) => p.id === input.facts.planPriceId) : void 0;
|
|
11558
|
-
if (chosen) push(toOffer(chosen, "chosen", chosen
|
|
11627
|
+
if (chosen) push(toOffer(chosen, "chosen", soldTrial(chosen) > 0 ? { trialDays: soldTrial(chosen) } : {}));
|
|
11559
11628
|
if (!declared.length) {
|
|
11560
11629
|
for (const s2 of sold) {
|
|
11561
11630
|
const price3 = stripe.find((p) => sameTerms(p, s2));
|
|
@@ -11570,7 +11639,7 @@ function siteOffers(input) {
|
|
|
11570
11639
|
const anchors = stripe.filter((s2) => out.some((o) => o.priceId === s2.id));
|
|
11571
11640
|
for (const s2 of stripe) {
|
|
11572
11641
|
if (out.some((o) => o.priceId === s2.id) || !anchors.some((a) => sameProductName(a.name, s2.name))) continue;
|
|
11573
|
-
push(toOffer(s2, "stripe", s2
|
|
11642
|
+
push(toOffer(s2, "stripe", soldTrial(s2) > 0 ? { trialDays: soldTrial(s2) } : {}));
|
|
11574
11643
|
}
|
|
11575
11644
|
}
|
|
11576
11645
|
const facts = input.facts;
|
|
@@ -12937,14 +13006,14 @@ var init_visible_html = __esm({
|
|
|
12937
13006
|
|
|
12938
13007
|
// ../rules/src/checks/context.ts
|
|
12939
13008
|
function heuristicReader() {
|
|
12940
|
-
const
|
|
13009
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
12941
13010
|
return {
|
|
12942
13011
|
mode: "heuristic",
|
|
12943
13012
|
async analyze(text) {
|
|
12944
|
-
let a =
|
|
13013
|
+
let a = cache2.get(text);
|
|
12945
13014
|
if (!a) {
|
|
12946
13015
|
a = analyzeTerms(text);
|
|
12947
|
-
|
|
13016
|
+
cache2.set(text, a);
|
|
12948
13017
|
}
|
|
12949
13018
|
return a;
|
|
12950
13019
|
}
|
|
@@ -13793,7 +13862,7 @@ function remediationFor(checkId, platform) {
|
|
|
13793
13862
|
if (guide?.snippet) rem.snippet = { ...guide.snippet };
|
|
13794
13863
|
return rem;
|
|
13795
13864
|
}
|
|
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;
|
|
13865
|
+
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
13866
|
var init_remediation = __esm({
|
|
13798
13867
|
"../rules/src/remediation.ts"() {
|
|
13799
13868
|
"use strict";
|
|
@@ -14109,13 +14178,32 @@ and click \u201CCancel subscription\u201D. You can cancel anytime online.</p>`;
|
|
|
14109
14178
|
],
|
|
14110
14179
|
snippet: { language: "html", code: CUSTOM_DISCLOSURE_HTML }
|
|
14111
14180
|
};
|
|
14181
|
+
PADDLE_CONSENT_HTML = `<!-- Replace your button that opens Paddle Checkout with this form (Paddle.js must be loaded and initialized). -->
|
|
14182
|
+
<form id="checkbox-paddle-checkout">
|
|
14183
|
+
<label style="display:flex;gap:8px;align-items:flex-start;font-size:14px;color:#1f2937;">
|
|
14184
|
+
<!-- Never add the "checked" attribute: consent must be an affirmative act -->
|
|
14185
|
+
<input type="checkbox" name="auto_renew_consent" value="yes" required>
|
|
14186
|
+
<span>I agree that my subscription will automatically renew every {interval} at {price} until I cancel.</span>
|
|
14187
|
+
</label>
|
|
14188
|
+
<button type="submit">Subscribe</button>
|
|
14189
|
+
</form>
|
|
14190
|
+
<script>
|
|
14191
|
+
document.getElementById('checkbox-paddle-checkout').addEventListener('submit', function (event) {
|
|
14192
|
+
event.preventDefault(); // the browser only gets here once the required box is ticked
|
|
14193
|
+
var label = this.querySelector('label span').textContent.trim();
|
|
14194
|
+
Paddle.Checkout.open({
|
|
14195
|
+
items: [{ priceId: 'pri_REPLACE_ME', quantity: 1 }],
|
|
14196
|
+
customData: { auto_renew_consent: { at: new Date().toISOString(), label: label } },
|
|
14197
|
+
});
|
|
14198
|
+
});
|
|
14199
|
+
</script>`;
|
|
14112
14200
|
PADDLE_CONSENT = {
|
|
14113
14201
|
steps: [
|
|
14114
14202
|
"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
|
-
"
|
|
14203
|
+
'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.',
|
|
14204
|
+
"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
14205
|
],
|
|
14118
|
-
snippet: { language: "html", code:
|
|
14206
|
+
snippet: { language: "html", code: PADDLE_CONSENT_HTML }
|
|
14119
14207
|
};
|
|
14120
14208
|
PADDLE_ACK = {
|
|
14121
14209
|
steps: [
|
|
@@ -16291,15 +16379,34 @@ function offlineOnlyCancelIn(text, cancelMethods) {
|
|
|
16291
16379
|
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
16380
|
return online ? null : offline;
|
|
16293
16381
|
}
|
|
16382
|
+
function inForceAmount(e, events, fallbackCurrency = "usd") {
|
|
16383
|
+
const own = e.noticeTerms;
|
|
16384
|
+
if (own && typeof own.amount === "number" && own.amount > 0) return own.amount / 10 ** currencyDigitsOf(typeof own.currency === "string" ? own.currency : fallbackCurrency);
|
|
16385
|
+
const ref = typeof e.subscriptionRef === "string" ? e.subscriptionRef.trim() : "";
|
|
16386
|
+
const sent2 = toDate(e.receivedAt)?.getTime();
|
|
16387
|
+
if (!ref || sent2 === void 0) return void 0;
|
|
16388
|
+
let best;
|
|
16389
|
+
let bestAt = -Infinity;
|
|
16390
|
+
for (const ev of events) {
|
|
16391
|
+
if (ev.subscriptionRef !== ref || !CHARGE_AMOUNT_EVENTS.has(ev.type) || !(typeof ev.amount === "number" && ev.amount > 0)) continue;
|
|
16392
|
+
const at = toDate(ev.occurredAt)?.getTime();
|
|
16393
|
+
if (at === void 0 || at > sent2 + 36e5 || at < bestAt) continue;
|
|
16394
|
+
best = ev;
|
|
16395
|
+
bestAt = at;
|
|
16396
|
+
}
|
|
16397
|
+
return best ? best.amount / 10 ** currencyDigitsOf(best.currency ?? fallbackCurrency) : void 0;
|
|
16398
|
+
}
|
|
16294
16399
|
function noticeContent(e, opts = {}) {
|
|
16295
16400
|
const body = emailBody(e);
|
|
16296
16401
|
const a = analyzeTerms(body);
|
|
16297
16402
|
const quotes = {};
|
|
16298
16403
|
const wrong = {};
|
|
16299
16404
|
const plans = opts.plans ?? [];
|
|
16405
|
+
const inForce = opts.events ? inForceAmount(e, opts.events, plans[0]?.currency) : void 0;
|
|
16300
16406
|
const expected = [
|
|
16301
16407
|
...plans.map((p) => p.amount / 10 ** currencyDigitsOf(p.currency)),
|
|
16302
|
-
...opts.extraAmounts ?? []
|
|
16408
|
+
...opts.extraAmounts ?? [],
|
|
16409
|
+
...inForce !== void 0 ? [inForce] : []
|
|
16303
16410
|
];
|
|
16304
16411
|
const amount = a.recurringPrices[0]?.money ?? positiveAmounts(a)[0];
|
|
16305
16412
|
const stated = positiveAmounts(a).filter((m) => m.amount !== null);
|
|
@@ -16364,7 +16471,7 @@ function contentFixFromText(checkId, text) {
|
|
|
16364
16471
|
const m = new RegExp(`^the ${noun} \\(.*?\\) .*? does not state (.+?)(?: correctly \\(| \u2014 )`, "i").exec(text.trim());
|
|
16365
16472
|
return m ? `Add ${m[1]} to the ${noun}; Checkbox's template has every required line.` : void 0;
|
|
16366
16473
|
}
|
|
16367
|
-
var ELEMENT_LABELS, CHARGE_FOLLOWS_RE, DATE_TAG_RE, TRIAL_END_WHEN_RE, PRODUCT_RE, CONTACT_RE, CANCEL_URL_RE, NOTICE_NOUN;
|
|
16474
|
+
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
16475
|
var init_notice_content = __esm({
|
|
16369
16476
|
"../rules/src/checks/notice-content.ts"() {
|
|
16370
16477
|
"use strict";
|
|
@@ -16390,6 +16497,7 @@ var init_notice_content = __esm({
|
|
|
16390
16497
|
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
16498
|
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
16499
|
CANCEL_URL_RE = /https?:\/\/\S*(?:cancel|billing\.stripe\.com|customer[-_]?portal|\/portal|manage|subscription|account)\S*/i;
|
|
16500
|
+
CHARGE_AMOUNT_EVENTS = /* @__PURE__ */ new Set(["subscription_created", "subscription_renewed", "upcoming_renewal", "trial_will_end"]);
|
|
16393
16501
|
NOTICE_NOUN = {
|
|
16394
16502
|
"reminder.trial_ending": "trial-ending reminder",
|
|
16395
16503
|
"reminder.annual_renewal": "renewal reminder",
|
|
@@ -16459,6 +16567,64 @@ var init_email_plan = __esm({
|
|
|
16459
16567
|
}
|
|
16460
16568
|
});
|
|
16461
16569
|
|
|
16570
|
+
// ../rules/src/checks/forwarded.ts
|
|
16571
|
+
function headerAddress(block, name) {
|
|
16572
|
+
const m = new RegExp(String.raw`^\s*[*>]*\s*${name}\s*:\s*\*?\s*(.+)$`, "im").exec(block);
|
|
16573
|
+
if (!m) return void 0;
|
|
16574
|
+
const a = ADDRESS_RE.exec(m[1]);
|
|
16575
|
+
return a ? a[0].toLowerCase().replace(/[.>]+$/, "") : void 0;
|
|
16576
|
+
}
|
|
16577
|
+
function forwardedInfo(e) {
|
|
16578
|
+
const hit = cache.get(e);
|
|
16579
|
+
if (hit) return hit;
|
|
16580
|
+
const text = e.text ?? "";
|
|
16581
|
+
const marker = FWD_MARKER_RE.exec(text);
|
|
16582
|
+
const forwarded = !!marker || FWD_SUBJECT_RE.test(e.subject ?? "");
|
|
16583
|
+
const info = { forwarded };
|
|
16584
|
+
if (forwarded) {
|
|
16585
|
+
const start = marker ? marker.index : 0;
|
|
16586
|
+
const block = text.slice(start, start + 1200);
|
|
16587
|
+
const from = headerAddress(block, "From");
|
|
16588
|
+
const to = headerAddress(block, "To");
|
|
16589
|
+
if (from) info.originalFrom = from;
|
|
16590
|
+
if (to) info.originalTo = to;
|
|
16591
|
+
}
|
|
16592
|
+
cache.set(e, info);
|
|
16593
|
+
return info;
|
|
16594
|
+
}
|
|
16595
|
+
function domainOf(address) {
|
|
16596
|
+
const m = /@([^\s<>"]+)/.exec(address ?? "");
|
|
16597
|
+
return m ? m[1].toLowerCase().replace(/[>.]+$/, "") : "";
|
|
16598
|
+
}
|
|
16599
|
+
function isMorDomain(domain) {
|
|
16600
|
+
return !!domain && MERCHANT_OF_RECORD_DOMAINS.some((d) => domain === d || domain.endsWith(`.${d}`));
|
|
16601
|
+
}
|
|
16602
|
+
function isMerchantOfRecordEmail(e) {
|
|
16603
|
+
return isMorDomain(domainOf(e.from)) || isMorDomain(domainOf(forwardedInfo(e).originalFrom));
|
|
16604
|
+
}
|
|
16605
|
+
function inboundDomain() {
|
|
16606
|
+
const env2 = typeof process !== "undefined" ? process.env.INBOUND_EMAIL_DOMAIN : void 0;
|
|
16607
|
+
return (env2 ?? "").trim().replace(/^@/, "").replace(/\.$/, "").toLowerCase() || "inbound.subcomply.localhost";
|
|
16608
|
+
}
|
|
16609
|
+
function sentStraightToCheckbox(e) {
|
|
16610
|
+
if (forwardedInfo(e).originalTo) return false;
|
|
16611
|
+
if (e.scanId || e.relatedSignupAt || e.subscriptionRef || e.testRun) return false;
|
|
16612
|
+
const addresses = (e.to ?? "").match(/[^\s<>,"]+@[^\s<>,"]+/g) ?? [];
|
|
16613
|
+
const domain = inboundDomain();
|
|
16614
|
+
return addresses.length > 0 && addresses.every((a) => domainOf(a) === domain);
|
|
16615
|
+
}
|
|
16616
|
+
var FWD_SUBJECT_RE, FWD_MARKER_RE, ADDRESS_RE, cache, MERCHANT_OF_RECORD_DOMAINS;
|
|
16617
|
+
var init_forwarded = __esm({
|
|
16618
|
+
"../rules/src/checks/forwarded.ts"() {
|
|
16619
|
+
"use strict";
|
|
16620
|
+
FWD_SUBJECT_RE = /^\s*(?:(?:re|aw|sv)\s*:\s*)*(?:fwd?|fw|wg|tr)\s*:/i;
|
|
16621
|
+
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;
|
|
16622
|
+
ADDRESS_RE = /[^\s<>,"':;()[\]]+@[^\s<>,"':;()[\]]+\.[a-z]{2,}/i;
|
|
16623
|
+
cache = /* @__PURE__ */ new WeakMap();
|
|
16624
|
+
MERCHANT_OF_RECORD_DOMAINS = ["paddle.com", "paddle.net", "lemonsqueezy.com", "fastspring.com"];
|
|
16625
|
+
}
|
|
16626
|
+
});
|
|
16627
|
+
|
|
16462
16628
|
// ../rules/src/checks/confirmation.ts
|
|
16463
16629
|
function isPostSignupEmail(e) {
|
|
16464
16630
|
if (e.kind !== "other" || !e.scanId || !e.relatedSignupAt) return false;
|
|
@@ -16471,10 +16637,12 @@ function findAcknowledgments(emails) {
|
|
|
16471
16637
|
if (cached) return cached;
|
|
16472
16638
|
const acks = [];
|
|
16473
16639
|
const receipts = [];
|
|
16474
|
-
const
|
|
16640
|
+
const explicit = emails.filter((e) => e.kind === "acknowledgment");
|
|
16641
|
+
const acknowledged = new Set(explicit.map((e) => recipientOf(e)).filter((r) => r !== ""));
|
|
16475
16642
|
for (const e of emails) {
|
|
16476
16643
|
if (e.kind === "acknowledgment") acks.push(e);
|
|
16477
16644
|
else if (acknowledged.has(recipientOf(e))) continue;
|
|
16645
|
+
else if (explicit.length && e.kind === "receipt" && (forwardedInfo(e).forwarded || isMerchantOfRecordEmail(e) || sentStraightToCheckbox(e))) continue;
|
|
16478
16646
|
else if (e.kind === "receipt" || isPostSignupEmail(e)) {
|
|
16479
16647
|
(looksLikeSubscriptionTerms(analyzeTerms(emailBody(e))) ? acks : receipts).push(e);
|
|
16480
16648
|
}
|
|
@@ -16484,6 +16652,8 @@ function findAcknowledgments(emails) {
|
|
|
16484
16652
|
return set;
|
|
16485
16653
|
}
|
|
16486
16654
|
function recipientOf(e) {
|
|
16655
|
+
const original = forwardedInfo(e).originalTo;
|
|
16656
|
+
if (original) return original;
|
|
16487
16657
|
const m = /[^\s<>,"]+@[^\s<>,"]+/.exec(e.to ?? "");
|
|
16488
16658
|
return m ? m[0].toLowerCase() : "";
|
|
16489
16659
|
}
|
|
@@ -16693,6 +16863,7 @@ var init_confirmation = __esm({
|
|
|
16693
16863
|
init_intro_terms();
|
|
16694
16864
|
init_offer();
|
|
16695
16865
|
init_email_plan();
|
|
16866
|
+
init_forwarded();
|
|
16696
16867
|
ACK_MAX_DELAY_HOURS = 24;
|
|
16697
16868
|
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
16869
|
ackCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -18309,6 +18480,17 @@ function requiredFor(reqs) {
|
|
|
18309
18480
|
if (!out.size) for (const el of CHARGE_NOTICE_REQUIRED) out.add(el);
|
|
18310
18481
|
return [...out];
|
|
18311
18482
|
}
|
|
18483
|
+
function chargeGapPhrase(content, gaps, expected) {
|
|
18484
|
+
const wrong = gaps.filter((g) => content.wrong?.[g]);
|
|
18485
|
+
const absent = gaps.filter((g) => !content.wrong?.[g]);
|
|
18486
|
+
const parts = [];
|
|
18487
|
+
if (absent.length) parts.push(`do not state ${describeMissing(absent)}`);
|
|
18488
|
+
for (const g of wrong) {
|
|
18489
|
+
const said = content.wrong[g];
|
|
18490
|
+
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`);
|
|
18491
|
+
}
|
|
18492
|
+
return parts.join("; ");
|
|
18493
|
+
}
|
|
18312
18494
|
async function runChargeNotice(input) {
|
|
18313
18495
|
const id = "reminder.charge_notice";
|
|
18314
18496
|
const configRefs = input.data.configRef ? [input.data.configRef] : [];
|
|
@@ -18336,6 +18518,12 @@ async function runChargeNotice(input) {
|
|
|
18336
18518
|
}
|
|
18337
18519
|
return c;
|
|
18338
18520
|
};
|
|
18521
|
+
const chargeExpected = (e) => {
|
|
18522
|
+
const own = inForceAmount(e, input.ctx.events ?? [], chargePlanOpts.plans[0]?.currency);
|
|
18523
|
+
const cur = (chargePlanOpts.plans[0]?.currency ?? "usd").toUpperCase();
|
|
18524
|
+
if (own !== void 0) return `this subscription\u2019s charge is ${own.toFixed(2)} ${cur}`;
|
|
18525
|
+
return chargePlanOpts.plans.length ? `the plan is ${chargePlanOpts.plans.map((p) => `${planAmount(p).toFixed(2)} ${p.currency.toUpperCase()} ${planEvery(p)}`).join(", ")}` : "";
|
|
18526
|
+
};
|
|
18339
18527
|
const counted = judged.filter((j) => j.state === "matched" || j.state === "partial" || j.state === "outside_window" || j.state === "missing");
|
|
18340
18528
|
const matched = counted.filter((j) => j.state === "matched");
|
|
18341
18529
|
const partial = counted.filter((j) => j.state === "partial");
|
|
@@ -18367,10 +18555,12 @@ async function runChargeNotice(input) {
|
|
|
18367
18555
|
return makeResult(
|
|
18368
18556
|
id,
|
|
18369
18557
|
"fail",
|
|
18370
|
-
`${incompleteMatched.length} of ${matched.length} charge notice(s)
|
|
18558
|
+
`${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
18559
|
[...refs2(incompleteMatched.map((i) => i.j)), ...refs2(missing), ...refs2(outside)],
|
|
18372
18560
|
0.75,
|
|
18373
|
-
|
|
18561
|
+
// Round 29 (pilot 28 item 11): when every failing notice is Checkbox Notices' own send, the plan fixes Checkbox's
|
|
18562
|
+
// template and settings; it never asks the merchant's agent to change an email it does not send.
|
|
18563
|
+
incompleteMatched.every((x2) => isCheckboxSentEmail(x2.j.email)) && !missing.length && !outside.length ? { ...data, contentByCheckbox: true } : data
|
|
18374
18564
|
);
|
|
18375
18565
|
}
|
|
18376
18566
|
if (outside.length) {
|
|
@@ -20663,9 +20853,9 @@ function limiter(max) {
|
|
|
20663
20853
|
run2();
|
|
20664
20854
|
}
|
|
20665
20855
|
};
|
|
20666
|
-
return async (fn) => new Promise((
|
|
20856
|
+
return async (fn) => new Promise((resolve10, reject) => {
|
|
20667
20857
|
queue.push(() => {
|
|
20668
|
-
fn().then(
|
|
20858
|
+
fn().then(resolve10, reject).finally(() => {
|
|
20669
20859
|
active--;
|
|
20670
20860
|
next();
|
|
20671
20861
|
});
|
|
@@ -22361,7 +22551,7 @@ var init_status = __esm({
|
|
|
22361
22551
|
)
|
|
22362
22552
|
})
|
|
22363
22553
|
},
|
|
22364
|
-
{ code: "consent_implied", category: "content", when: has(/consent is implied by clicking/), build: () => ({ status: "needs_fix", fix: '
|
|
22554
|
+
{ 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
22555
|
{ 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
22556
|
// Round 23 (pilot 22 nontech-founder done blocker): "does not appear to be required" means the capture could not see
|
|
22367
22557
|
// whether the box is enforced (a script may block the purchase, as Stripe's own page does). A person who can try it
|
|
@@ -22804,7 +22994,7 @@ var init_status = __esm({
|
|
|
22804
22994
|
"signup.disclosure.term_length": 'Say how often it renews ("renews every month until you cancel").',
|
|
22805
22995
|
"signup.disclosure.proximity": "Put the renewal line under the price, before the card and email fields and the pay button.",
|
|
22806
22996
|
"signup.disclosure.conspicuous": "Show the renewal line at body size, semibold, in your normal text colour.",
|
|
22807
|
-
"signup.consent.affirmative": '
|
|
22997
|
+
"signup.consent.affirmative": 'Ask for a separate yes to the renewal: usually one unchecked box, "I agree to the automatic renewal terms above".',
|
|
22808
22998
|
"signup.consent.not_prechecked": "Leave the consent checkbox unchecked by default.",
|
|
22809
22999
|
"confirmation.email.sent": "Send an acknowledgment email right after sign-up (Checkbox has a template).",
|
|
22810
23000
|
"confirmation.email.terms": "Restate the renewal terms (price, frequency, until cancelled) in the acknowledgment.",
|
|
@@ -25904,7 +26094,7 @@ __export(check_exports, {
|
|
|
25904
26094
|
default: () => check_default
|
|
25905
26095
|
});
|
|
25906
26096
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
25907
|
-
import { resolve } from "node:path";
|
|
26097
|
+
import { resolve as resolve2 } from "node:path";
|
|
25908
26098
|
function operationFor(endpoint) {
|
|
25909
26099
|
const op = Object.values(OPERATIONS).find((o) => o.httpMethod === "POST" && o.path === endpoint);
|
|
25910
26100
|
if (!op) throw new Error(`No API operation for POST ${endpoint}`);
|
|
@@ -25913,7 +26103,7 @@ function operationFor(endpoint) {
|
|
|
25913
26103
|
async function readInput(ctx, file) {
|
|
25914
26104
|
if (file === "-") return ctx.readStdin();
|
|
25915
26105
|
try {
|
|
25916
|
-
return readFileSync3(
|
|
26106
|
+
return readFileSync3(resolve2(ctx.cwd, file), "utf8");
|
|
25917
26107
|
} catch (err) {
|
|
25918
26108
|
throw new UsageError(`Cannot read ${file}: ${err.code ?? err.message}`);
|
|
25919
26109
|
}
|
|
@@ -26201,7 +26391,7 @@ var init_check = __esm({
|
|
|
26201
26391
|
|
|
26202
26392
|
// src/ci/config.ts
|
|
26203
26393
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
26204
|
-
import { dirname as
|
|
26394
|
+
import { dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
|
|
26205
26395
|
function planList(config, ref) {
|
|
26206
26396
|
if (Array.isArray(ref)) return ref;
|
|
26207
26397
|
if (ref === "all" && !(config.plans ?? []).some((p) => p.name === "all")) return (config.plans ?? []).map((p) => p.name);
|
|
@@ -26209,7 +26399,7 @@ function planList(config, ref) {
|
|
|
26209
26399
|
}
|
|
26210
26400
|
function findConfig(cwd, explicit) {
|
|
26211
26401
|
if (explicit) {
|
|
26212
|
-
const p = isAbsolute(explicit) ? explicit :
|
|
26402
|
+
const p = isAbsolute(explicit) ? explicit : resolve3(cwd, explicit);
|
|
26213
26403
|
return existsSync3(p) ? p : void 0;
|
|
26214
26404
|
}
|
|
26215
26405
|
for (const f of CONFIG_FILES) if (existsSync3(join3(cwd, f))) return join3(cwd, f);
|
|
@@ -26230,7 +26420,7 @@ function loadConfig(path) {
|
|
|
26230
26420
|
}
|
|
26231
26421
|
const issues = validateConfig(value);
|
|
26232
26422
|
if (issues.length) throw new ConfigError(path, issues);
|
|
26233
|
-
return { config: value, path, dir:
|
|
26423
|
+
return { config: value, path, dir: dirname3(path), raw };
|
|
26234
26424
|
}
|
|
26235
26425
|
function validateConfig(value) {
|
|
26236
26426
|
const is = new Issues();
|
|
@@ -26932,11 +27122,17 @@ function findStripeKey(env2, processEnv) {
|
|
|
26932
27122
|
sources.push([`${k} (environment)`, processEnv[k]]);
|
|
26933
27123
|
sources.push([`${k} (.env)`, env2[k]]);
|
|
26934
27124
|
}
|
|
26935
|
-
const found = sources.filter((s2) => typeof s2[1] === "string" && /^(sk|rk)
|
|
26936
|
-
const
|
|
26937
|
-
|
|
26938
|
-
|
|
26939
|
-
|
|
27125
|
+
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()]);
|
|
27126
|
+
const hint = (key) => `${key.slice(0, 8)}\u2026${key.slice(-4)}`;
|
|
27127
|
+
const pick = found.find((s2) => s2[1].startsWith("rk_test_"));
|
|
27128
|
+
if (pick) return { key: { key: pick[1], hint: hint(pick[1]), source: pick[0], restricted: true } };
|
|
27129
|
+
const other = found.find((s2) => s2[1].startsWith("sk_test_")) ?? found[0];
|
|
27130
|
+
if (!other) return {};
|
|
27131
|
+
return { refused: { hint: hint(other[1]), source: other[0], kind: /_live_/.test(other[1]) ? "live" : "secret" } };
|
|
27132
|
+
}
|
|
27133
|
+
function stripeKeyRefusedMessage(r) {
|
|
27134
|
+
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.`;
|
|
27135
|
+
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
27136
|
}
|
|
26941
27137
|
function sessionLiteral(text, index) {
|
|
26942
27138
|
const open2 = text.indexOf("{", index);
|
|
@@ -27018,7 +27214,8 @@ function detectProject(root, processEnv = {}) {
|
|
|
27018
27214
|
checkoutCode,
|
|
27019
27215
|
payButtons,
|
|
27020
27216
|
checkoutSessions,
|
|
27021
|
-
...stripeKey ? { stripeTestKey: stripeKey } : {},
|
|
27217
|
+
...stripeKey.key ? { stripeTestKey: stripeKey.key } : {},
|
|
27218
|
+
...stripeKey.refused ? { stripeKeyRefused: stripeKey.refused } : {},
|
|
27022
27219
|
...brand ? { brand } : {},
|
|
27023
27220
|
plans: plansFromSessions(checkoutSessions),
|
|
27024
27221
|
env: env2
|
|
@@ -27059,7 +27256,7 @@ var init_detect = __esm({
|
|
|
27059
27256
|
|
|
27060
27257
|
// src/ci/scaffold.ts
|
|
27061
27258
|
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
27062
|
-
import { basename, dirname as
|
|
27259
|
+
import { basename, dirname as dirname4, join as join5, relative as relative2 } from "node:path";
|
|
27063
27260
|
function walk3(root, visit) {
|
|
27064
27261
|
let n = 0;
|
|
27065
27262
|
const go = (dir, depth) => {
|
|
@@ -27128,7 +27325,7 @@ function paramsBuilder(root, file, line2) {
|
|
|
27128
27325
|
const imp = new RegExp(`import\\s*\\{[^}]*\\b${fn}\\b[^}]*\\}\\s*from\\s*['"](\\.{1,2}/[^'"]+)['"]`).exec(text);
|
|
27129
27326
|
let target;
|
|
27130
27327
|
if (imp) {
|
|
27131
|
-
const base = join5(
|
|
27328
|
+
const base = join5(dirname4(file), imp[1]).split("\\").join("/");
|
|
27132
27329
|
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
27330
|
} else if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fn}\\b|export\\s+const\\s+${fn}\\b`).test(text)) target = file;
|
|
27134
27331
|
return target ? { module: `${target}#${fn}`, takesArgs } : void 0;
|
|
@@ -27315,7 +27512,7 @@ var init_scaffold = __esm({
|
|
|
27315
27512
|
|
|
27316
27513
|
// src/ci/coverage.ts
|
|
27317
27514
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
27318
|
-
import { join as join6, relative as relative3, resolve as
|
|
27515
|
+
import { join as join6, relative as relative3, resolve as resolve4 } from "node:path";
|
|
27319
27516
|
function callsIn(text, file) {
|
|
27320
27517
|
const hits = [];
|
|
27321
27518
|
for (const [re, kind] of PATTERNS) {
|
|
@@ -27337,7 +27534,7 @@ function callsIn(text, file) {
|
|
|
27337
27534
|
}
|
|
27338
27535
|
function coverageOf(loaded, root, files) {
|
|
27339
27536
|
const cfg = loaded.config;
|
|
27340
|
-
const toRoot = (p) => relative3(root,
|
|
27537
|
+
const toRoot = (p) => relative3(root, resolve4(loaded.dir, p)).split("\\").join("/");
|
|
27341
27538
|
const calls = [];
|
|
27342
27539
|
const visit = (rel2) => {
|
|
27343
27540
|
if (!CODE.test(rel2) || TESTISH.test(rel2)) return;
|
|
@@ -27438,10 +27635,10 @@ var init_coverage = __esm({
|
|
|
27438
27635
|
// src/ci/inputs.ts
|
|
27439
27636
|
import { execFile } from "node:child_process";
|
|
27440
27637
|
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
27441
|
-
import { dirname as
|
|
27638
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join7, relative as relative4, resolve as resolve5 } from "node:path";
|
|
27442
27639
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
27443
27640
|
function readRel(dir, file) {
|
|
27444
|
-
const abs = isAbsolute2(file) ? file :
|
|
27641
|
+
const abs = isAbsolute2(file) ? file : resolve5(dir, file);
|
|
27445
27642
|
try {
|
|
27446
27643
|
return readFileSync8(abs, "utf8");
|
|
27447
27644
|
} catch (err) {
|
|
@@ -27452,26 +27649,42 @@ function readRel(dir, file) {
|
|
|
27452
27649
|
function hasTsx(dir) {
|
|
27453
27650
|
for (let d = dir, i = 0; i < 8; i++) {
|
|
27454
27651
|
if (existsSync6(join7(d, "node_modules", "tsx", "package.json"))) return true;
|
|
27455
|
-
const up =
|
|
27652
|
+
const up = dirname5(d);
|
|
27653
|
+
if (up === d) break;
|
|
27654
|
+
d = up;
|
|
27655
|
+
}
|
|
27656
|
+
return false;
|
|
27657
|
+
}
|
|
27658
|
+
function declaresTsx(dir) {
|
|
27659
|
+
for (let d = dir, i = 0; i < 8; i++) {
|
|
27660
|
+
try {
|
|
27661
|
+
const pkg = JSON.parse(readFileSync8(join7(d, "package.json"), "utf8"));
|
|
27662
|
+
if (pkg.dependencies?.tsx || pkg.devDependencies?.tsx) return true;
|
|
27663
|
+
} catch {
|
|
27664
|
+
}
|
|
27665
|
+
const up = dirname5(d);
|
|
27456
27666
|
if (up === d) break;
|
|
27457
27667
|
d = up;
|
|
27458
27668
|
}
|
|
27459
27669
|
return false;
|
|
27460
27670
|
}
|
|
27671
|
+
function addTsx(dir, detail = "") {
|
|
27672
|
+
return declaresTsx(dir) ? NOT_INSTALLED : `Add tsx to the project (npm install -D tsx${detail})`;
|
|
27673
|
+
}
|
|
27461
27674
|
function loaderFlags(file, dir, nodeVersion = process.versions.node, features = process.features) {
|
|
27462
27675
|
if (/\.[cm]?js$/.test(file)) return [];
|
|
27463
27676
|
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`;
|
|
27677
|
+
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
27678
|
if (features.typescript) return [];
|
|
27466
27679
|
const [major, minor] = nodeVersion.split(".").map(Number);
|
|
27467
27680
|
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`;
|
|
27681
|
+
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
27682
|
}
|
|
27470
27683
|
function runModule(dir, ref, args = [], env2 = process.env, timeoutMs = 3e4) {
|
|
27471
27684
|
const hash = ref.lastIndexOf("#");
|
|
27472
27685
|
const file = hash > 0 ? ref.slice(0, hash) : ref;
|
|
27473
27686
|
const name = hash > 0 ? ref.slice(hash + 1) : "default";
|
|
27474
|
-
const abs =
|
|
27687
|
+
const abs = resolve5(dir, file);
|
|
27475
27688
|
if (!existsSync6(abs)) return Promise.reject(new InputError(`${file} does not exist (paths are relative to checkbox.json)`));
|
|
27476
27689
|
const flags = loaderFlags(file, dir);
|
|
27477
27690
|
if (typeof flags === "string") return Promise.reject(new InputError(flags));
|
|
@@ -27512,13 +27725,13 @@ function moduleErrorMessage(stderr, dir, flags = []) {
|
|
|
27512
27725
|
}
|
|
27513
27726
|
const noExt = !/\.[cm]?[jt]sx?$|\.json$/.test(target);
|
|
27514
27727
|
if (noExt && !tsx) {
|
|
27515
|
-
return `${rel2(from)} imports ${rel2(target)} without a file extension, which Node cannot resolve on its own.
|
|
27728
|
+
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
27729
|
}
|
|
27517
27730
|
return `${rel2(from)} imports ${rel2(target)}, which does not exist${noExt ? " (tsx found no .ts / .js file of that name either)" : ""}.`;
|
|
27518
27731
|
}
|
|
27519
27732
|
if (/ERR_UNKNOWN_FILE_EXTENSION/.test(text)) {
|
|
27520
27733
|
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"}.
|
|
27734
|
+
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
27735
|
}
|
|
27523
27736
|
const lines = text.split("\n").map((l) => l.trim()).filter((l) => l && !/^at /.test(l) && !/^Node\.js v\d/.test(l));
|
|
27524
27737
|
const first2 = lines.find((l) => /^(?:[A-Z]\w*)?Error\b.*:/.test(l)) ?? lines.find((l) => /error/i.test(l)) ?? lines[lines.length - 1] ?? "";
|
|
@@ -27552,7 +27765,7 @@ function exportLine(dir, ref) {
|
|
|
27552
27765
|
const name = hash > 0 ? ref.slice(hash + 1) : "default";
|
|
27553
27766
|
let text;
|
|
27554
27767
|
try {
|
|
27555
|
-
text = readFileSync8(
|
|
27768
|
+
text = readFileSync8(resolve5(dir, file), "utf8");
|
|
27556
27769
|
} catch {
|
|
27557
27770
|
return 1;
|
|
27558
27771
|
}
|
|
@@ -27591,7 +27804,7 @@ function planTerms(config, plan2) {
|
|
|
27591
27804
|
return t;
|
|
27592
27805
|
}
|
|
27593
27806
|
function rel(loaded, root, file) {
|
|
27594
|
-
return relative4(root,
|
|
27807
|
+
return relative4(root, resolve5(loaded.dir, file)).split("\\").join("/") || file;
|
|
27595
27808
|
}
|
|
27596
27809
|
function emailFromFile(content, file, subject) {
|
|
27597
27810
|
let body = content;
|
|
@@ -27726,14 +27939,20 @@ function pageJob(loaded, e, o, root) {
|
|
|
27726
27939
|
};
|
|
27727
27940
|
}
|
|
27728
27941
|
if (e.url) {
|
|
27729
|
-
if (!o.online) job.skip =
|
|
27942
|
+
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
27943
|
else job.body = { url: e.url, ...common3 };
|
|
27731
27944
|
return job;
|
|
27732
27945
|
}
|
|
27733
27946
|
try {
|
|
27734
27947
|
if (!e.source) job.target = { file: rel(loaded, root, e.file), line: 1 };
|
|
27735
27948
|
job.inputs.push(e.file);
|
|
27736
|
-
|
|
27949
|
+
let html;
|
|
27950
|
+
try {
|
|
27951
|
+
html = readRel(dir, e.file);
|
|
27952
|
+
} catch (err) {
|
|
27953
|
+
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).`);
|
|
27954
|
+
throw err;
|
|
27955
|
+
}
|
|
27737
27956
|
if (!e.source) job.locate = (needles) => lineOfAny(html, needles);
|
|
27738
27957
|
job.body = { html, ...common3 };
|
|
27739
27958
|
} catch (err) {
|
|
@@ -27800,7 +28019,7 @@ async function resolveJobs(loaded, o) {
|
|
|
27800
28019
|
}
|
|
27801
28020
|
return jobs;
|
|
27802
28021
|
}
|
|
27803
|
-
var InputError, CHILD, jurisdictionsOf;
|
|
28022
|
+
var InputError, NOT_INSTALLED, CHILD, jurisdictionsOf;
|
|
27804
28023
|
var init_inputs = __esm({
|
|
27805
28024
|
"src/ci/inputs.ts"() {
|
|
27806
28025
|
"use strict";
|
|
@@ -27813,6 +28032,7 @@ var init_inputs = __esm({
|
|
|
27813
28032
|
this.name = "InputError";
|
|
27814
28033
|
}
|
|
27815
28034
|
};
|
|
28035
|
+
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
28036
|
CHILD = `
|
|
27817
28037
|
const [url, name, argsJson] = process.argv.slice(1);
|
|
27818
28038
|
const mod = await import(url);
|
|
@@ -27831,106 +28051,6 @@ process.exit(0);
|
|
|
27831
28051
|
}
|
|
27832
28052
|
});
|
|
27833
28053
|
|
|
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
28054
|
// ../rules/src/fixes/conflicts.ts
|
|
27935
28055
|
var init_conflicts = __esm({
|
|
27936
28056
|
"../rules/src/fixes/conflicts.ts"() {
|
|
@@ -28435,10 +28555,27 @@ function onlineEngine(client) {
|
|
|
28435
28555
|
return { mode: "online", run: (kind, body) => client.sdk.call(operationFor2(kind), body) };
|
|
28436
28556
|
}
|
|
28437
28557
|
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");
|
|
28558
|
+
return text.replace(/Pass every plan the page sells \(plans: \[\.\.\.\]\)/g, 'Set "plan": "all" on this page entry in checkbox.json (every plan the page sells)').replace(/Pass plans with every plan the page sells/g, 'Set "plan": "all" on this page entry in checkbox.json').replace(/\(page_kind "checkout"\)/g, '(a pages entry with "kind": "checkout")').replace(/page_kind "checkout"/g, '"kind": "checkout"').replace(/check the renewal (text|text itself) with check_disclosure/g, 'check the renewal $1 with a "disclosures" entry').replace(/\bcheck_disclosure\b/g, 'a "disclosures" entry').replace(/pass merchant\.price \(e\.g\. 19\.99\), merchant\.currency and merchant\.interval/g, 'add the plan to "plans" in checkbox.json (price, currency, interval) and name it in "plan" on this entry').replace(/merchant\.price \+ merchant\.interval/g, 'a plan in "plans" of checkbox.json (and "plan" on this entry)').replace(/pass merchant\.price and merchant\.interval/gi, 'add the plan to "plans" in checkbox.json and name it in "plan" on this entry').replace(/merchant\.intro_percent_off \(or intro_amount_off\) and merchant\.intro_periods/g, "intro_percent_off (or intro_amount_off) and intro_periods on the plan in checkbox.json").replace(/merchant\.intro_\*/g, "the plan's intro_* in checkbox.json").replace(/merchant\.trial_days \((\d+)\)/g, "trial_days ($1) of the plan in checkbox.json").replace(/merchant\.price was ignored/g, "the plan's price in checkbox.json was ignored").replace(/In test mode, pass the page source as html\./g, 'With a test key, list the built page under "file" in checkbox.json instead of "url" (or run --online with a live key, ck_live_\u2026).');
|
|
28559
|
+
}
|
|
28560
|
+
function pricingPageFixes(fixes) {
|
|
28561
|
+
const reworded = fixes.map(
|
|
28562
|
+
(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")
|
|
28563
|
+
);
|
|
28564
|
+
const out = [];
|
|
28565
|
+
const renewalLineFor = /* @__PURE__ */ new Set();
|
|
28566
|
+
for (const f of reworded) {
|
|
28567
|
+
const card = /^(Plan card [^:]+): /.exec(f)?.[1] ?? "";
|
|
28568
|
+
const isRenewalLine = /\b(?:Add one line under the price|Put the renewal line under the price|Move the renewal line under the price)/.test(f);
|
|
28569
|
+
if (isRenewalLine) {
|
|
28570
|
+
if (renewalLineFor.has(card)) continue;
|
|
28571
|
+
renewalLineFor.add(card);
|
|
28572
|
+
}
|
|
28573
|
+
if (!out.includes(f)) out.push(f);
|
|
28574
|
+
}
|
|
28575
|
+
return out;
|
|
28439
28576
|
}
|
|
28440
28577
|
function stripeConsentFix(text, params) {
|
|
28441
|
-
if (!/^Add one unchecked checkbox/.test(text)) return text;
|
|
28578
|
+
if (!/^(Ask for a separate yes to the renewal|Add one unchecked checkbox)/.test(text)) return text;
|
|
28442
28579
|
const cc = params?.consent_collection;
|
|
28443
28580
|
if (cc?.terms_of_service === "required") return text;
|
|
28444
28581
|
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.';
|
|
@@ -28596,7 +28733,9 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28596
28733
|
if (alerts.length) Object.assign(base, { alerts });
|
|
28597
28734
|
const status = ciStatusOf(report);
|
|
28598
28735
|
const params = job.type === "stripe_checkout" ? job.body?.params ?? void 0 : void 0;
|
|
28599
|
-
|
|
28736
|
+
let fixes = (status === "pass" || status === "not_applicable" ? [] : fixesOf(report)).map((f) => ciVocabulary(job.type === "stripe_checkout" ? stripeConsentFix(f, params) : f));
|
|
28737
|
+
const pricingPage = job.type === "page" && job.body?.page_kind === "pricing";
|
|
28738
|
+
if (pricingPage) fixes = pricingPageFixes(fixes);
|
|
28600
28739
|
const unreadText = (job.unresolved ?? []).filter((p) => /^(?:custom_text|consent_collection|discounts|subscription_data|allow_promotion_codes|payment_method_collection)\b/.test(p));
|
|
28601
28740
|
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
28741
|
if (unread) {
|
|
@@ -28621,7 +28760,15 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28621
28760
|
const line2 = needles.length ? job.locate(needles) : void 0;
|
|
28622
28761
|
if (line2) base.target = { ...base.target, line: line2 };
|
|
28623
28762
|
}
|
|
28624
|
-
const
|
|
28763
|
+
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;
|
|
28764
|
+
if (declaredCoupon) {
|
|
28765
|
+
const id = /discounts use (coupon|promotion code) (\S+?),/.exec(declaredCoupon.message);
|
|
28766
|
+
const what = id ? `${id[1]} ${id[2]}` : "the coupon";
|
|
28767
|
+
fixes.unshift(
|
|
28768
|
+
engine.mode === "offline" ? `The intro price is declared in checkbox.json, not verified: offline, Checkbox cannot read ${what} from Stripe. Run with --online (CHECKBOX_SECRET_KEY, and the site's Stripe key connected) so Checkbox reads it and compares its percent / amount off and duration with the declared intro.` : `The intro price is declared in checkbox.json, not verified: Checkbox could not read ${what} through the site's connected Stripe key (is the site known, via --site or CHECKBOX_SITE, and does the key have Coupons: Read?). Until it is read, its percent / amount off and duration are not compared with the declared intro.`
|
|
28769
|
+
);
|
|
28770
|
+
}
|
|
28771
|
+
const finalStatus = unread ? literal.length ? "fail" : "needs_review" : declaredCoupon ? "needs_review" : status;
|
|
28625
28772
|
const corrected = unread ? void 0 : correctedOf(job.type, report, job.partial, job.placeholders);
|
|
28626
28773
|
if (corrected?.format === "json" && job.placeholders) {
|
|
28627
28774
|
const unset = job.warnings.filter((w) => /(?:was|were) not set when Checkbox ran/.test(w));
|
|
@@ -28632,17 +28779,17 @@ async function runJob(job, engine, config, opts = {}) {
|
|
|
28632
28779
|
...base,
|
|
28633
28780
|
status: finalStatus,
|
|
28634
28781
|
...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),
|
|
28782
|
+
label: unread ? `${literal.length ? `${literal.length} fix${literal.length === 1 ? "" : "es"} needed \xB7 ` : ""}Not checked: ${unreadText[0]} is an expression` : declaredCoupon ? "Needs review \xB7 coupon declared, not verified" : countedLabel(report.compliance?.label || DEFAULT_LABEL[status], report, pricingPage ? (f) => pricingPageFixes(f.map(ciVocabulary)) : void 0),
|
|
28636
28783
|
...fixes[0] ? { fix: fixes[0] } : {},
|
|
28637
28784
|
fixes,
|
|
28638
28785
|
...corrected ? { corrected } : {},
|
|
28639
28786
|
report
|
|
28640
28787
|
};
|
|
28641
28788
|
}
|
|
28642
|
-
function countedLabel(label, r) {
|
|
28789
|
+
function countedLabel(label, r, adjust) {
|
|
28643
28790
|
const m = /^(\d+) fix(?:es)? needed/.exec(label);
|
|
28644
28791
|
if (!m) return label;
|
|
28645
|
-
const n = fixesOf(r, true).length;
|
|
28792
|
+
const n = (adjust ? adjust(fixesOf(r, true)) : fixesOf(r, true)).length;
|
|
28646
28793
|
if (!n || n === Number(m[1])) return label;
|
|
28647
28794
|
return label.replace(m[0], `${n} fix${n === 1 ? "" : "es"} needed`);
|
|
28648
28795
|
}
|
|
@@ -28652,6 +28799,7 @@ function exitCodeOf(checks, strict) {
|
|
|
28652
28799
|
if (checks.some((c) => c.status === "error")) return { status: "error", exit: EXIT.usage };
|
|
28653
28800
|
if (checks.some((c) => c.status === "fail")) return { status: "fail", exit: EXIT.fail };
|
|
28654
28801
|
if (checks.some((c) => c.status === "needs_review")) return { status: "needs_review", exit: strict || checks.some((c) => c.blocking) ? EXIT.needs_review : EXIT.ok };
|
|
28802
|
+
if (!checks.some((c) => c.status === "pass")) return { status: "skipped", exit: EXIT.needs_review };
|
|
28655
28803
|
return { status: "pass", exit: EXIT.ok };
|
|
28656
28804
|
}
|
|
28657
28805
|
function mergeSourcePatches(checks) {
|
|
@@ -28673,6 +28821,11 @@ function mergeSourcePatches(checks) {
|
|
|
28673
28821
|
}
|
|
28674
28822
|
}
|
|
28675
28823
|
}
|
|
28824
|
+
function nothingCheckedMessage(run2) {
|
|
28825
|
+
const urlSkipped = run2.checks.some((c) => c.status === "skipped" && /^URL pages/.test(c.message ?? ""));
|
|
28826
|
+
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).';
|
|
28827
|
+
return `Nothing was checked: every check was skipped or does not apply, so this run is not a pass (exit 2).${how}`;
|
|
28828
|
+
}
|
|
28676
28829
|
async function runCi(jobs, engine, opts) {
|
|
28677
28830
|
const checks = [...opts.extra ?? []];
|
|
28678
28831
|
for (const j of jobs) checks.push(await runJob(j, engine, opts.config, { ...opts.notices ? { notices: opts.notices } : {} }));
|
|
@@ -28738,8 +28891,112 @@ var init_runner = __esm({
|
|
|
28738
28891
|
}
|
|
28739
28892
|
});
|
|
28740
28893
|
|
|
28894
|
+
// src/ci/report.ts
|
|
28895
|
+
function icon(s2, c) {
|
|
28896
|
+
if (s2 === "pass") return c.green("\u2713");
|
|
28897
|
+
if (s2 === "fail" || s2 === "error") return c.red("\u2717");
|
|
28898
|
+
if (s2 === "needs_review") return c.yellow("?");
|
|
28899
|
+
return c.dim("\u2013");
|
|
28900
|
+
}
|
|
28901
|
+
function overallLine(run2) {
|
|
28902
|
+
const s2 = run2.summary;
|
|
28903
|
+
const parts = [];
|
|
28904
|
+
if (s2.error) parts.push(`${s2.error} could not run`);
|
|
28905
|
+
if (s2.fail) parts.push(`${s2.fail} need${s2.fail === 1 ? "s" : ""} a fix`);
|
|
28906
|
+
if (s2.needs_review) parts.push(`${s2.needs_review} need${s2.needs_review === 1 ? "s" : ""} review${run2.strict ? " (strict)" : ""}`);
|
|
28907
|
+
if (s2.pass) parts.push(`${s2.pass} pass`);
|
|
28908
|
+
if (s2.not_applicable) parts.push(`${s2.not_applicable} not required`);
|
|
28909
|
+
if (s2.skipped) parts.push(`${s2.skipped} skipped`);
|
|
28910
|
+
return parts.join(" \xB7 ") || "nothing checked";
|
|
28911
|
+
}
|
|
28912
|
+
function renderHuman(run2, c, opts = {}) {
|
|
28913
|
+
const lines = [];
|
|
28914
|
+
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
28915
|
+
lines.push(`${c.bold("Checkbox CI")} ${run2.config} \xB7 ${run2.mode}${versions} \xB7 ${run2.checks.length} check${run2.checks.length === 1 ? "" : "s"}`);
|
|
28916
|
+
const width = Math.min(40, Math.max(...run2.checks.map((x) => x.name.length), 10));
|
|
28917
|
+
for (const x of run2.checks) {
|
|
28918
|
+
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);
|
|
28919
|
+
lines.push(` ${icon(x.status, c)} ${x.name.padEnd(width)} \u2192 ${label}`);
|
|
28920
|
+
if (x.message) lines.push(c.dim(` ${wrap(x.message, 110, " ")}`));
|
|
28921
|
+
if (x.fix) lines.push(` fix: ${wrap(x.fix, 110, " ")}${c.dim(` (${where(x)})`)}`);
|
|
28922
|
+
if (opts.verbose) for (const f of x.fixes.slice(1)) lines.push(` fix: ${wrap(f, 110, " ")}`);
|
|
28923
|
+
const alerts = x.alerts ?? [];
|
|
28924
|
+
for (const a of alerts) lines.push(c.yellow(` warning: ${wrap(a, 110, " ")}`));
|
|
28925
|
+
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, " ")}`));
|
|
28926
|
+
if (x.corrected && opts.verbose) lines.push(c.dim(` ${x.corrected.title}:`), ...x.corrected.body.split("\n").map((l) => c.dim(` ${l}`)));
|
|
28927
|
+
}
|
|
28928
|
+
const tone = run2.status === "pass" ? c.green : run2.status === "needs_review" && run2.exit_code === 0 ? c.yellow : c.red;
|
|
28929
|
+
if (run2.status === "skipped") lines.push("", c.red(wrap(nothingCheckedMessage(run2), 110, "")));
|
|
28930
|
+
lines.push("", `${tone(overallLine(run2))} \xB7 exit ${run2.exit_code}`);
|
|
28931
|
+
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."));
|
|
28932
|
+
lines.push(c.dim(run2.disclaimer));
|
|
28933
|
+
return lines;
|
|
28934
|
+
}
|
|
28935
|
+
function annotations(run2) {
|
|
28936
|
+
const out = [];
|
|
28937
|
+
for (const x of run2.checks) {
|
|
28938
|
+
const alerts = x.alerts ?? [];
|
|
28939
|
+
const level = x.status === "fail" || x.status === "error" ? "error" : x.status === "needs_review" ? run2.strict || x.blocking ? "error" : "warning" : alerts.length ? "warning" : void 0;
|
|
28940
|
+
if (!level) continue;
|
|
28941
|
+
const props = [`file=${escProp(x.target.file)}`, ...x.target.line ? [`line=${x.target.line}`] : [], `title=${escProp(`Checkbox: ${x.name} \u2014 ${x.label}`)}`];
|
|
28942
|
+
const body = x.status === "error" ? x.fix ? `${x.message ?? ""}
|
|
28943
|
+
${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");
|
|
28944
|
+
out.push(`::${level} ${props.join(",")}::${escData(body)}`);
|
|
28945
|
+
}
|
|
28946
|
+
if (run2.status === "skipped") out.push(`::error file=${escProp(run2.config)},title=${escProp("Checkbox: nothing was checked")}::${escData(nothingCheckedMessage(run2))}`);
|
|
28947
|
+
return out;
|
|
28948
|
+
}
|
|
28949
|
+
function stepSummary(run2) {
|
|
28950
|
+
const lines = [];
|
|
28951
|
+
lines.push("## Checkbox compliance checks", "");
|
|
28952
|
+
const versions = run2.rules_version ? ` \xB7 rules ${run2.rules_version} \xB7 catalog ${run2.catalog_version}` : "";
|
|
28953
|
+
lines.push(`**${overallLine(run2)}** \u2014 \`${run2.config}\` \xB7 ${run2.mode}${versions}`, "");
|
|
28954
|
+
if (run2.status === "skipped") lines.push(`\u274C ${nothingCheckedMessage(run2)}`, "");
|
|
28955
|
+
lines.push("| Check | Status | Fix | File |", "|---|---|---|---|");
|
|
28956
|
+
for (const x of run2.checks) {
|
|
28957
|
+
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 : "";
|
|
28958
|
+
const fix = [fix0, ...(x.alerts ?? []).map((a) => `\u26A0\uFE0F ${a}`)].filter(Boolean).join("\n");
|
|
28959
|
+
lines.push(`| ${STATUS_MD[x.status]} ${cell(x.name)}<br><sub>${TYPE_LABEL[x.type]}</sub> | ${cell(x.label)} | ${cell(fix)} | \`${cell(where(x))}\` |`);
|
|
28960
|
+
}
|
|
28961
|
+
const detailed = run2.checks.filter((x) => x.corrected || x.fixes.length > 1);
|
|
28962
|
+
for (const x of detailed) {
|
|
28963
|
+
lines.push("", `<details><summary><b>${escapeHtml(x.name)}</b>: ${escapeHtml(x.corrected ? x.corrected.title.toLowerCase() : `${x.fixes.length} fixes`)}</summary>`, "");
|
|
28964
|
+
if (x.fixes.length) lines.push(...x.fixes.map((f) => `- ${f}`), "");
|
|
28965
|
+
if (x.corrected) {
|
|
28966
|
+
const fence = x.corrected.body.includes("```") ? "````" : "```";
|
|
28967
|
+
lines.push(`${fence}${x.corrected.format === "json" ? "json" : "text"}`, x.corrected.body, fence, "");
|
|
28968
|
+
}
|
|
28969
|
+
lines.push("</details>");
|
|
28970
|
+
}
|
|
28971
|
+
lines.push("", `<sub>${run2.disclaimer} Docs: https://checkbox.site/docs/guides/ci</sub>`, "");
|
|
28972
|
+
return lines.join("\n");
|
|
28973
|
+
}
|
|
28974
|
+
function escapeHtml(s2) {
|
|
28975
|
+
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28976
|
+
}
|
|
28977
|
+
var TYPE_LABEL, where, escData, escProp, cell, STATUS_MD;
|
|
28978
|
+
var init_report = __esm({
|
|
28979
|
+
"src/ci/report.ts"() {
|
|
28980
|
+
"use strict";
|
|
28981
|
+
init_output();
|
|
28982
|
+
init_runner();
|
|
28983
|
+
TYPE_LABEL = {
|
|
28984
|
+
stripe_checkout: "Stripe Checkout",
|
|
28985
|
+
disclosure: "Disclosure",
|
|
28986
|
+
page: "Page",
|
|
28987
|
+
email: "Email",
|
|
28988
|
+
notice_schedule: "Notice schedule"
|
|
28989
|
+
};
|
|
28990
|
+
where = (c) => `${c.target.file}${c.target.line && c.target.line > 1 ? `:${c.target.line}` : ""}`;
|
|
28991
|
+
escData = (s2) => s2.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
28992
|
+
escProp = (s2) => escData(s2).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
28993
|
+
cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
28994
|
+
STATUS_MD = { pass: "\u2705", fail: "\u274C", needs_review: "\u26A0\uFE0F", error: "\u274C", skipped: "\u23ED\uFE0F", not_applicable: "\u2796" };
|
|
28995
|
+
}
|
|
28996
|
+
});
|
|
28997
|
+
|
|
28741
28998
|
// src/commands/plan/lib.ts
|
|
28742
|
-
import { dirname as
|
|
28999
|
+
import { dirname as dirname6, resolve as resolve6 } from "node:path";
|
|
28743
29000
|
async function resolveSite(ctx, flag, client) {
|
|
28744
29001
|
const explicit = flag ?? ctx.env.CHECKBOX_SITE;
|
|
28745
29002
|
if (explicit) {
|
|
@@ -28747,10 +29004,10 @@ async function resolveSite(ctx, flag, client) {
|
|
|
28747
29004
|
return explicit;
|
|
28748
29005
|
}
|
|
28749
29006
|
const projects = ctx.config.load().init?.projects ?? {};
|
|
28750
|
-
for (let dir =
|
|
29007
|
+
for (let dir = resolve6(ctx.cwd); ; dir = dirname6(dir)) {
|
|
28751
29008
|
const site = projects[dir]?.site;
|
|
28752
29009
|
if (site) return site;
|
|
28753
|
-
if (
|
|
29010
|
+
if (dirname6(dir) === dir) break;
|
|
28754
29011
|
}
|
|
28755
29012
|
const list2 = await client.sdk.sites.list({ limit: 3 });
|
|
28756
29013
|
const sites = list2?.data ?? [];
|
|
@@ -28807,12 +29064,28 @@ var init_lib = __esm({
|
|
|
28807
29064
|
});
|
|
28808
29065
|
|
|
28809
29066
|
// src/ci/workflow.ts
|
|
28810
|
-
|
|
28811
|
-
|
|
28812
|
-
|
|
28813
|
-
|
|
28814
|
-
|
|
28815
|
-
|
|
29067
|
+
import { execFileSync } from "node:child_process";
|
|
29068
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
29069
|
+
import { join as join8 } from "node:path";
|
|
29070
|
+
function workflowYaml(needs = {}) {
|
|
29071
|
+
const node = !!(needs.install || needs.build);
|
|
29072
|
+
const steps = [" - uses: actions/checkout@v4"];
|
|
29073
|
+
if (node) {
|
|
29074
|
+
steps.push(" - uses: actions/setup-node@v4", " with:", " node-version: 20");
|
|
29075
|
+
if (needs.install) steps.push(` # ${needs.why?.install ?? 'checkbox.json "module" entries import your code and its dependencies.'}`, ` - run: ${needs.install}`);
|
|
29076
|
+
if (needs.build) steps.push(` # ${needs.why?.build ?? 'checkbox.json "pages" read built HTML, which a fresh clone does not have.'}`, ` - run: ${needs.build}`);
|
|
29077
|
+
} else {
|
|
29078
|
+
steps.push(' # Only needed when checkbox.json uses "module" entries that import your dependencies:', " # - run: npm ci");
|
|
29079
|
+
}
|
|
29080
|
+
steps.push(" - uses: checkbox-site/checkbox@v1", " # with:", " # strict: true # also fail when a check needs review");
|
|
29081
|
+
if (needs.urlPages) {
|
|
29082
|
+
steps.push(
|
|
29083
|
+
' # args: --online # "url" pages are fetched by the API: needs a LIVE key (ck_live_\u2026) on an',
|
|
29084
|
+
' # # activated account. Without one, list the built page as "file" instead.'
|
|
29085
|
+
);
|
|
29086
|
+
} else steps.push(" # args: --online # URL pages via the API; needs the env below");
|
|
29087
|
+
steps.push(" # env:", " # CHECKBOX_SECRET_KEY: ${{ secrets.CHECKBOX_SECRET_KEY }}");
|
|
29088
|
+
return `# Checkbox: automatic-renewal law checks on every pull request (https://checkbox.site/docs/guides/ci).
|
|
28816
29089
|
# Runs the checks listed in checkbox.json with the Checkbox rules engine, offline (no key, no network), annotates the
|
|
28817
29090
|
# lines that need a fix and writes a summary with the corrected params / text to the job page.
|
|
28818
29091
|
name: Checkbox
|
|
@@ -28830,35 +29103,85 @@ jobs:
|
|
|
28830
29103
|
runs-on: ubuntu-latest
|
|
28831
29104
|
timeout-minutes: 10
|
|
28832
29105
|
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 }}
|
|
29106
|
+
${steps.join("\n")}
|
|
28842
29107
|
`;
|
|
29108
|
+
}
|
|
29109
|
+
function installCommand(root) {
|
|
29110
|
+
if (existsSync7(join8(root, "pnpm-lock.yaml"))) return "corepack enable && pnpm install --frozen-lockfile";
|
|
29111
|
+
if (existsSync7(join8(root, "yarn.lock"))) return "corepack enable && yarn install --immutable";
|
|
29112
|
+
if (existsSync7(join8(root, "bun.lockb")) || existsSync7(join8(root, "bun.lock"))) return "npx --yes bun install --frozen-lockfile";
|
|
29113
|
+
if (existsSync7(join8(root, "package-lock.json")) || existsSync7(join8(root, "npm-shrinkwrap.json"))) return "npm ci";
|
|
29114
|
+
return "npm install";
|
|
29115
|
+
}
|
|
29116
|
+
function runScript(root, script) {
|
|
29117
|
+
if (existsSync7(join8(root, "pnpm-lock.yaml"))) return `pnpm run ${script}`;
|
|
29118
|
+
if (existsSync7(join8(root, "yarn.lock"))) return `yarn run ${script}`;
|
|
29119
|
+
return `npm run ${script}`;
|
|
29120
|
+
}
|
|
29121
|
+
function notInClone(root, files) {
|
|
29122
|
+
if (!files.length) return [];
|
|
29123
|
+
let tracked = null;
|
|
29124
|
+
try {
|
|
29125
|
+
const out = execFileSync("git", ["ls-files", "-z", "--", ...files], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
|
|
29126
|
+
tracked = new Set(out.split("\0").filter(Boolean));
|
|
29127
|
+
} catch {
|
|
29128
|
+
tracked = null;
|
|
29129
|
+
}
|
|
29130
|
+
return files.filter((f) => tracked ? !tracked.has(f.replace(/^\.\//, "")) : !existsSync7(join8(root, f)));
|
|
29131
|
+
}
|
|
29132
|
+
function workflowNeeds(root, config) {
|
|
29133
|
+
const checks = config.checks ?? {};
|
|
29134
|
+
const list2 = (k) => Array.isArray(checks[k]) ? checks[k] : [];
|
|
29135
|
+
const modules = [...list2("stripe_checkout"), ...list2("emails"), ...list2("disclosures")].some((e) => typeof e.module === "string" && !!e.module);
|
|
29136
|
+
const pageFiles = list2("pages").flatMap((e) => typeof e.file === "string" && !e.file.startsWith("TODO") ? [e.file] : []);
|
|
29137
|
+
const urlPages = list2("pages").some((e) => typeof e.url === "string");
|
|
29138
|
+
let pkg = null;
|
|
29139
|
+
try {
|
|
29140
|
+
pkg = JSON.parse(readFileSync9(join8(root, "package.json"), "utf8"));
|
|
29141
|
+
} catch {
|
|
29142
|
+
pkg = null;
|
|
29143
|
+
}
|
|
29144
|
+
const built = notInClone(root, pageFiles);
|
|
29145
|
+
const buildScript = pkg?.scripts?.build ? "build" : void 0;
|
|
29146
|
+
const needs = { why: {} };
|
|
29147
|
+
if (built.length && buildScript) {
|
|
29148
|
+
needs.build = runScript(root, buildScript);
|
|
29149
|
+
needs.why.build = `checkbox.json "pages" read ${built.slice(0, 2).join(", ")}${built.length > 2 ? ", \u2026" : ""}, built here (not in git).`;
|
|
29150
|
+
}
|
|
29151
|
+
if (pkg && (modules || needs.build)) {
|
|
29152
|
+
needs.install = installCommand(root);
|
|
29153
|
+
needs.why.install = modules ? 'checkbox.json "module" entries import your code and its dependencies.' : "The build needs your dependencies.";
|
|
29154
|
+
}
|
|
29155
|
+
if (urlPages) needs.urlPages = true;
|
|
29156
|
+
return needs;
|
|
29157
|
+
}
|
|
29158
|
+
var WORKFLOW_PATH, WORKFLOW_YAML;
|
|
29159
|
+
var init_workflow = __esm({
|
|
29160
|
+
"src/ci/workflow.ts"() {
|
|
29161
|
+
"use strict";
|
|
29162
|
+
WORKFLOW_PATH = ".github/workflows/checkbox.yml";
|
|
29163
|
+
WORKFLOW_YAML = workflowYaml();
|
|
28843
29164
|
}
|
|
28844
29165
|
});
|
|
28845
29166
|
|
|
28846
29167
|
// src/commands/ci/index.ts
|
|
28847
29168
|
var ci_exports = {};
|
|
28848
29169
|
__export(ci_exports, {
|
|
29170
|
+
attachSiteForCoupons: () => attachSiteForCoupons,
|
|
29171
|
+
couponIdsIn: () => couponIdsIn,
|
|
28849
29172
|
default: () => ci_default,
|
|
28850
29173
|
touchesConfig: () => touchesConfig,
|
|
28851
29174
|
undiffableInputs: () => undiffableInputs
|
|
28852
29175
|
});
|
|
28853
|
-
import { execFileSync } from "node:child_process";
|
|
28854
|
-
import { appendFileSync, existsSync as
|
|
29176
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
29177
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
28855
29178
|
import { createInterface } from "node:readline";
|
|
28856
|
-
import { dirname as
|
|
29179
|
+
import { dirname as dirname7, join as join9, relative as relative5, resolve as resolve7 } from "node:path";
|
|
28857
29180
|
function gitChanged(cwd, base) {
|
|
28858
29181
|
try {
|
|
28859
|
-
const root =
|
|
28860
|
-
const out =
|
|
28861
|
-
return { root, files: out.split("\n").map((l) => l.trim()).filter(Boolean).map((f) =>
|
|
29182
|
+
const root = execFileSync2("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
29183
|
+
const out = execFileSync2("git", ["diff", "--name-only", `${base}...HEAD`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
29184
|
+
return { root, files: out.split("\n").map((l) => l.trim()).filter(Boolean).map((f) => resolve7(root, f)) };
|
|
28862
29185
|
} catch (err) {
|
|
28863
29186
|
const msg2 = String(err.stderr || err.message).trim().split("\n")[0];
|
|
28864
29187
|
return `git diff ${base}...HEAD failed (${msg2}); fetch the base branch (actions/checkout with fetch-depth: 0)`;
|
|
@@ -28868,13 +29191,13 @@ function undiffableInputs(loaded, jobs, gitRoot) {
|
|
|
28868
29191
|
const need = [];
|
|
28869
29192
|
const out = [];
|
|
28870
29193
|
for (const e of loaded.config.checks.pages ?? []) {
|
|
28871
|
-
if (e.source) need.push({ path:
|
|
29194
|
+
if (e.source) need.push({ path: resolve7(loaded.dir, e.source), label: `pages "${e.name}" source ${e.source}` });
|
|
28872
29195
|
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:
|
|
29196
|
+
else if (e.file) need.push({ path: resolve7(loaded.dir, e.file), label: `pages "${e.name}" file ${e.file}` });
|
|
28874
29197
|
}
|
|
28875
29198
|
for (const j of jobs) {
|
|
28876
29199
|
if (j.type === "page") continue;
|
|
28877
|
-
for (const f of j.inputs) need.push({ path:
|
|
29200
|
+
for (const f of j.inputs) need.push({ path: resolve7(loaded.dir, f), label: `"${j.name}" reads ${f}` });
|
|
28878
29201
|
}
|
|
28879
29202
|
if (!need.length) return out;
|
|
28880
29203
|
const real = (p) => {
|
|
@@ -28889,8 +29212,8 @@ function undiffableInputs(loaded, jobs, gitRoot) {
|
|
|
28889
29212
|
let tracked;
|
|
28890
29213
|
try {
|
|
28891
29214
|
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) =>
|
|
29215
|
+
const listed = execFileSync2("git", ["ls-files", "-z", "--", ...rels], { cwd: gitRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
29216
|
+
tracked = new Set(listed.split("\0").filter(Boolean).map((f) => resolve7(gitRoot, f)));
|
|
28894
29217
|
} catch (err) {
|
|
28895
29218
|
return `git ls-files failed (${String(err.stderr || err.message).trim().split("\n")[0]})`;
|
|
28896
29219
|
}
|
|
@@ -28902,7 +29225,7 @@ function touchesConfig(loaded, jobs, changed) {
|
|
|
28902
29225
|
if (set.has(loaded.path)) return true;
|
|
28903
29226
|
const codeChanged = changed.some((f) => /\.(?:[cm]?[jt]sx?)$/.test(f));
|
|
28904
29227
|
for (const j of jobs) {
|
|
28905
|
-
for (const f of j.inputs) if (set.has(
|
|
29228
|
+
for (const f of j.inputs) if (set.has(resolve7(loaded.dir, f))) return true;
|
|
28906
29229
|
}
|
|
28907
29230
|
const hasModules = [...loaded.config.checks.stripe_checkout ?? [], ...loaded.config.checks.emails ?? []].some((e) => "module" in e && e.module);
|
|
28908
29231
|
return hasModules && codeChanged;
|
|
@@ -28927,24 +29250,26 @@ async function runInit(ctx, argv) {
|
|
|
28927
29250
|
const p = parseArgs(argv, { boolean: ["yes", "force", "dry-run", "workflow"], string: ["config"], alias: { y: "yes" } });
|
|
28928
29251
|
if (p.positionals.length) throw new UsageError(`Unexpected argument "${p.positionals[0]}"`);
|
|
28929
29252
|
const explicit = str(p.flags, "config");
|
|
28930
|
-
const configPath = explicit ?
|
|
28931
|
-
const workflowPath =
|
|
29253
|
+
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]);
|
|
29254
|
+
const workflowPath = join9(ctx.cwd, WORKFLOW_PATH);
|
|
28932
29255
|
const force = bool(p.flags, "force") === true;
|
|
28933
29256
|
const withWorkflow = bool(p.flags, "workflow") !== false;
|
|
28934
29257
|
const projects = ctx.config.load().init?.projects ?? {};
|
|
28935
29258
|
let facts = {};
|
|
28936
|
-
for (let dir =
|
|
29259
|
+
for (let dir = resolve7(ctx.cwd); ; dir = dirname7(dir)) {
|
|
28937
29260
|
if (projects[dir]?.facts) {
|
|
28938
29261
|
facts = projects[dir].facts;
|
|
28939
29262
|
break;
|
|
28940
29263
|
}
|
|
28941
|
-
if (
|
|
29264
|
+
if (dirname7(dir) === dir) break;
|
|
28942
29265
|
}
|
|
28943
29266
|
const s2 = scaffoldConfig(ctx.cwd, ctx.env, facts);
|
|
28944
|
-
const actionFor = (f) =>
|
|
29267
|
+
const actionFor = (f) => existsSync8(f) ? force ? "overwrite" : "keep" : "create";
|
|
28945
29268
|
const files = [{ path: configPath, content: `${JSON.stringify(s2.config, null, 2)}
|
|
28946
29269
|
`, action: actionFor(configPath) }];
|
|
28947
|
-
|
|
29270
|
+
const needs = workflowNeeds(ctx.cwd, s2.config);
|
|
29271
|
+
const workflowSteps = [...needs.install ? [needs.install] : [], ...needs.build ? [needs.build] : []];
|
|
29272
|
+
if (withWorkflow) files.push({ path: workflowPath, content: workflowYaml(needs), action: actionFor(workflowPath) });
|
|
28948
29273
|
const toWrite = files.filter((f) => f.action !== "keep");
|
|
28949
29274
|
const dry = bool(p.flags, "dry-run") === true;
|
|
28950
29275
|
let write = !dry && toWrite.length > 0;
|
|
@@ -28956,7 +29281,7 @@ async function runInit(ctx, argv) {
|
|
|
28956
29281
|
}
|
|
28957
29282
|
if (write) {
|
|
28958
29283
|
for (const f of toWrite) {
|
|
28959
|
-
mkdirSync2(
|
|
29284
|
+
mkdirSync2(dirname7(f.path), { recursive: true });
|
|
28960
29285
|
writeFileSync2(f.path, f.content);
|
|
28961
29286
|
}
|
|
28962
29287
|
}
|
|
@@ -28967,6 +29292,7 @@ async function runInit(ctx, argv) {
|
|
|
28967
29292
|
kept: files.filter((f) => f.action === "keep").map((f) => relName(f.path)),
|
|
28968
29293
|
...write ? {} : { would_write: toWrite.map((f) => relName(f.path)) },
|
|
28969
29294
|
found: s2.found,
|
|
29295
|
+
...withWorkflow ? { workflow_steps: workflowSteps } : {},
|
|
28970
29296
|
todos: s2.todos,
|
|
28971
29297
|
config: s2.config,
|
|
28972
29298
|
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 +29302,8 @@ async function runInit(ctx, argv) {
|
|
|
28976
29302
|
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
29303
|
if (!write && toWrite.length && !dry) lines.push(ctx.out.c.dim("Nothing written: add --yes to write without asking."));
|
|
28978
29304
|
if (s2.found.length) lines.push("", "Found:", ...s2.found.map((f) => ` - ${f}`));
|
|
29305
|
+
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.`);
|
|
29306
|
+
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
29307
|
if (s2.todos.length) lines.push("", "To do:", ...s2.todos.map((t) => ` - ${t}`));
|
|
28980
29308
|
lines.push("", `${ctx.out.c.cyan("Next:")} ${result.next_action.message}`);
|
|
28981
29309
|
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 +29325,22 @@ async function readNotices(ctx, client, siteFlag) {
|
|
|
28997
29325
|
return { error: `Could not read the Notice settings of ${site} (${err.message}).` };
|
|
28998
29326
|
}
|
|
28999
29327
|
}
|
|
29328
|
+
async function attachSiteForCoupons(ctx, client, jobs, siteFlag) {
|
|
29329
|
+
const withCoupon = jobs.filter((j) => j.type === "stripe_checkout" && j.body && couponIdsIn(j.body.params).length);
|
|
29330
|
+
if (!withCoupon.length) return;
|
|
29331
|
+
let site;
|
|
29332
|
+
try {
|
|
29333
|
+
site = await resolveSite(ctx, siteFlag, client);
|
|
29334
|
+
} catch (err) {
|
|
29335
|
+
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).`);
|
|
29336
|
+
return;
|
|
29337
|
+
}
|
|
29338
|
+
for (const j of withCoupon) j.body = { ...j.body, site };
|
|
29339
|
+
}
|
|
29340
|
+
function couponIdsIn(params) {
|
|
29341
|
+
const discounts = params?.discounts;
|
|
29342
|
+
return (Array.isArray(discounts) ? discounts : []).flatMap((d) => d && typeof d === "object" && typeof d.coupon === "string" ? [d.coupon] : []);
|
|
29343
|
+
}
|
|
29000
29344
|
var HELP2, NOTICE_KIND_TO_EMAIL, ci_default;
|
|
29001
29345
|
var init_ci = __esm({
|
|
29002
29346
|
"src/commands/ci/index.ts"() {
|
|
@@ -29017,7 +29361,8 @@ var init_ci = __esm({
|
|
|
29017
29361
|
"",
|
|
29018
29362
|
"Flags:",
|
|
29019
29363
|
" --config <path> Config file (default: checkbox.json, then .checkbox.json)",
|
|
29020
|
-
" --online Use the API (CHECKBOX_SECRET_KEY / CHECKBOX_API_KEY): URL pages
|
|
29364
|
+
" --online Use the API (CHECKBOX_SECRET_KEY / CHECKBOX_API_KEY): URL pages (these need a live-mode",
|
|
29365
|
+
' key on an activated account; with a test key list the built page as "file") and site-aware checks;',
|
|
29021
29366
|
` with "notices" in checkbox.json, also checks that the site's Checkbox Notices really send them`,
|
|
29022
29367
|
" --site <site_\u2026> The site for --online (default: $CHECKBOX_SITE, the site `checkbox init` saved, or the only one)",
|
|
29023
29368
|
' --strict "Needs review" fails too (exit 2)',
|
|
@@ -29033,12 +29378,14 @@ var init_ci = __esm({
|
|
|
29033
29378
|
"",
|
|
29034
29379
|
"Exit: 0 pass (or needs review without --strict) \xB7 1 a check needs a fix \xB7 2 needs review with --strict, or a",
|
|
29035
29380
|
" 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
|
|
29381
|
+
' checkout entry with "strict": true that needs review, or nothing was checked (every check skipped or',
|
|
29382
|
+
" not applicable, e.g. url pages without --online) \xB7",
|
|
29037
29383
|
" 3 config / usage error \xB7 4 auth (--online) \xB7 5 API or network error",
|
|
29038
29384
|
"",
|
|
29039
29385
|
"checkbox ci init [--yes] [--force] [--dry-run] [--no-workflow]",
|
|
29040
29386
|
' Writes checkbox.json from the project (Stripe Checkout calls, plans, email templates, pages; "TODO" where it',
|
|
29041
|
-
|
|
29387
|
+
' cannot tell) and .github/workflows/checkbox.yml (with the install and build steps a fresh clone needs for "module"',
|
|
29388
|
+
" entries and built pages). Asks first on a terminal; --yes writes without asking (agents).",
|
|
29042
29389
|
"",
|
|
29043
29390
|
"Docs: https://checkbox.site/docs/guides/ci"
|
|
29044
29391
|
].join("\n");
|
|
@@ -29081,7 +29428,7 @@ var init_ci = __esm({
|
|
|
29081
29428
|
const workspace = ctx.env.GITHUB_WORKSPACE ?? ctx.cwd;
|
|
29082
29429
|
const extra = (loaded.config.coverage === false ? [] : coverageResults(coverageOf(loaded, loaded.dir))).map((r) => ({
|
|
29083
29430
|
...r,
|
|
29084
|
-
target: { ...r.target, file: relative5(workspace,
|
|
29431
|
+
target: { ...r.target, file: relative5(workspace, join9(loaded.dir, r.target.file)).split("\\").join("/") || r.target.file }
|
|
29085
29432
|
}));
|
|
29086
29433
|
if (bool(p.flags, "changed-only")) {
|
|
29087
29434
|
const base = str(p.flags, "base") ?? (ctx.env.GITHUB_BASE_REF ? `origin/${ctx.env.GITHUB_BASE_REF}` : void 0);
|
|
@@ -29118,6 +29465,7 @@ ${message}
|
|
|
29118
29465
|
return EXIT.auth;
|
|
29119
29466
|
}
|
|
29120
29467
|
}
|
|
29468
|
+
if (client) await attachSiteForCoupons(ctx, client, jobs, str(p.flags, "site"));
|
|
29121
29469
|
const engine = client ? onlineEngine(client) : offlineEngine();
|
|
29122
29470
|
const notices = client && declaredByCheckbox(loaded.config).length ? await readNotices(ctx, client, str(p.flags, "site")) : void 0;
|
|
29123
29471
|
const noticesLine = Math.max(1, loaded.raw.split("\n").findIndex((l) => l.includes('"sent_by_checkbox"')) + 1);
|
|
@@ -29353,8 +29701,8 @@ var init_patch = __esm({
|
|
|
29353
29701
|
});
|
|
29354
29702
|
|
|
29355
29703
|
// src/commands/plan/fix.ts
|
|
29356
|
-
import { existsSync as
|
|
29357
|
-
import { relative as relative6, resolve as
|
|
29704
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
|
|
29705
|
+
import { relative as relative6, resolve as resolve8 } from "node:path";
|
|
29358
29706
|
function fixForTask(task, cwd, file) {
|
|
29359
29707
|
const base = {
|
|
29360
29708
|
object: "task_fix",
|
|
@@ -29366,12 +29714,12 @@ function fixForTask(task, cwd, file) {
|
|
|
29366
29714
|
verify: task.verify
|
|
29367
29715
|
};
|
|
29368
29716
|
const verifyCmd = `checkbox verify ${task.id} --wait`;
|
|
29369
|
-
const candidate = file ?? (task.where.file_hint &&
|
|
29717
|
+
const candidate = file ?? (task.where.file_hint && existsSync9(resolve8(cwd, task.where.file_hint)) ? task.where.file_hint : void 0);
|
|
29370
29718
|
if (task.change.type === "patch" && task.change.edits.length && candidate) {
|
|
29371
|
-
const path =
|
|
29719
|
+
const path = resolve8(cwd, candidate);
|
|
29372
29720
|
let src;
|
|
29373
29721
|
try {
|
|
29374
|
-
src =
|
|
29722
|
+
src = readFileSync10(path, "utf8");
|
|
29375
29723
|
} catch (err) {
|
|
29376
29724
|
throw new UsageError(`Cannot read ${candidate}: ${err.code ?? err.message}`);
|
|
29377
29725
|
}
|
|
@@ -29431,7 +29779,7 @@ var init_fix = __esm({
|
|
|
29431
29779
|
const task = await ctx.client().sdk.tasks.retrieve(id);
|
|
29432
29780
|
const result = fixForTask(task, ctx.cwd, str(flags, "file"));
|
|
29433
29781
|
const out = str(flags, "out");
|
|
29434
|
-
if (out && result.diff) writeFileSync3(
|
|
29782
|
+
if (out && result.diff) writeFileSync3(resolve8(ctx.cwd, out), result.diff);
|
|
29435
29783
|
ctx.out.result({ ...result, task_object: task }, () => {
|
|
29436
29784
|
const c = ctx.out.c;
|
|
29437
29785
|
const lines = [`${c.bold(task.title)} ${c.dim(`(${task.priority}, ${task.id})`)}`, c.dim(`${task.requirement.citation}`), task.why, ""];
|
|
@@ -29533,12 +29881,12 @@ var init_device = __esm({
|
|
|
29533
29881
|
});
|
|
29534
29882
|
|
|
29535
29883
|
// src/commands/init/patch.ts
|
|
29536
|
-
import { execFileSync as
|
|
29537
|
-
import { existsSync as
|
|
29538
|
-
import { dirname as
|
|
29884
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
29885
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
29886
|
+
import { dirname as dirname8, join as join10 } from "node:path";
|
|
29539
29887
|
function read(root, rel2) {
|
|
29540
29888
|
try {
|
|
29541
|
-
return
|
|
29889
|
+
return readFileSync11(join10(root, rel2), "utf8");
|
|
29542
29890
|
} catch {
|
|
29543
29891
|
return null;
|
|
29544
29892
|
}
|
|
@@ -29716,7 +30064,7 @@ function mask(v) {
|
|
|
29716
30064
|
}
|
|
29717
30065
|
function isGitTracked(root, file) {
|
|
29718
30066
|
try {
|
|
29719
|
-
|
|
30067
|
+
execFileSync3("git", ["ls-files", "--error-unmatch", "--", file], { cwd: root, stdio: "ignore", timeout: 5e3 });
|
|
29720
30068
|
return true;
|
|
29721
30069
|
} catch {
|
|
29722
30070
|
return false;
|
|
@@ -29733,7 +30081,7 @@ function planGitignore(d) {
|
|
|
29733
30081
|
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
30082
|
if (covered) return null;
|
|
29735
30083
|
}
|
|
29736
|
-
if (existing === null && !
|
|
30084
|
+
if (existing === null && !existsSync10(join10(d.root, ".git"))) return null;
|
|
29737
30085
|
const text = `${existing ?? ""}${existing && !existing.endsWith("\n") ? "\n" : ""}# Checkbox keys (checkbox init)
|
|
29738
30086
|
${env2}
|
|
29739
30087
|
`;
|
|
@@ -29749,8 +30097,8 @@ function planWellKnown(d, token) {
|
|
|
29749
30097
|
}
|
|
29750
30098
|
function applyChange(root, c) {
|
|
29751
30099
|
if (c.action === "unchanged" || c.contents === void 0) return;
|
|
29752
|
-
const abs =
|
|
29753
|
-
mkdirSync3(
|
|
30100
|
+
const abs = join10(root, c.file);
|
|
30101
|
+
mkdirSync3(dirname8(abs), { recursive: true });
|
|
29754
30102
|
writeFileSync4(abs, c.contents, c.file.startsWith(".env") ? { mode: 384 } : void 0);
|
|
29755
30103
|
}
|
|
29756
30104
|
var MARK_START, MARK_END, NEXT_DOCUMENT, NUXT_PLUGIN;
|
|
@@ -29797,8 +30145,8 @@ __export(init_exports, {
|
|
|
29797
30145
|
parsePlanFlag: () => parsePlanFlag,
|
|
29798
30146
|
syncCheckoutConfigs: () => syncCheckoutConfigs
|
|
29799
30147
|
});
|
|
29800
|
-
import { existsSync as
|
|
29801
|
-
import { join as
|
|
30148
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
30149
|
+
import { join as join11 } from "node:path";
|
|
29802
30150
|
import { createInterface as createInterface2 } from "node:readline";
|
|
29803
30151
|
function loadState(ctx, root) {
|
|
29804
30152
|
const cfg = ctx.config.load().init ?? {};
|
|
@@ -29818,7 +30166,7 @@ function saveState(ctx, root, patch) {
|
|
|
29818
30166
|
async function ask2(question) {
|
|
29819
30167
|
const rl = createInterface2({ input: process.stdin, output: process.stderr, terminal: true });
|
|
29820
30168
|
try {
|
|
29821
|
-
return await new Promise((
|
|
30169
|
+
return await new Promise((resolve10) => rl.question(question, resolve10));
|
|
29822
30170
|
} finally {
|
|
29823
30171
|
rl.close();
|
|
29824
30172
|
}
|
|
@@ -29896,7 +30244,7 @@ async function syncCheckoutConfigs(client, site, sessions, root) {
|
|
|
29896
30244
|
const from = configFileOf(c.name);
|
|
29897
30245
|
if (!from) continue;
|
|
29898
30246
|
const sameFile = perFile.has(from.file);
|
|
29899
|
-
const gone = !sameFile && !
|
|
30247
|
+
const gone = !sameFile && !existsSync11(join11(root, from.file));
|
|
29900
30248
|
const current = sameFile && from.call !== void 0 && from.call <= perFile.get(from.file) && c.name === configNameOf(from.file, from.call);
|
|
29901
30249
|
if (!sameFile && !gone || current) continue;
|
|
29902
30250
|
if (await client.sdk.stripeCheckoutConfigs.del(c.id).then(() => true, () => false)) {
|
|
@@ -29936,6 +30284,7 @@ function detectionSummary(d, siteUrl) {
|
|
|
29936
30284
|
pay_buttons: d.payButtons.map((c) => ({ file: c.file, line: c.line })),
|
|
29937
30285
|
checkout_sessions: d.checkoutSessions.map((s2) => ({ file: s2.file, line: s2.line, readable: !!s2.params, unresolved: s2.unresolved })),
|
|
29938
30286
|
stripe_test_key: d.stripeTestKey ? { hint: d.stripeTestKey.hint, source: d.stripeTestKey.source, restricted: d.stripeTestKey.restricted } : null,
|
|
30287
|
+
...d.stripeKeyRefused ? { stripe_key_refused: { ...d.stripeKeyRefused, message: stripeKeyRefusedMessage(d.stripeKeyRefused) } } : {},
|
|
29939
30288
|
merchant_name: d.brand ?? null,
|
|
29940
30289
|
plans: d.plans
|
|
29941
30290
|
};
|
|
@@ -30030,7 +30379,7 @@ var init_init = __esm({
|
|
|
30030
30379
|
" 3. writes CHECKBOX_SECRET_KEY and the publishable key to .env.local / .env (and git-ignores it)",
|
|
30031
30380
|
" 4. installs the Checkbox snippet in the root layout (marked with checkbox:snippet comments)",
|
|
30032
30381
|
" 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",
|
|
30382
|
+
" 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
30383
|
" 7. runs the first checks and prints the next step (`checkbox plan --json`); the human gets the summary email",
|
|
30035
30384
|
" when the setup is finished (every check passes, or `checkbox init --complete`)",
|
|
30036
30385
|
"",
|
|
@@ -30226,7 +30575,20 @@ var init_init = __esm({
|
|
|
30226
30575
|
}
|
|
30227
30576
|
const policyFromPaths = (kind) => siteUrl && d.policyPaths[kind] ? `${siteUrl}${d.policyPaths[kind]}` : void 0;
|
|
30228
30577
|
const agent = detectAgent(ctx.env, agentName);
|
|
30229
|
-
|
|
30578
|
+
let plans = flagPlans ?? (d.plans.length ? d.plans.map(({ source: _source, ...p }) => p) : void 0);
|
|
30579
|
+
if (plans?.length && !planFlag) {
|
|
30580
|
+
let current = null;
|
|
30581
|
+
try {
|
|
30582
|
+
current = await client.sdk.setupSessions.retrieve(sessionId);
|
|
30583
|
+
} catch {
|
|
30584
|
+
current = null;
|
|
30585
|
+
}
|
|
30586
|
+
const saved = Array.isArray(current?.business?.plans) ? current.business.plans : [];
|
|
30587
|
+
if (saved.length) {
|
|
30588
|
+
plans = void 0;
|
|
30589
|
+
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"}).`);
|
|
30590
|
+
}
|
|
30591
|
+
}
|
|
30230
30592
|
const brand = merchantName ?? d.brand?.name;
|
|
30231
30593
|
const update = {
|
|
30232
30594
|
...siteUrl ? { site_url: siteUrl } : {},
|
|
@@ -30292,13 +30654,16 @@ var init_init = __esm({
|
|
|
30292
30654
|
if (consent && d.stripeTestKey && !session.stripe.test_credential_stored) {
|
|
30293
30655
|
try {
|
|
30294
30656
|
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.`);
|
|
30657
|
+
stripe.test_credential = { stored: true, key: d.stripeTestKey.hint, restricted: true };
|
|
30297
30658
|
} catch (err) {
|
|
30298
30659
|
stripe.test_credential = { stored: false, error: err instanceof ApiError ? `${err.code ?? ""} ${err.message}`.trim() : String(err) };
|
|
30299
30660
|
warnings.push(`Stripe test key not stored: ${stripe.test_credential.error}`);
|
|
30300
30661
|
}
|
|
30301
30662
|
}
|
|
30663
|
+
if (!d.stripeTestKey && d.stripeKeyRefused) {
|
|
30664
|
+
stripe.test_credential = { stored: false, refused: d.stripeKeyRefused.kind };
|
|
30665
|
+
warnings.push(stripeKeyRefusedMessage(d.stripeKeyRefused));
|
|
30666
|
+
}
|
|
30302
30667
|
const configs = await syncCheckoutConfigs(client, site, d.checkoutSessions, root);
|
|
30303
30668
|
if (configs.length) stripe.checkout_configs = configs;
|
|
30304
30669
|
const registered = configs.find((c) => c.id);
|
|
@@ -30401,10 +30766,11 @@ var init_whoami = __esm({
|
|
|
30401
30766
|
object: "whoami",
|
|
30402
30767
|
key: creds ? maskKey(creds.apiKey) : null,
|
|
30403
30768
|
key_source: creds?.source ?? null,
|
|
30769
|
+
...creds?.file ? { key_file: creds.file } : {},
|
|
30404
30770
|
api_base: client.apiBase,
|
|
30405
30771
|
account: account.raw
|
|
30406
30772
|
},
|
|
30407
|
-
() => [describeAccount(account), ctx.out.c.dim(`key ${creds ? maskKey(creds.apiKey) : "?"} from ${creds
|
|
30773
|
+
() => [describeAccount(account), ctx.out.c.dim(`key ${creds ? maskKey(creds.apiKey) : "?"} from ${creds ? describeKeySource(creds, ctx.env) : "?"} \xB7 ${client.apiBase}`)]
|
|
30408
30774
|
);
|
|
30409
30775
|
return EXIT.ok;
|
|
30410
30776
|
}
|
|
@@ -30421,7 +30787,7 @@ import { createInterface as createInterface3 } from "node:readline";
|
|
|
30421
30787
|
async function prompt(question) {
|
|
30422
30788
|
const rl = createInterface3({ input: process.stdin, output: process.stderr, terminal: true });
|
|
30423
30789
|
try {
|
|
30424
|
-
return await new Promise((
|
|
30790
|
+
return await new Promise((resolve10) => rl.question(question, resolve10));
|
|
30425
30791
|
} finally {
|
|
30426
30792
|
rl.close();
|
|
30427
30793
|
}
|
|
@@ -30555,11 +30921,11 @@ var init_plan = __esm({
|
|
|
30555
30921
|
});
|
|
30556
30922
|
|
|
30557
30923
|
// src/commands/plan/verify.ts
|
|
30558
|
-
import { readFileSync as
|
|
30559
|
-
import { resolve as
|
|
30924
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
30925
|
+
import { resolve as resolve9 } from "node:path";
|
|
30560
30926
|
function readJson2(ctx, file, what) {
|
|
30561
30927
|
try {
|
|
30562
|
-
const v = JSON.parse(
|
|
30928
|
+
const v = JSON.parse(readFileSync12(resolve9(ctx.cwd, file), "utf8"));
|
|
30563
30929
|
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("not a JSON object");
|
|
30564
30930
|
return v;
|
|
30565
30931
|
} catch (err) {
|
|
@@ -30605,7 +30971,7 @@ var init_verify2 = __esm({
|
|
|
30605
30971
|
const htmlFile = str(flags, "html-file");
|
|
30606
30972
|
if (htmlFile) {
|
|
30607
30973
|
try {
|
|
30608
|
-
body.html =
|
|
30974
|
+
body.html = readFileSync12(resolve9(ctx.cwd, htmlFile), "utf8");
|
|
30609
30975
|
} catch (err) {
|
|
30610
30976
|
throw new UsageError(`Cannot read ${htmlFile}: ${err.code ?? err.message}`);
|
|
30611
30977
|
}
|