@reddoorla/maintenance 0.97.0 → 0.98.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/{announce-5ZD2SBGD.js → announce-2FH2KBQQ.js} +4 -4
- package/dist/{bump-deps-775VLDYB.js → bump-deps-54K4ECP3.js} +2 -2
- package/dist/{chunk-YXWBM26P.js → chunk-3YALLSBF.js} +2 -2
- package/dist/{chunk-LAJ3FF54.js → chunk-5UI4FERC.js} +16 -5
- package/dist/chunk-5UI4FERC.js.map +1 -0
- package/dist/{chunk-VMGHQUTV.js → chunk-B2BRFWJY.js} +2 -2
- package/dist/{chunk-IOTOOTII.js → chunk-CP6MVZ5X.js} +16 -1
- package/dist/{chunk-IOTOOTII.js.map → chunk-CP6MVZ5X.js.map} +1 -1
- package/dist/{chunk-L3S3QZXL.js → chunk-DXZRWW2F.js} +64 -9
- package/dist/chunk-DXZRWW2F.js.map +1 -0
- package/dist/chunk-DZZ5KRTB.js +23 -0
- package/dist/chunk-DZZ5KRTB.js.map +1 -0
- package/dist/{chunk-U2ZI6G7H.js → chunk-GRF2RYEO.js} +2 -2
- package/dist/{chunk-QFSL2L2B.js → chunk-L5F3WYIY.js} +81 -31
- package/dist/chunk-L5F3WYIY.js.map +1 -0
- package/dist/{chunk-MMSMZC7S.js → chunk-MNRWHSFS.js} +2 -2
- package/dist/{chunk-PARKXDXI.js → chunk-ORTOMEYR.js} +2 -2
- package/dist/{chunk-FP6HOW6J.js → chunk-S2PLX4XK.js} +2 -2
- package/dist/{chunk-A5QKNOXW.js → chunk-TTT6HYPW.js} +47 -10
- package/dist/chunk-TTT6HYPW.js.map +1 -0
- package/dist/{chunk-BIYQN7FH.js → chunk-VCPTOZ56.js} +31 -1
- package/dist/{chunk-BIYQN7FH.js.map → chunk-VCPTOZ56.js.map} +1 -1
- package/dist/{chunk-LDHRIQUW.js → chunk-ZEE5WQYU.js} +2 -2
- package/dist/cli/bin.js +27 -27
- package/dist/cli/commands/audit.js +10 -10
- package/dist/client-3XGSZMLU.js +11 -0
- package/dist/{convert-to-pnpm-KLWA53N5.js → convert-to-pnpm-U2CRNKNY.js} +2 -2
- package/dist/{db-II5WAK4D.js → db-7XYTBIPH.js} +20 -13
- package/dist/db-7XYTBIPH.js.map +1 -0
- package/dist/{deadletter-G4M3HSVM.js → deadletter-A7RQVNG3.js} +8 -4
- package/dist/{digest-CWKK2DMR.js → digest-AMXRGWZA.js} +20 -20
- package/dist/{digest-collectors-CYTN2EUX.js → digest-collectors-CX2NCGS7.js} +7 -7
- package/dist/{ensure-site-2QZHFVQ3.js → ensure-site-F2WFCWWA.js} +2 -2
- package/dist/{forms-notify-target-X6UEOI2M.js → forms-notify-target-DK5R326N.js} +3 -3
- package/dist/{github-signals-QIRTHHET.js → github-signals-RGRPYTUB.js} +6 -6
- package/dist/{header-image-H7ZJAV26.js → header-image-43ZIEJKP.js} +3 -3
- package/dist/{health-endpoint-BJ6BKRHF.js → health-endpoint-K5LXDPGE.js} +2 -2
- package/dist/{health-mirror-KU3G7V2Y.js → health-mirror-WZUHWCCV.js} +3 -3
- package/dist/index.js +10 -11
- package/dist/index.js.map +1 -1
- package/dist/{init-GOWDA5QS.js → init-OFITGMOP.js} +5 -5
- package/dist/{launch-7N3X2MXB.js → launch-AJY3TGTQ.js} +7 -7
- package/dist/{lighthouse-52WSPZ5D.js → lighthouse-RFMFREEX.js} +2 -2
- package/dist/{match-harness-GBCDSVPS.js → match-harness-CFELJA57.js} +2 -2
- package/dist/migrate-EZL55AWA.js +7 -0
- package/dist/{onboard-JQQIMQW7.js → onboard-F7RTLMDZ.js} +2 -2
- package/dist/{orchestrate-L2A5BZFY.js → orchestrate-SAIFXZIW.js} +5 -5
- package/dist/{pipeline-UUPXOS2J.js → pipeline-TCSNVDQZ.js} +3 -3
- package/dist/{preflight-O7Q6FWHU.js → preflight-6XGVVO2U.js} +7 -7
- package/dist/{prismic-ci-NAPWOXXD.js → prismic-ci-XMX7A5E4.js} +2 -2
- package/dist/{prismic-models-LL6ORTQ2.js → prismic-models-7GKFAAFA.js} +4 -4
- package/dist/prospect/types.d.ts +44 -2
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-FMPGQIRQ.js → prospect-audit-KWP5P5U2.js} +37 -5
- package/dist/prospect-audit-KWP5P5U2.js.map +1 -0
- package/dist/{render-MHUUHPLN.js → render-NWUAX6RN.js} +3 -3
- package/dist/{renovate-dispatch-2IHAZDVA.js → renovate-dispatch-ZZQXT7DA.js} +2 -2
- package/dist/replay-HB44TAII.js +77 -0
- package/dist/replay-HB44TAII.js.map +1 -0
- package/dist/{report-IAHXN5CW.js → report-MVPWS7WP.js} +13 -13
- package/dist/{report-mirror-4QM57LMS.js → report-mirror-O6IDFDWU.js} +3 -3
- package/dist/{roster-GDNSIRAK.js → roster-EJ3ILR52.js} +2 -2
- package/dist/{screenouts-7P5GOZMV.js → screenouts-7NQUWHOJ.js} +14 -1
- package/dist/screenouts-7NQUWHOJ.js.map +1 -0
- package/dist/{self-updating-ZUWPGYUP.js → self-updating-KX5FMGEO.js} +2 -2
- package/dist/{selftest-G4W3LIBQ.js → selftest-KABY2CYJ.js} +5 -5
- package/dist/{site-mirror-WR7ACXOV.js → site-mirror-TKTDWYP3.js} +3 -3
- package/dist/{smoke-suite-WAUPI6EJ.js → smoke-suite-QQMV7TNA.js} +2 -2
- package/dist/{submissions-JDZP35JM.js → submissions-4I66W6QK.js} +11 -1
- package/dist/{submissions-JDZP35JM.js.map → submissions-4I66W6QK.js.map} +1 -1
- package/dist/{submissions-2SHCFOKD.js → submissions-K5UXGZUN.js} +4 -4
- package/dist/{svelte-codemods-VOLOKKFI.js → svelte-codemods-YFN4TEGV.js} +2 -2
- package/dist/{sync-configs-OXSWKVZ2.js → sync-configs-HBXS3D7C.js} +2 -2
- package/dist/{upgrade-EVAL3JTO.js → upgrade-XOJMDIT4.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-A5QKNOXW.js.map +0 -1
- package/dist/chunk-L3S3QZXL.js.map +0 -1
- package/dist/chunk-LAJ3FF54.js.map +0 -1
- package/dist/chunk-QFSL2L2B.js.map +0 -1
- package/dist/client-DGTO65WB.js +0 -11
- package/dist/db-II5WAK4D.js.map +0 -1
- package/dist/migrate-XZ7U5DFC.js +0 -7
- package/dist/prospect-audit-FMPGQIRQ.js.map +0 -1
- package/dist/replay-4FVFOMHH.js +0 -54
- package/dist/replay-4FVFOMHH.js.map +0 -1
- package/dist/screenouts-7P5GOZMV.js.map +0 -1
- /package/dist/{announce-5ZD2SBGD.js.map → announce-2FH2KBQQ.js.map} +0 -0
- /package/dist/{bump-deps-775VLDYB.js.map → bump-deps-54K4ECP3.js.map} +0 -0
- /package/dist/{chunk-YXWBM26P.js.map → chunk-3YALLSBF.js.map} +0 -0
- /package/dist/{chunk-VMGHQUTV.js.map → chunk-B2BRFWJY.js.map} +0 -0
- /package/dist/{chunk-U2ZI6G7H.js.map → chunk-GRF2RYEO.js.map} +0 -0
- /package/dist/{chunk-MMSMZC7S.js.map → chunk-MNRWHSFS.js.map} +0 -0
- /package/dist/{chunk-PARKXDXI.js.map → chunk-ORTOMEYR.js.map} +0 -0
- /package/dist/{chunk-FP6HOW6J.js.map → chunk-S2PLX4XK.js.map} +0 -0
- /package/dist/{chunk-LDHRIQUW.js.map → chunk-ZEE5WQYU.js.map} +0 -0
- /package/dist/{client-DGTO65WB.js.map → client-3XGSZMLU.js.map} +0 -0
- /package/dist/{convert-to-pnpm-KLWA53N5.js.map → convert-to-pnpm-U2CRNKNY.js.map} +0 -0
- /package/dist/{deadletter-G4M3HSVM.js.map → deadletter-A7RQVNG3.js.map} +0 -0
- /package/dist/{digest-CWKK2DMR.js.map → digest-AMXRGWZA.js.map} +0 -0
- /package/dist/{digest-collectors-CYTN2EUX.js.map → digest-collectors-CX2NCGS7.js.map} +0 -0
- /package/dist/{ensure-site-2QZHFVQ3.js.map → ensure-site-F2WFCWWA.js.map} +0 -0
- /package/dist/{forms-notify-target-X6UEOI2M.js.map → forms-notify-target-DK5R326N.js.map} +0 -0
- /package/dist/{github-signals-QIRTHHET.js.map → github-signals-RGRPYTUB.js.map} +0 -0
- /package/dist/{header-image-H7ZJAV26.js.map → header-image-43ZIEJKP.js.map} +0 -0
- /package/dist/{health-endpoint-BJ6BKRHF.js.map → health-endpoint-K5LXDPGE.js.map} +0 -0
- /package/dist/{health-mirror-KU3G7V2Y.js.map → health-mirror-WZUHWCCV.js.map} +0 -0
- /package/dist/{init-GOWDA5QS.js.map → init-OFITGMOP.js.map} +0 -0
- /package/dist/{launch-7N3X2MXB.js.map → launch-AJY3TGTQ.js.map} +0 -0
- /package/dist/{lighthouse-52WSPZ5D.js.map → lighthouse-RFMFREEX.js.map} +0 -0
- /package/dist/{match-harness-GBCDSVPS.js.map → match-harness-CFELJA57.js.map} +0 -0
- /package/dist/{migrate-XZ7U5DFC.js.map → migrate-EZL55AWA.js.map} +0 -0
- /package/dist/{onboard-JQQIMQW7.js.map → onboard-F7RTLMDZ.js.map} +0 -0
- /package/dist/{orchestrate-L2A5BZFY.js.map → orchestrate-SAIFXZIW.js.map} +0 -0
- /package/dist/{pipeline-UUPXOS2J.js.map → pipeline-TCSNVDQZ.js.map} +0 -0
- /package/dist/{preflight-O7Q6FWHU.js.map → preflight-6XGVVO2U.js.map} +0 -0
- /package/dist/{prismic-ci-NAPWOXXD.js.map → prismic-ci-XMX7A5E4.js.map} +0 -0
- /package/dist/{prismic-models-LL6ORTQ2.js.map → prismic-models-7GKFAAFA.js.map} +0 -0
- /package/dist/{render-MHUUHPLN.js.map → render-NWUAX6RN.js.map} +0 -0
- /package/dist/{renovate-dispatch-2IHAZDVA.js.map → renovate-dispatch-ZZQXT7DA.js.map} +0 -0
- /package/dist/{report-IAHXN5CW.js.map → report-MVPWS7WP.js.map} +0 -0
- /package/dist/{report-mirror-4QM57LMS.js.map → report-mirror-O6IDFDWU.js.map} +0 -0
- /package/dist/{roster-GDNSIRAK.js.map → roster-EJ3ILR52.js.map} +0 -0
- /package/dist/{self-updating-ZUWPGYUP.js.map → self-updating-KX5FMGEO.js.map} +0 -0
- /package/dist/{selftest-G4W3LIBQ.js.map → selftest-KABY2CYJ.js.map} +0 -0
- /package/dist/{site-mirror-WR7ACXOV.js.map → site-mirror-TKTDWYP3.js.map} +0 -0
- /package/dist/{smoke-suite-WAUPI6EJ.js.map → smoke-suite-QQMV7TNA.js.map} +0 -0
- /package/dist/{submissions-2SHCFOKD.js.map → submissions-K5UXGZUN.js.map} +0 -0
- /package/dist/{svelte-codemods-VOLOKKFI.js.map → svelte-codemods-YFN4TEGV.js.map} +0 -0
- /package/dist/{sync-configs-OXSWKVZ2.js.map → sync-configs-HBXS3D7C.js.map} +0 -0
- /package/dist/{upgrade-EVAL3JTO.js.map → upgrade-XOJMDIT4.js.map} +0 -0
package/dist/client-DGTO65WB.js
DELETED
package/dist/db-II5WAK4D.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/commands/db.ts"],"sourcesContent":["export type DbCommandOptions = {\n /** Override the libSQL url (tests use \":memory:\"); otherwise read from env. */\n url?: string;\n /** verify-dump: path to the dump file to load into a scratch engine. */\n file?: string;\n /** usage: org slug override; defaults to TURSO_ORG, else discovered. */\n org?: string;\n /** import-airtable / sync: run despite the freeze — a deliberate\n * rollback-window converge from the frozen Airtable shadow. */\n force?: boolean;\n /** replay-deadletters: abandon this slug's queued leads (or one `dl_…` row id)\n * as resolved-by-decision instead of replaying them (#786). */\n abandon?: string;\n /** replay-deadletters --abandon: why. Required — an undocumented write-off of\n * a client's leads is the thing this is meant to stop being necessary. */\n reason?: string;\n /** replay-deadletters --abandon: who decided. Defaults to OPERATOR_EMAIL. */\n by?: string;\n cwd?: string;\n verbose?: boolean;\n};\n\n/** Injected seams — deliberately NOT part of DbCommandOptions, which is the set\n * of things a shell can type (a registration gate asserts exactly that). The\n * platform token lives here rather than on a flag because a secret passed on\n * argv is readable from `ps`. */\nexport type DbCommandDeps = {\n /** Tests pass \"\" to exercise the unconfigured path, so the implementation\n * must use `?? env` and never `|| env`. */\n platformToken?: string;\n fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;\n now?: Date;\n /** restore: auth token for the TARGET database; defaults to\n * TURSO_RESTORE_AUTH_TOKEN. Deliberately does NOT fall back to the ambient\n * TURSO_AUTH_TOKEN — that one belongs to production, and inheriting it would\n * undo the whole point of making --url explicit. */\n restoreAuthToken?: string;\n};\n\n/** #643 (the freeze): the scheduled import retired with the flip, but the\n * MANUAL import survives as the rollback-window converge tool — and run out of\n * habit it would overwrite authoritative Turso rows with the frozen Airtable\n * archive, including any post-flip write whose best-effort shadow was\n * swallowed. So the writing actions refuse under the freeze unless the\n * operator says `--force`. `parity` stays unguarded: it only compares, and\n * \"did the shadow drift?\" is exactly the rollback-window question.\n *\n * Pure and exported so the test injects BOTH switch states; `runDbCommand`\n * passes the shipped constant. Returns the refusal, or null to proceed. */\nexport function freezeGuardsDbWrite(\n action: string,\n force: boolean,\n authoritative: boolean,\n): { output: string; code: number } | null {\n if (!authoritative) return null;\n if (action !== \"import-airtable\" && action !== \"sync\") return null;\n if (force) return null;\n return {\n output:\n `db ${action} refused: TURSO_IS_AUTHORITATIVE is on (the freeze, 2026-08-31). ` +\n `An import now OVERWRITES authoritative Turso rows with the frozen Airtable ` +\n `archive. Pass --force only for a deliberate rollback-window converge.`,\n code: 1,\n };\n}\n\n/** `db <action>` — migrate | replay-deadletters | import-airtable | parity | sync | dump | verify-dump. The db layer is imported\n * dynamically so a non-db CLI invocation (and `--help`) never loads\n * @libsql/client. Config is resolved inside each branch so an unknown action\n * returns without needing any Turso env. */\nexport async function runDbCommand(\n action: string,\n opts: DbCommandOptions,\n deps: DbCommandDeps = {},\n): Promise<{ output: string; code: number }> {\n if (action === \"migrate\") {\n const { readDbConfig } = await import(\"../../db/client.js\");\n const cfg = opts.url ? { url: opts.url } : readDbConfig();\n const { runMigrations } = await import(\"../../db/migrate.js\");\n const { createClient } = await import(\"@libsql/client\");\n const client = createClient(cfg.url === \":memory:\" ? { url: \":memory:\" } : cfg);\n const ran = await runMigrations(client);\n return {\n output: ran.length ? `Applied migrations: ${ran.join(\", \")}` : \"Already up to date.\",\n code: 0,\n };\n }\n\n // Re-run every lead that dead-lettered during a site-lookup outage (#539\n // Phase 0) through the normal ingest pipeline. Exit 1 while any row is STILL\n // failing — the store hasn't recovered and the operator should not read the\n // run as \"all leads landed\". Zero rows is a clean 0: nothing owed.\n if (action === \"replay-deadletters\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n // #786. `--abandon` is the escape hatch for the queue #785 deliberately\n // stopped draining: a slug that is genuinely dead but still deployed keeps\n // dead-lettering leads, holds this command at exit 1, and leaves a standing\n // CRITICAL cockpit item. Validated BEFORE opening any store, so a missing\n // reason refuses without needing Turso creds.\n if (opts.abandon !== undefined) {\n const reason = (opts.reason ?? \"\").trim();\n if (reason === \"\") {\n return {\n output:\n 'db replay-deadletters --abandon refused: pass --reason \"…\". Abandoning writes ' +\n \"off a client's captured leads, and the record of WHY has to outlive the decision.\",\n code: 1,\n };\n }\n const by = opts.by ?? process.env.OPERATOR_EMAIL ?? \"operator\";\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { abandonDeadLetters } = await import(\"../../db/deadletter.js\");\n // Row ids are minted `dl_…` (newDeadLetterId), and a site slug never is —\n // so the one argument can name either without a second flag.\n const target = opts.abandon.startsWith(\"dl_\") ? { id: opts.abandon } : { slug: opts.abandon };\n const ids = await abandonDeadLetters(db, { ...target, by, reason, now: new Date() });\n const lines = [\n ...ids.map((id) => `abandoned ${id}`),\n `DEADLETTER_ABANDONED target=${opts.abandon} rows=${ids.length} by=${by}`,\n ];\n return { output: lines.join(\"\\n\"), code: 0 };\n }\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { getWebsiteBySlug } = await import(\"../../reports/airtable/websites.js\");\n const { getSiteBySlug } = await import(\"../../db/fleet-state.js\");\n const { makeLazySiteLookup } = await import(\"../../forms/site-lookup.js\");\n // #645. Recovery now resolves sites through the SAME lookup the live ingest\n // path uses. It used to call the Airtable `getWebsiteBySlug` directly, so\n // post-#643 the two disagreed about what the fleet is: a site created since\n // the freeze was invisible to the replay, and a row only Airtable still held\n // would have attached a recovered lead to a site the system no longer\n // believes in. `openBase` is passed UNCALLED — under the freeze no Airtable\n // credential is read at all, where before a missing PAT refused the whole\n // replay (`readAirtableConfig()` throws) with real leads sitting in the queue.\n const lookupSite = makeLazySiteLookup({\n fromDb: (s) => getSiteBySlug(db, s),\n openAirtable: () => openBase(readAirtableConfig()),\n fromAirtable: (base, s) => getWebsiteBySlug(base, s),\n });\n const {\n createSubmission,\n stampNotified,\n stampFanout,\n findRecentDuplicateSubmissions,\n listRecentSubmissionsForEmail,\n markSubmissionsSpamRetro,\n } = await import(\"../../db/submissions.js\");\n const { makeNotify } = await import(\"../../forms/notify.js\");\n const { classifySpam } = await import(\"../../forms/spam-classifier.js\");\n const { forwardNewsletterToWebhook } = await import(\"../../forms/webhook.js\");\n const { addMailchimpMember, mailchimpTagsFor } = await import(\"../../forms/mailchimp.js\");\n const { defaultResendClient } = await import(\"../../reports/send/resend.js\");\n const { replayDeadLetters } = await import(\"../../forms/replay.js\");\n\n // Same degradation as the ingest handler: an unconfigured Resend key means\n // replayed leads land un-emailed (notify=failed) rather than blocking replay.\n let send = null;\n try {\n send = defaultResendClient().send;\n } catch (err) {\n console.error(`[db] Resend unconfigured; replaying without email: ${String(err)}`);\n }\n\n // Mirrors the production handler's deps minus `deadLetter` (replayDeadLetters\n // forbids and strips it — a throwing lookup must retry, not duplicate) and\n // minus `defer` (a CLI has no post-response phase; the inline tail is fine).\n const result = await replayDeadLetters(db, {\n getWebsiteBySlug: lookupSite,\n createSubmission: (input) => createSubmission(db, input),\n notify: makeNotify(send),\n stampNotified: (id, status, messageId) => stampNotified(db, id, status, messageId),\n now: () => new Date(),\n classifySpam: (n, outcome) =>\n classifySpam({\n name: n.name,\n email: n.email,\n ...(n.message !== undefined ? { message: n.message } : {}),\n formType: n.formType,\n extraFields: n.extraFields,\n turnstile: outcome,\n }),\n findRecentDuplicates: (message, since) =>\n findRecentDuplicateSubmissions(db, message, since.toISOString()),\n listRecentSubmissionsForEmail: (email, since) =>\n listRecentSubmissionsForEmail(db, email, since.toISOString()),\n retroBucket: (ids, reason) => markSubmissionsSpamRetro(db, ids, reason),\n forwardNewsletter: (url, submission, site) =>\n forwardNewsletterToWebhook(url, submission, site),\n addToMailchimp: (site, submission) =>\n addMailchimpMember({\n apiKey: site.mailchimpApiKey ?? \"\",\n audienceId: site.mailchimpAudienceId ?? \"\",\n email: submission.email,\n name: submission.name,\n tags: mailchimpTagsFor(submission.formType),\n }),\n stampFanout: (id, fanoutStatus) => stampFanout(db, id, fanoutStatus),\n });\n\n const lines = [\n ...result.replayed.map(\n (r) => `replayed ${r.id} → ${r.outcome}${r.submissionId ? ` (${r.submissionId})` : \"\"}`,\n ),\n ...result.stillFailing.map((r) => `still failing ${r.id}: ${r.error}`),\n `DEADLETTER_REPLAY replayed=${result.replayed.length} still_failing=${result.stillFailing.length}`,\n ];\n return { output: lines.join(\"\\n\"), code: result.stillFailing.length > 0 ? 1 : 0 };\n }\n\n // Phase 1.3/1.4 of #539. Both read the same two Airtable tables raw (id +\n // fields, no mapRow coercion — the importer's mapping is the authority) and\n // share that mapping, so parity is definitionally checked against what the\n // importer writes.\n if (action === \"import-airtable\" || action === \"parity\" || action === \"sync\") {\n const { TURSO_IS_AUTHORITATIVE } = await import(\"../../db/freeze.js\");\n const refused = freezeGuardsDbWrite(action, opts.force === true, TURSO_IS_AUTHORITATIVE);\n if (refused) return refused;\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const listRaw = async (table: string) =>\n (await base(table).select().all()).map((r) => ({\n id: r.id,\n fields: r.fields as Record<string, unknown>,\n }));\n const io = {\n listWebsiteRecords: () => listRaw(\"Websites\"),\n listReportRecords: () => listRaw(\"Reports\"),\n now: () => new Date(),\n };\n\n if (action === \"import-airtable\") {\n const { importFleetState, formatReapSummary } = await import(\"../../db/import-airtable.js\");\n const summary = await importFleetState(db, {\n ...io,\n // Attachment bodies ride expiring signed URLs; a failed fetch imports the\n // row with rendered_html null and is NAMED in the summary, never silent.\n fetchAttachment: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? await res.text() : null;\n } catch {\n return null;\n }\n },\n });\n const lines = [\n `imported ${summary.sites} site(s) → sites/site_health/site_schedule`,\n `imported ${summary.reports} report(s)`,\n ];\n if (summary.renderedHtmlMisses.length > 0) {\n lines.push(\n `⚠ ${summary.renderedHtmlMisses.length} report(s) imported WITHOUT Rendered HTML ` +\n `(fetch failed / URL expired): ${summary.renderedHtmlMisses.join(\", \")}`,\n );\n }\n // The import deletes rows Airtable no longer has, so this one-shot path\n // reports the reap exactly as `db sync` does — same formatter, no second\n // copy to fall out of step.\n lines.push(...formatReapSummary(summary.reaped));\n return { output: lines.join(\"\\n\"), code: 0 };\n }\n\n // Phase 2 backbone (#539): one hourly pass = import (attachment fetches\n // only where the stored row lacks a body) + parity + one retry to absorb\n // the import-read/parity-read race. Exit 1 on persistent mismatch.\n if (action === \"sync\") {\n const { syncFleetState, formatSyncResult } = await import(\"../../db/sync.js\");\n const result = await syncFleetState(db, {\n ...io,\n fetchAttachment: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? await res.text() : null;\n } catch {\n return null;\n }\n },\n });\n return {\n output: formatSyncResult(result),\n code: result.parity.mismatches.length > 0 ? 1 : 0,\n };\n }\n\n const { checkFleetParity, formatParityResult } = await import(\"../../db/parity.js\");\n const result = await checkFleetParity(db, io);\n return { output: formatParityResult(result), code: result.mismatches.length > 0 ? 1 : 0 };\n }\n\n // One-shot completion of design D5 (#539 Phase 2): copy every site's CURRENT\n // Airtable \"Header image\" attachment into sites.header_image*. Idempotent —\n // an already-populated BLOB is never overwritten (a re-run must not clobber\n // a freshly generated image with a stale Airtable copy). Exit 1 when any\n // fetch failed, so a partial backfill is never read as complete.\n if (action === \"backfill-header-images\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const { backfillHeaderImages, formatBackfillResult } =\n await import(\"../../db/header-images.js\");\n const result = await backfillHeaderImages(db, {\n listWebsiteRecords: async () =>\n (await base(\"Websites\").select().all()).map((r) => ({\n id: r.id,\n fields: r.fields as Record<string, unknown>,\n })),\n fetchBytes: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? new Uint8Array(await res.arrayBuffer()) : null;\n } catch {\n return null;\n }\n },\n });\n return { output: formatBackfillResult(result), code: result.failed.length > 0 ? 1 : 0 };\n }\n\n // #609: one-shot copy of the single Airtable \"Digest State\" row into Turso,\n // so the first digest run after the read repoint sees yesterday's snapshot\n // instead of an empty one. An empty read is not a crash — it badges EVERY\n // item NEW, which lands in the operator's inbox reading as \"the whole fleet\n // degraded overnight\". REFUSES to overwrite a snapshot Turso already holds:\n // a re-run must never replace a fresher snapshot with a stale Airtable copy.\n if (action === \"backfill-digest-state\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { readDigestState: readTurso, writeDigestState: writeTurso } =\n await import(\"../../db/digest-state.js\");\n const existing = await readTurso(db);\n if (Object.keys(existing).length > 0) {\n return {\n output: `DIGEST_BACKFILL skipped=1 reason=turso-already-populated keys=${Object.keys(existing).length}`,\n code: 0,\n };\n }\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const { readDigestState: readAirtable, DIGEST_STATE_TABLE } =\n await import(\"../../alerts/digest-state.js\");\n // `source` separates \"Airtable has no row\" from \"Airtable has a row holding\n // an empty snapshot\" — the reader collapses BOTH to {}, so copied=0 alone\n // cannot tell a quiet fleet from a failed read. Learned by running this: the\n // first real run printed copied=0 and only a hand probe showed the row was\n // there and genuinely empty.\n const rows = await base(DIGEST_STATE_TABLE).select({ maxRecords: 1, pageSize: 1 }).all();\n const snap = await readAirtable(base);\n const keys = Object.keys(snap).length;\n await writeTurso(db, snap);\n // Read it BACK. A returning write is not evidence the row landed — the same\n // rule forms-notify-target learned on 2026-08-03. `stored` counts the ROW,\n // not its keys, so an empty-but-present snapshot verifies as written.\n const stored = (await db.selectFrom(\"digest_state\").selectAll().execute()).length;\n const after = Object.keys(await readTurso(db)).length;\n return {\n output:\n `DIGEST_BACKFILL source=${rows.length > 0 ? \"row\" : \"absent\"} ` +\n `copied=${keys} verified=${after} rows=${stored}`,\n code: after === keys && stored === 1 ? 0 : 1,\n };\n }\n\n // Phase 1.5 of #539: platform-auth-free SQL dump to stdout-adjacent output.\n // The nightly backup workflow redirects this to a file, encrypts, uploads;\n // the rehearsed restore loads it into stock sqlite3 and compares row counts.\n if (action === \"dump\") {\n const { readDbConfig } = await import(\"../../db/client.js\");\n const cfg = opts.url ? { url: opts.url } : readDbConfig();\n const { createClient } = await import(\"@libsql/client\");\n const client = createClient(cfg.url === \":memory:\" ? { url: \":memory:\" } : cfg);\n const { dumpDatabase } = await import(\"../../db/dump.js\");\n const sql = await dumpDatabase(\n {\n execute: async (q) => {\n const r = await client.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n },\n new Date().toISOString(),\n );\n return { output: sql, code: 0 };\n }\n\n // Load a dump back into a REAL libSQL target (#612 review). The nightly\n // rehearsal loads into `:memory:`, which proves the SQL parses — it does not\n // prove you can get the data back into Turso, and that is the operation an\n // actual recovery needs. Replaying ~17 MB of SQL with megabytes of inline hex\n // over HTTP is materially different from an in-process load, and it had never\n // been done. Refuses to touch a database that already holds rows: a restore\n // is for an EMPTY target, and pointing this at production by mistake should\n // cost nothing.\n if (action === \"restore\") {\n const file = opts.file;\n if (!file) return { output: \"restore: pass the dump path via --file\", code: 1 };\n if (!opts.url) {\n return {\n output:\n \"restore: pass the TARGET database via --url (never defaults, to keep production out of reach)\",\n code: 1,\n };\n }\n // Classify the target BEFORE reading the dump, so a missing token names\n // itself instead of arriving as an opaque 401 (or, worse, as an ENOENT that\n // sends you hunting for the dump file). This command built its client from\n // a url alone until 2026-08-26, which worked against every target the tests\n // and rehearsals used — `:memory:` and a local `turso dev` — and failed\n // against every target an actual recovery has.\n const { parseDumpManifest, requiresAuthToken } = await import(\"../../db/dump.js\");\n const authToken = deps.restoreAuthToken ?? process.env.TURSO_RESTORE_AUTH_TOKEN ?? \"\";\n if (requiresAuthToken(opts.url) && !authToken) {\n return { output: \"RESTORE refused=auth-token-absent\", code: 1 };\n }\n const { readFile } = await import(\"node:fs/promises\");\n const sql = await readFile(file, \"utf-8\");\n const manifest = parseDumpManifest(sql);\n if (!manifest) return { output: \"RESTORE refused=manifest-absent\", code: 1 };\n const { createClient } = await import(\"@libsql/client\");\n const target = createClient(authToken ? { url: opts.url, authToken } : { url: opts.url });\n const existing = await target.execute(\n \"SELECT COUNT(*) AS n FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\",\n );\n if (Number(existing.rows[0]?.n ?? 0) > 0) {\n return { output: \"RESTORE refused=target-not-empty\", code: 1 };\n }\n await target.executeMultiple(sql);\n const { tableCounts, headerImageBytes } = await import(\"../../db/dump.js\");\n const exec = {\n execute: async (q: string) => {\n const r = await target.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n };\n const counts = await tableCounts(exec);\n const bytes = await headerImageBytes(exec);\n // Same origin-anchored comparison as verify-dump: a restore that \"succeeded\"\n // with fewer rows than the origin held is not a restore.\n const bad: string[] = [];\n for (const [t, want] of Object.entries(manifest.tables)) {\n if ((counts[t] ?? 0) !== want) bad.push(`${t}: origin=${want} restored=${counts[t] ?? 0}`);\n }\n if (bytes !== manifest.blobBytes)\n bad.push(`header_image bytes: origin=${manifest.blobBytes} restored=${bytes}`);\n const rows = Object.values(counts).reduce((a, b) => a + b, 0);\n return {\n output: [\n ...bad.map((b) => `✗ ${b}`),\n `RESTORE loaded=true tables=${Object.keys(counts).length} rows=${rows} blob_bytes=${bytes} mismatches=${bad.length}`,\n ].join(\"\\n\"),\n code: bad.length > 0 ? 1 : 0,\n };\n }\n\n // How much of the plan's quota the fleet has burned this billing cycle\n // (#539 HIGH-10). The starter plan carries `overages: false`, so crossing a\n // quota BLOCKS reads and writes rather than billing for them — and once the\n // Airtable cutover lands, Turso is the only store there is. This is the one\n // alarm that fires before a wall rather than after it.\n //\n // Needs a PLATFORM token, which is a different credential from the\n // database-level TURSO_AUTH_TOKEN the rest of the fleet runs on: the database\n // token cannot read quota state at all.\n if (action === \"usage\") {\n const token = deps.platformToken ?? process.env.TURSO_FLEET_USAGE ?? \"\";\n // An unconfigured alarm must not read as a quiet, healthy one. The tell for\n // \"never ran\" has to be a failure, not a missing line (#585).\n if (!token) {\n return {\n output:\n \"FLEET_DB_USAGE verdict=no-token — set TURSO_FLEET_USAGE (turso auth api-tokens mint …). \" +\n \"This is the PLATFORM token, not the database TURSO_AUTH_TOKEN.\",\n code: 1,\n };\n }\n const { collectUsage, assessUsage } = await import(\"../../db/usage.js\");\n const input = await collectUsage({\n token,\n org: opts.org ?? process.env.TURSO_ORG,\n fetchImpl: deps.fetchImpl,\n now: deps.now ?? new Date(),\n });\n const r = assessUsage(input);\n const window = `${input.cycleStart.toISOString().slice(0, 10)} → ${input.cycleEnd\n .toISOString()\n .slice(0, 10)}`;\n return {\n output: [`Turso plan=${input.plan} billing cycle ${window}`, ...r.lines, \"\", r.marker].join(\n \"\\n\",\n ),\n code: r.code,\n };\n }\n\n // The restore rehearsal (Phase 1.5's hard gate), runnable every night: load\n // the dump into a FRESH in-memory engine and compare what came back against\n // the ORIGIN MANIFEST the dump carries.\n //\n // It used to compare against INSERT counts parsed out of the dump text — i.e.\n // the dump against itself. Both sides derived from one artifact, so a dump\n // that collected 5 of 44 sites shrank both numbers together and verified\n // clean. The manifest is read from the live database before any row is\n // serialised, which is the only measurement that can notice a short dump.\n //\n // A dump that cannot restore is not a backup — and per the repo's instrument\n // rule the check emits its machine line on every run, clean included.\n if (action === \"verify-dump\") {\n const file = opts.file;\n if (!file) return { output: \"verify-dump: pass the dump path via --file\", code: 1 };\n const { readFile } = await import(\"node:fs/promises\");\n const sql = await readFile(file, \"utf-8\");\n const { createClient } = await import(\"@libsql/client\");\n const scratch = createClient({ url: \":memory:\" });\n try {\n await scratch.executeMultiple(sql);\n } catch (err) {\n return { output: `DUMP_VERIFY loaded=false error=${String(err)}`, code: 1 };\n }\n const { tableCounts, headerImageBytes, parseDumpManifest } = await import(\"../../db/dump.js\");\n const manifest = parseDumpManifest(sql);\n if (!manifest) {\n // Refuse rather than fall back to self-comparison. Falling back would\n // re-enable exactly the blind spot the manifest exists to close, and it\n // would do so silently on the one artifact nobody was watching.\n return {\n output: \"DUMP_VERIFY loaded=true manifest=absent — dump predates the origin manifest\",\n code: 1,\n };\n }\n const exec = {\n execute: async (q: string) => {\n const r = await scratch.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n };\n const restored = await tableCounts(exec);\n const restoredBlobBytes = await headerImageBytes(exec);\n const mismatches: string[] = [];\n for (const [table, want] of Object.entries(manifest.tables)) {\n if ((restored[table] ?? 0) !== want) {\n mismatches.push(`${table}: origin=${want} restored=${restored[table] ?? 0}`);\n }\n }\n // A table the origin held and the dump never mentioned would otherwise be\n // invisible: absent from `restored` AND absent from the loop above.\n for (const table of Object.keys(restored)) {\n if (!(table in manifest.tables)) mismatches.push(`${table}: not in origin manifest`);\n }\n // Coverage: every table the APP owns must be present. `tables=N` used to be\n // printed and never asserted, so a table a migration failed to create — or\n // that the dump lost — rode green forever. `digest_state` and\n // `prospect_audits` were both absent from every artifact the night this was\n // found, purely because they postdated the last run.\n const { DATABASE_TABLES } = await import(\"../../db/schema.js\");\n for (const table of DATABASE_TABLES) {\n if (!(table in restored)) mismatches.push(`${table}: MISSING from the backup entirely`);\n }\n if (restoredBlobBytes !== manifest.blobBytes) {\n mismatches.push(\n `header_image bytes: origin=${manifest.blobBytes} restored=${restoredBlobBytes}`,\n );\n }\n const total = Object.values(restored).reduce((a, b) => a + b, 0);\n const lines = [\n ...mismatches.map((m) => `✗ ${m}`),\n `DUMP_VERIFY loaded=true tables=${Object.keys(restored).length} rows=${total} blob_bytes=${restoredBlobBytes} mismatches=${mismatches.length}`,\n ];\n return { output: lines.join(\"\\n\"), code: mismatches.length > 0 ? 1 : 0 };\n }\n\n return {\n output: `unknown db action '${action}'. Use: migrate, replay-deadletters, import-airtable, parity, sync, backfill-header-images, backfill-digest-state, dump, verify-dump, restore.`,\n code: 1,\n };\n}\n"],"mappings":"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","names":["db","lines","result"]}
|
package/dist/migrate-XZ7U5DFC.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/commands/prospect-audit.ts","../src/prospect/report-url.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { hostnameOf, isHttpUrl, isPrivateOrLoopbackHost } from \"../../util/url.js\";\nimport { reportUrl, reportPrintUrl } from \"../../prospect/report-url.js\";\nimport type { ProspectAuditStatus } from \"../../db/prospect-audits.js\";\nimport type { PipelineDeps, StageName } from \"../../prospect/pipeline.js\";\nimport type { ProspectAuditResult } from \"../../prospect/types.js\";\nimport type { SendAuditEmailResult } from \"../../prospect/email.js\";\nimport type { SiteGoal } from \"../../prospect/goals.js\";\n\n/** The operator-selectable goals, in the order the dispatch dropdown lists\n * them. `unknown` is deliberately absent: it is a finding the audit can reach\n * on its own, never something an operator would choose. */\nexport const GOALS = [\n \"book\",\n \"enquire\",\n \"call\",\n \"visit\",\n \"buy\",\n \"demo\",\n \"partner\",\n] as const satisfies readonly SiteGoal[];\n\nfunction isSiteGoal(value: string): value is SiteGoal {\n return (GOALS as readonly string[]).includes(value);\n}\n\nexport type ProspectAuditCliOptions = {\n business?: string;\n /**\n * The one action the site should produce from a visitor, chosen by the\n * operator instead of inferred from the site.\n *\n * Worth overriding because the operator knows things the site does not say.\n * A practice that wants online bookings but has never built one reads as\n * \"call\" from its own pages, and grading it as \"call\" would score it well for\n * doing the thing they are trying to stop doing.\n */\n goal?: string;\n /** Comma-separated competitor domains. */\n competitors?: string;\n /** cac sets this false for `--no-probes`. */\n probes?: boolean;\n out?: string;\n json?: boolean;\n /**\n * Email the internal sheet (scores, what wasn't measured and why, top\n * fixes, and the shareable link) to `PROSPECT_AUDIT_RECIPIENTS`, once the\n * report is rendered and the `--out`/Turso delivery attempts finish. A\n * missing/empty recipients var, or a Resend failure, is a warning — never a\n * crash — because by the time this runs the audit is already delivered (or\n * recovered) by the code above it.\n */\n email?: boolean;\n /**\n * #676. Search terms and buyer questions chosen by hand, comma-separated.\n *\n * Blank generates them, exactly as before — this is an override for a client\n * we already know, and for a comparison over time, where the terms have to\n * stay fixed between runs.\n */\n terms?: string;\n questions?: string;\n /** Test seam: injected pipeline deps. Never set from the CLI. */\n deps?: PipelineDeps;\n};\n\ntype RecoveryWrite = { htmlPath: string; jsonPath: string };\n\nfunction fail(message: string): { output: string; code: number } {\n return { output: message, code: 2 };\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction scoreLine(label: string, value: number | null): string {\n return `${label.padEnd(14)} ${value === null ? \"not measured\" : String(value).padStart(3)}`;\n}\n\n/** Item 3: the `status` column exists to answer, without deserializing the\n * whole result_json, whether this was a clean run or a degraded one. crawl\n * is excluded — it's the one fatal stage (see pipeline.ts), so by the time a\n * ProspectAuditResult exists it was always ok. Every other stage's\n * StageResult already collapses \"failed\" and \"deliberately skipped\"\n * (--no-probes, the checks→analyze cascade) into the same `ok: false` — both\n * mean the report has a \"not measured\" section, so both count as partial\n * here too. */\nfunction auditStatus(result: ProspectAuditResult): ProspectAuditStatus {\n const allStagesOk =\n result.checks.ok && result.lighthouse.ok && result.analyze.ok && result.probes.ok;\n return allStagesOk ? \"complete\" : \"partial\";\n}\n\n/** Filesystem-safe, never empty. Collapses anything outside [a-z0-9.-] to a\n * single dash so a hostname becomes a sane filename fragment. */\nfunction slugify(s: string): string {\n const slug = s\n .toLowerCase()\n .replace(/[^a-z0-9.-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug || \"prospect\";\n}\n\n/**\n * Last-resort write when NEITHER `--out` nor the database persist made it —\n * the one situation a paid, already-run audit must never end in silently. Lands\n * in `reports/`, mirroring header-image.ts's own convention for CLI-written\n * artifacts (mkdir -p, resolved against cwd), rather than a bare file dropped\n * into whatever directory the operator happened to be standing in when they\n * ran the command — often unrelated to this repo at all in fleet-tool usage.\n */\nasync function writeRecoveryFiles(\n result: ProspectAuditResult,\n html: string,\n): Promise<RecoveryWrite> {\n const dir = resolve(\"reports\");\n await mkdir(dir, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const base = `prospect-audit-${slugify(hostnameOf(result.url))}-${stamp}`;\n const htmlPath = resolve(dir, `${base}.html`);\n const jsonPath = resolve(dir, `${base}.json`);\n await writeFile(htmlPath, html, \"utf-8\");\n await writeFile(jsonPath, JSON.stringify(result, null, 2), \"utf-8\");\n return { htmlPath, jsonPath };\n}\n\nfunction summarize(\n result: ProspectAuditResult,\n link: string | null,\n file: string | null,\n warnings: string[],\n recovery: RecoveryWrite | null,\n email: SendAuditEmailResult | null,\n): string {\n const lines = [\n `Prospect audit — ${result.businessName ?? result.url}`,\n \"\",\n scoreLine(\"Findability\", result.scores.findability),\n scoreLine(\"Readability\", result.scores.readability),\n scoreLine(\"Answers\", result.scores.answers),\n // No AI Visibility line: the score was removed from every presentation\n // surface (web, print, email, here) — the measurement lives on as the\n // report's receipts section, not as a number that reads as ours to move.\n ];\n for (const [name, stage] of [\n [\"checks\", result.checks],\n [\"lighthouse\", result.lighthouse],\n [\"analyze\", result.analyze],\n [\"probes\", result.probes],\n ] as const) {\n if (!stage.ok) lines.push(` ! ${name} not measured — ${stage.error}`);\n }\n if (file) lines.push(\"\", `Report written to ${file}`);\n if (link) lines.push(\"\", `Shareable link: ${link}`);\n if (email?.sent) {\n lines.push(\"\", `Emailed to ${email.recipients.join(\", \")} (${email.messageId})`);\n }\n for (const w of warnings) lines.push(\"\", `! ${w}`);\n if (recovery) {\n lines.push(\n \"\",\n \"Neither --out nor the database took this report — wrote a recovery copy instead:\",\n ` ${recovery.htmlPath}`,\n ` ${recovery.jsonPath}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Run one prospect audit end to end. Progress goes to stderr so `--json` stdout\n * stays pipeable. Persistence needs Turso; without it `--out` is mandatory,\n * because an audit nobody can read afterwards is just a bill.\n *\n * By the time `runProspectAudit` resolves, the money is already spent — only\n * the crawl is fatal, every other stage degrades rather than throwing. So the\n * `--out` write and the database persist below are each wrapped in their own\n * try/catch and attempted independently: a bad `--out` path must not cost a\n * working Turso persist (or vice versa), and if BOTH fail — or persist fails\n * with no `--out` given at all — a recovery copy goes to `reports/` rather\n * than the run ending with nothing anywhere. `--json` only changes the SHAPE\n * of stdout; it still runs the same `--out`/persist/recovery logic beneath it.\n */\nexport async function runProspectAuditCommand(\n url: string,\n opts: ProspectAuditCliOptions,\n): Promise<{ output: string; code: number }> {\n if (!isHttpUrl(url)) {\n return fail(`\"${url}\" is not a URL. Pass the full address, e.g. https://example.com`);\n }\n // #612 review: this validated only the SHAPE. The sole private-address guard\n // was in `triggerProspectAudit` — one layer at the far end of the chain — so\n // anyone running the CLI directly, or any future second caller, had none.\n // `crawlSite` now refuses too; this keeps the refusal a clean message instead\n // of a thrown crawl error.\n if (isPrivateOrLoopbackHost(new URL(url).hostname)) {\n return fail(`\"${url}\" is a private or loopback address — refusing to audit it.`);\n }\n const canPersist = Boolean(process.env.TURSO_DATABASE_URL);\n if (!canPersist && !opts.out) {\n return fail(\n \"No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials.\",\n );\n }\n\n const { runProspectAudit } = await import(\"../../prospect/pipeline.js\");\n const onStage = (name: StageName, status: \"start\" | \"ok\" | \"fail\", detail?: string): void => {\n if (status === \"start\") console.error(`… ${name}`);\n else if (status === \"ok\") console.error(`✓ ${name}`);\n else console.error(`! ${name} — ${detail ?? \"failed\"}`);\n };\n\n const business = opts.business?.trim();\n const goal = opts.goal?.trim();\n // #676. Comma-separated on a flag (a newline is not typeable in one); the\n // cockpit's textareas split on newlines instead. Both drop blanks, and both\n // treat \"nothing usable\" as \"generate\".\n const splitList = (v: string | undefined): string[] =>\n (v ?? \"\")\n .split(\",\")\n .map((x) => x.trim())\n .filter((x) => x !== \"\");\n const chosenTerms = splitList(opts.terms);\n const chosenQuestions = splitList(opts.questions);\n if (goal !== undefined && goal !== \"\" && !isSiteGoal(goal)) {\n return fail(`--goal must be one of: ${GOALS.join(\", \")}`);\n }\n\n const result = await runProspectAudit(\n url,\n {\n ...(business ? { business } : {}),\n // An empty string is what a blank dispatch field sends; it must read as\n // \"not supplied\" so the model's own inference still runs, rather than\n // overriding it with nothing.\n ...(goal ? { goal } : {}),\n ...(opts.competitors\n ? {\n competitors: opts.competitors\n .split(\",\")\n .map((c) => c.trim())\n .filter(Boolean),\n }\n : {}),\n ...(opts.probes === false ? { probes: false } : {}),\n ...(chosenTerms.length > 0 ? { terms: chosenTerms } : {}),\n ...(chosenQuestions.length > 0 ? { questions: chosenQuestions } : {}),\n },\n { ...(opts.deps ?? {}), onStage },\n );\n\n const { renderProspectReport } = await import(\"../../prospect/render.js\");\n const html = renderProspectReport(result);\n\n const warnings: string[] = [];\n\n let file: string | null = null;\n if (opts.out) {\n try {\n await writeFile(opts.out, html, \"utf-8\");\n file = opts.out;\n } catch (err) {\n warnings.push(`Could not write --out (${opts.out}): ${errorMessage(err)}`);\n }\n }\n\n let link: string | null = null;\n let token: string | null = null;\n let auditId: string | null = null;\n if (canPersist) {\n try {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { createProspectAudit } = await import(\"../../db/prospect-audits.js\");\n const db = await openDb(readDbConfig());\n const created = await createProspectAudit(db, {\n url: result.url,\n // Map at the boundary: ProspectAuditResult.businessName is the field\n // name (Item 2 — it's a resolved NAME, not a description); the\n // `prospect_audits.business` column keeps its existing name, since\n // renaming a column needs its own migration for no benefit here.\n business: result.businessName,\n status: auditStatus(result),\n resultJson: JSON.stringify(result),\n // #676: on the ROW, not only in result_json — a re-run reuses them and\n // the /audits listing marks them without reading the blob.\n chosenTerms: chosenTerms.length > 0 ? chosenTerms : null,\n chosenQuestions: chosenQuestions.length > 0 ? chosenQuestions : null,\n });\n auditId = created.id;\n token = created.token;\n link = reportUrl(token);\n } catch (err) {\n warnings.push(`Could not save to the database: ${errorMessage(err)}`);\n }\n }\n\n const delivered = file !== null || link !== null;\n const recovery = delivered ? null : await writeRecoveryFiles(result, html);\n const code = delivered ? 0 : 1;\n\n // The audit is already delivered (or recovered) above — an email failure\n // must never take that away. Same independent try/catch shape as the\n // --out write and the Turso persist: record a warning, keep going, never\n // touch `code`. Dynamically imported for the same reason db/client.js and\n // prospect/render.js are above: it reaches `resend` (a devDependency) and\n // must stay out of bin.js's static import graph (see bin.ts's\n // central-dep-blocker comment).\n // The PDF leave-behind, printed from the website's print route — a document\n // designed for paper, not the interactive report with its evidence folded\n // away behind disclosures.\n //\n // Best-effort by design. Every other stage in this pipeline degrades rather\n // than throwing, and an attachment is not worth losing a delivered report\n // over: if this fails the email still goes, with the link, and a warning says\n // the PDF is missing. It also needs a token — with no persisted report there\n // is no page to print.\n let pdf: Buffer | null = null;\n if (opts.email && token) {\n try {\n const { renderReportPdf } = await import(\"../../prospect/pdf.js\");\n pdf = await renderReportPdf(reportPrintUrl(token));\n } catch (err) {\n warnings.push(`Could not attach the PDF: ${errorMessage(err)}`);\n }\n }\n\n let email: SendAuditEmailResult | null = null;\n if (opts.email) {\n try {\n const { sendAuditEmail, parseProspectAuditRecipients } =\n await import(\"../../prospect/email.js\");\n const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);\n email = await sendAuditEmail(result, { link, recipients, auditId, pdf });\n if (!email.sent) {\n warnings.push(`Could not email the audit sheet: ${email.reason}`);\n }\n } catch (err) {\n warnings.push(`Could not email the audit sheet: ${errorMessage(err)}`);\n }\n }\n\n if (opts.json) {\n return {\n output: JSON.stringify(\n { result, out: file, link, token, recovery, warnings, email },\n null,\n 2,\n ),\n code,\n };\n }\n\n return { output: summarize(result, link, file, warnings, recovery, email), code };\n}\n","/** Where the prospect-facing report lives now: a real, branded route on the\n * marketing site rather than generated HTML on the ops app's domain. */\nexport const DEFAULT_REPORT_BASE_URL = \"https://reddoorla.com\";\n\n/**\n * Resolve the public report origin.\n *\n * Deliberately its own variable rather than reusing `DASHBOARD_BASE_URL`. That\n * one addresses operators and points at the ops app; this one addresses a\n * prospect and points at reddoorla.com. They are different audiences on\n * different domains, and sharing a variable would mean one of them silently\n * moving the day the other is repointed.\n */\nexport function resolveReportBaseUrl(raw: string | undefined): string {\n return (raw?.trim() || DEFAULT_REPORT_BASE_URL).replace(/\\/$/, \"\");\n}\n\n/** The link a prospect opens. */\nexport function reportUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${resolveReportBaseUrl(raw)}/audit/${token}`;\n}\n\n/** The print-designed variant, which the runner renders to a PDF. Same token,\n * same guards — a separate document, not a stylesheet over the page. */\nexport function reportPrintUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${reportUrl(token, raw)}/print`;\n}\n"],"mappings":"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","names":[]}
|
package/dist/replay-4FVFOMHH.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
listUnreplayedDeadLetters,
|
|
3
|
-
markDeadLetterReplayed
|
|
4
|
-
} from "./chunk-A5QKNOXW.js";
|
|
5
|
-
import {
|
|
6
|
-
ingestSubmission
|
|
7
|
-
} from "./chunk-Z7FDJ35L.js";
|
|
8
|
-
import "./chunk-NO74DXDP.js";
|
|
9
|
-
import "./chunk-I7NHLVVI.js";
|
|
10
|
-
import "./chunk-2OP4JKIQ.js";
|
|
11
|
-
|
|
12
|
-
// src/forms/replay.ts
|
|
13
|
-
async function replayDeadLetters(db, deps) {
|
|
14
|
-
const rows = await listUnreplayedDeadLetters(db);
|
|
15
|
-
const result = { replayed: [], stillFailing: [] };
|
|
16
|
-
for (const row of rows) {
|
|
17
|
-
const outcome = await replayOne(db, deps, row);
|
|
18
|
-
if (outcome.terminal) {
|
|
19
|
-
result.replayed.push({
|
|
20
|
-
id: row.id,
|
|
21
|
-
outcome: outcome.outcome,
|
|
22
|
-
submissionId: outcome.submissionId
|
|
23
|
-
});
|
|
24
|
-
} else {
|
|
25
|
-
result.stillFailing.push({ id: row.id, error: outcome.error });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
async function replayOne(db, deps, row) {
|
|
31
|
-
const { deadLetter: _stripped, ...safeDeps } = deps;
|
|
32
|
-
void _stripped;
|
|
33
|
-
let res;
|
|
34
|
-
try {
|
|
35
|
-
res = await ingestSubmission(safeDeps, row.siteSlug, row.payload, row.turnstile);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
return { terminal: false, error: String(err) };
|
|
38
|
-
}
|
|
39
|
-
const now = deps.now();
|
|
40
|
-
if (res.status === "accepted") {
|
|
41
|
-
await markDeadLetterReplayed(db, row.id, "accepted", res.submissionId, now);
|
|
42
|
-
return { terminal: true, outcome: "accepted", submissionId: res.submissionId };
|
|
43
|
-
}
|
|
44
|
-
if (res.status === "unknown-site") {
|
|
45
|
-
return { terminal: false, error: `unknown-site: no fleet row for '${row.siteSlug}'` };
|
|
46
|
-
}
|
|
47
|
-
const outcome = `rejected:${res.reason}`;
|
|
48
|
-
await markDeadLetterReplayed(db, row.id, outcome, null, now);
|
|
49
|
-
return { terminal: true, outcome, submissionId: null };
|
|
50
|
-
}
|
|
51
|
-
export {
|
|
52
|
-
replayDeadLetters
|
|
53
|
-
};
|
|
54
|
-
//# sourceMappingURL=replay-4FVFOMHH.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/forms/replay.ts"],"sourcesContent":["import type { Db } from \"../db/client.js\";\nimport {\n listUnreplayedDeadLetters,\n markDeadLetterReplayed,\n type DeadLetterRow,\n} from \"../db/deadletter.js\";\nimport { ingestSubmission, type IngestDeps } from \"./ingest.js\";\n\nexport type ReplayResult = {\n /** Terminal outcomes this run: the row will never be picked up again. */\n replayed: Array<{ id: string; outcome: string; submissionId: string | null }>;\n /** Rows whose lookup threw AGAIN — left unreplayed for the next run. */\n stillFailing: Array<{ id: string; error: string }>;\n};\n\n/**\n * Run every unreplayed dead-letter back through `ingestSubmission` — the SAME\n * pipeline a live submission takes, so a replayed lead gets real spam\n * classification, notify, and fan-out, not a bespoke re-implementation.\n *\n * Each row replays with the Turnstile verification computed at receipt (tokens\n * expire in 300s; re-verifying is impossible by design, see deadletter.ts).\n *\n * `deps` must NOT wire `deadLetter`: a lookup that throws during replay would\n * otherwise dead-letter the SAME lead a second time while the original row stays\n * unreplayed — every retry would mint a duplicate. Enforced here by stripping the\n * key rather than trusting the caller. A thrown lookup leaves the row untouched\n * for the next run; `accepted` and `rejected` are terminal — re-running cannot\n * improve on an answer the store actually gave. `unknown-site` USED to be\n * terminal too and no longer is: see replayOne below (#645).\n *\n * Oldest-first (listUnreplayedDeadLetters orders by received_at) so the\n * duplicate/velocity spam signals see submissions in arrival order.\n */\nexport async function replayDeadLetters(\n db: Db,\n deps: Omit<IngestDeps, \"deadLetter\">,\n): Promise<ReplayResult> {\n const rows = await listUnreplayedDeadLetters(db);\n const result: ReplayResult = { replayed: [], stillFailing: [] };\n\n for (const row of rows) {\n const outcome = await replayOne(db, deps, row);\n if (outcome.terminal) {\n result.replayed.push({\n id: row.id,\n outcome: outcome.outcome,\n submissionId: outcome.submissionId,\n });\n } else {\n result.stillFailing.push({ id: row.id, error: outcome.error });\n }\n }\n return result;\n}\n\nasync function replayOne(\n db: Db,\n deps: Omit<IngestDeps, \"deadLetter\">,\n row: DeadLetterRow,\n): Promise<\n | { terminal: true; outcome: string; submissionId: string | null }\n | { terminal: false; error: string }\n> {\n // Strip deadLetter defensively even though the type forbids it — a structurally\n // wider object passed through `as` must not re-arm the duplicate loop.\n const { deadLetter: _stripped, ...safeDeps } = deps as IngestDeps;\n void _stripped;\n let res;\n try {\n res = await ingestSubmission(safeDeps, row.siteSlug, row.payload, row.turnstile);\n } catch (err) {\n return { terminal: false, error: String(err) };\n }\n const now = deps.now();\n if (res.status === \"accepted\") {\n await markDeadLetterReplayed(db, row.id, \"accepted\", res.submissionId, now);\n return { terminal: true, outcome: \"accepted\", submissionId: res.submissionId };\n }\n // #645. `unknown-site` is NO LONGER terminal. It used to be, on the reasoning\n // that a slug the store rejects \"can never improve\" — true while nothing could\n // create the missing row. `ensure-site` can now heal one (#645 item 1), and\n // `ingestSubmission` now dead-letters `unknown-site` leads instead of dropping\n // them, so this queue holds real client leads whose site is merely absent.\n //\n // Terminality would then be an ORDERING TRAP: replay-before-heal marks every\n // such lead replayed-and-lost, and that is the order a person reaches for\n // first. Leaving the row queued makes both orders safe.\n //\n // The cost is a slug that is genuinely gone piling up rows and holding this\n // command at exit 1. Deliberate: loud and recoverable beats silent and not.\n if (res.status === \"unknown-site\") {\n return { terminal: false, error: `unknown-site: no fleet row for '${row.siteSlug}'` };\n }\n const outcome = `rejected:${res.reason}`;\n await markDeadLetterReplayed(db, row.id, outcome, null, now);\n return { terminal: true, outcome, submissionId: null };\n}\n"],"mappings":";;;;;;;;;;;;AAkCA,eAAsB,kBACpB,IACA,MACuB;AACvB,QAAM,OAAO,MAAM,0BAA0B,EAAE;AAC/C,QAAM,SAAuB,EAAE,UAAU,CAAC,GAAG,cAAc,CAAC,EAAE;AAE9D,aAAW,OAAO,MAAM;AACtB,UAAM,UAAU,MAAM,UAAU,IAAI,MAAM,GAAG;AAC7C,QAAI,QAAQ,UAAU;AACpB,aAAO,SAAS,KAAK;AAAA,QACnB,IAAI,IAAI;AAAA,QACR,SAAS,QAAQ;AAAA,QACjB,cAAc,QAAQ;AAAA,MACxB,CAAC;AAAA,IACH,OAAO;AACL,aAAO,aAAa,KAAK,EAAE,IAAI,IAAI,IAAI,OAAO,QAAQ,MAAM,CAAC;AAAA,IAC/D;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,UACb,IACA,MACA,KAIA;AAGA,QAAM,EAAE,YAAY,WAAW,GAAG,SAAS,IAAI;AAC/C,OAAK;AACL,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,iBAAiB,UAAU,IAAI,UAAU,IAAI,SAAS,IAAI,SAAS;AAAA,EACjF,SAAS,KAAK;AACZ,WAAO,EAAE,UAAU,OAAO,OAAO,OAAO,GAAG,EAAE;AAAA,EAC/C;AACA,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,IAAI,WAAW,YAAY;AAC7B,UAAM,uBAAuB,IAAI,IAAI,IAAI,YAAY,IAAI,cAAc,GAAG;AAC1E,WAAO,EAAE,UAAU,MAAM,SAAS,YAAY,cAAc,IAAI,aAAa;AAAA,EAC/E;AAaA,MAAI,IAAI,WAAW,gBAAgB;AACjC,WAAO,EAAE,UAAU,OAAO,OAAO,mCAAmC,IAAI,QAAQ,IAAI;AAAA,EACtF;AACA,QAAM,UAAU,YAAY,IAAI,MAAM;AACtC,QAAM,uBAAuB,IAAI,IAAI,IAAI,SAAS,MAAM,GAAG;AAC3D,SAAO,EAAE,UAAU,MAAM,SAAS,cAAc,KAAK;AACvD;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/db/screenouts.ts"],"sourcesContent":["import { sql } from \"kysely\";\nimport type { Db } from \"./client.js\";\n\nexport type ScreenOutReason = \"honeypot\" | \"too-fast\";\nexport type ScreenOutTotals = { honeypot: number; tooFast: number; markedSpam: number };\n\nconst COLUMN: Record<ScreenOutReason, \"honeypot\" | \"too_fast\"> = {\n honeypot: \"honeypot\",\n \"too-fast\": \"too_fast\",\n};\n\n/** Atomically increment the caught counter for a reason on the (site, date) bucket.\n * ON CONFLICT keeps the count exact — no read-modify-write race, no duplicate\n * buckets. A swallowed failure must never error a screened bot (caller's job). */\nexport async function recordScreenOut(\n db: Db,\n siteId: string,\n reason: ScreenOutReason,\n date: string,\n): Promise<void> {\n const col = COLUMN[reason];\n await sql`\n INSERT INTO spam_screenouts (site_id, date, ${sql.ref(col)})\n VALUES (${siteId}, ${date}, 1)\n ON CONFLICT (site_id, date) DO UPDATE SET ${sql.ref(col)} = ${sql.ref(col)} + 1\n `.execute(db);\n}\n\n/** Per-site spam totals over the window `>= sinceDate`.\n *\n * `honeypot`/`tooFast` are summed from the ingest-time `spam_screenouts` buckets\n * (bots screened BEFORE storage — there's no row to count, so an atomic counter\n * is the right source).\n *\n * `markedSpam` is DERIVED from the submissions table — a live `COUNT(*) WHERE\n * status = 'spam'` — NOT a mutable counter. A counter incremented on every\n * →spam transition double-counts when an operator re-marks a submission\n * (spam → new → spam) and can never self-correct an un-mark; counting the rows\n * themselves is exact and idempotent. Windowed by `submitted_at` so it's\n * arrival-dated, matching the honeypot/too-fast buckets (which key on the\n * screen = arrival day). The legacy `marked_spam` column is no longer read. */\nexport async function listScreenOutsSince(\n db: Db,\n sinceDate: string,\n): Promise<Map<string, ScreenOutTotals>> {\n const screenoutRows = await db\n .selectFrom(\"spam_screenouts\")\n .select((eb) => [\n \"site_id\",\n eb.fn.sum<number>(\"honeypot\").as(\"honeypot\"),\n eb.fn.sum<number>(\"too_fast\").as(\"too_fast\"),\n ])\n .where(\"date\", \">=\", sinceDate)\n .groupBy(\"site_id\")\n .execute();\n\n const markedSpamRows = await db\n .selectFrom(\"submissions\")\n .select((eb) => [\"site_id\", eb.fn.countAll<number>().as(\"marked_spam\")])\n .where(\"status\", \"=\", \"spam\")\n .where(\"submitted_at\", \">=\", sinceDate)\n .groupBy(\"site_id\")\n .execute();\n\n const out = new Map<string, ScreenOutTotals>();\n for (const r of screenoutRows) {\n out.set(r.site_id, {\n honeypot: Number(r.honeypot) || 0,\n tooFast: Number(r.too_fast) || 0,\n markedSpam: 0,\n });\n }\n for (const r of markedSpamRows) {\n const totals = out.get(r.site_id) ?? { honeypot: 0, tooFast: 0, markedSpam: 0 };\n totals.markedSpam = Number(r.marked_spam) || 0;\n out.set(r.site_id, totals);\n }\n return out;\n}\n\n/** The ISO date (YYYY-MM-DD) `days` before `now`, for the window queries.\n * Verbatim from the Airtable module so the windows match exactly. */\nexport function screenOutsSince(now: Date, days: number): string {\n const d = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);\n return d.toISOString().slice(0, 10);\n}\n\n/** Set the (site, date) bucket to exact totals. Replace-upsert (DO UPDATE SET col\n * = excluded.col) so re-running the backfill is idempotent. The caller pre-sums\n * duplicate same-day Airtable buckets in JS before calling this. */\nexport async function backfillScreenoutBucket(\n db: Db,\n b: { siteId: string; date: string; honeypot: number; tooFast: number; markedSpam: number },\n): Promise<void> {\n await sql`\n INSERT INTO spam_screenouts (site_id, date, honeypot, too_fast, marked_spam)\n VALUES (${b.siteId}, ${b.date}, ${b.honeypot}, ${b.tooFast}, ${b.markedSpam})\n ON CONFLICT (site_id, date) DO UPDATE SET\n honeypot = excluded.honeypot,\n too_fast = excluded.too_fast,\n marked_spam = excluded.marked_spam\n `.execute(db);\n}\n"],"mappings":";AAAA,SAAS,WAAW;AAMpB,IAAM,SAA2D;AAAA,EAC/D,UAAU;AAAA,EACV,YAAY;AACd;AAKA,eAAsB,gBACpB,IACA,QACA,QACA,MACe;AACf,QAAM,MAAM,OAAO,MAAM;AACzB,QAAM;AAAA,kDAC0C,IAAI,IAAI,GAAG,CAAC;AAAA,cAChD,MAAM,KAAK,IAAI;AAAA,gDACmB,IAAI,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,GAAG,CAAC;AAAA,IAC1E,QAAQ,EAAE;AACd;AAeA,eAAsB,oBACpB,IACA,WACuC;AACvC,QAAM,gBAAgB,MAAM,GACzB,WAAW,iBAAiB,EAC5B,OAAO,CAAC,OAAO;AAAA,IACd;AAAA,IACA,GAAG,GAAG,IAAY,UAAU,EAAE,GAAG,UAAU;AAAA,IAC3C,GAAG,GAAG,IAAY,UAAU,EAAE,GAAG,UAAU;AAAA,EAC7C,CAAC,EACA,MAAM,QAAQ,MAAM,SAAS,EAC7B,QAAQ,SAAS,EACjB,QAAQ;AAEX,QAAM,iBAAiB,MAAM,GAC1B,WAAW,aAAa,EACxB,OAAO,CAAC,OAAO,CAAC,WAAW,GAAG,GAAG,SAAiB,EAAE,GAAG,aAAa,CAAC,CAAC,EACtE,MAAM,UAAU,KAAK,MAAM,EAC3B,MAAM,gBAAgB,MAAM,SAAS,EACrC,QAAQ,SAAS,EACjB,QAAQ;AAEX,QAAM,MAAM,oBAAI,IAA6B;AAC7C,aAAW,KAAK,eAAe;AAC7B,QAAI,IAAI,EAAE,SAAS;AAAA,MACjB,UAAU,OAAO,EAAE,QAAQ,KAAK;AAAA,MAChC,SAAS,OAAO,EAAE,QAAQ,KAAK;AAAA,MAC/B,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AACA,aAAW,KAAK,gBAAgB;AAC9B,UAAM,SAAS,IAAI,IAAI,EAAE,OAAO,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,YAAY,EAAE;AAC9E,WAAO,aAAa,OAAO,EAAE,WAAW,KAAK;AAC7C,QAAI,IAAI,EAAE,SAAS,MAAM;AAAA,EAC3B;AACA,SAAO;AACT;AAIO,SAAS,gBAAgB,KAAW,MAAsB;AAC/D,QAAM,IAAI,IAAI,KAAK,IAAI,QAAQ,IAAI,OAAO,KAAK,KAAK,KAAK,GAAI;AAC7D,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpC;AAKA,eAAsB,wBACpB,IACA,GACe;AACf,QAAM;AAAA;AAAA,cAEM,EAAE,MAAM,KAAK,EAAE,IAAI,KAAK,EAAE,QAAQ,KAAK,EAAE,OAAO,KAAK,EAAE,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,IAK3E,QAAQ,EAAE;AACd;","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|