@reddoorla/maintenance 0.91.0 → 0.92.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/dist/{bump-deps-KGH5UYSF.js → bump-deps-GI7PHMOC.js} +6 -6
- package/dist/{chunk-A6T2R63Z.js → chunk-3HS6LPFX.js} +2 -2
- package/dist/{chunk-UXJZU23T.js → chunk-4NUKMPOO.js} +9 -9
- package/dist/{chunk-BQRFQSGY.js → chunk-4R3YAQNV.js} +2 -2
- package/dist/{chunk-JRHIFA5R.js → chunk-5XMKJX74.js} +10 -4
- package/dist/chunk-5XMKJX74.js.map +1 -0
- package/dist/{chunk-XIDUYLSR.js → chunk-7CJ3DVZW.js} +2 -2
- package/dist/{chunk-Z3KYFUN6.js → chunk-B6NUZ2ZX.js} +2 -2
- package/dist/{chunk-WVVT6SJK.js → chunk-BJQCIJOY.js} +169 -17
- package/dist/chunk-BJQCIJOY.js.map +1 -0
- package/dist/{chunk-Y53WOQIJ.js → chunk-CVJF2IC5.js} +2 -2
- package/dist/{chunk-QD427NIO.js → chunk-JB4Q3C3M.js} +7 -7
- package/dist/{chunk-IYSEC543.js → chunk-LS6QPANO.js} +2 -2
- package/dist/{chunk-SJOXWJ5Y.js → chunk-M2XF6JAI.js} +80 -9
- package/dist/chunk-M2XF6JAI.js.map +1 -0
- package/dist/{chunk-7RJ2HMMJ.js → chunk-NTLSMJHW.js} +2 -2
- package/dist/chunk-PJ23RDHT.js +48 -0
- package/dist/chunk-PJ23RDHT.js.map +1 -0
- package/dist/{chunk-2NJQLSEU.js → chunk-PPIZHDJL.js} +2 -2
- package/dist/{chunk-RQLXEMEE.js → chunk-T7ZUUOZ2.js} +2 -2
- package/dist/chunk-T7ZUUOZ2.js.map +1 -0
- package/dist/{chunk-TVL5MVUF.js → chunk-TCF6S7AD.js} +4 -4
- package/dist/{chunk-GSWTT2J6.js → chunk-WHWVYL7B.js} +4 -4
- package/dist/{chunk-3G25KIWW.js → chunk-ZTPPCHDB.js} +4 -4
- package/dist/cli/bin.js +17 -17
- package/dist/cli/commands/audit.js +10 -10
- package/dist/{convert-to-pnpm-NWB5FCQ2.js → convert-to-pnpm-ONFP3JKF.js} +6 -6
- package/dist/{db-IMMQSRAB.js → db-DJVW6ZSE.js} +3 -3
- package/dist/digest-EOA4JLTV.js +1 -1
- package/dist/digest-collectors-ZA3BTS3R.js +1 -1
- package/dist/{email-PLPDJUDH.js → email-I4BCETI4.js} +16 -65
- package/dist/email-I4BCETI4.js.map +1 -0
- package/dist/forms/index.d.ts +5 -1
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +1 -1
- package/dist/forms/prismic.d.ts +69 -0
- package/dist/forms/prismic.js +75 -0
- package/dist/forms/prismic.js.map +1 -0
- package/dist/{forms-notify-target-YCZUAHTY.js → forms-notify-target-MNNOGSJ7.js} +4 -3
- package/dist/{forms-notify-target-YCZUAHTY.js.map → forms-notify-target-MNNOGSJ7.js.map} +1 -1
- package/dist/{health-endpoint-UVX6R5N5.js → health-endpoint-DHKJUPRM.js} +6 -6
- package/dist/index.js +30 -23
- package/dist/index.js.map +1 -1
- package/dist/{init-NXZ4NXTL.js → init-ZRKWYCKA.js} +15 -15
- package/dist/{launch-WPEQ3TY2.js → launch-6X5OXPF2.js} +11 -11
- package/dist/{notify-4HP4GZ2H.js → notify-FYSOWKMJ.js} +4 -3
- package/dist/{onboard-BKCYBAVW.js → onboard-6NHGCKGB.js} +6 -6
- package/dist/orchestrate-KMQIUP3H.js +1 -1
- package/dist/{pipeline-HQJYZ443.js → pipeline-AQBFG5WM.js} +5 -3
- package/dist/preflight-VPHQYWTY.js +1 -1
- package/dist/{prismic-ci-6YGRIOHT.js → prismic-ci-A2HI7NNJ.js} +8 -8
- package/dist/{prismic-models-FPDSUXDO.js → prismic-models-QTKIMFBC.js} +4 -4
- package/dist/prospect/types.d.ts +17 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-TFIXSTE5.js → prospect-audit-RKUVSTLN.js} +4 -4
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{chunk-H6BLTEFN.js → render-7G43BSK5.js} +3 -3
- package/dist/{chunk-H6BLTEFN.js.map → render-7G43BSK5.js.map} +1 -1
- package/dist/{replay-4S6ENYCH.js → replay-HV7MGKBU.js} +3 -2
- package/dist/{replay-4S6ENYCH.js.map → replay-HV7MGKBU.js.map} +1 -1
- package/dist/{self-updating-VQIW24RZ.js → self-updating-D5XOWS7V.js} +5 -5
- package/dist/selftest-IFGSJP4X.js +1 -1
- package/dist/{smoke-suite-4JFZZYVW.js → smoke-suite-GXC5JZPH.js} +6 -6
- package/dist/{svelte-codemods-QAPDZC6F.js → svelte-codemods-A3EARSRP.js} +6 -6
- package/dist/{sync-configs-PVRLZRKL.js → sync-configs-KSESOHFF.js} +6 -6
- package/dist/{upgrade-IR2OO3KP.js → upgrade-ZLNVEIEA.js} +6 -6
- package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
- package/package.json +5 -1
- package/dist/chunk-JRHIFA5R.js.map +0 -1
- package/dist/chunk-RQLXEMEE.js.map +0 -1
- package/dist/chunk-SJOXWJ5Y.js.map +0 -1
- package/dist/chunk-WVVT6SJK.js.map +0 -1
- package/dist/email-PLPDJUDH.js.map +0 -1
- package/dist/render-3GYW2J34.js +0 -11
- package/dist/render-3GYW2J34.js.map +0 -1
- /package/dist/{bump-deps-KGH5UYSF.js.map → bump-deps-GI7PHMOC.js.map} +0 -0
- /package/dist/{chunk-A6T2R63Z.js.map → chunk-3HS6LPFX.js.map} +0 -0
- /package/dist/{chunk-UXJZU23T.js.map → chunk-4NUKMPOO.js.map} +0 -0
- /package/dist/{chunk-BQRFQSGY.js.map → chunk-4R3YAQNV.js.map} +0 -0
- /package/dist/{chunk-XIDUYLSR.js.map → chunk-7CJ3DVZW.js.map} +0 -0
- /package/dist/{chunk-Z3KYFUN6.js.map → chunk-B6NUZ2ZX.js.map} +0 -0
- /package/dist/{chunk-Y53WOQIJ.js.map → chunk-CVJF2IC5.js.map} +0 -0
- /package/dist/{chunk-QD427NIO.js.map → chunk-JB4Q3C3M.js.map} +0 -0
- /package/dist/{chunk-IYSEC543.js.map → chunk-LS6QPANO.js.map} +0 -0
- /package/dist/{chunk-7RJ2HMMJ.js.map → chunk-NTLSMJHW.js.map} +0 -0
- /package/dist/{chunk-2NJQLSEU.js.map → chunk-PPIZHDJL.js.map} +0 -0
- /package/dist/{chunk-TVL5MVUF.js.map → chunk-TCF6S7AD.js.map} +0 -0
- /package/dist/{chunk-GSWTT2J6.js.map → chunk-WHWVYL7B.js.map} +0 -0
- /package/dist/{chunk-3G25KIWW.js.map → chunk-ZTPPCHDB.js.map} +0 -0
- /package/dist/{convert-to-pnpm-NWB5FCQ2.js.map → convert-to-pnpm-ONFP3JKF.js.map} +0 -0
- /package/dist/{db-IMMQSRAB.js.map → db-DJVW6ZSE.js.map} +0 -0
- /package/dist/{health-endpoint-UVX6R5N5.js.map → health-endpoint-DHKJUPRM.js.map} +0 -0
- /package/dist/{init-NXZ4NXTL.js.map → init-ZRKWYCKA.js.map} +0 -0
- /package/dist/{launch-WPEQ3TY2.js.map → launch-6X5OXPF2.js.map} +0 -0
- /package/dist/{notify-4HP4GZ2H.js.map → notify-FYSOWKMJ.js.map} +0 -0
- /package/dist/{onboard-BKCYBAVW.js.map → onboard-6NHGCKGB.js.map} +0 -0
- /package/dist/{pipeline-HQJYZ443.js.map → pipeline-AQBFG5WM.js.map} +0 -0
- /package/dist/{prismic-ci-6YGRIOHT.js.map → prismic-ci-A2HI7NNJ.js.map} +0 -0
- /package/dist/{prismic-models-FPDSUXDO.js.map → prismic-models-QTKIMFBC.js.map} +0 -0
- /package/dist/{prospect-audit-TFIXSTE5.js.map → prospect-audit-RKUVSTLN.js.map} +0 -0
- /package/dist/{self-updating-VQIW24RZ.js.map → self-updating-D5XOWS7V.js.map} +0 -0
- /package/dist/{smoke-suite-4JFZZYVW.js.map → smoke-suite-GXC5JZPH.js.map} +0 -0
- /package/dist/{svelte-codemods-QAPDZC6F.js.map → svelte-codemods-A3EARSRP.js.map} +0 -0
- /package/dist/{sync-configs-PVRLZRKL.js.map → sync-configs-KSESOHFF.js.map} +0 -0
- /package/dist/{upgrade-IR2OO3KP.js.map → upgrade-ZLNVEIEA.js.map} +0 -0
- /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy for the submitter's confirmation email, resolved by the SITE from its own
|
|
3
|
+
* CMS and forwarded in the reserved `_reply` envelope.
|
|
4
|
+
*
|
|
5
|
+
* Everything here arrives over an untrusted boundary twice — once off the wire,
|
|
6
|
+
* once back out of the persisted `extraFields` JSON — so `parseReplyCopy` is the
|
|
7
|
+
* single gate both paths go through. It drops field by field rather than
|
|
8
|
+
* rejecting whole: a usable subject should still improve the email when the
|
|
9
|
+
* calendar block is malformed.
|
|
10
|
+
*/
|
|
11
|
+
type ReplyCalendar = {
|
|
12
|
+
title: string;
|
|
13
|
+
/** ISO 8601. Validated as parseable, not as any particular shape. */
|
|
14
|
+
start: string;
|
|
15
|
+
end?: string;
|
|
16
|
+
location?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
};
|
|
20
|
+
type ReplyCopy = {
|
|
21
|
+
subject?: string;
|
|
22
|
+
paragraphs?: string[];
|
|
23
|
+
signature?: string;
|
|
24
|
+
calendar?: ReplyCalendar;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a submitter's confirmation copy from a Prismic repository.
|
|
29
|
+
*
|
|
30
|
+
* Published as its own subpath (`@reddoorla/maintenance/forms/prismic`) so the
|
|
31
|
+
* CMS-agnostic core stays that way: `./forms` must remain importable by a site
|
|
32
|
+
* that uses no CMS at all.
|
|
33
|
+
*
|
|
34
|
+
* The client is STRUCTURALLY typed rather than imported from
|
|
35
|
+
* `@prismicio/client`. The two reads below are the whole surface, every fleet
|
|
36
|
+
* site already constructs its own client, and duck-typing keeps this package's
|
|
37
|
+
* dependency list — and a consuming site's bundle — untouched. It also makes the
|
|
38
|
+
* tests a two-line object instead of a mocked SDK.
|
|
39
|
+
*
|
|
40
|
+
* Every read is wrapped: a CMS failure must cost the visitor nothing, because
|
|
41
|
+
* the submission itself is captured either way.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
type PrismicReader = {
|
|
45
|
+
getSingle: (type: string) => Promise<unknown>;
|
|
46
|
+
getByUID: (type: string, uid: string) => Promise<unknown>;
|
|
47
|
+
};
|
|
48
|
+
type ResolveReplyCopyOptions = {
|
|
49
|
+
/** The submission's form type; selects the entry in the singleton. */
|
|
50
|
+
formType: string;
|
|
51
|
+
/** Present for event forms only. A miss is not an error. */
|
|
52
|
+
eventUid?: string;
|
|
53
|
+
/** Custom type holding events. Default "rsvp". */
|
|
54
|
+
eventType?: string;
|
|
55
|
+
/** Singleton holding site-level defaults. Default "form_replies". */
|
|
56
|
+
settingsType?: string;
|
|
57
|
+
/** Calendar location when the event names none — usually the venue address. */
|
|
58
|
+
defaultLocation?: string;
|
|
59
|
+
/** Canonical URL of the event page, attached to the calendar entry. */
|
|
60
|
+
eventUrl?: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Site defaults for `formType`, overridden field-by-field by the event document
|
|
64
|
+
* when `eventUid` names one. Undefined means "nothing authored" — the caller
|
|
65
|
+
* omits `_reply` entirely and the shared package's own fallbacks apply.
|
|
66
|
+
*/
|
|
67
|
+
declare function resolveReplyCopy(client: PrismicReader, opts: ResolveReplyCopyOptions): Promise<ReplyCopy | undefined>;
|
|
68
|
+
|
|
69
|
+
export { type PrismicReader, type ResolveReplyCopyOptions, resolveReplyCopy };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseReplyCopy
|
|
3
|
+
} from "../chunk-PJ23RDHT.js";
|
|
4
|
+
|
|
5
|
+
// src/forms/prismic.ts
|
|
6
|
+
var DEFAULT_DURATION_MS = 2 * 60 * 60 * 1e3;
|
|
7
|
+
function record(v) {
|
|
8
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
9
|
+
}
|
|
10
|
+
function text(v) {
|
|
11
|
+
if (typeof v !== "string") return void 0;
|
|
12
|
+
const t = v.trim();
|
|
13
|
+
return t === "" ? void 0 : t;
|
|
14
|
+
}
|
|
15
|
+
function paragraphs(v) {
|
|
16
|
+
if (!Array.isArray(v)) return void 0;
|
|
17
|
+
const out = v.map((b) => text(record(b).text)).filter((t) => t !== void 0);
|
|
18
|
+
return out.length > 0 ? out : void 0;
|
|
19
|
+
}
|
|
20
|
+
function timestamp(v) {
|
|
21
|
+
const s = text(v);
|
|
22
|
+
if (!s) return void 0;
|
|
23
|
+
const ms = Date.parse(s);
|
|
24
|
+
return Number.isNaN(ms) ? void 0 : new Date(ms).toISOString();
|
|
25
|
+
}
|
|
26
|
+
async function attempt(run) {
|
|
27
|
+
try {
|
|
28
|
+
return await run();
|
|
29
|
+
} catch {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function calendarFrom(data, opts) {
|
|
34
|
+
const start = timestamp(data.start_time);
|
|
35
|
+
const title = text(data.name);
|
|
36
|
+
if (!start || !title) return void 0;
|
|
37
|
+
const cal = {
|
|
38
|
+
title,
|
|
39
|
+
start,
|
|
40
|
+
end: timestamp(data.end_time) ?? new Date(Date.parse(start) + DEFAULT_DURATION_MS).toISOString()
|
|
41
|
+
};
|
|
42
|
+
const location = text(data.location) ?? opts.defaultLocation;
|
|
43
|
+
if (location) cal.location = location;
|
|
44
|
+
if (opts.eventUrl) cal.url = opts.eventUrl;
|
|
45
|
+
return cal;
|
|
46
|
+
}
|
|
47
|
+
async function resolveReplyCopy(client, opts) {
|
|
48
|
+
const settings = record(
|
|
49
|
+
await attempt(() => client.getSingle(opts.settingsType ?? "form_replies"))
|
|
50
|
+
);
|
|
51
|
+
const settingsData = record(settings.data);
|
|
52
|
+
const entries = Array.isArray(settingsData.replies) ? settingsData.replies : [];
|
|
53
|
+
const entry = record(entries.find((e) => record(e).form_type === opts.formType));
|
|
54
|
+
const draft = {
|
|
55
|
+
subject: text(entry.subject),
|
|
56
|
+
paragraphs: paragraphs(entry.body),
|
|
57
|
+
signature: paragraphs(settingsData.signature)?.join(" ")
|
|
58
|
+
};
|
|
59
|
+
if (opts.eventUid) {
|
|
60
|
+
const event = record(
|
|
61
|
+
await attempt(() => client.getByUID(opts.eventType ?? "rsvp", opts.eventUid))
|
|
62
|
+
);
|
|
63
|
+
const data = record(event.data);
|
|
64
|
+
if (Object.keys(data).length > 0) {
|
|
65
|
+
draft.subject = text(data.reply_subject) ?? draft.subject;
|
|
66
|
+
draft.paragraphs = paragraphs(data.reply_body) ?? draft.paragraphs;
|
|
67
|
+
draft.calendar = calendarFrom(data, opts);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return parseReplyCopy(draft);
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
resolveReplyCopy
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=prismic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/forms/prismic.ts"],"sourcesContent":["/**\n * Resolve a submitter's confirmation copy from a Prismic repository.\n *\n * Published as its own subpath (`@reddoorla/maintenance/forms/prismic`) so the\n * CMS-agnostic core stays that way: `./forms` must remain importable by a site\n * that uses no CMS at all.\n *\n * The client is STRUCTURALLY typed rather than imported from\n * `@prismicio/client`. The two reads below are the whole surface, every fleet\n * site already constructs its own client, and duck-typing keeps this package's\n * dependency list — and a consuming site's bundle — untouched. It also makes the\n * tests a two-line object instead of a mocked SDK.\n *\n * Every read is wrapped: a CMS failure must cost the visitor nothing, because\n * the submission itself is captured either way.\n */\nimport { parseReplyCopy, type ReplyCalendar, type ReplyCopy } from \"./reply-copy.js\";\n\nexport type PrismicReader = {\n getSingle: (type: string) => Promise<unknown>;\n getByUID: (type: string, uid: string) => Promise<unknown>;\n};\n\nexport type ResolveReplyCopyOptions = {\n /** The submission's form type; selects the entry in the singleton. */\n formType: string;\n /** Present for event forms only. A miss is not an error. */\n eventUid?: string;\n /** Custom type holding events. Default \"rsvp\". */\n eventType?: string;\n /** Singleton holding site-level defaults. Default \"form_replies\". */\n settingsType?: string;\n /** Calendar location when the event names none — usually the venue address. */\n defaultLocation?: string;\n /** Canonical URL of the event page, attached to the calendar entry. */\n eventUrl?: string;\n};\n\nconst DEFAULT_DURATION_MS = 2 * 60 * 60 * 1000;\n\nfunction record(v: unknown): Record<string, unknown> {\n return v && typeof v === \"object\" && !Array.isArray(v) ? (v as Record<string, unknown>) : {};\n}\n\nfunction text(v: unknown): string | undefined {\n if (typeof v !== \"string\") return undefined;\n const t = v.trim();\n return t === \"\" ? undefined : t;\n}\n\n/** Prismic Rich Text is an array of blocks each carrying a flat `.text`. That\n * flat field is exactly what a plain-text email wants, so no renderer is\n * needed — and no rich-text HTML can escape into the message. */\nfunction paragraphs(v: unknown): string[] | undefined {\n if (!Array.isArray(v)) return undefined;\n const out = v.map((b) => text(record(b).text)).filter((t): t is string => t !== undefined);\n return out.length > 0 ? out : undefined;\n}\n\n/** A Prismic Timestamp (\"2026-09-12T18:00:00+0000\") as an ISO string, or\n * undefined when the field is blank or unparseable. */\nfunction timestamp(v: unknown): string | undefined {\n const s = text(v);\n if (!s) return undefined;\n const ms = Date.parse(s);\n return Number.isNaN(ms) ? undefined : new Date(ms).toISOString();\n}\n\n/** Never let a CMS read fail the caller. */\nasync function attempt<T>(run: () => Promise<T>): Promise<T | undefined> {\n try {\n return await run();\n } catch {\n return undefined;\n }\n}\n\nfunction calendarFrom(\n data: Record<string, unknown>,\n opts: ResolveReplyCopyOptions,\n): ReplyCalendar | undefined {\n const start = timestamp(data.start_time);\n const title = text(data.name);\n // No start, no calendar — and deliberately no guess. An invite for the wrong\n // evening is worse for a guest than no invite at all.\n if (!start || !title) return undefined;\n const cal: ReplyCalendar = {\n title,\n start,\n end:\n timestamp(data.end_time) ?? new Date(Date.parse(start) + DEFAULT_DURATION_MS).toISOString(),\n };\n const location = text(data.location) ?? opts.defaultLocation;\n if (location) cal.location = location;\n if (opts.eventUrl) cal.url = opts.eventUrl;\n return cal;\n}\n\n/**\n * Site defaults for `formType`, overridden field-by-field by the event document\n * when `eventUid` names one. Undefined means \"nothing authored\" — the caller\n * omits `_reply` entirely and the shared package's own fallbacks apply.\n */\nexport async function resolveReplyCopy(\n client: PrismicReader,\n opts: ResolveReplyCopyOptions,\n): Promise<ReplyCopy | undefined> {\n const settings = record(\n await attempt(() => client.getSingle(opts.settingsType ?? \"form_replies\")),\n );\n const settingsData = record(settings.data);\n const entries = Array.isArray(settingsData.replies) ? settingsData.replies : [];\n const entry = record(entries.find((e) => record(e).form_type === opts.formType));\n\n const draft: Record<string, unknown> = {\n subject: text(entry.subject),\n paragraphs: paragraphs(entry.body),\n signature: paragraphs(settingsData.signature)?.join(\" \"),\n };\n\n if (opts.eventUid) {\n const event = record(\n await attempt(() => client.getByUID(opts.eventType ?? \"rsvp\", opts.eventUid as string)),\n );\n const data = record(event.data);\n if (Object.keys(data).length > 0) {\n draft.subject = text(data.reply_subject) ?? draft.subject;\n draft.paragraphs = paragraphs(data.reply_body) ?? draft.paragraphs;\n draft.calendar = calendarFrom(data, opts);\n }\n }\n\n return parseReplyCopy(draft);\n}\n"],"mappings":";;;;;AAsCA,IAAM,sBAAsB,IAAI,KAAK,KAAK;AAE1C,SAAS,OAAO,GAAqC;AACnD,SAAO,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,IAAK,IAAgC,CAAC;AAC7F;AAEA,SAAS,KAAK,GAAgC;AAC5C,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,QAAM,IAAI,EAAE,KAAK;AACjB,SAAO,MAAM,KAAK,SAAY;AAChC;AAKA,SAAS,WAAW,GAAkC;AACpD,MAAI,CAAC,MAAM,QAAQ,CAAC,EAAG,QAAO;AAC9B,QAAM,MAAM,EAAE,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,MAAmB,MAAM,MAAS;AACzF,SAAO,IAAI,SAAS,IAAI,MAAM;AAChC;AAIA,SAAS,UAAU,GAAgC;AACjD,QAAM,IAAI,KAAK,CAAC;AAChB,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,KAAK,KAAK,MAAM,CAAC;AACvB,SAAO,OAAO,MAAM,EAAE,IAAI,SAAY,IAAI,KAAK,EAAE,EAAE,YAAY;AACjE;AAGA,eAAe,QAAW,KAA+C;AACvE,MAAI;AACF,WAAO,MAAM,IAAI;AAAA,EACnB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aACP,MACA,MAC2B;AAC3B,QAAM,QAAQ,UAAU,KAAK,UAAU;AACvC,QAAM,QAAQ,KAAK,KAAK,IAAI;AAG5B,MAAI,CAAC,SAAS,CAAC,MAAO,QAAO;AAC7B,QAAM,MAAqB;AAAA,IACzB;AAAA,IACA;AAAA,IACA,KACE,UAAU,KAAK,QAAQ,KAAK,IAAI,KAAK,KAAK,MAAM,KAAK,IAAI,mBAAmB,EAAE,YAAY;AAAA,EAC9F;AACA,QAAM,WAAW,KAAK,KAAK,QAAQ,KAAK,KAAK;AAC7C,MAAI,SAAU,KAAI,WAAW;AAC7B,MAAI,KAAK,SAAU,KAAI,MAAM,KAAK;AAClC,SAAO;AACT;AAOA,eAAsB,iBACpB,QACA,MACgC;AAChC,QAAM,WAAW;AAAA,IACf,MAAM,QAAQ,MAAM,OAAO,UAAU,KAAK,gBAAgB,cAAc,CAAC;AAAA,EAC3E;AACA,QAAM,eAAe,OAAO,SAAS,IAAI;AACzC,QAAM,UAAU,MAAM,QAAQ,aAAa,OAAO,IAAI,aAAa,UAAU,CAAC;AAC9E,QAAM,QAAQ,OAAO,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC,EAAE,cAAc,KAAK,QAAQ,CAAC;AAE/E,QAAM,QAAiC;AAAA,IACrC,SAAS,KAAK,MAAM,OAAO;AAAA,IAC3B,YAAY,WAAW,MAAM,IAAI;AAAA,IACjC,WAAW,WAAW,aAAa,SAAS,GAAG,KAAK,GAAG;AAAA,EACzD;AAEA,MAAI,KAAK,UAAU;AACjB,UAAM,QAAQ;AAAA,MACZ,MAAM,QAAQ,MAAM,OAAO,SAAS,KAAK,aAAa,QAAQ,KAAK,QAAkB,CAAC;AAAA,IACxF;AACA,UAAM,OAAO,OAAO,MAAM,IAAI;AAC9B,QAAI,OAAO,KAAK,IAAI,EAAE,SAAS,GAAG;AAChC,YAAM,UAAU,KAAK,KAAK,aAAa,KAAK,MAAM;AAClD,YAAM,aAAa,WAAW,KAAK,UAAU,KAAK,MAAM;AACxD,YAAM,WAAW,aAAa,MAAM,IAAI;AAAA,IAC1C;AAAA,EACF;AAEA,SAAO,eAAe,KAAK;AAC7B;","names":[]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
describeNotifyTarget
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-M2XF6JAI.js";
|
|
4
|
+
import "./chunk-5XMKJX74.js";
|
|
5
5
|
import "./chunk-5ML7FMBD.js";
|
|
6
6
|
import "./chunk-NO74DXDP.js";
|
|
7
|
+
import "./chunk-PJ23RDHT.js";
|
|
7
8
|
import {
|
|
8
9
|
openBase,
|
|
9
10
|
readAirtableConfig
|
|
@@ -163,4 +164,4 @@ export {
|
|
|
163
164
|
formatNotifyTarget,
|
|
164
165
|
runFormsNotifyTargetCommand
|
|
165
166
|
};
|
|
166
|
-
//# sourceMappingURL=forms-notify-target-
|
|
167
|
+
//# sourceMappingURL=forms-notify-target-MNNOGSJ7.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recipes/forms-notify-target.ts","../src/cli/commands/forms-notify-target.ts"],"sourcesContent":["import { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport type { SiteMirror } from \"../db/site-mirror.js\";\nimport {\n listWebsites,\n siteSlug,\n updateSiteField,\n type Status,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport { canonicalizeStatus, toAirtableStatus } from \"../reports/airtable/site-status.js\";\nimport { describeNotifyTarget, type NotifyTarget } from \"../forms/notify.js\";\n\n/** The Airtable column the pre-launch guard actually lives in. */\nexport const STATUS_COLUMN = \"Status\";\n\n/** The two ends of the verify flip. Deliberately the ONLY transition this\n * performs: a site in any other status is already guarded (or is deliberately\n * something else, like \"hosted-only\"), and silently rewriting that would be the\n * same class of unseen change as the incident. */\nexport const LIVE_STATUS: Status = \"maintained\";\nexport const VERIFY_STATUS: Status = \"launching\";\n\nexport type FormsNotifyTargetDeps = {\n base?: AirtableBase;\n /** Site slug or the Airtable Websites NAME (both accepted). */\n site: string;\n /** `on` routes notifications to the operator; `off` restores. Omit to read. */\n set?: \"on\" | \"off\";\n /** Status to restore with `--set off`. Required, never inferred. */\n restore?: string;\n /** #539 Phase 5: Turso write-through for the Status cell this flips. Injected\n * at the CLI root, never defaulted — the unit suite calls this with a fake\n * base and must not open a real libSQL handle. */\n siteMirror?: SiteMirror;\n};\n\nexport type FormsNotifyTargetResult = {\n site: string;\n status: Status | null;\n target: NotifyTarget;\n /** Present only when a flip was attempted. `confirmed` is the whole point:\n * it comes from RE-READING the row, not from the write call returning. */\n flip?: { from: Status | null; to: Status; confirmed: boolean };\n};\n\n/**\n * The exact Airtable cell to write for an operator-supplied `--restore` value:\n * the operator's own string, verbatim.\n *\n * This is the ONE non-revertible surface in the #539 Phase 4 stage-1 rename —\n * every other change is code, and `git revert` undoes code. It cannot undo a\n * rewritten Airtable cell. So the rule here is stricter than everywhere else:\n * substitute only when the substitution is provably lossless, i.e. when\n * canonicalizing and mapping back ROUND-TRIPS to the operator's own string.\n *\n * That condition is, today, never false in a way that changes the answer — which\n * is the point. `toAirtableStatus(canonicalizeStatus(raw))` either equals `raw`\n * (so writing it is writing `raw`) or it does not (so we must write `raw`). The\n * function therefore reduces to \"write raw\", and it is written this way so the\n * reduction is visible rather than assumed. The case that made it matter:\n * `--restore legacy` canonicalizes to `archived`, which maps back to\n * \"deprecated\" — a different, real, operator-visible Airtable option that nobody\n * asked for. `hosting` → `hosted-only` → \"hosting\" round-trips and is safe.\n *\n * Airtable, not this module, is the authority on which option strings the\n * \"Status\" single-select accepts. Writing verbatim delegates to it: a typo is\n * rejected loudly at the API, exactly as it was before the rename.\n */\nexport function restoreCell(raw: string): string {\n const canonical = canonicalizeStatus(raw);\n if (canonical === null) return raw;\n const roundTripped = toAirtableStatus(canonical);\n return roundTripped === raw ? roundTripped : raw;\n}\n\nfunction findSite(rows: WebsiteRow[], site: string): WebsiteRow | undefined {\n const wanted = site.trim().toLowerCase();\n return rows.find(\n (r) => siteSlug(r.name) === siteSlug(site) || r.name.trim().toLowerCase() === wanted,\n );\n}\n\n/**\n * Answer \"who would a form submission on this site email?\" — and optionally\n * flip the pre-launch guard, confirming the flip by reading it back.\n *\n * The guard is a single Airtable `Status` cell. Nothing between \"I intended to\n * flip it\" and \"the client received a test lead\" reported the current state, so\n * on 2026-08-03 a flip that never landed sent a real client a test submission.\n * The fix is not a better intention, it is feedback: this reads the row back\n * after every write and refuses to call the flip confirmed on anything less.\n */\nexport async function formsNotifyTarget(\n deps: FormsNotifyTargetDeps,\n): Promise<FormsNotifyTargetResult> {\n const base = deps.base ?? openBase(readAirtableConfig());\n const rows = await listWebsites(base);\n const row = findSite(rows, deps.site);\n if (!row) {\n // The Websites NAME is not the repo slug (\"Sonder\", not \"gallerysonder\"),\n // and that mismatch has cost time before — so name the near misses rather\n // than making the operator go read Airtable to find the spelling.\n const needle = siteSlug(deps.site);\n const near = rows\n .map((r) => r.name)\n .filter((n) => siteSlug(n).includes(needle) || needle.includes(siteSlug(n)));\n throw Object.assign(\n new Error(\n `No Websites row matches '${deps.site}'. The Websites NAME is not always the repo ` +\n `slug (Sonder, not gallerysonder).` +\n (near.length > 0 ? ` Did you mean: ${near.join(\", \")}?` : \"\"),\n ),\n { exitCode: 2 },\n );\n }\n\n if (!deps.set) {\n return { site: row.name, status: row.status, target: describeNotifyTarget(row) };\n }\n\n // `--restore` is operator free text. It is canonicalized for what this command\n // REPORTS (`flip.to`, and the predicates downstream), but NEVER for what it\n // WRITES — see `restoreCell` below.\n const restoreRaw = deps.restore?.trim();\n const to = deps.set === \"on\" ? VERIFY_STATUS : (canonicalizeStatus(restoreRaw) ?? undefined);\n if (deps.set === \"off\" && !to) {\n throw Object.assign(\n new Error(\n `--set off needs --restore <status>: the status to return to is never inferred. ` +\n `Guessing \"${LIVE_STATUS}\" for a site that was \"hosted-only\" or \"archived\" would start ` +\n `sending real client notifications — the inverse of the failure this command exists ` +\n `to prevent.`,\n ),\n { exitCode: 2 },\n );\n }\n // Only ever flip a LIVE site into verify mode. A site already outside\n // \"maintained\" is guarded already, and rewriting its status would destroy a\n // real value nobody asked us to touch.\n if (deps.set === \"on\" && row.status !== LIVE_STATUS) {\n throw Object.assign(\n new Error(\n `${row.name} is \"${row.status ?? \"blank\"}\", not \"${LIVE_STATUS}\" — notifications ` +\n `already go to the operator only, so there is nothing to flip. Leaving the status ` +\n `untouched.`,\n ),\n { exitCode: 2 },\n );\n }\n\n // `--set on` writes a status this MODULE owns (VERIFY_STATUS), so mapping it to\n // the current Airtable vocabulary is correct. `--set off` writes the operator's\n // own string — see restoreCell.\n const cell = deps.set === \"on\" ? toAirtableStatus(VERIFY_STATUS) : restoreCell(restoreRaw!);\n await updateSiteField(base, row.id, STATUS_COLUMN, cell);\n // Mirror the same cell. The console reads Status from Turso, so without this a\n // site flipped into verify mode still reads as live for up to an hour — on the\n // one surface an operator would check to confirm the flip.\n await deps.siteMirror?.site(row.id, { [STATUS_COLUMN]: cell });\n\n // Read it back. The write returning is NOT evidence the field changed.\n const after = findSite(await listWebsites(base), deps.site);\n if (!after) {\n throw Object.assign(new Error(`${row.name} vanished from Websites during the flip.`), {\n exitCode: 1,\n });\n }\n return {\n site: after.name,\n status: after.status,\n target: describeNotifyTarget(after),\n // `confirmed` compares the RAW cell against the exact string written, not\n // canonical-to-canonical. Canonical equality is too weak to be a read-back\n // guard here: `legacy` and `deprecated` are the same canonical status, so a\n // cell holding either would confirm a flip to the other. Comparing raw is the\n // only comparison that can actually catch \"the cell is not what I sent\". The\n // canonical check rides along so a reader that stopped canonicalizing is\n // caught too.\n flip: {\n from: row.status,\n to: to!,\n confirmed: after.statusRaw === cell && after.status === to,\n },\n };\n}\n","import {\n formsNotifyTarget,\n LIVE_STATUS,\n VERIFY_STATUS,\n type FormsNotifyTargetResult,\n} from \"../../recipes/forms-notify-target.js\";\n\nexport type FormsNotifyTargetCommandOptions = {\n set?: string;\n restore?: string;\n cwd?: string;\n};\n\nconst AUDIENCE_LABEL = {\n client: \"THE CLIENT\",\n operator: \"OPERATOR ONLY\",\n nobody: \"NOBODY\",\n} as const;\n\n/** The line that decides whether it is safe to test-submit. Deliberately the\n * loudest thing on screen: the incident happened because the answer was\n * invisible, not because it was subtle. */\nexport function formatNotifyTarget(r: FormsNotifyTargetResult): string {\n const lines: string[] = [];\n if (r.flip) {\n const arrow = `${r.flip.from ?? \"blank\"} → ${r.flip.to}`;\n lines.push(\n r.flip.confirmed\n ? `${r.site} Status: ${arrow} ✓ confirmed by read-back`\n : `${r.site} Status: ${arrow} ✗ NOT CONFIRMED — the row still reads \"${r.status ?? \"blank\"}\". Nothing was verified; do not test-submit.`,\n );\n } else {\n lines.push(`${r.site} Status: ${r.status ?? \"blank\"}`);\n }\n\n lines.push(`A submission right now would notify: ${AUDIENCE_LABEL[r.target.audience]}`);\n if (r.target.to.length > 0) lines.push(` to: ${r.target.to.join(\", \")}`);\n if (r.target.cc.length > 0) lines.push(` cc: ${r.target.cc.join(\", \")}`);\n lines.push(` ${r.target.reason}`);\n\n if (r.target.audience === \"client\") {\n lines.push(\n \"\",\n `⚠️ A test submission WILL email the client, and email cannot be recalled.`,\n ` Route it to yourself first: reddoor forms-notify-target ${r.site} --set on`,\n );\n }\n if (r.flip?.confirmed && r.flip.to === VERIFY_STATUS) {\n lines.push(\n \"\",\n `Safe to test. When you are done, restore it:`,\n ` reddoor forms-notify-target ${r.site} --set off --restore ${r.flip.from ?? LIVE_STATUS}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * `forms-notify-target <site>` — show who a form submission would email, and\n * optionally flip the pre-launch guard with a read-back confirmation.\n *\n * Read-only by default: answering the question must never be riskier than not\n * asking it.\n */\nexport async function runFormsNotifyTargetCommand(\n site: string | undefined,\n opts: FormsNotifyTargetCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (!site?.trim()) {\n return { output: \"forms-notify-target requires <site> (slug or Airtable name)\", code: 2 };\n }\n const set = opts.set?.trim().toLowerCase();\n if (set !== undefined && set !== \"on\" && set !== \"off\") {\n return { output: `--set must be 'on' or 'off' (got '${opts.set}')`, code: 2 };\n }\n try {\n // #539 Phase 5: the flip writes Status on the Websites row; mirror it so the\n // console (which reads Turso) shows the new routing immediately.\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n const result = await formsNotifyTarget({\n siteMirror: await makeSiteMirror(),\n site: site.trim(),\n ...(set ? { set: set as \"on\" | \"off\" } : {}),\n ...(opts.restore ? { restore: opts.restore } : {}),\n });\n // An unconfirmed flip must not exit 0: a script (or a person skimming) that\n // reads exit status would otherwise take \"I flipped it\" on faith, which is\n // exactly the assumption that sent a client a test lead.\n return {\n output: formatNotifyTarget(result),\n code: result.flip && !result.flip.confirmed ? 1 : 0,\n };\n } catch (err) {\n const e = err as { message?: string; exitCode?: number };\n return { output: e.message ?? String(err), code: e.exitCode ?? 1 };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAcO,IAAM,gBAAgB;AAMtB,IAAM,cAAsB;AAC5B,IAAM,gBAAwB;AAgD9B,SAAS,YAAY,KAAqB;AAC/C,QAAM,YAAY,mBAAmB,GAAG;AACxC,MAAI,cAAc,KAAM,QAAO;AAC/B,QAAM,eAAe,iBAAiB,SAAS;AAC/C,SAAO,iBAAiB,MAAM,eAAe;AAC/C;AAEA,SAAS,SAAS,MAAoB,MAAsC;AAC1E,QAAM,SAAS,KAAK,KAAK,EAAE,YAAY;AACvC,SAAO,KAAK;AAAA,IACV,CAAC,MAAM,SAAS,EAAE,IAAI,MAAM,SAAS,IAAI,KAAK,EAAE,KAAK,KAAK,EAAE,YAAY,MAAM;AAAA,EAChF;AACF;AAYA,eAAsB,kBACpB,MACkC;AAClC,QAAM,OAAO,KAAK,QAAQ,SAAS,mBAAmB,CAAC;AACvD,QAAM,OAAO,MAAM,aAAa,IAAI;AACpC,QAAM,MAAM,SAAS,MAAM,KAAK,IAAI;AACpC,MAAI,CAAC,KAAK;AAIR,UAAM,SAAS,SAAS,KAAK,IAAI;AACjC,UAAM,OAAO,KACV,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,OAAO,CAAC,MAAM,SAAS,CAAC,EAAE,SAAS,MAAM,KAAK,OAAO,SAAS,SAAS,CAAC,CAAC,CAAC;AAC7E,UAAM,OAAO;AAAA,MACX,IAAI;AAAA,QACF,4BAA4B,KAAK,IAAI,mFAElC,KAAK,SAAS,IAAI,kBAAkB,KAAK,KAAK,IAAI,CAAC,MAAM;AAAA,MAC9D;AAAA,MACA,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AAEA,MAAI,CAAC,KAAK,KAAK;AACb,WAAO,EAAE,MAAM,IAAI,MAAM,QAAQ,IAAI,QAAQ,QAAQ,qBAAqB,GAAG,EAAE;AAAA,EACjF;AAKA,QAAM,aAAa,KAAK,SAAS,KAAK;AACtC,QAAM,KAAK,KAAK,QAAQ,OAAO,gBAAiB,mBAAmB,UAAU,KAAK;AAClF,MAAI,KAAK,QAAQ,SAAS,CAAC,IAAI;AAC7B,UAAM,OAAO;AAAA,MACX,IAAI;AAAA,QACF,4FACe,WAAW;AAAA,MAG5B;AAAA,MACA,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AAIA,MAAI,KAAK,QAAQ,QAAQ,IAAI,WAAW,aAAa;AACnD,UAAM,OAAO;AAAA,MACX,IAAI;AAAA,QACF,GAAG,IAAI,IAAI,QAAQ,IAAI,UAAU,OAAO,WAAW,WAAW;AAAA,MAGhE;AAAA,MACA,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AAKA,QAAM,OAAO,KAAK,QAAQ,OAAO,iBAAiB,aAAa,IAAI,YAAY,UAAW;AAC1F,QAAM,gBAAgB,MAAM,IAAI,IAAI,eAAe,IAAI;AAIvD,QAAM,KAAK,YAAY,KAAK,IAAI,IAAI,EAAE,CAAC,aAAa,GAAG,KAAK,CAAC;AAG7D,QAAM,QAAQ,SAAS,MAAM,aAAa,IAAI,GAAG,KAAK,IAAI;AAC1D,MAAI,CAAC,OAAO;AACV,UAAM,OAAO,OAAO,IAAI,MAAM,GAAG,IAAI,IAAI,0CAA0C,GAAG;AAAA,MACpF,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,QAAQ,MAAM;AAAA,IACd,QAAQ,qBAAqB,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQlC,MAAM;AAAA,MACJ,MAAM,IAAI;AAAA,MACV;AAAA,MACA,WAAW,MAAM,cAAc,QAAQ,MAAM,WAAW;AAAA,IAC1D;AAAA,EACF;AACF;;;AC5KA,IAAM,iBAAiB;AAAA,EACrB,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AACV;AAKO,SAAS,mBAAmB,GAAoC;AACrE,QAAM,QAAkB,CAAC;AACzB,MAAI,EAAE,MAAM;AACV,UAAM,QAAQ,GAAG,EAAE,KAAK,QAAQ,OAAO,WAAM,EAAE,KAAK,EAAE;AACtD,UAAM;AAAA,MACJ,EAAE,KAAK,YACH,GAAG,EAAE,IAAI,aAAa,KAAK,oCAC3B,GAAG,EAAE,IAAI,aAAa,KAAK,sDAA4C,EAAE,UAAU,OAAO;AAAA,IAChG;AAAA,EACF,OAAO;AACL,UAAM,KAAK,GAAG,EAAE,IAAI,aAAa,EAAE,UAAU,OAAO,EAAE;AAAA,EACxD;AAEA,QAAM,KAAK,wCAAwC,eAAe,EAAE,OAAO,QAAQ,CAAC,EAAE;AACtF,MAAI,EAAE,OAAO,GAAG,SAAS,EAAG,OAAM,KAAK,SAAS,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,EAAE;AACxE,MAAI,EAAE,OAAO,GAAG,SAAS,EAAG,OAAM,KAAK,SAAS,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,EAAE;AACxE,QAAM,KAAK,KAAK,EAAE,OAAO,MAAM,EAAE;AAEjC,MAAI,EAAE,OAAO,aAAa,UAAU;AAClC,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,+DAA+D,EAAE,IAAI;AAAA,IACvE;AAAA,EACF;AACA,MAAI,EAAE,MAAM,aAAa,EAAE,KAAK,OAAO,eAAe;AACpD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,iCAAiC,EAAE,IAAI,wBAAwB,EAAE,KAAK,QAAQ,WAAW;AAAA,IAC3F;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AASA,eAAsB,4BACpB,MACA,MAC2C;AAC3C,MAAI,CAAC,MAAM,KAAK,GAAG;AACjB,WAAO,EAAE,QAAQ,+DAA+D,MAAM,EAAE;AAAA,EAC1F;AACA,QAAM,MAAM,KAAK,KAAK,KAAK,EAAE,YAAY;AACzC,MAAI,QAAQ,UAAa,QAAQ,QAAQ,QAAQ,OAAO;AACtD,WAAO,EAAE,QAAQ,qCAAqC,KAAK,GAAG,MAAM,MAAM,EAAE;AAAA,EAC9E;AACA,MAAI;AAGF,UAAM,EAAE,eAAe,IAAI,MAAM,OAAO,2BAAyB;AACjE,UAAM,SAAS,MAAM,kBAAkB;AAAA,MACrC,YAAY,MAAM,eAAe;AAAA,MACjC,MAAM,KAAK,KAAK;AAAA,MAChB,GAAI,MAAM,EAAE,IAAyB,IAAI,CAAC;AAAA,MAC1C,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,IAClD,CAAC;AAID,WAAO;AAAA,MACL,QAAQ,mBAAmB,MAAM;AAAA,MACjC,MAAM,OAAO,QAAQ,CAAC,OAAO,KAAK,YAAY,IAAI;AAAA,IACpD;AAAA,EACF,SAAS,KAAK;AACZ,UAAM,IAAI;AACV,WAAO,EAAE,QAAQ,EAAE,WAAW,OAAO,GAAG,GAAG,MAAM,EAAE,YAAY,EAAE;AAAA,EACnE;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/recipes/forms-notify-target.ts","../src/cli/commands/forms-notify-target.ts"],"sourcesContent":["import { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport type { SiteMirror } from \"../db/site-mirror.js\";\nimport {\n listWebsites,\n siteSlug,\n updateSiteField,\n type Status,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport { canonicalizeStatus, toAirtableStatus } from \"../reports/airtable/site-status.js\";\nimport { describeNotifyTarget, type NotifyTarget } from \"../forms/notify.js\";\n\n/** The Airtable column the pre-launch guard actually lives in. */\nexport const STATUS_COLUMN = \"Status\";\n\n/** The two ends of the verify flip. Deliberately the ONLY transition this\n * performs: a site in any other status is already guarded (or is deliberately\n * something else, like \"hosted-only\"), and silently rewriting that would be the\n * same class of unseen change as the incident. */\nexport const LIVE_STATUS: Status = \"maintained\";\nexport const VERIFY_STATUS: Status = \"launching\";\n\nexport type FormsNotifyTargetDeps = {\n base?: AirtableBase;\n /** Site slug or the Airtable Websites NAME (both accepted). */\n site: string;\n /** `on` routes notifications to the operator; `off` restores. Omit to read. */\n set?: \"on\" | \"off\";\n /** Status to restore with `--set off`. Required, never inferred. */\n restore?: string;\n /** #539 Phase 5: Turso write-through for the Status cell this flips. Injected\n * at the CLI root, never defaulted — the unit suite calls this with a fake\n * base and must not open a real libSQL handle. */\n siteMirror?: SiteMirror;\n};\n\nexport type FormsNotifyTargetResult = {\n site: string;\n status: Status | null;\n target: NotifyTarget;\n /** Present only when a flip was attempted. `confirmed` is the whole point:\n * it comes from RE-READING the row, not from the write call returning. */\n flip?: { from: Status | null; to: Status; confirmed: boolean };\n};\n\n/**\n * The exact Airtable cell to write for an operator-supplied `--restore` value:\n * the operator's own string, verbatim.\n *\n * This is the ONE non-revertible surface in the #539 Phase 4 stage-1 rename —\n * every other change is code, and `git revert` undoes code. It cannot undo a\n * rewritten Airtable cell. So the rule here is stricter than everywhere else:\n * substitute only when the substitution is provably lossless, i.e. when\n * canonicalizing and mapping back ROUND-TRIPS to the operator's own string.\n *\n * That condition is, today, never false in a way that changes the answer — which\n * is the point. `toAirtableStatus(canonicalizeStatus(raw))` either equals `raw`\n * (so writing it is writing `raw`) or it does not (so we must write `raw`). The\n * function therefore reduces to \"write raw\", and it is written this way so the\n * reduction is visible rather than assumed. The case that made it matter:\n * `--restore legacy` canonicalizes to `archived`, which maps back to\n * \"deprecated\" — a different, real, operator-visible Airtable option that nobody\n * asked for. `hosting` → `hosted-only` → \"hosting\" round-trips and is safe.\n *\n * Airtable, not this module, is the authority on which option strings the\n * \"Status\" single-select accepts. Writing verbatim delegates to it: a typo is\n * rejected loudly at the API, exactly as it was before the rename.\n */\nexport function restoreCell(raw: string): string {\n const canonical = canonicalizeStatus(raw);\n if (canonical === null) return raw;\n const roundTripped = toAirtableStatus(canonical);\n return roundTripped === raw ? roundTripped : raw;\n}\n\nfunction findSite(rows: WebsiteRow[], site: string): WebsiteRow | undefined {\n const wanted = site.trim().toLowerCase();\n return rows.find(\n (r) => siteSlug(r.name) === siteSlug(site) || r.name.trim().toLowerCase() === wanted,\n );\n}\n\n/**\n * Answer \"who would a form submission on this site email?\" — and optionally\n * flip the pre-launch guard, confirming the flip by reading it back.\n *\n * The guard is a single Airtable `Status` cell. Nothing between \"I intended to\n * flip it\" and \"the client received a test lead\" reported the current state, so\n * on 2026-08-03 a flip that never landed sent a real client a test submission.\n * The fix is not a better intention, it is feedback: this reads the row back\n * after every write and refuses to call the flip confirmed on anything less.\n */\nexport async function formsNotifyTarget(\n deps: FormsNotifyTargetDeps,\n): Promise<FormsNotifyTargetResult> {\n const base = deps.base ?? openBase(readAirtableConfig());\n const rows = await listWebsites(base);\n const row = findSite(rows, deps.site);\n if (!row) {\n // The Websites NAME is not the repo slug (\"Sonder\", not \"gallerysonder\"),\n // and that mismatch has cost time before — so name the near misses rather\n // than making the operator go read Airtable to find the spelling.\n const needle = siteSlug(deps.site);\n const near = rows\n .map((r) => r.name)\n .filter((n) => siteSlug(n).includes(needle) || needle.includes(siteSlug(n)));\n throw Object.assign(\n new Error(\n `No Websites row matches '${deps.site}'. The Websites NAME is not always the repo ` +\n `slug (Sonder, not gallerysonder).` +\n (near.length > 0 ? ` Did you mean: ${near.join(\", \")}?` : \"\"),\n ),\n { exitCode: 2 },\n );\n }\n\n if (!deps.set) {\n return { site: row.name, status: row.status, target: describeNotifyTarget(row) };\n }\n\n // `--restore` is operator free text. It is canonicalized for what this command\n // REPORTS (`flip.to`, and the predicates downstream), but NEVER for what it\n // WRITES — see `restoreCell` below.\n const restoreRaw = deps.restore?.trim();\n const to = deps.set === \"on\" ? VERIFY_STATUS : (canonicalizeStatus(restoreRaw) ?? undefined);\n if (deps.set === \"off\" && !to) {\n throw Object.assign(\n new Error(\n `--set off needs --restore <status>: the status to return to is never inferred. ` +\n `Guessing \"${LIVE_STATUS}\" for a site that was \"hosted-only\" or \"archived\" would start ` +\n `sending real client notifications — the inverse of the failure this command exists ` +\n `to prevent.`,\n ),\n { exitCode: 2 },\n );\n }\n // Only ever flip a LIVE site into verify mode. A site already outside\n // \"maintained\" is guarded already, and rewriting its status would destroy a\n // real value nobody asked us to touch.\n if (deps.set === \"on\" && row.status !== LIVE_STATUS) {\n throw Object.assign(\n new Error(\n `${row.name} is \"${row.status ?? \"blank\"}\", not \"${LIVE_STATUS}\" — notifications ` +\n `already go to the operator only, so there is nothing to flip. Leaving the status ` +\n `untouched.`,\n ),\n { exitCode: 2 },\n );\n }\n\n // `--set on` writes a status this MODULE owns (VERIFY_STATUS), so mapping it to\n // the current Airtable vocabulary is correct. `--set off` writes the operator's\n // own string — see restoreCell.\n const cell = deps.set === \"on\" ? toAirtableStatus(VERIFY_STATUS) : restoreCell(restoreRaw!);\n await updateSiteField(base, row.id, STATUS_COLUMN, cell);\n // Mirror the same cell. The console reads Status from Turso, so without this a\n // site flipped into verify mode still reads as live for up to an hour — on the\n // one surface an operator would check to confirm the flip.\n await deps.siteMirror?.site(row.id, { [STATUS_COLUMN]: cell });\n\n // Read it back. The write returning is NOT evidence the field changed.\n const after = findSite(await listWebsites(base), deps.site);\n if (!after) {\n throw Object.assign(new Error(`${row.name} vanished from Websites during the flip.`), {\n exitCode: 1,\n });\n }\n return {\n site: after.name,\n status: after.status,\n target: describeNotifyTarget(after),\n // `confirmed` compares the RAW cell against the exact string written, not\n // canonical-to-canonical. Canonical equality is too weak to be a read-back\n // guard here: `legacy` and `deprecated` are the same canonical status, so a\n // cell holding either would confirm a flip to the other. Comparing raw is the\n // only comparison that can actually catch \"the cell is not what I sent\". The\n // canonical check rides along so a reader that stopped canonicalizing is\n // caught too.\n flip: {\n from: row.status,\n to: to!,\n confirmed: after.statusRaw === cell && after.status === to,\n },\n };\n}\n","import {\n formsNotifyTarget,\n LIVE_STATUS,\n VERIFY_STATUS,\n type FormsNotifyTargetResult,\n} from \"../../recipes/forms-notify-target.js\";\n\nexport type FormsNotifyTargetCommandOptions = {\n set?: string;\n restore?: string;\n cwd?: string;\n};\n\nconst AUDIENCE_LABEL = {\n client: \"THE CLIENT\",\n operator: \"OPERATOR ONLY\",\n nobody: \"NOBODY\",\n} as const;\n\n/** The line that decides whether it is safe to test-submit. Deliberately the\n * loudest thing on screen: the incident happened because the answer was\n * invisible, not because it was subtle. */\nexport function formatNotifyTarget(r: FormsNotifyTargetResult): string {\n const lines: string[] = [];\n if (r.flip) {\n const arrow = `${r.flip.from ?? \"blank\"} → ${r.flip.to}`;\n lines.push(\n r.flip.confirmed\n ? `${r.site} Status: ${arrow} ✓ confirmed by read-back`\n : `${r.site} Status: ${arrow} ✗ NOT CONFIRMED — the row still reads \"${r.status ?? \"blank\"}\". Nothing was verified; do not test-submit.`,\n );\n } else {\n lines.push(`${r.site} Status: ${r.status ?? \"blank\"}`);\n }\n\n lines.push(`A submission right now would notify: ${AUDIENCE_LABEL[r.target.audience]}`);\n if (r.target.to.length > 0) lines.push(` to: ${r.target.to.join(\", \")}`);\n if (r.target.cc.length > 0) lines.push(` cc: ${r.target.cc.join(\", \")}`);\n lines.push(` ${r.target.reason}`);\n\n if (r.target.audience === \"client\") {\n lines.push(\n \"\",\n `⚠️ A test submission WILL email the client, and email cannot be recalled.`,\n ` Route it to yourself first: reddoor forms-notify-target ${r.site} --set on`,\n );\n }\n if (r.flip?.confirmed && r.flip.to === VERIFY_STATUS) {\n lines.push(\n \"\",\n `Safe to test. When you are done, restore it:`,\n ` reddoor forms-notify-target ${r.site} --set off --restore ${r.flip.from ?? LIVE_STATUS}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * `forms-notify-target <site>` — show who a form submission would email, and\n * optionally flip the pre-launch guard with a read-back confirmation.\n *\n * Read-only by default: answering the question must never be riskier than not\n * asking it.\n */\nexport async function runFormsNotifyTargetCommand(\n site: string | undefined,\n opts: FormsNotifyTargetCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (!site?.trim()) {\n return { output: \"forms-notify-target requires <site> (slug or Airtable name)\", code: 2 };\n }\n const set = opts.set?.trim().toLowerCase();\n if (set !== undefined && set !== \"on\" && set !== \"off\") {\n return { output: `--set must be 'on' or 'off' (got '${opts.set}')`, code: 2 };\n }\n try {\n // #539 Phase 5: the flip writes Status on the Websites row; mirror it so the\n // console (which reads Turso) shows the new routing immediately.\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n const result = await formsNotifyTarget({\n siteMirror: await makeSiteMirror(),\n site: site.trim(),\n ...(set ? { set: set as \"on\" | \"off\" } : {}),\n ...(opts.restore ? { restore: opts.restore } : {}),\n });\n // An unconfirmed flip must not exit 0: a script (or a person skimming) that\n // reads exit status would otherwise take \"I flipped it\" on faith, which is\n // exactly the assumption that sent a client a test lead.\n return {\n output: formatNotifyTarget(result),\n code: result.flip && !result.flip.confirmed ? 1 : 0,\n };\n } catch (err) {\n const e = err as { message?: string; exitCode?: number };\n return { output: e.message ?? String(err), code: e.exitCode ?? 1 };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -5,20 +5,20 @@ import {
|
|
|
5
5
|
appendSkipNotice,
|
|
6
6
|
fleetWorkdir,
|
|
7
7
|
prepareFleetSites
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-TCF6S7AD.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
11
|
} from "./chunk-Q47DU255.js";
|
|
12
12
|
import {
|
|
13
13
|
healthEndpoint
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-4R3YAQNV.js";
|
|
15
15
|
import "./chunk-R53VL3RI.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-ZTPPCHDB.js";
|
|
17
17
|
import "./chunk-Z3JXLULD.js";
|
|
18
|
-
import "./chunk-RARREDQE.js";
|
|
19
|
-
import "./chunk-XTK5VIQB.js";
|
|
20
18
|
import "./chunk-LBYOLBW7.js";
|
|
19
|
+
import "./chunk-RARREDQE.js";
|
|
21
20
|
import "./chunk-XXTZBPUY.js";
|
|
21
|
+
import "./chunk-XTK5VIQB.js";
|
|
22
22
|
|
|
23
23
|
// src/cli/commands/health-endpoint.ts
|
|
24
24
|
import { resolve } from "path";
|
|
@@ -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-DHKJUPRM.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import "./chunk-2SLED3NB.js";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_INIT_STEPS,
|
|
4
|
+
a11yFixturesPage,
|
|
5
|
+
init
|
|
6
|
+
} from "./chunk-4NUKMPOO.js";
|
|
2
7
|
import {
|
|
3
8
|
draftReportForSite
|
|
4
9
|
} from "./chunk-ZRI5VRPG.js";
|
|
5
10
|
import "./chunk-YFAXOD7Z.js";
|
|
6
11
|
import {
|
|
7
12
|
upgradeSvelte4to5
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_INIT_STEPS,
|
|
11
|
-
a11yFixturesPage,
|
|
12
|
-
init
|
|
13
|
-
} from "./chunk-UXJZU23T.js";
|
|
13
|
+
} from "./chunk-B6NUZ2ZX.js";
|
|
14
14
|
import {
|
|
15
15
|
convertToPnpm
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-7CJ3DVZW.js";
|
|
17
17
|
import {
|
|
18
18
|
svelteCodemods
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-LS6QPANO.js";
|
|
20
20
|
import "./chunk-J7SZQUCW.js";
|
|
21
|
-
import "./chunk-
|
|
22
|
-
import "./chunk-
|
|
21
|
+
import "./chunk-4R3YAQNV.js";
|
|
22
|
+
import "./chunk-CVJF2IC5.js";
|
|
23
|
+
import "./chunk-R53VL3RI.js";
|
|
23
24
|
import {
|
|
24
25
|
onboard,
|
|
25
26
|
selfCaretRange,
|
|
26
27
|
selfPackageVersion
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-NTLSMJHW.js";
|
|
28
29
|
import {
|
|
29
30
|
bumpDeps
|
|
30
|
-
} from "./chunk-
|
|
31
|
-
import "./chunk-R53VL3RI.js";
|
|
31
|
+
} from "./chunk-PPIZHDJL.js";
|
|
32
32
|
import "./chunk-MVDMKKBN.js";
|
|
33
33
|
import {
|
|
34
34
|
approveBlockers
|
|
@@ -82,10 +82,11 @@ import {
|
|
|
82
82
|
toAirtableStatus
|
|
83
83
|
} from "./chunk-7NYN2P6I.js";
|
|
84
84
|
import "./chunk-WEKL2HYJ.js";
|
|
85
|
+
import "./chunk-T7ZUUOZ2.js";
|
|
85
86
|
import {
|
|
86
87
|
syncConfigs
|
|
87
|
-
} from "./chunk-
|
|
88
|
-
import "./chunk-
|
|
88
|
+
} from "./chunk-3HS6LPFX.js";
|
|
89
|
+
import "./chunk-ZTPPCHDB.js";
|
|
89
90
|
import "./chunk-MLT4QY5X.js";
|
|
90
91
|
import "./chunk-OTGB6YQO.js";
|
|
91
92
|
import {
|
|
@@ -96,7 +97,7 @@ import {
|
|
|
96
97
|
runAudits,
|
|
97
98
|
runAuditsAcross,
|
|
98
99
|
securityAudit
|
|
99
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-JB4Q3C3M.js";
|
|
100
101
|
import "./chunk-LKGVSM2O.js";
|
|
101
102
|
import "./chunk-GASBX52O.js";
|
|
102
103
|
import "./chunk-XHHMM5HX.js";
|
|
@@ -106,20 +107,20 @@ import {
|
|
|
106
107
|
} from "./chunk-Z3JXLULD.js";
|
|
107
108
|
import "./chunk-DMPHP7UP.js";
|
|
108
109
|
import "./chunk-22GKJO5F.js";
|
|
109
|
-
import "./chunk-UDRHAQT7.js";
|
|
110
|
-
import "./chunk-RARREDQE.js";
|
|
111
110
|
import "./chunk-OCNSIBMP.js";
|
|
112
|
-
import "./chunk-A3WDWNN5.js";
|
|
113
|
-
import {
|
|
114
|
-
isLeadFormType
|
|
115
|
-
} from "./chunk-2OP4JKIQ.js";
|
|
116
|
-
import "./chunk-XTK5VIQB.js";
|
|
117
111
|
import {
|
|
118
112
|
lighthouseAudit
|
|
119
113
|
} from "./chunk-2NQUCS7O.js";
|
|
114
|
+
import "./chunk-UDRHAQT7.js";
|
|
120
115
|
import "./chunk-PTDMVGKL.js";
|
|
121
116
|
import "./chunk-LBYOLBW7.js";
|
|
117
|
+
import "./chunk-RARREDQE.js";
|
|
118
|
+
import "./chunk-A3WDWNN5.js";
|
|
119
|
+
import {
|
|
120
|
+
isLeadFormType
|
|
121
|
+
} from "./chunk-2OP4JKIQ.js";
|
|
122
122
|
import "./chunk-XXTZBPUY.js";
|
|
123
|
+
import "./chunk-XTK5VIQB.js";
|
|
123
124
|
import "./chunk-6VAL7XJT.js";
|
|
124
125
|
import "./chunk-NMAWXBJA.js";
|
|
125
126
|
import "./chunk-YLYXW5PH.js";
|
|
@@ -1450,6 +1451,9 @@ h1 { margin: 0 0 0.25rem; font-size: 1.75rem; }
|
|
|
1450
1451
|
.muted { color:#999; }
|
|
1451
1452
|
.filters { display:flex; flex-wrap:wrap; gap:0.4rem; margin-bottom:1.25rem; }
|
|
1452
1453
|
.filters button { font:inherit; font-size:0.85rem; padding:0.25rem 0.7rem; border:1px solid #ccc; border-radius:999px; background:transparent; color:inherit; cursor:pointer; }
|
|
1454
|
+
.top-nav { margin: 0.5rem 0 1rem; }
|
|
1455
|
+
.nav-btn { display:inline-block; font-size:0.9rem; padding:0.35rem 0.9rem; border:1px solid currentColor; border-radius:999px; text-decoration:none; }
|
|
1456
|
+
.nav-btn:hover { background: rgba(127,127,127,0.12); }
|
|
1453
1457
|
.filters button[aria-pressed="true"] { background:#1a1a1a; color:#fff; border-color:#1a1a1a; }
|
|
1454
1458
|
@media (prefers-color-scheme: dark) { .filters button[aria-pressed="true"] { background:#e8e8e8; color:#111; } }
|
|
1455
1459
|
.fleet-actions { margin-bottom:1.25rem; }
|
|
@@ -1795,6 +1799,9 @@ function renderCockpitHtml(model, operatorEmail = null) {
|
|
|
1795
1799
|
${renderAuthChrome(operatorEmail)}
|
|
1796
1800
|
<h1>Reddoor fleet cockpit</h1>
|
|
1797
1801
|
<div class="meta">${total} site${total === 1 ? "" : "s"} on the Reddoor stack. <a href="/fleet">Fleet table \u2192</a></div>
|
|
1802
|
+
<nav class="top-nav" aria-label="Cockpit pages">
|
|
1803
|
+
<a class="nav-btn" href="/audits">Prospect audits \u2192</a>
|
|
1804
|
+
</nav>
|
|
1798
1805
|
${verdictBar(model, feed)}
|
|
1799
1806
|
${renderNeedsYouFeed(feed)}
|
|
1800
1807
|
${renderFleetBrowsePanel(model)}
|