@reddoorla/maintenance 0.76.0 → 0.79.0
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 +34 -7
- package/dist/{blux-RIJBRVCF.js → blux-2P4FGCUL.js} +5 -5
- package/dist/{blux-RIJBRVCF.js.map → blux-2P4FGCUL.js.map} +1 -1
- package/dist/{bump-deps-OQR3IXLN.js → bump-deps-XB2LJTVN.js} +3 -3
- package/dist/{chunk-RHPDQW6D.js → chunk-2SLED3NB.js} +1 -1
- package/dist/{chunk-RHPDQW6D.js.map → chunk-2SLED3NB.js.map} +1 -1
- package/dist/{chunk-3U3IGVAQ.js → chunk-3LWKIKZ2.js} +47 -9
- package/dist/chunk-3LWKIKZ2.js.map +1 -0
- package/dist/{chunk-HIN3P4ZN.js → chunk-4SOCPK7A.js} +10 -1
- package/dist/chunk-4SOCPK7A.js.map +1 -0
- package/dist/{chunk-CASCWQQX.js → chunk-73UFRZKY.js} +34 -19
- package/dist/chunk-73UFRZKY.js.map +1 -0
- package/dist/chunk-A3WDWNN5.js +70 -0
- package/dist/chunk-A3WDWNN5.js.map +1 -0
- package/dist/{chunk-D3DQFC32.js → chunk-ATJ6ZA2V.js} +2 -2
- package/dist/{chunk-ZO7WNY57.js → chunk-B5ZUQZFL.js} +2 -2
- package/dist/{chunk-PH3AGF4Z.js → chunk-DC5H7KEQ.js} +2 -2
- package/dist/{chunk-DYKG53IR.js → chunk-FAQIMC26.js} +9 -9
- package/dist/{chunk-4VAUSRSS.js → chunk-FVVGDIGX.js} +2 -2
- package/dist/{chunk-7MJBTV2R.js → chunk-FX7RAMGC.js} +2 -2
- package/dist/{chunk-PKVZMFEB.js → chunk-K5JZ5KLO.js} +2 -2
- package/dist/{chunk-CZAMYVOP.js → chunk-KPQXB5CR.js} +2 -2
- package/dist/{chunk-BTEBRTW4.js → chunk-LS6QPANO.js} +2 -2
- package/dist/{chunk-2X322ZES.js → chunk-MGJ72BA4.js} +2 -2
- package/dist/chunk-MNDIED4M.js +137 -0
- package/dist/chunk-MNDIED4M.js.map +1 -0
- package/dist/{chunk-ZYBOT5PM.js → chunk-NT4WIP7I.js} +2 -2
- package/dist/{chunk-JNXCDUYB.js → chunk-Q276MZ7E.js} +3 -4
- package/dist/chunk-Q276MZ7E.js.map +1 -0
- package/dist/{chunk-GYNRZCM6.js → chunk-QG6OSL4M.js} +2 -2
- package/dist/{chunk-IRRZG66U.js → chunk-QMXQUL5Z.js} +34 -18
- package/dist/chunk-QMXQUL5Z.js.map +1 -0
- package/dist/{chunk-YEGHUODU.js → chunk-VSHC4HOI.js} +96 -1
- package/dist/chunk-VSHC4HOI.js.map +1 -0
- package/dist/{chunk-JTRTG5CU.js → chunk-YIQ7IWE3.js} +2 -2
- package/dist/{chunk-XKOZUVK2.js → chunk-ZTPPCHDB.js} +1 -1
- package/dist/chunk-ZTPPCHDB.js.map +1 -0
- package/dist/cli/bin.js +32 -21
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +1 -1
- package/dist/cli/commands/audit.js +7 -5
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-X5WUJWDE.js +11 -0
- package/dist/configs/eslint.js +10 -0
- package/dist/configs/eslint.js.map +1 -1
- package/dist/{convert-to-pnpm-OVMASATZ.js → convert-to-pnpm-FZHSLUQB.js} +3 -3
- package/dist/{db-3QW2XEXA.js → db-JSIZYT23.js} +3 -3
- package/dist/{digest-75FWSC74.js → digest-NQAQI34P.js} +11 -11
- package/dist/forms/index.js +6 -62
- package/dist/forms/index.js.map +1 -1
- package/dist/{github-signals-TF3VSNIN.js → github-signals-4LWQGD5V.js} +5 -5
- package/dist/{health-endpoint-JZGYCMTV.js → health-endpoint-LFVE26QS.js} +3 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +32 -24
- package/dist/index.js.map +1 -1
- package/dist/{init-2KOISBCZ.js → init-Y5O33BUL.js} +14 -12
- package/dist/{init-2KOISBCZ.js.map → init-Y5O33BUL.js.map} +1 -1
- package/dist/{launch-K4EHKTXC.js → launch-3SYLX263.js} +8 -5
- package/dist/launch-3SYLX263.js.map +1 -0
- package/dist/migrate-LJUFGXQZ.js +7 -0
- package/dist/{onboard-PX36YDZA.js → onboard-RDGHVCNG.js} +3 -3
- package/dist/{orchestrate-CPR6KVS2.js → orchestrate-QOQXOJLI.js} +5 -5
- package/dist/{preflight-QRRMPI7K.js → preflight-2PWVVIRR.js} +6 -6
- package/dist/protection-audit-X3BHT2MB.js +140 -0
- package/dist/protection-audit-X3BHT2MB.js.map +1 -0
- package/dist/recipes/sync-configs.d.ts +1 -1
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{renovate-dispatch-6II2JVOX.js → renovate-dispatch-2VYCEH42.js} +2 -2
- package/dist/renovate-dispatch-2VYCEH42.js.map +1 -0
- package/dist/{report-SLGBDJ6O.js → report-IU4CLJS5.js} +3 -3
- package/dist/{self-updating-2YSVNMPP.js → self-updating-ALMUWJNX.js} +5 -4
- package/dist/{self-updating-2YSVNMPP.js.map → self-updating-ALMUWJNX.js.map} +1 -1
- package/dist/{selftest-RVASRO3Z.js → selftest-PJFABZDM.js} +5 -5
- package/dist/{smoke-suite-CSP23NZM.js → smoke-suite-SUEJFBYP.js} +3 -3
- package/dist/{submissions-MDKWJYMM.js → submissions-3I3GAQAX.js} +13 -5
- package/dist/submissions-3I3GAQAX.js.map +1 -0
- package/dist/{submissions-BNRGY3JQ.js → submissions-SURN7BV5.js} +3 -3
- package/dist/{svelte-codemods-V54KOBWU.js → svelte-codemods-IWO7X3F3.js} +3 -3
- package/dist/{sync-configs-UBCJRASZ.js → sync-configs-7XGHFC6P.js} +4 -4
- package/dist/{types-0jreMCF5.d.ts → types-DnEL3NRn.d.ts} +1 -1
- package/dist/{upgrade-WR6ICPAG.js → upgrade-AM4S7JF3.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-3U3IGVAQ.js.map +0 -1
- package/dist/chunk-CASCWQQX.js.map +0 -1
- package/dist/chunk-HIN3P4ZN.js.map +0 -1
- package/dist/chunk-IRRZG66U.js.map +0 -1
- package/dist/chunk-JNXCDUYB.js.map +0 -1
- package/dist/chunk-XKOZUVK2.js.map +0 -1
- package/dist/chunk-YEGHUODU.js.map +0 -1
- package/dist/client-NVVQK3HH.js +0 -11
- package/dist/launch-K4EHKTXC.js.map +0 -1
- package/dist/migrate-35DNNF3H.js +0 -7
- package/dist/renovate-dispatch-6II2JVOX.js.map +0 -1
- package/dist/submissions-MDKWJYMM.js.map +0 -1
- /package/dist/{bump-deps-OQR3IXLN.js.map → bump-deps-XB2LJTVN.js.map} +0 -0
- /package/dist/{chunk-D3DQFC32.js.map → chunk-ATJ6ZA2V.js.map} +0 -0
- /package/dist/{chunk-ZO7WNY57.js.map → chunk-B5ZUQZFL.js.map} +0 -0
- /package/dist/{chunk-PH3AGF4Z.js.map → chunk-DC5H7KEQ.js.map} +0 -0
- /package/dist/{chunk-DYKG53IR.js.map → chunk-FAQIMC26.js.map} +0 -0
- /package/dist/{chunk-4VAUSRSS.js.map → chunk-FVVGDIGX.js.map} +0 -0
- /package/dist/{chunk-7MJBTV2R.js.map → chunk-FX7RAMGC.js.map} +0 -0
- /package/dist/{chunk-PKVZMFEB.js.map → chunk-K5JZ5KLO.js.map} +0 -0
- /package/dist/{chunk-CZAMYVOP.js.map → chunk-KPQXB5CR.js.map} +0 -0
- /package/dist/{chunk-BTEBRTW4.js.map → chunk-LS6QPANO.js.map} +0 -0
- /package/dist/{chunk-2X322ZES.js.map → chunk-MGJ72BA4.js.map} +0 -0
- /package/dist/{chunk-ZYBOT5PM.js.map → chunk-NT4WIP7I.js.map} +0 -0
- /package/dist/{chunk-GYNRZCM6.js.map → chunk-QG6OSL4M.js.map} +0 -0
- /package/dist/{chunk-JTRTG5CU.js.map → chunk-YIQ7IWE3.js.map} +0 -0
- /package/dist/{client-NVVQK3HH.js.map → client-X5WUJWDE.js.map} +0 -0
- /package/dist/{convert-to-pnpm-OVMASATZ.js.map → convert-to-pnpm-FZHSLUQB.js.map} +0 -0
- /package/dist/{db-3QW2XEXA.js.map → db-JSIZYT23.js.map} +0 -0
- /package/dist/{digest-75FWSC74.js.map → digest-NQAQI34P.js.map} +0 -0
- /package/dist/{github-signals-TF3VSNIN.js.map → github-signals-4LWQGD5V.js.map} +0 -0
- /package/dist/{health-endpoint-JZGYCMTV.js.map → health-endpoint-LFVE26QS.js.map} +0 -0
- /package/dist/{migrate-35DNNF3H.js.map → migrate-LJUFGXQZ.js.map} +0 -0
- /package/dist/{onboard-PX36YDZA.js.map → onboard-RDGHVCNG.js.map} +0 -0
- /package/dist/{orchestrate-CPR6KVS2.js.map → orchestrate-QOQXOJLI.js.map} +0 -0
- /package/dist/{preflight-QRRMPI7K.js.map → preflight-2PWVVIRR.js.map} +0 -0
- /package/dist/{report-SLGBDJ6O.js.map → report-IU4CLJS5.js.map} +0 -0
- /package/dist/{selftest-RVASRO3Z.js.map → selftest-PJFABZDM.js.map} +0 -0
- /package/dist/{smoke-suite-CSP23NZM.js.map → smoke-suite-SUEJFBYP.js.map} +0 -0
- /package/dist/{submissions-BNRGY3JQ.js.map → submissions-SURN7BV5.js.map} +0 -0
- /package/dist/{svelte-codemods-V54KOBWU.js.map → svelte-codemods-IWO7X3F3.js.map} +0 -0
- /package/dist/{sync-configs-UBCJRASZ.js.map → sync-configs-7XGHFC6P.js.map} +0 -0
- /package/dist/{upgrade-WR6ICPAG.js.map → upgrade-AM4S7JF3.js.map} +0 -0
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
diffAttention,
|
|
13
13
|
readDigestState,
|
|
14
14
|
writeDigestState
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
} from "./chunk-KPQXB5CR.js";
|
|
16
|
+
import "./chunk-YIQ7IWE3.js";
|
|
17
17
|
import "./chunk-D4FVAIEJ.js";
|
|
18
18
|
import {
|
|
19
19
|
isIdempotencyConflict
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-FVVGDIGX.js";
|
|
21
21
|
import "./chunk-GECESMVS.js";
|
|
22
22
|
import {
|
|
23
23
|
escapeHtml,
|
|
@@ -28,9 +28,9 @@ import "./chunk-MPBXOJ3N.js";
|
|
|
28
28
|
import {
|
|
29
29
|
defaultResendClient
|
|
30
30
|
} from "./chunk-WEKL2HYJ.js";
|
|
31
|
-
import "./chunk-
|
|
32
|
-
import "./chunk-
|
|
33
|
-
import "./chunk-
|
|
31
|
+
import "./chunk-QG6OSL4M.js";
|
|
32
|
+
import "./chunk-DC5H7KEQ.js";
|
|
33
|
+
import "./chunk-4SOCPK7A.js";
|
|
34
34
|
import "./chunk-U5SZ4FOP.js";
|
|
35
35
|
import {
|
|
36
36
|
listWebsites,
|
|
@@ -139,8 +139,8 @@ async function listPendingApproval(base) {
|
|
|
139
139
|
async function fetchNotifyBounceCounts(now) {
|
|
140
140
|
try {
|
|
141
141
|
const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] = await Promise.all([
|
|
142
|
-
import("./client-
|
|
143
|
-
import("./submissions-
|
|
142
|
+
import("./client-X5WUJWDE.js"),
|
|
143
|
+
import("./submissions-3I3GAQAX.js"),
|
|
144
144
|
import("./screenouts-7P5GOZMV.js")
|
|
145
145
|
]);
|
|
146
146
|
const db = await openDb(readDbConfig());
|
|
@@ -154,8 +154,8 @@ var SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
|
154
154
|
async function fetchSubmissionsDigestCounts(now) {
|
|
155
155
|
try {
|
|
156
156
|
const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([
|
|
157
|
-
import("./client-
|
|
158
|
-
import("./submissions-
|
|
157
|
+
import("./client-X5WUJWDE.js"),
|
|
158
|
+
import("./submissions-3I3GAQAX.js")
|
|
159
159
|
]);
|
|
160
160
|
const db = await openDb(readDbConfig());
|
|
161
161
|
return await countSubmissionsSinceBySite(
|
|
@@ -330,4 +330,4 @@ export {
|
|
|
330
330
|
renderDigestHtml,
|
|
331
331
|
runDigest
|
|
332
332
|
};
|
|
333
|
-
//# sourceMappingURL=digest-
|
|
333
|
+
//# sourceMappingURL=digest-NQAQI34P.js.map
|
package/dist/forms/index.js
CHANGED
|
@@ -1,69 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MIN_FILL_MS,
|
|
3
|
+
screenSubmission,
|
|
4
|
+
submitScreenOut,
|
|
5
|
+
submitToIngest
|
|
6
|
+
} from "../chunk-A3WDWNN5.js";
|
|
1
7
|
import {
|
|
2
8
|
SUBMISSION_FORM_TYPES
|
|
3
9
|
} from "../chunk-2OP4JKIQ.js";
|
|
4
10
|
|
|
5
|
-
// src/forms/client.ts
|
|
6
|
-
var INGEST_TIMEOUT_MS = 8e3;
|
|
7
|
-
async function submitToIngest(opts) {
|
|
8
|
-
const doFetch = opts.fetch ?? fetch;
|
|
9
|
-
const controller = new AbortController();
|
|
10
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? INGEST_TIMEOUT_MS);
|
|
11
|
-
let res;
|
|
12
|
-
try {
|
|
13
|
-
res = await doFetch(opts.url, {
|
|
14
|
-
method: "POST",
|
|
15
|
-
headers: { "content-type": "application/json", "x-forms-token": opts.token },
|
|
16
|
-
body: JSON.stringify(opts.payload),
|
|
17
|
-
signal: controller.signal
|
|
18
|
-
});
|
|
19
|
-
} catch (err) {
|
|
20
|
-
return { ok: false, status: 0, error: `network error: ${String(err)}` };
|
|
21
|
-
} finally {
|
|
22
|
-
clearTimeout(timer);
|
|
23
|
-
}
|
|
24
|
-
let body = null;
|
|
25
|
-
try {
|
|
26
|
-
body = await res.json();
|
|
27
|
-
} catch {
|
|
28
|
-
}
|
|
29
|
-
const obj = body && typeof body === "object" ? body : null;
|
|
30
|
-
if (res.ok && obj && obj.ok === true) {
|
|
31
|
-
return { ok: true, id: String(obj.id ?? "") };
|
|
32
|
-
}
|
|
33
|
-
const error = obj && typeof obj.error === "string" ? obj.error : `ingest failed (${res.status})`;
|
|
34
|
-
return { ok: false, status: res.status, error };
|
|
35
|
-
}
|
|
36
|
-
async function submitScreenOut(opts) {
|
|
37
|
-
const doFetch = opts.fetch ?? fetch;
|
|
38
|
-
const controller = new AbortController();
|
|
39
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1500);
|
|
40
|
-
try {
|
|
41
|
-
const res = await doFetch(opts.url, {
|
|
42
|
-
method: "POST",
|
|
43
|
-
headers: { "content-type": "application/json", "x-forms-token": opts.token },
|
|
44
|
-
// `_screenOut` is a reserved wire key (underscore-namespaced like `_meta`)
|
|
45
|
-
// so a site's real form field named "screenOut" can never read as a beacon.
|
|
46
|
-
body: JSON.stringify({ _screenOut: opts.reason }),
|
|
47
|
-
signal: controller.signal
|
|
48
|
-
});
|
|
49
|
-
return { ok: res.ok, status: res.status };
|
|
50
|
-
} catch {
|
|
51
|
-
return { ok: false, status: 0 };
|
|
52
|
-
} finally {
|
|
53
|
-
clearTimeout(timer);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
var MIN_FILL_MS = 800;
|
|
57
|
-
function screenSubmission(input) {
|
|
58
|
-
if (typeof input.botField === "string" && input.botField.trim().length > 0) {
|
|
59
|
-
return { ok: false, reason: "honeypot" };
|
|
60
|
-
}
|
|
61
|
-
if (typeof input.elapsedMs === "number" && input.elapsedMs < MIN_FILL_MS) {
|
|
62
|
-
return { ok: false, reason: "too-fast" };
|
|
63
|
-
}
|
|
64
|
-
return { ok: true };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
11
|
// src/forms/action.ts
|
|
68
12
|
import { fail, redirect } from "@sveltejs/kit";
|
|
69
13
|
|
package/dist/forms/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/forms/client.ts","../../src/forms/action.ts","../../src/forms/meta.ts","../../src/forms/endpoint.ts"],"sourcesContent":["import { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { SubmissionMeta } from \"./meta.js\";\n\n/**\n * The JSON a fleet site forwards to the dashboard ingest endpoint. Typed fields\n * are optional; the index signature lets a site include its own extra fields\n * (e.g. `company`) which the dashboard normalizer captures into `extraFields`.\n *\n * Each typed field allows `string | undefined` (not just `string`) so a\n * `buildPayload` mapping can use the idiomatic `form.get(\"name\")?.toString()`\n * pattern under `exactOptionalPropertyTypes` without a cast — an absent field\n * and an explicit `undefined` both serialize away in the JSON body.\n */\nexport type SubmissionPayload = {\n formType?: FormType | string | undefined;\n name?: string | undefined;\n firstName?: string | undefined;\n lastName?: string | undefined;\n email?: string | undefined;\n phone?: string | undefined;\n message?: string | undefined;\n sourceUrl?: string | undefined;\n utm?: string | undefined;\n /** Reserved transport envelope (token/IP/UA); stripped centrally, never persisted. */\n _meta?: SubmissionMeta | undefined;\n [key: string]: unknown;\n};\n\nexport type IngestClientResult =\n | { ok: true; id: string }\n | { ok: false; status: number; error: string };\n\nexport type SubmitToIngestOptions = {\n /** Full ingest endpoint incl. the site slug, e.g. https://…/api/forms/reddoor */\n url: string;\n /** The shared FORMS_INGEST_TOKEN. */\n token: string;\n payload: SubmissionPayload;\n /** Injectable fetch (pass SvelteKit's `event.fetch`); defaults to global fetch. */\n fetch?: typeof fetch;\n /** Abort budget for the central call. Default INGEST_TIMEOUT_MS. */\n timeoutMs?: number;\n};\n\n/** Default abort budget for the site→central ingest call. A central function hung\n * mid-deploy otherwise leaves the visitor's submit awaiting until Netlify kills\n * the SITE function at its 10s sync limit — a broken response instead of the\n * friendly error copy (espada's 2026-07-10 form-e2e warns caught exactly this).\n * 8s clears warm (~0.3s) and cold (~1-3s) central latency by a wide margin while\n * leaving the action ~2s of the 10s envelope to respond cleanly. */\nexport const INGEST_TIMEOUT_MS = 8000;\n\n/**\n * Forward a submission to the dashboard ingest endpoint. Never throws — a network\n * failure or a non-2xx response is returned as `{ ok: false }` so the caller can\n * show a friendly error rather than a 500.\n */\nexport async function submitToIngest(opts: SubmitToIngestOptions): Promise<IngestClientResult> {\n const doFetch = opts.fetch ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? INGEST_TIMEOUT_MS);\n let res: Response;\n try {\n res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n body: JSON.stringify(opts.payload),\n signal: controller.signal,\n });\n } catch (err) {\n return { ok: false, status: 0, error: `network error: ${String(err)}` };\n } finally {\n clearTimeout(timer);\n }\n let body: unknown = null;\n try {\n body = await res.json();\n } catch {\n // non-JSON response — fall through to the error path\n }\n const obj = body && typeof body === \"object\" ? (body as Record<string, unknown>) : null;\n if (res.ok && obj && obj.ok === true) {\n return { ok: true, id: String(obj.id ?? \"\") };\n }\n const error = obj && typeof obj.error === \"string\" ? obj.error : `ingest failed (${res.status})`;\n return { ok: false, status: res.status, error };\n}\n\nexport type SubmitScreenOutOptions = {\n /** Same ingest endpoint the site already posts submissions to. */\n url: string;\n token: string;\n reason: \"honeypot\" | \"too-fast\";\n fetch?: typeof fetch;\n /** Abort budget so a slow/hung beacon can't delay the (already-successful) response. */\n timeoutMs?: number;\n};\n\n/**\n * Best-effort screen-out beacon: tells the central ingest \"a bot was screened here\"\n * (no PII) so caught-vs-delivered is observable. Never throws — a failure is returned\n * as { ok: false } and the caller ignores it (the visitor already saw success).\n */\nexport async function submitScreenOut(\n opts: SubmitScreenOutOptions,\n): Promise<{ ok: boolean; status: number }> {\n const doFetch = opts.fetch ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1500);\n try {\n const res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n // `_screenOut` is a reserved wire key (underscore-namespaced like `_meta`)\n // so a site's real form field named \"screenOut\" can never read as a beacon.\n body: JSON.stringify({ _screenOut: opts.reason }),\n signal: controller.signal,\n });\n return { ok: res.ok, status: res.status };\n } catch {\n return { ok: false, status: 0 };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport type ScreenInput = { botField?: string | null; elapsedMs?: number | null };\nexport type ScreenResult = { ok: true } | { ok: false; reason: \"honeypot\" | \"too-fast\" };\n\n/**\n * Minimum plausible fill time; faster than this reads as a bot. Kept low (800ms)\n * on purpose: a too-fast fill is dropped *silently* (the visitor sees success),\n * so a real human who happens to be quick — autofill, a short form, a returning\n * visitor — would lose their lead with no trace. Below this, a submit is\n * effectively instant (page render → fill → click → network all under ~0.8s),\n * which a human realistically never beats but a script does. The honeypot is the\n * primary bot signal; this is the secondary one, so it errs toward letting\n * borderline-fast humans through.\n */\nexport const MIN_FILL_MS = 800;\n\n/**\n * Cheap bot screen for the site action. A filled honeypot is a bot; a submission\n * faster than MIN_FILL_MS is a bot. Missing timing data (null/undefined) is NOT a\n * rejection — a prerendered/cached page can't plant a fresh timestamp, and the\n * honeypot remains the primary signal.\n *\n * Any numeric elapsed BELOW the floor — including a NEGATIVE value — is too-fast. A\n * negative elapsed time is impossible for a real fill: it means the client posted a\n * FUTURE timestamp. The earlier `>= 0` guard let that slip through (negative skipped\n * the `< MIN_FILL_MS` branch and returned ok), silently bypassing the timing gate.\n */\nexport function screenSubmission(input: ScreenInput): ScreenResult {\n if (typeof input.botField === \"string\" && input.botField.trim().length > 0) {\n return { ok: false, reason: \"honeypot\" };\n }\n if (typeof input.elapsedMs === \"number\" && input.elapsedMs < MIN_FILL_MS) {\n return { ok: false, reason: \"too-fast\" };\n }\n return { ok: true };\n}\n\nexport { SUBMISSION_FORM_TYPES, type FormType };\n","import { fail, redirect, type ActionFailure, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/** Endpoint + token for the dashboard ingest, read per-request from site env. */\nexport type IngestActionConfig = { url?: string; token?: string };\n\nexport type CreateIngestActionOptions = {\n /** Stamped onto every payload as `formType` (a SUBMISSION_FORM_TYPES value). */\n formType: string;\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map this form's fields to a payload. The factory's `formType` is always\n * authoritative and cannot be overridden by `buildPayload`.\n */\n buildPayload: (form: FormData, event: RequestEvent) => SubmissionPayload;\n /** Honeypot input name. Default \"bot-field\". */\n botFieldName?: string;\n /** Hidden timestamp input name (planted in `load`). Default \"ts\". */\n tsFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** fail(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** fail(502) copy when the ingest endpoint rejects/errors. */\n errorMessage?: string;\n /** Injectable clock for tests. Default Date.now. */\n now?: () => number;\n /** If set, a successful OR bot-screened submission throws redirect(303, redirectTo)\n * instead of returning {success:true} (e.g. a dedicated /thank-you page). */\n redirectTo?: string;\n};\n\nexport type IngestActionData = { success: true } | ActionFailure<{ error: string }>;\n\n/**\n * Build a SvelteKit `default` form action that screens for bots, forwards the\n * submission to the dashboard ingest endpoint, and returns SvelteKit-shaped\n * results. The per-form field mapping is the only thing a site must supply.\n */\nexport function createIngestAction(\n opts: CreateIngestActionOptions,\n): (event: RequestEvent) => Promise<IngestActionData> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const tsFieldName = opts.tsFieldName ?? \"ts\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const now = opts.now ?? Date.now;\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let form: FormData;\n try {\n form = await event.request.formData();\n } catch {\n console.error(`[forms-ingest] ${opts.formType}: could not parse form body`);\n return fail(400, { error: failed });\n }\n\n // Bot screen: a filled honeypot OR an implausibly fast fill is silently\n // accepted (return success, do NOT forward) so bots get no signal.\n const screen = screenSubmission({\n botField: form.get(botFieldName)?.toString() ?? null,\n elapsedMs: elapsedMs(form.get(tsFieldName), now),\n });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // succeed exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return succeed();\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${opts.formType}`);\n return fail(500, { error: unavailable });\n }\n\n // buildPayload runs on untrusted form data; a careless field access (e.g.\n // `form.get(\"email\")!.toString()` on an absent field) would otherwise escape\n // as an uncaught 500. Treat a throw as a malformed request (400), mirroring\n // endpoint.ts's guard and its \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(form, event),\n formType: opts.formType,\n _meta: buildSubmissionMeta(event, form.get(turnstileFieldName)?.toString()),\n };\n } catch (err) {\n console.error(`[forms-ingest] ${opts.formType}: buildPayload threw: ${String(err)}`);\n return fail(400, { error: failed });\n }\n\n const result = await submitToIngest({\n url,\n token,\n fetch: event.fetch,\n payload,\n });\n if (!result.ok) {\n console.error(`[forms-ingest] ${opts.formType} → ${result.status}: ${result.error}`);\n return fail(502, { error: failed });\n }\n return succeed();\n };\n\n // Single success path: redirect when configured (e.g. a dedicated /thank-you\n // page), otherwise return the SvelteKit-shaped success. `redirect()` throws, so\n // the trailing `return` keeps the `{ success: true }` type.\n function succeed(): { success: true } {\n if (opts.redirectTo) redirect(303, opts.redirectTo);\n return { success: true };\n }\n}\n\n// `FormDataEntryValue` is a DOM-lib global; this package compiles with only the\n// ES2022 lib + @types/node, where it is not in scope. Derive the type from the\n// in-scope `FormData.get` return instead — same value, no DOM-lib dependency.\nfunction elapsedMs(tsRaw: ReturnType<FormData[\"get\"]>, now: () => number): number | null {\n const ts = Number(tsRaw);\n if (!Number.isFinite(ts) || ts <= 0) return null;\n // Clamp at 0: elapsed time can't be negative. A future `ts` (clock skew or a bot\n // forging one) would otherwise yield a negative value; clamping makes it read as\n // 0ms (effectively instant) so the MIN_FILL_MS gate still trips. Defense-in-depth\n // with screenSubmission, which also treats any sub-floor elapsed as too-fast.\n return Math.max(0, now() - ts);\n}\n","/** The reserved wire envelope a fleet site forwards alongside the lead fields.\n * (Older package versions also sent a `userAgent` — central never consumed it,\n * so it is no longer forwarded; `readMeta` simply ignores it from old senders.) */\nexport type SubmissionMeta = {\n turnstileToken?: string;\n clientIp?: string;\n};\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v.trim() : \"\";\n}\n\n/**\n * Defensively read the reserved `_meta` envelope off an untrusted ingest payload\n * (CENTRAL side, used by the ingest handler). Keeps only non-blank string fields\n * among the KNOWN keys, dropping everything else — a bot cannot smuggle a\n * non-string clientIp in, and unknown fields (e.g. the `userAgent` older package\n * versions still send) are silently ignored. The token/IP read here are used\n * transiently (Turnstile verify + `remoteip`) and are NEVER persisted; the token\n * is never stored. The SITE-side writer (`buildSubmissionMeta`) is added by the\n * site-factory task; `readMeta` is the single reader (there is no `parseMeta`).\n */\nexport function readMeta(payload: unknown): SubmissionMeta {\n const meta: SubmissionMeta = {};\n if (typeof payload !== \"object\" || payload === null) return meta;\n const raw = (payload as Record<string, unknown>)._meta;\n if (typeof raw !== \"object\" || raw === null) return meta;\n const m = raw as Record<string, unknown>;\n const token = str(m.turnstileToken);\n if (token) meta.turnstileToken = token;\n const ip = str(m.clientIp);\n if (ip) meta.clientIp = ip;\n return meta;\n}\n\n/**\n * SITE-side event shape `buildSubmissionMeta` reads. Structural (not SvelteKit's\n * `RequestEvent`) so this leaf stays SDK-free; a real `RequestEvent` is\n * structurally assignable (`getClientAddress: () => string`).\n */\ntype MetaEvent = {\n getClientAddress?: () => string;\n};\n\n/**\n * Build the transient `_meta` envelope a site forwards to central ingest:\n * `{ turnstileToken?, clientIp? }`. Returns `undefined` when no field yields a\n * value so callers can attach it unconditionally without polluting the payload\n * (an `undefined` value is dropped by `JSON.stringify`). `getClientAddress` is\n * guarded (some adapters lack a client address and can throw). The visitor's\n * user-agent is deliberately NOT forwarded — central never consumed it, so\n * shipping it was pure transient-PII surface. None of this is ever persisted.\n */\nexport function buildSubmissionMeta(\n event: MetaEvent,\n turnstileToken: string | null | undefined,\n): SubmissionMeta | undefined {\n const meta: SubmissionMeta = {};\n\n const token = typeof turnstileToken === \"string\" ? turnstileToken.trim() : \"\";\n if (token) meta.turnstileToken = token;\n\n if (typeof event.getClientAddress === \"function\") {\n try {\n const ip = event.getClientAddress();\n if (typeof ip === \"string\" && ip.trim()) meta.clientIp = ip.trim();\n } catch {\n // Some adapters have no client address and throw; drop clientIp silently.\n }\n }\n\n return Object.keys(meta).length > 0 ? meta : undefined;\n}\n","import { json, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { IngestActionConfig } from \"./action.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/**\n * Options for {@link createIngestEndpoint} — the JSON sibling of\n * `createIngestAction` for client-driven forms (modals / lightboxes / fetch)\n * that POST JSON to a `+server.ts` route instead of using a form action.\n */\nexport type CreateIngestEndpointOptions = {\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map the parsed JSON body to a payload. Must set `formType` UNLESS the fixed\n * `formType` option is provided (then that is authoritative and overrides it).\n */\n buildPayload: (body: Record<string, unknown>, event: RequestEvent) => SubmissionPayload;\n /** Fixed formType for single-type endpoints; omit for multi-type endpoints\n * where `buildPayload` derives formType from the body. */\n formType?: string;\n /** Honeypot field name in the JSON body. Default \"bot-field\". */\n botFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** json(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** json(400/502) copy for bad input / ingest failure. */\n errorMessage?: string;\n};\n\nfunction isFormType(v: unknown): v is FormType {\n return typeof v === \"string\" && (SUBMISSION_FORM_TYPES as readonly string[]).includes(v);\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === \"string\" ? v : undefined;\n}\n\n/**\n * Build a JSON `POST` handler that screens for bots, forwards the submission to\n * the dashboard ingest endpoint, and returns `{ ok }`-shaped JSON. The per-form\n * field mapping (`buildPayload`) is the only thing a site must supply. The\n * returned function is structurally a SvelteKit `RequestHandler`.\n */\nexport function createIngestEndpoint(\n opts: CreateIngestEndpointOptions,\n): (event: RequestEvent) => Promise<Response> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let body: Record<string, unknown>;\n try {\n const parsed: unknown = await event.request.json();\n if (!parsed || typeof parsed !== \"object\") throw new Error(\"body is not an object\");\n body = parsed as Record<string, unknown>;\n } catch {\n console.error(\"[forms-ingest] could not parse JSON body\");\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n // Bot screen: honeypot only. A client POST carries no server-planted ts, and\n // screenSubmission treats a missing elapsedMs as OK. A filled honeypot is\n // silently accepted (return ok, do NOT forward) so bots get no signal.\n const screen = screenSubmission({ botField: str(body[botFieldName]) ?? null });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // return success exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return json({ ok: true });\n }\n\n // buildPayload runs on untrusted JSON; a careless field access (e.g.\n // `body.name.trim()` on a non-string) would otherwise escape as a 500. Treat\n // a throw as a malformed request (400), keeping the \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(body, event),\n ...(opts.formType ? { formType: opts.formType } : {}),\n _meta: buildSubmissionMeta(event, str(body[turnstileFieldName])),\n };\n } catch (err) {\n console.error(`[forms-ingest] buildPayload threw: ${String(err)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n if (!isFormType(payload.formType)) {\n console.error(`[forms-ingest] invalid formType: ${String(payload.formType)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${payload.formType}`);\n return json({ ok: false, error: unavailable }, { status: 500 });\n }\n\n const result = await submitToIngest({ url, token, fetch: event.fetch, payload });\n if (!result.ok) {\n console.error(`[forms-ingest] ${payload.formType} → ${result.status}: ${result.error}`);\n return json({ ok: false, error: failed }, { status: 502 });\n }\n return json({ ok: true });\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/forms/action.ts","../../src/forms/meta.ts","../../src/forms/endpoint.ts"],"sourcesContent":["import { fail, redirect, type ActionFailure, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/** Endpoint + token for the dashboard ingest, read per-request from site env. */\nexport type IngestActionConfig = { url?: string; token?: string };\n\nexport type CreateIngestActionOptions = {\n /** Stamped onto every payload as `formType` (a SUBMISSION_FORM_TYPES value). */\n formType: string;\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map this form's fields to a payload. The factory's `formType` is always\n * authoritative and cannot be overridden by `buildPayload`.\n */\n buildPayload: (form: FormData, event: RequestEvent) => SubmissionPayload;\n /** Honeypot input name. Default \"bot-field\". */\n botFieldName?: string;\n /** Hidden timestamp input name (planted in `load`). Default \"ts\". */\n tsFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** fail(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** fail(502) copy when the ingest endpoint rejects/errors. */\n errorMessage?: string;\n /** Injectable clock for tests. Default Date.now. */\n now?: () => number;\n /** If set, a successful OR bot-screened submission throws redirect(303, redirectTo)\n * instead of returning {success:true} (e.g. a dedicated /thank-you page). */\n redirectTo?: string;\n};\n\nexport type IngestActionData = { success: true } | ActionFailure<{ error: string }>;\n\n/**\n * Build a SvelteKit `default` form action that screens for bots, forwards the\n * submission to the dashboard ingest endpoint, and returns SvelteKit-shaped\n * results. The per-form field mapping is the only thing a site must supply.\n */\nexport function createIngestAction(\n opts: CreateIngestActionOptions,\n): (event: RequestEvent) => Promise<IngestActionData> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const tsFieldName = opts.tsFieldName ?? \"ts\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const now = opts.now ?? Date.now;\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let form: FormData;\n try {\n form = await event.request.formData();\n } catch {\n console.error(`[forms-ingest] ${opts.formType}: could not parse form body`);\n return fail(400, { error: failed });\n }\n\n // Bot screen: a filled honeypot OR an implausibly fast fill is silently\n // accepted (return success, do NOT forward) so bots get no signal.\n const screen = screenSubmission({\n botField: form.get(botFieldName)?.toString() ?? null,\n elapsedMs: elapsedMs(form.get(tsFieldName), now),\n });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // succeed exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return succeed();\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${opts.formType}`);\n return fail(500, { error: unavailable });\n }\n\n // buildPayload runs on untrusted form data; a careless field access (e.g.\n // `form.get(\"email\")!.toString()` on an absent field) would otherwise escape\n // as an uncaught 500. Treat a throw as a malformed request (400), mirroring\n // endpoint.ts's guard and its \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(form, event),\n formType: opts.formType,\n _meta: buildSubmissionMeta(event, form.get(turnstileFieldName)?.toString()),\n };\n } catch (err) {\n console.error(`[forms-ingest] ${opts.formType}: buildPayload threw: ${String(err)}`);\n return fail(400, { error: failed });\n }\n\n const result = await submitToIngest({\n url,\n token,\n fetch: event.fetch,\n payload,\n });\n if (!result.ok) {\n console.error(`[forms-ingest] ${opts.formType} → ${result.status}: ${result.error}`);\n return fail(502, { error: failed });\n }\n return succeed();\n };\n\n // Single success path: redirect when configured (e.g. a dedicated /thank-you\n // page), otherwise return the SvelteKit-shaped success. `redirect()` throws, so\n // the trailing `return` keeps the `{ success: true }` type.\n function succeed(): { success: true } {\n if (opts.redirectTo) redirect(303, opts.redirectTo);\n return { success: true };\n }\n}\n\n// `FormDataEntryValue` is a DOM-lib global; this package compiles with only the\n// ES2022 lib + @types/node, where it is not in scope. Derive the type from the\n// in-scope `FormData.get` return instead — same value, no DOM-lib dependency.\nfunction elapsedMs(tsRaw: ReturnType<FormData[\"get\"]>, now: () => number): number | null {\n const ts = Number(tsRaw);\n if (!Number.isFinite(ts) || ts <= 0) return null;\n // Clamp at 0: elapsed time can't be negative. A future `ts` (clock skew or a bot\n // forging one) would otherwise yield a negative value; clamping makes it read as\n // 0ms (effectively instant) so the MIN_FILL_MS gate still trips. Defense-in-depth\n // with screenSubmission, which also treats any sub-floor elapsed as too-fast.\n return Math.max(0, now() - ts);\n}\n","/** The reserved wire envelope a fleet site forwards alongside the lead fields.\n * (Older package versions also sent a `userAgent` — central never consumed it,\n * so it is no longer forwarded; `readMeta` simply ignores it from old senders.) */\nexport type SubmissionMeta = {\n turnstileToken?: string;\n clientIp?: string;\n};\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v.trim() : \"\";\n}\n\n/**\n * Defensively read the reserved `_meta` envelope off an untrusted ingest payload\n * (CENTRAL side, used by the ingest handler). Keeps only non-blank string fields\n * among the KNOWN keys, dropping everything else — a bot cannot smuggle a\n * non-string clientIp in, and unknown fields (e.g. the `userAgent` older package\n * versions still send) are silently ignored. The token/IP read here are used\n * transiently (Turnstile verify + `remoteip`) and are NEVER persisted; the token\n * is never stored. The SITE-side writer (`buildSubmissionMeta`) is added by the\n * site-factory task; `readMeta` is the single reader (there is no `parseMeta`).\n */\nexport function readMeta(payload: unknown): SubmissionMeta {\n const meta: SubmissionMeta = {};\n if (typeof payload !== \"object\" || payload === null) return meta;\n const raw = (payload as Record<string, unknown>)._meta;\n if (typeof raw !== \"object\" || raw === null) return meta;\n const m = raw as Record<string, unknown>;\n const token = str(m.turnstileToken);\n if (token) meta.turnstileToken = token;\n const ip = str(m.clientIp);\n if (ip) meta.clientIp = ip;\n return meta;\n}\n\n/**\n * SITE-side event shape `buildSubmissionMeta` reads. Structural (not SvelteKit's\n * `RequestEvent`) so this leaf stays SDK-free; a real `RequestEvent` is\n * structurally assignable (`getClientAddress: () => string`).\n */\ntype MetaEvent = {\n getClientAddress?: () => string;\n};\n\n/**\n * Build the transient `_meta` envelope a site forwards to central ingest:\n * `{ turnstileToken?, clientIp? }`. Returns `undefined` when no field yields a\n * value so callers can attach it unconditionally without polluting the payload\n * (an `undefined` value is dropped by `JSON.stringify`). `getClientAddress` is\n * guarded (some adapters lack a client address and can throw). The visitor's\n * user-agent is deliberately NOT forwarded — central never consumed it, so\n * shipping it was pure transient-PII surface. None of this is ever persisted.\n */\nexport function buildSubmissionMeta(\n event: MetaEvent,\n turnstileToken: string | null | undefined,\n): SubmissionMeta | undefined {\n const meta: SubmissionMeta = {};\n\n const token = typeof turnstileToken === \"string\" ? turnstileToken.trim() : \"\";\n if (token) meta.turnstileToken = token;\n\n if (typeof event.getClientAddress === \"function\") {\n try {\n const ip = event.getClientAddress();\n if (typeof ip === \"string\" && ip.trim()) meta.clientIp = ip.trim();\n } catch {\n // Some adapters have no client address and throw; drop clientIp silently.\n }\n }\n\n return Object.keys(meta).length > 0 ? meta : undefined;\n}\n","import { json, type RequestEvent } from \"@sveltejs/kit\";\nimport {\n submitToIngest,\n screenSubmission,\n submitScreenOut,\n type SubmissionPayload,\n} from \"./client.js\";\nimport { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\nimport type { IngestActionConfig } from \"./action.js\";\nimport { buildSubmissionMeta } from \"./meta.js\";\n\n/**\n * Options for {@link createIngestEndpoint} — the JSON sibling of\n * `createIngestAction` for client-driven forms (modals / lightboxes / fetch)\n * that POST JSON to a `+server.ts` route instead of using a form action.\n */\nexport type CreateIngestEndpointOptions = {\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map the parsed JSON body to a payload. Must set `formType` UNLESS the fixed\n * `formType` option is provided (then that is authoritative and overrides it).\n */\n buildPayload: (body: Record<string, unknown>, event: RequestEvent) => SubmissionPayload;\n /** Fixed formType for single-type endpoints; omit for multi-type endpoints\n * where `buildPayload` derives formType from the body. */\n formType?: string;\n /** Honeypot field name in the JSON body. Default \"bot-field\". */\n botFieldName?: string;\n /** Field carrying the Cloudflare Turnstile token. Default \"cf-turnstile-response\". */\n turnstileFieldName?: string;\n /** json(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** json(400/502) copy for bad input / ingest failure. */\n errorMessage?: string;\n};\n\nfunction isFormType(v: unknown): v is FormType {\n return typeof v === \"string\" && (SUBMISSION_FORM_TYPES as readonly string[]).includes(v);\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === \"string\" ? v : undefined;\n}\n\n/**\n * Build a JSON `POST` handler that screens for bots, forwards the submission to\n * the dashboard ingest endpoint, and returns `{ ok }`-shaped JSON. The per-form\n * field mapping (`buildPayload`) is the only thing a site must supply. The\n * returned function is structurally a SvelteKit `RequestHandler`.\n */\nexport function createIngestEndpoint(\n opts: CreateIngestEndpointOptions,\n): (event: RequestEvent) => Promise<Response> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const turnstileFieldName = opts.turnstileFieldName ?? \"cf-turnstile-response\";\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let body: Record<string, unknown>;\n try {\n const parsed: unknown = await event.request.json();\n if (!parsed || typeof parsed !== \"object\") throw new Error(\"body is not an object\");\n body = parsed as Record<string, unknown>;\n } catch {\n console.error(\"[forms-ingest] could not parse JSON body\");\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n // Bot screen: honeypot only. A client POST carries no server-planted ts, and\n // screenSubmission treats a missing elapsedMs as OK. A filled honeypot is\n // silently accepted (return ok, do NOT forward) so bots get no signal.\n const screen = screenSubmission({ botField: str(body[botFieldName]) ?? null });\n if (!screen.ok) {\n // Best-effort screen-out beacon (no PII) so catch-rate is observable, then\n // return success exactly as before — the bot/visitor still sees success.\n const cfg = opts.getConfig();\n if (cfg.url && cfg.token) {\n await submitScreenOut({\n url: cfg.url,\n token: cfg.token,\n reason: screen.reason,\n fetch: event.fetch,\n });\n }\n return json({ ok: true });\n }\n\n // buildPayload runs on untrusted JSON; a careless field access (e.g.\n // `body.name.trim()` on a non-string) would otherwise escape as a 500. Treat\n // a throw as a malformed request (400), keeping the \"never 500s\" guarantee.\n let payload: SubmissionPayload;\n try {\n payload = {\n ...opts.buildPayload(body, event),\n ...(opts.formType ? { formType: opts.formType } : {}),\n _meta: buildSubmissionMeta(event, str(body[turnstileFieldName])),\n };\n } catch (err) {\n console.error(`[forms-ingest] buildPayload threw: ${String(err)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n if (!isFormType(payload.formType)) {\n console.error(`[forms-ingest] invalid formType: ${String(payload.formType)}`);\n return json({ ok: false, error: failed }, { status: 400 });\n }\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${payload.formType}`);\n return json({ ok: false, error: unavailable }, { status: 500 });\n }\n\n const result = await submitToIngest({ url, token, fetch: event.fetch, payload });\n if (!result.ok) {\n console.error(`[forms-ingest] ${payload.formType} → ${result.status}: ${result.error}`);\n return json({ ok: false, error: failed }, { status: 502 });\n }\n return json({ ok: true });\n };\n}\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,MAAM,gBAAuD;;;ACqD/D,SAAS,oBACd,OACA,gBAC4B;AAC5B,QAAM,OAAuB,CAAC;AAE9B,QAAM,QAAQ,OAAO,mBAAmB,WAAW,eAAe,KAAK,IAAI;AAC3E,MAAI,MAAO,MAAK,iBAAiB;AAEjC,MAAI,OAAO,MAAM,qBAAqB,YAAY;AAChD,QAAI;AACF,YAAM,KAAK,MAAM,iBAAiB;AAClC,UAAI,OAAO,OAAO,YAAY,GAAG,KAAK,EAAG,MAAK,WAAW,GAAG,KAAK;AAAA,IACnE,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,OAAO,KAAK,IAAI,EAAE,SAAS,IAAI,OAAO;AAC/C;;;AD1BO,SAAS,mBACd,MACoD;AACpD,QAAM,eAAe,KAAK,gBAAgB;AAC1C,QAAM,cAAc,KAAK,eAAe;AACxC,QAAM,qBAAqB,KAAK,sBAAsB;AACtD,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,QAAM,cACJ,KAAK,sBAAsB;AAC7B,QAAM,SACJ,KAAK,gBAAgB;AAEvB,SAAO,OAAO,UAAU;AACtB,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,MAAM,QAAQ,SAAS;AAAA,IACtC,QAAQ;AACN,cAAQ,MAAM,kBAAkB,KAAK,QAAQ,6BAA6B;AAC1E,aAAO,KAAK,KAAK,EAAE,OAAO,OAAO,CAAC;AAAA,IACpC;AAIA,UAAM,SAAS,iBAAiB;AAAA,MAC9B,UAAU,KAAK,IAAI,YAAY,GAAG,SAAS,KAAK;AAAA,MAChD,WAAW,UAAU,KAAK,IAAI,WAAW,GAAG,GAAG;AAAA,IACjD,CAAC;AACD,QAAI,CAAC,OAAO,IAAI;AAGd,YAAM,MAAM,KAAK,UAAU;AAC3B,UAAI,IAAI,OAAO,IAAI,OAAO;AACxB,cAAM,gBAAgB;AAAA,UACpB,KAAK,IAAI;AAAA,UACT,OAAO,IAAI;AAAA,UACX,QAAQ,OAAO;AAAA,UACf,OAAO,MAAM;AAAA,QACf,CAAC;AAAA,MACH;AACA,aAAO,QAAQ;AAAA,IACjB;AAEA,UAAM,EAAE,KAAK,MAAM,IAAI,KAAK,UAAU;AACtC,QAAI,CAAC,OAAO,CAAC,OAAO;AAClB,cAAQ,MAAM,8CAA8C,KAAK,QAAQ,EAAE;AAC3E,aAAO,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;AAAA,IACzC;AAMA,QAAI;AACJ,QAAI;AACF,gBAAU;AAAA,QACR,GAAG,KAAK,aAAa,MAAM,KAAK;AAAA,QAChC,UAAU,KAAK;AAAA,QACf,OAAO,oBAAoB,OAAO,KAAK,IAAI,kBAAkB,GAAG,SAAS,CAAC;AAAA,MAC5E;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,MAAM,kBAAkB,KAAK,QAAQ,yBAAyB,OAAO,GAAG,CAAC,EAAE;AACnF,aAAO,KAAK,KAAK,EAAE,OAAO,OAAO,CAAC;AAAA,IACpC;AAEA,UAAM,SAAS,MAAM,eAAe;AAAA,MAClC;AAAA,MACA;AAAA,MACA,OAAO,MAAM;AAAA,MACb;AAAA,IACF,CAAC;AACD,QAAI,CAAC,OAAO,IAAI;AACd,cAAQ,MAAM,kBAAkB,KAAK,QAAQ,WAAM,OAAO,MAAM,KAAK,OAAO,KAAK,EAAE;AACnF,aAAO,KAAK,KAAK,EAAE,OAAO,OAAO,CAAC;AAAA,IACpC;AACA,WAAO,QAAQ;AAAA,EACjB;AAKA,WAAS,UAA6B;AACpC,QAAI,KAAK,WAAY,UAAS,KAAK,KAAK,UAAU;AAClD,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACF;AAKA,SAAS,UAAU,OAAoC,KAAkC;AACvF,QAAM,KAAK,OAAO,KAAK;AACvB,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,EAAG,QAAO;AAK5C,SAAO,KAAK,IAAI,GAAG,IAAI,IAAI,EAAE;AAC/B;;;AE/IA,SAAS,YAA+B;AAqCxC,SAAS,WAAW,GAA2B;AAC7C,SAAO,OAAO,MAAM,YAAa,sBAA4C,SAAS,CAAC;AACzF;AAEA,SAAS,IAAI,GAAgC;AAC3C,SAAO,OAAO,MAAM,WAAW,IAAI;AACrC;AAQO,SAAS,qBACd,MAC4C;AAC5C,QAAM,eAAe,KAAK,gBAAgB;AAC1C,QAAM,qBAAqB,KAAK,sBAAsB;AACtD,QAAM,cACJ,KAAK,sBAAsB;AAC7B,QAAM,SACJ,KAAK,gBAAgB;AAEvB,SAAO,OAAO,UAAU;AACtB,QAAI;AACJ,QAAI;AACF,YAAM,SAAkB,MAAM,MAAM,QAAQ,KAAK;AACjD,UAAI,CAAC,UAAU,OAAO,WAAW,SAAU,OAAM,IAAI,MAAM,uBAAuB;AAClF,aAAO;AAAA,IACT,QAAQ;AACN,cAAQ,MAAM,0CAA0C;AACxD,aAAO,KAAK,EAAE,IAAI,OAAO,OAAO,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3D;AAKA,UAAM,SAAS,iBAAiB,EAAE,UAAU,IAAI,KAAK,YAAY,CAAC,KAAK,KAAK,CAAC;AAC7E,QAAI,CAAC,OAAO,IAAI;AAGd,YAAM,MAAM,KAAK,UAAU;AAC3B,UAAI,IAAI,OAAO,IAAI,OAAO;AACxB,cAAM,gBAAgB;AAAA,UACpB,KAAK,IAAI;AAAA,UACT,OAAO,IAAI;AAAA,UACX,QAAQ,OAAO;AAAA,UACf,OAAO,MAAM;AAAA,QACf,CAAC;AAAA,MACH;AACA,aAAO,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,IAC1B;AAKA,QAAI;AACJ,QAAI;AACF,gBAAU;AAAA,QACR,GAAG,KAAK,aAAa,MAAM,KAAK;AAAA,QAChC,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;AAAA,QACnD,OAAO,oBAAoB,OAAO,IAAI,KAAK,kBAAkB,CAAC,CAAC;AAAA,MACjE;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,MAAM,sCAAsC,OAAO,GAAG,CAAC,EAAE;AACjE,aAAO,KAAK,EAAE,IAAI,OAAO,OAAO,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3D;AACA,QAAI,CAAC,WAAW,QAAQ,QAAQ,GAAG;AACjC,cAAQ,MAAM,oCAAoC,OAAO,QAAQ,QAAQ,CAAC,EAAE;AAC5E,aAAO,KAAK,EAAE,IAAI,OAAO,OAAO,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3D;AAEA,UAAM,EAAE,KAAK,MAAM,IAAI,KAAK,UAAU;AACtC,QAAI,CAAC,OAAO,CAAC,OAAO;AAClB,cAAQ,MAAM,8CAA8C,QAAQ,QAAQ,EAAE;AAC9E,aAAO,KAAK,EAAE,IAAI,OAAO,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAChE;AAEA,UAAM,SAAS,MAAM,eAAe,EAAE,KAAK,OAAO,OAAO,MAAM,OAAO,QAAQ,CAAC;AAC/E,QAAI,CAAC,OAAO,IAAI;AACd,cAAQ,MAAM,kBAAkB,QAAQ,QAAQ,WAAM,OAAO,MAAM,KAAK,OAAO,KAAK,EAAE;AACtF,aAAO,KAAK,EAAE,IAAI,OAAO,OAAO,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3D;AACA,WAAO,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EAC1B;AACF;","names":[]}
|
|
@@ -11,12 +11,12 @@ import {
|
|
|
11
11
|
} from "./chunk-5L3FPQIJ.js";
|
|
12
12
|
import {
|
|
13
13
|
makeGitHub
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-VSHC4HOI.js";
|
|
15
15
|
import {
|
|
16
16
|
recordFleetEventsBestEffort
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import "./chunk-
|
|
17
|
+
} from "./chunk-QG6OSL4M.js";
|
|
18
|
+
import "./chunk-DC5H7KEQ.js";
|
|
19
|
+
import "./chunk-4SOCPK7A.js";
|
|
20
20
|
import "./chunk-HVOOCK6L.js";
|
|
21
21
|
import {
|
|
22
22
|
listWebsites,
|
|
@@ -134,4 +134,4 @@ export {
|
|
|
134
134
|
githubSignalsExitCode,
|
|
135
135
|
runGitHubSignalsCommand
|
|
136
136
|
};
|
|
137
|
-
//# sourceMappingURL=github-signals-
|
|
137
|
+
//# sourceMappingURL=github-signals-4LWQGD5V.js.map
|
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
} from "./chunk-4BHNP6Y2.js";
|
|
12
12
|
import {
|
|
13
13
|
healthEndpoint
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-FX7RAMGC.js";
|
|
15
15
|
import "./chunk-KRDLHAKK.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-ZTPPCHDB.js";
|
|
17
17
|
import "./chunk-FDIFTIXC.js";
|
|
18
18
|
import "./chunk-HVOOCK6L.js";
|
|
19
19
|
import "./chunk-XXTZBPUY.js";
|
|
@@ -51,4 +51,4 @@ async function runHealthEndpointCommand(site, opts) {
|
|
|
51
51
|
export {
|
|
52
52
|
runHealthEndpointCommand
|
|
53
53
|
};
|
|
54
|
-
//# sourceMappingURL=health-endpoint-
|
|
54
|
+
//# sourceMappingURL=health-endpoint-LFVE26QS.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as AuditResult, S as Site, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-
|
|
2
|
-
export { C as ConfigName } from './types-
|
|
1
|
+
import { a as AuditResult, S as Site, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-DnEL3NRn.js';
|
|
2
|
+
export { C as ConfigName } from './types-DnEL3NRn.js';
|
|
3
3
|
import * as airtable_lib_airtable_base_js from 'airtable/lib/airtable_base.js';
|
|
4
4
|
import { F as FormType } from './types-RXY-vY-5.js';
|
|
5
5
|
export { SyncConfigsOptions, syncConfigs } from './recipes/sync-configs.js';
|
|
@@ -110,13 +110,16 @@ type VerifyDeps = {
|
|
|
110
110
|
/** Outcome of driving one site's contact form. `formPresent:false` ⇒ n/a
|
|
111
111
|
* (persisted). `testModeUndeclared` ⇒ the site's /health does not declare
|
|
112
112
|
* `forms.testMode`, so the probe refused to submit — a plain skip, prior
|
|
113
|
-
* verdict preserved.
|
|
113
|
+
* verdict preserved. `elapsedMs` times the submit itself (click → success
|
|
114
|
+
* banner) and feeds the budget-headroom check; absent means "not measured",
|
|
115
|
+
* which never manufactures a verdict. */
|
|
114
116
|
type FormSubmitOutcome = {
|
|
115
117
|
formPresent: false;
|
|
116
118
|
} | {
|
|
117
119
|
formPresent: true;
|
|
118
120
|
success: boolean;
|
|
119
121
|
detail?: string;
|
|
122
|
+
elapsedMs?: number;
|
|
120
123
|
} | {
|
|
121
124
|
testModeUndeclared: true;
|
|
122
125
|
};
|
|
@@ -1008,6 +1011,11 @@ type SubmissionRow = {
|
|
|
1008
1011
|
spamScore?: number | null;
|
|
1009
1012
|
/** Comma-joined classifier reason codes (e.g. "links:3,disposable-email"); null when unscored. */
|
|
1010
1013
|
spamReason?: string | null;
|
|
1014
|
+
/** Comma-joined newsletter fan-out outcome per destination — `webhook:ok`,
|
|
1015
|
+
* `mailchimp:401`, `mailchimp:threw`, `mailchimp-tags:failed`. Null means nothing
|
|
1016
|
+
* was attempted: a non-newsletter form, a spam row, a site with no destination
|
|
1017
|
+
* configured, or a row written before 0005. See ingest.ts for the token grammar. */
|
|
1018
|
+
fanoutStatus?: string | null;
|
|
1011
1019
|
};
|
|
1012
1020
|
|
|
1013
1021
|
type ScreenOutTotals = {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-KPQXB5CR.js";
|
|
2
2
|
import {
|
|
3
3
|
draftReportForSite
|
|
4
4
|
} from "./chunk-6SZBNALD.js";
|
|
@@ -10,39 +10,39 @@ import {
|
|
|
10
10
|
} from "./chunk-37S6L6G4.js";
|
|
11
11
|
import {
|
|
12
12
|
approveBlockers
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-YIQ7IWE3.js";
|
|
14
14
|
import {
|
|
15
15
|
findDueReports
|
|
16
16
|
} from "./chunk-D4FVAIEJ.js";
|
|
17
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-2SLED3NB.js";
|
|
18
18
|
import {
|
|
19
19
|
upgradeSvelte4to5
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-B5ZUQZFL.js";
|
|
21
21
|
import {
|
|
22
22
|
DEFAULT_INIT_STEPS,
|
|
23
23
|
a11yFixturesPage,
|
|
24
24
|
init
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-FAQIMC26.js";
|
|
26
26
|
import {
|
|
27
27
|
convertToPnpm
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-MGJ72BA4.js";
|
|
29
29
|
import {
|
|
30
30
|
svelteCodemods
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-LS6QPANO.js";
|
|
32
32
|
import "./chunk-J7SZQUCW.js";
|
|
33
|
-
import "./chunk-
|
|
34
|
-
import "./chunk-
|
|
33
|
+
import "./chunk-FX7RAMGC.js";
|
|
34
|
+
import "./chunk-K5JZ5KLO.js";
|
|
35
35
|
import "./chunk-KRDLHAKK.js";
|
|
36
36
|
import {
|
|
37
37
|
onboard,
|
|
38
38
|
selfCaretRange,
|
|
39
39
|
selfPackageVersion
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-NT4WIP7I.js";
|
|
41
41
|
import {
|
|
42
42
|
parseAddresses,
|
|
43
43
|
sendApprovedReports,
|
|
44
44
|
withGlobalCc
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-FVVGDIGX.js";
|
|
46
46
|
import "./chunk-GECESMVS.js";
|
|
47
47
|
import {
|
|
48
48
|
BLURRED_CID,
|
|
@@ -60,15 +60,12 @@ import "./chunk-MPBXOJ3N.js";
|
|
|
60
60
|
import "./chunk-WEKL2HYJ.js";
|
|
61
61
|
import {
|
|
62
62
|
bumpDeps
|
|
63
|
-
} from "./chunk-
|
|
64
|
-
import {
|
|
65
|
-
isLeadFormType
|
|
66
|
-
} from "./chunk-2OP4JKIQ.js";
|
|
63
|
+
} from "./chunk-ATJ6ZA2V.js";
|
|
67
64
|
import {
|
|
68
65
|
syncConfigs
|
|
69
|
-
} from "./chunk-
|
|
70
|
-
import "./chunk-
|
|
71
|
-
import "./chunk-
|
|
66
|
+
} from "./chunk-Q276MZ7E.js";
|
|
67
|
+
import "./chunk-73UFRZKY.js";
|
|
68
|
+
import "./chunk-ZTPPCHDB.js";
|
|
72
69
|
import "./chunk-OTGB6YQO.js";
|
|
73
70
|
import {
|
|
74
71
|
ALL_AUDIT_NAMES,
|
|
@@ -79,18 +76,22 @@ import {
|
|
|
79
76
|
runAudits,
|
|
80
77
|
runAuditsAcross,
|
|
81
78
|
securityAudit
|
|
82
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-3LWKIKZ2.js";
|
|
83
80
|
import "./chunk-GASBX52O.js";
|
|
84
|
-
import "./chunk-
|
|
81
|
+
import "./chunk-VSHC4HOI.js";
|
|
85
82
|
import "./chunk-LKGVSM2O.js";
|
|
86
|
-
import "./chunk-
|
|
87
|
-
import "./chunk-
|
|
88
|
-
import "./chunk-
|
|
83
|
+
import "./chunk-QG6OSL4M.js";
|
|
84
|
+
import "./chunk-DC5H7KEQ.js";
|
|
85
|
+
import "./chunk-4SOCPK7A.js";
|
|
89
86
|
import {
|
|
90
87
|
fromJsonFile,
|
|
91
88
|
localPath
|
|
92
89
|
} from "./chunk-FDIFTIXC.js";
|
|
93
90
|
import "./chunk-HVOOCK6L.js";
|
|
91
|
+
import "./chunk-A3WDWNN5.js";
|
|
92
|
+
import {
|
|
93
|
+
isLeadFormType
|
|
94
|
+
} from "./chunk-2OP4JKIQ.js";
|
|
94
95
|
import "./chunk-XXTZBPUY.js";
|
|
95
96
|
import "./chunk-XTK5VIQB.js";
|
|
96
97
|
import {
|
|
@@ -195,6 +196,9 @@ function isVisibleInStrip(s) {
|
|
|
195
196
|
function spamReasonTokens(reason) {
|
|
196
197
|
return (reason ?? "").split(",").map((t) => t.trim()).filter((t) => t !== "");
|
|
197
198
|
}
|
|
199
|
+
function fanoutFailures(fanoutStatus) {
|
|
200
|
+
return (fanoutStatus ?? "").split(",").map((t) => t.trim()).filter((t) => t !== "" && !t.endsWith(":ok"));
|
|
201
|
+
}
|
|
198
202
|
var REASON_CHIP_MAX_TOKENS = 3;
|
|
199
203
|
function reasonChipText(tokens) {
|
|
200
204
|
const shown = tokens.slice(0, REASON_CHIP_MAX_TOKENS).join(" \xB7 ");
|
|
@@ -215,6 +219,8 @@ function renderSubmissionRowInner(s) {
|
|
|
215
219
|
const provenance = s.status === "spam_auto" && s.spamScore !== null && s.spamScore !== void 0 ? ` <span class="subm-provenance" title="${escapeHtml(s.spamReason ?? "")}">auto-spam \xB7 ${escapeHtml(String(s.spamScore))}</span>${reasonChip}` : "";
|
|
216
220
|
const recover = s.status === "spam_auto" ? btn("Not spam \u2192 new", "new") : "";
|
|
217
221
|
const bounceChip = s.notifyStatus === "bounced" ? ` <span class="subm-bounce" title="The lead notification email bounced \u2014 the point-of-contact address may be dead">notify bounced</span>` : "";
|
|
222
|
+
const fanoutBad = fanoutFailures(s.fanoutStatus);
|
|
223
|
+
const fanoutChip = fanoutBad.length > 0 ? ` <span class="subm-fanout-fail" title="This signup did not reach every configured destination">fan-out: ${escapeHtml(fanoutBad.join(" \xB7 "))}</span>` : "";
|
|
218
224
|
const kv = (label, value) => value === null || value === "" ? "" : `<div class="subm-kv"><span class="k">${label}</span> ${escapeHtml(String(value))}</div>`;
|
|
219
225
|
const sourceLink = s.sourceUrl ? `<div class="subm-kv"><span class="k">Source</span> <a href="${escapeHtml(safeUrl(s.sourceUrl))}" rel="noopener noreferrer">${escapeHtml(s.sourceUrl)}</a></div>` : "";
|
|
220
226
|
const messageBlock = s.message ? `<div class="subm-kv"><span class="k">Message</span></div><div class="subm-msg">${escapeHtml(s.message)}</div>` : "";
|
|
@@ -228,11 +234,12 @@ function renderSubmissionRowInner(s) {
|
|
|
228
234
|
extraFieldsList(s.extraFields),
|
|
229
235
|
kv("Spam", spamDetail),
|
|
230
236
|
kv("Notify", s.notifyStatus),
|
|
237
|
+
kv("Fan-out", s.fanoutStatus ?? null),
|
|
231
238
|
kv("Resend ID", s.resendMessageId),
|
|
232
239
|
kv("Submission #", s.submissionId)
|
|
233
240
|
].join("");
|
|
234
241
|
return `<details>
|
|
235
|
-
<summary class="subm-head"><strong>${type}</strong> \xB7 ${who} <span class="muted">${email}</span> <span class="pill subm-${status}">${status}</span>${provenance}${bounceChip} <span class="muted">${when}</span></summary>
|
|
242
|
+
<summary class="subm-head"><strong>${type}</strong> \xB7 ${who} <span class="muted">${email}</span> <span class="pill subm-${status}">${status}</span>${provenance}${bounceChip}${fanoutChip} <span class="muted">${when}</span></summary>
|
|
236
243
|
<div class="subm-detail">${details}</div>
|
|
237
244
|
</details>
|
|
238
245
|
<div class="subm-actions">${recover}${btn("Read", "read")}${btn("Archive", "archived")}${btn("Spam", "spam")}</div>`;
|
|
@@ -262,6 +269,7 @@ button.subm-status:disabled { opacity: 0.6; cursor: default; }
|
|
|
262
269
|
.pill.subm-spam_auto { background: #fff4e5; color: #a65a00; }
|
|
263
270
|
.subm-provenance { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; background: #fff4e5; color: #a65a00; }
|
|
264
271
|
.subm-bounce { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; background: #fdecea; color: #b00; font-weight: 700; }
|
|
272
|
+
.subm-fanout-fail { font-size: 0.72rem; border-radius: 0.25rem; padding: 0 0.35rem; white-space: nowrap; background: #fdecea; color: #b00; font-weight: 700; }
|
|
265
273
|
.subm-reasons { font-size: 0.72rem; color: #999; }
|
|
266
274
|
.subm-viewall { font-size: 0.8rem; font-weight: normal; margin-left: 0.4rem; white-space: nowrap; }`;
|
|
267
275
|
var SUBMISSION_STATUS_SCRIPT = `document.querySelectorAll("button.subm-status").forEach((b) => {
|