@reddoorla/maintenance 0.34.0 → 0.36.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.
@@ -1,21 +1,27 @@
1
1
  import { F as FormType } from '../types-RXY-vY-5.js';
2
2
  export { S as SUBMISSION_FORM_TYPES } from '../types-RXY-vY-5.js';
3
+ import { RequestEvent, ActionFailure } from '@sveltejs/kit';
3
4
 
4
5
  /**
5
6
  * The JSON a fleet site forwards to the dashboard ingest endpoint. Typed fields
6
7
  * are optional; the index signature lets a site include its own extra fields
7
8
  * (e.g. `company`) which the dashboard normalizer captures into `extraFields`.
9
+ *
10
+ * Each typed field allows `string | undefined` (not just `string`) so a
11
+ * `buildPayload` mapping can use the idiomatic `form.get("name")?.toString()`
12
+ * pattern under `exactOptionalPropertyTypes` without a cast — an absent field
13
+ * and an explicit `undefined` both serialize away in the JSON body.
8
14
  */
9
15
  type SubmissionPayload = {
10
- formType?: FormType | string;
11
- name?: string;
12
- firstName?: string;
13
- lastName?: string;
14
- email?: string;
15
- phone?: string;
16
- message?: string;
17
- sourceUrl?: string;
18
- utm?: string;
16
+ formType?: FormType | string | undefined;
17
+ name?: string | undefined;
18
+ firstName?: string | undefined;
19
+ lastName?: string | undefined;
20
+ email?: string | undefined;
21
+ phone?: string | undefined;
22
+ message?: string | undefined;
23
+ sourceUrl?: string | undefined;
24
+ utm?: string | undefined;
19
25
  [key: string]: unknown;
20
26
  };
21
27
  type IngestClientResult = {
@@ -61,4 +67,45 @@ declare const MIN_FILL_MS = 2000;
61
67
  */
62
68
  declare function screenSubmission(input: ScreenInput): ScreenResult;
63
69
 
64
- export { FormType, type IngestClientResult, MIN_FILL_MS, type ScreenInput, type ScreenResult, type SubmissionPayload, type SubmitToIngestOptions, screenSubmission, submitToIngest };
70
+ /** Endpoint + token for the dashboard ingest, read per-request from site env. */
71
+ type IngestActionConfig = {
72
+ url?: string;
73
+ token?: string;
74
+ };
75
+ type CreateIngestActionOptions = {
76
+ /** Stamped onto every payload as `formType` (a SUBMISSION_FORM_TYPES value). */
77
+ formType: string;
78
+ /** Read at call time so SvelteKit's dynamic private env resolves per-request. */
79
+ getConfig: () => IngestActionConfig;
80
+ /**
81
+ * Map this form's fields to a payload. The factory's `formType` is always
82
+ * authoritative and cannot be overridden by `buildPayload`.
83
+ */
84
+ buildPayload: (form: FormData, event: RequestEvent) => SubmissionPayload;
85
+ /** Honeypot input name. Default "bot-field". */
86
+ botFieldName?: string;
87
+ /** Hidden timestamp input name (planted in `load`). Default "ts". */
88
+ tsFieldName?: string;
89
+ /** fail(500) copy when env vars are unset. */
90
+ unavailableMessage?: string;
91
+ /** fail(502) copy when the ingest endpoint rejects/errors. */
92
+ errorMessage?: string;
93
+ /** Injectable clock for tests. Default Date.now. */
94
+ now?: () => number;
95
+ /** If set, a successful OR bot-screened submission throws redirect(303, redirectTo)
96
+ * instead of returning {success:true} (e.g. a dedicated /thank-you page). */
97
+ redirectTo?: string;
98
+ };
99
+ type IngestActionData = {
100
+ success: true;
101
+ } | ActionFailure<{
102
+ error: string;
103
+ }>;
104
+ /**
105
+ * Build a SvelteKit `default` form action that screens for bots, forwards the
106
+ * submission to the dashboard ingest endpoint, and returns SvelteKit-shaped
107
+ * results. The per-form field mapping is the only thing a site must supply.
108
+ */
109
+ declare function createIngestAction(opts: CreateIngestActionOptions): (event: RequestEvent) => Promise<IngestActionData>;
110
+
111
+ export { type CreateIngestActionOptions, FormType, type IngestActionConfig, type IngestActionData, type IngestClientResult, MIN_FILL_MS, type ScreenInput, type ScreenResult, type SubmissionPayload, type SubmitToIngestOptions, createIngestAction, screenSubmission, submitToIngest };
@@ -42,9 +42,59 @@ function screenSubmission(input) {
42
42
  }
43
43
  return { ok: true };
44
44
  }
45
+
46
+ // src/forms/action.ts
47
+ import { fail, redirect } from "@sveltejs/kit";
48
+ function createIngestAction(opts) {
49
+ const botFieldName = opts.botFieldName ?? "bot-field";
50
+ const tsFieldName = opts.tsFieldName ?? "ts";
51
+ const now = opts.now ?? Date.now;
52
+ const unavailable = opts.unavailableMessage ?? "This form is temporarily unavailable. Please email us directly.";
53
+ const failed = opts.errorMessage ?? "Something went wrong sending your message. Please try again.";
54
+ return async (event) => {
55
+ let form;
56
+ try {
57
+ form = await event.request.formData();
58
+ } catch {
59
+ console.error(`[forms-ingest] ${opts.formType}: could not parse form body`);
60
+ return fail(400, { error: failed });
61
+ }
62
+ const screen = screenSubmission({
63
+ botField: form.get(botFieldName)?.toString() ?? null,
64
+ elapsedMs: elapsedMs(form.get(tsFieldName), now)
65
+ });
66
+ if (!screen.ok) return succeed();
67
+ const { url, token } = opts.getConfig();
68
+ if (!url || !token) {
69
+ console.error(`[forms-ingest] config missing for formType=${opts.formType}`);
70
+ return fail(500, { error: unavailable });
71
+ }
72
+ const result = await submitToIngest({
73
+ url,
74
+ token,
75
+ fetch: event.fetch,
76
+ payload: { ...opts.buildPayload(form, event), formType: opts.formType }
77
+ });
78
+ if (!result.ok) {
79
+ console.error(`[forms-ingest] ${opts.formType} \u2192 ${result.status}: ${result.error}`);
80
+ return fail(502, { error: failed });
81
+ }
82
+ return succeed();
83
+ };
84
+ function succeed() {
85
+ if (opts.redirectTo) redirect(303, opts.redirectTo);
86
+ return { success: true };
87
+ }
88
+ }
89
+ function elapsedMs(tsRaw, now) {
90
+ const ts = Number(tsRaw);
91
+ if (!Number.isFinite(ts) || ts <= 0) return null;
92
+ return now() - ts;
93
+ }
45
94
  export {
46
95
  MIN_FILL_MS,
47
96
  SUBMISSION_FORM_TYPES,
97
+ createIngestAction,
48
98
  screenSubmission,
49
99
  submitToIngest
50
100
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/forms/types.ts","../../src/forms/client.ts"],"sourcesContent":["/**\n * Form-type enum, kept in a leaf module (no Airtable/Resend imports) so it can\n * be shared with fleet sites via the `@reddoorla/maintenance/forms` subpath\n * without dragging server SDKs into a site bundle.\n */\nexport const SUBMISSION_FORM_TYPES = [\n \"contact\",\n \"inquiry\",\n \"newsletter\",\n \"rsvp\",\n \"reserve\",\n] as const;\nexport type FormType = (typeof SUBMISSION_FORM_TYPES)[number];\n","import { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\n\n/**\n * The JSON a fleet site forwards to the dashboard ingest endpoint. Typed fields\n * are optional; the index signature lets a site include its own extra fields\n * (e.g. `company`) which the dashboard normalizer captures into `extraFields`.\n */\nexport type SubmissionPayload = {\n formType?: FormType | string;\n name?: string;\n firstName?: string;\n lastName?: string;\n email?: string;\n phone?: string;\n message?: string;\n sourceUrl?: string;\n utm?: string;\n [key: string]: unknown;\n};\n\nexport type IngestClientResult =\n | { ok: true; id: string }\n | { ok: false; status: number; error: string };\n\nexport type SubmitToIngestOptions = {\n /** Full ingest endpoint incl. the site slug, e.g. https://…/api/forms/reddoor */\n url: string;\n /** The shared FORMS_INGEST_TOKEN. */\n token: string;\n payload: SubmissionPayload;\n /** Injectable fetch (pass SvelteKit's `event.fetch`); defaults to global fetch. */\n fetch?: typeof fetch;\n};\n\n/**\n * Forward a submission to the dashboard ingest endpoint. Never throws — a network\n * failure or a non-2xx response is returned as `{ ok: false }` so the caller can\n * show a friendly error rather than a 500.\n */\nexport async function submitToIngest(opts: SubmitToIngestOptions): Promise<IngestClientResult> {\n const doFetch = opts.fetch ?? fetch;\n let res: Response;\n try {\n res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n body: JSON.stringify(opts.payload),\n });\n } catch (err) {\n return { ok: false, status: 0, error: `network error: ${String(err)}` };\n }\n let body: unknown = null;\n try {\n body = await res.json();\n } catch {\n // non-JSON response — fall through to the error path\n }\n const obj = body && typeof body === \"object\" ? (body as Record<string, unknown>) : null;\n if (res.ok && obj && obj.ok === true) {\n return { ok: true, id: String(obj.id ?? \"\") };\n }\n const error = obj && typeof obj.error === \"string\" ? obj.error : `ingest failed (${res.status})`;\n return { ok: false, status: res.status, error };\n}\n\nexport type ScreenInput = { botField?: string | null; elapsedMs?: number | null };\nexport type ScreenResult = { ok: true } | { ok: false; reason: \"honeypot\" | \"too-fast\" };\n\n/** Minimum plausible fill time; faster than this reads as a bot. */\nexport const MIN_FILL_MS = 2000;\n\n/**\n * Cheap bot screen for the site action. A filled honeypot is a bot; a submission\n * faster than MIN_FILL_MS is a bot. Missing timing data (null) is NOT a rejection\n * — a prerendered/cached page can't plant a fresh timestamp, and the honeypot\n * remains the primary signal.\n */\nexport function screenSubmission(input: ScreenInput): ScreenResult {\n if (typeof input.botField === \"string\" && input.botField.trim().length > 0) {\n return { ok: false, reason: \"honeypot\" };\n }\n if (\n typeof input.elapsedMs === \"number\" &&\n input.elapsedMs >= 0 &&\n input.elapsedMs < MIN_FILL_MS\n ) {\n return { ok: false, reason: \"too-fast\" };\n }\n return { ok: true };\n}\n\nexport { SUBMISSION_FORM_TYPES, type FormType };\n"],"mappings":";AAKO,IAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AC4BA,eAAsB,eAAe,MAA0D;AAC7F,QAAM,UAAU,KAAK,SAAS;AAC9B,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,QAAQ,KAAK,KAAK;AAAA,MAC5B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oBAAoB,iBAAiB,KAAK,MAAM;AAAA,MAC3E,MAAM,KAAK,UAAU,KAAK,OAAO;AAAA,IACnC,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,QAAQ,GAAG,OAAO,kBAAkB,OAAO,GAAG,CAAC,GAAG;AAAA,EACxE;AACA,MAAI,OAAgB;AACpB,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AAAA,EAER;AACA,QAAM,MAAM,QAAQ,OAAO,SAAS,WAAY,OAAmC;AACnF,MAAI,IAAI,MAAM,OAAO,IAAI,OAAO,MAAM;AACpC,WAAO,EAAE,IAAI,MAAM,IAAI,OAAO,IAAI,MAAM,EAAE,EAAE;AAAA,EAC9C;AACA,QAAM,QAAQ,OAAO,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,kBAAkB,IAAI,MAAM;AAC7F,SAAO,EAAE,IAAI,OAAO,QAAQ,IAAI,QAAQ,MAAM;AAChD;AAMO,IAAM,cAAc;AAQpB,SAAS,iBAAiB,OAAkC;AACjE,MAAI,OAAO,MAAM,aAAa,YAAY,MAAM,SAAS,KAAK,EAAE,SAAS,GAAG;AAC1E,WAAO,EAAE,IAAI,OAAO,QAAQ,WAAW;AAAA,EACzC;AACA,MACE,OAAO,MAAM,cAAc,YAC3B,MAAM,aAAa,KACnB,MAAM,YAAY,aAClB;AACA,WAAO,EAAE,IAAI,OAAO,QAAQ,WAAW;AAAA,EACzC;AACA,SAAO,EAAE,IAAI,KAAK;AACpB;","names":[]}
1
+ {"version":3,"sources":["../../src/forms/types.ts","../../src/forms/client.ts","../../src/forms/action.ts"],"sourcesContent":["/**\n * Form-type enum, kept in a leaf module (no Airtable/Resend imports) so it can\n * be shared with fleet sites via the `@reddoorla/maintenance/forms` subpath\n * without dragging server SDKs into a site bundle.\n */\nexport const SUBMISSION_FORM_TYPES = [\n \"contact\",\n \"inquiry\",\n \"newsletter\",\n \"rsvp\",\n \"reserve\",\n] as const;\nexport type FormType = (typeof SUBMISSION_FORM_TYPES)[number];\n","import { SUBMISSION_FORM_TYPES, type FormType } from \"./types.js\";\n\n/**\n * The JSON a fleet site forwards to the dashboard ingest endpoint. Typed fields\n * are optional; the index signature lets a site include its own extra fields\n * (e.g. `company`) which the dashboard normalizer captures into `extraFields`.\n *\n * Each typed field allows `string | undefined` (not just `string`) so a\n * `buildPayload` mapping can use the idiomatic `form.get(\"name\")?.toString()`\n * pattern under `exactOptionalPropertyTypes` without a cast — an absent field\n * and an explicit `undefined` both serialize away in the JSON body.\n */\nexport type SubmissionPayload = {\n formType?: FormType | string | undefined;\n name?: string | undefined;\n firstName?: string | undefined;\n lastName?: string | undefined;\n email?: string | undefined;\n phone?: string | undefined;\n message?: string | undefined;\n sourceUrl?: string | undefined;\n utm?: string | undefined;\n [key: string]: unknown;\n};\n\nexport type IngestClientResult =\n | { ok: true; id: string }\n | { ok: false; status: number; error: string };\n\nexport type SubmitToIngestOptions = {\n /** Full ingest endpoint incl. the site slug, e.g. https://…/api/forms/reddoor */\n url: string;\n /** The shared FORMS_INGEST_TOKEN. */\n token: string;\n payload: SubmissionPayload;\n /** Injectable fetch (pass SvelteKit's `event.fetch`); defaults to global fetch. */\n fetch?: typeof fetch;\n};\n\n/**\n * Forward a submission to the dashboard ingest endpoint. Never throws — a network\n * failure or a non-2xx response is returned as `{ ok: false }` so the caller can\n * show a friendly error rather than a 500.\n */\nexport async function submitToIngest(opts: SubmitToIngestOptions): Promise<IngestClientResult> {\n const doFetch = opts.fetch ?? fetch;\n let res: Response;\n try {\n res = await doFetch(opts.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-forms-token\": opts.token },\n body: JSON.stringify(opts.payload),\n });\n } catch (err) {\n return { ok: false, status: 0, error: `network error: ${String(err)}` };\n }\n let body: unknown = null;\n try {\n body = await res.json();\n } catch {\n // non-JSON response — fall through to the error path\n }\n const obj = body && typeof body === \"object\" ? (body as Record<string, unknown>) : null;\n if (res.ok && obj && obj.ok === true) {\n return { ok: true, id: String(obj.id ?? \"\") };\n }\n const error = obj && typeof obj.error === \"string\" ? obj.error : `ingest failed (${res.status})`;\n return { ok: false, status: res.status, error };\n}\n\nexport type ScreenInput = { botField?: string | null; elapsedMs?: number | null };\nexport type ScreenResult = { ok: true } | { ok: false; reason: \"honeypot\" | \"too-fast\" };\n\n/** Minimum plausible fill time; faster than this reads as a bot. */\nexport const MIN_FILL_MS = 2000;\n\n/**\n * Cheap bot screen for the site action. A filled honeypot is a bot; a submission\n * faster than MIN_FILL_MS is a bot. Missing timing data (null) is NOT a rejection\n * — a prerendered/cached page can't plant a fresh timestamp, and the honeypot\n * remains the primary signal.\n */\nexport function screenSubmission(input: ScreenInput): ScreenResult {\n if (typeof input.botField === \"string\" && input.botField.trim().length > 0) {\n return { ok: false, reason: \"honeypot\" };\n }\n if (\n typeof input.elapsedMs === \"number\" &&\n input.elapsedMs >= 0 &&\n input.elapsedMs < MIN_FILL_MS\n ) {\n return { ok: false, reason: \"too-fast\" };\n }\n return { ok: true };\n}\n\nexport { SUBMISSION_FORM_TYPES, type FormType };\n","import { fail, redirect, type ActionFailure, type RequestEvent } from \"@sveltejs/kit\";\nimport { submitToIngest, screenSubmission, type SubmissionPayload } from \"./client.js\";\n\n/** Endpoint + token for the dashboard ingest, read per-request from site env. */\nexport type IngestActionConfig = { url?: string; token?: string };\n\nexport type CreateIngestActionOptions = {\n /** Stamped onto every payload as `formType` (a SUBMISSION_FORM_TYPES value). */\n formType: string;\n /** Read at call time so SvelteKit's dynamic private env resolves per-request. */\n getConfig: () => IngestActionConfig;\n /**\n * Map this form's fields to a payload. The factory's `formType` is always\n * authoritative and cannot be overridden by `buildPayload`.\n */\n buildPayload: (form: FormData, event: RequestEvent) => SubmissionPayload;\n /** Honeypot input name. Default \"bot-field\". */\n botFieldName?: string;\n /** Hidden timestamp input name (planted in `load`). Default \"ts\". */\n tsFieldName?: string;\n /** fail(500) copy when env vars are unset. */\n unavailableMessage?: string;\n /** fail(502) copy when the ingest endpoint rejects/errors. */\n errorMessage?: string;\n /** Injectable clock for tests. Default Date.now. */\n now?: () => number;\n /** If set, a successful OR bot-screened submission throws redirect(303, redirectTo)\n * instead of returning {success:true} (e.g. a dedicated /thank-you page). */\n redirectTo?: string;\n};\n\nexport type IngestActionData = { success: true } | ActionFailure<{ error: string }>;\n\n/**\n * Build a SvelteKit `default` form action that screens for bots, forwards the\n * submission to the dashboard ingest endpoint, and returns SvelteKit-shaped\n * results. The per-form field mapping is the only thing a site must supply.\n */\nexport function createIngestAction(\n opts: CreateIngestActionOptions,\n): (event: RequestEvent) => Promise<IngestActionData> {\n const botFieldName = opts.botFieldName ?? \"bot-field\";\n const tsFieldName = opts.tsFieldName ?? \"ts\";\n const now = opts.now ?? Date.now;\n const unavailable =\n opts.unavailableMessage ?? \"This form is temporarily unavailable. Please email us directly.\";\n const failed =\n opts.errorMessage ?? \"Something went wrong sending your message. Please try again.\";\n\n return async (event) => {\n let form: FormData;\n try {\n form = await event.request.formData();\n } catch {\n console.error(`[forms-ingest] ${opts.formType}: could not parse form body`);\n return fail(400, { error: failed });\n }\n\n // Bot screen: a filled honeypot OR an implausibly fast fill is silently\n // accepted (return success, do NOT forward) so bots get no signal.\n const screen = screenSubmission({\n botField: form.get(botFieldName)?.toString() ?? null,\n elapsedMs: elapsedMs(form.get(tsFieldName), now),\n });\n if (!screen.ok) return succeed();\n\n const { url, token } = opts.getConfig();\n if (!url || !token) {\n console.error(`[forms-ingest] config missing for formType=${opts.formType}`);\n return fail(500, { error: unavailable });\n }\n\n const result = await submitToIngest({\n url,\n token,\n fetch: event.fetch,\n payload: { ...opts.buildPayload(form, event), formType: opts.formType },\n });\n if (!result.ok) {\n console.error(`[forms-ingest] ${opts.formType} → ${result.status}: ${result.error}`);\n return fail(502, { error: failed });\n }\n return succeed();\n };\n\n // Single success path: redirect when configured (e.g. a dedicated /thank-you\n // page), otherwise return the SvelteKit-shaped success. `redirect()` throws, so\n // the trailing `return` keeps the `{ success: true }` type.\n function succeed(): { success: true } {\n if (opts.redirectTo) redirect(303, opts.redirectTo);\n return { success: true };\n }\n}\n\n// `FormDataEntryValue` is a DOM-lib global; this package compiles with only the\n// ES2022 lib + @types/node, where it is not in scope. Derive the type from the\n// in-scope `FormData.get` return instead — same value, no DOM-lib dependency.\nfunction elapsedMs(tsRaw: ReturnType<FormData[\"get\"]>, now: () => number): number | null {\n const ts = Number(tsRaw);\n if (!Number.isFinite(ts) || ts <= 0) return null;\n return now() - ts;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddoorla/maintenance",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Canonical maintenance configs, audits, and recipes for the reddoor stack.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -72,9 +72,18 @@
72
72
  "tinyglobby": "^0.2.10",
73
73
  "typescript-eslint": "^8.59.1"
74
74
  },
75
+ "peerDependencies": {
76
+ "@sveltejs/kit": "^2.0.0"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "@sveltejs/kit": {
80
+ "optional": true
81
+ }
82
+ },
75
83
  "devDependencies": {
76
84
  "@changesets/cli": "^2.31.0",
77
85
  "@netlify/functions": "^5.2.2",
86
+ "@sveltejs/kit": "^2.61.1",
78
87
  "@types/mjml": "^4.7.4",
79
88
  "@types/node": "^22.0.0",
80
89
  "@vitest/coverage-v8": "^4.1.8",