@ez-gform/core 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/encode.ts","../src/errors.ts","../src/extract.ts","../src/parse.ts","../src/url.ts","../src/submit.ts"],"sourcesContent":["export const VERSION = \"0.1.0\";\n\nexport { encodeValues, validateValues } from \"./encode.js\";\nexport { ParseError, ValidationError } from \"./errors.js\";\nexport { extractPublicLoadData } from \"./extract.js\";\nexport { parseFormData, parseFormHtml } from \"./parse.js\";\nexport { buildPrefillUrl, buildSubmitBody, submitForm } from \"./submit.js\";\nexport { formUrls, normalizeFormId } from \"./url.js\";\n\n// Re-export the shared schema/value/submit types so existing consumers of\n// `@ez-gform/core` keep working after they were extracted to\n// `@ez-gform/types`.\nexport type * from \"@ez-gform/types\";\n","import type {\n DateValue,\n FieldValue,\n FormSchema,\n FormValues,\n OtherValue,\n TimeValue,\n ValidationIssue,\n ValidationResult,\n} from \"@ez-gform/types\";\n\nconst OTHER_SENTINEL = \"__other_option__\";\n\nconst isOtherValue = (v: unknown): v is OtherValue => {\n return (\n typeof v === \"object\" && v !== null && !Array.isArray(v) && \"other\" in v\n );\n};\n\nconst isDateValue = (v: unknown): v is DateValue => {\n return (\n typeof v === \"object\" &&\n v !== null &&\n !Array.isArray(v) &&\n \"day\" in v &&\n \"month\" in v\n );\n};\n\nconst isTimeValue = (v: unknown): v is TimeValue => {\n return (\n typeof v === \"object\" &&\n v !== null &&\n !Array.isArray(v) &&\n \"hour\" in v &&\n \"minute\" in v &&\n !(\"day\" in v)\n );\n};\n\nconst pad2 = (n: number): string => {\n return String(n).padStart(2, \"0\");\n};\n\nconst appendScalar = (\n params: URLSearchParams,\n key: string,\n value: string | number | undefined | null,\n): void => {\n if (value === undefined || value === null) return;\n const str = String(value);\n if (str.length === 0) return;\n params.append(key, str);\n};\n\nconst appendChoiceItem = (\n params: URLSearchParams,\n entryId: string,\n item: string | OtherValue,\n): void => {\n if (isOtherValue(item)) {\n params.append(entryId, OTHER_SENTINEL);\n appendScalar(params, `${entryId}.other_option_response`, item.other);\n } else {\n appendScalar(params, entryId, item);\n }\n};\n\nconst appendGridMap = (\n params: URLSearchParams,\n gridMap: Record<string, string | string[]>,\n): void => {\n for (const [rowEntryId, colValue] of Object.entries(gridMap)) {\n if (Array.isArray(colValue)) {\n for (const v of colValue) appendScalar(params, rowEntryId, v);\n } else {\n appendScalar(params, rowEntryId, colValue);\n }\n }\n};\n\n/**\n * Encodes `FormValues` into `entry.NNN`-keyed `URLSearchParams`, per every\n * rule in `docs/research/google-forms-internals.md` §3. Pure: never throws\n * on invalid/unknown data, and never validates against a schema — pair with\n * `validateValues` for that. The second parameter is accepted (and ignored)\n * for call-site compatibility with `validateValues(values, schema)`.\n */\nexport const encodeValues = (\n values: FormValues,\n _?: FormSchema,\n): URLSearchParams => {\n const params = new URLSearchParams();\n\n for (const [entryId, rawValue] of Object.entries(values)) {\n encodeOne(params, entryId, rawValue);\n }\n\n return params;\n};\n\nconst encodeOne = (\n params: URLSearchParams,\n entryId: string,\n value: FieldValue,\n): void => {\n if (value === null || value === undefined) return;\n\n if (Array.isArray(value)) {\n for (const item of value) appendChoiceItem(params, entryId, item);\n return;\n }\n\n if (typeof value === \"string\" || typeof value === \"number\") {\n appendScalar(params, entryId, value);\n return;\n }\n\n if (isOtherValue(value)) {\n appendChoiceItem(params, entryId, value);\n return;\n }\n\n if (isDateValue(value)) {\n if (value.year !== undefined)\n appendScalar(params, `${entryId}_year`, value.year);\n appendScalar(params, `${entryId}_month`, value.month);\n appendScalar(params, `${entryId}_day`, value.day);\n if (value.hour !== undefined)\n appendScalar(params, `${entryId}_hour`, pad2(value.hour));\n if (value.minute !== undefined)\n appendScalar(params, `${entryId}_minute`, pad2(value.minute));\n return;\n }\n\n if (isTimeValue(value)) {\n appendScalar(params, `${entryId}_hour`, pad2(value.hour));\n appendScalar(params, `${entryId}_minute`, pad2(value.minute));\n return;\n }\n\n // Otherwise: grid row map, Record<string, string | string[]>.\n appendGridMap(params, value);\n};\n\n/** Validates `values` against a parsed `FormSchema`: unknown entry ids and missing required questions. */\nexport const validateValues = (\n values: FormValues,\n schema: FormSchema,\n): ValidationResult => {\n const errors: ValidationIssue[] = [];\n\n const knownEntryIds = new Set<string>();\n for (const q of schema.questions) {\n knownEntryIds.add(q.entryId);\n for (const row of q.rows ?? []) knownEntryIds.add(row.entryId);\n }\n\n for (const key of Object.keys(values)) {\n if (!knownEntryIds.has(key)) {\n errors.push({\n entryId: key,\n message: `Unknown entry id \"${key}\" is not present in the form schema`,\n });\n }\n }\n\n for (const q of schema.questions) {\n if (!q.required) continue;\n const idsToCheck =\n q.rows && q.rows.length > 0\n ? q.rows.map((r) => {\n return r.entryId;\n })\n : [q.entryId];\n for (const id of idsToCheck) {\n const v = values[id];\n const isEmpty =\n v === undefined ||\n v === null ||\n v === \"\" ||\n (Array.isArray(v) && v.length === 0) ||\n (typeof v === \"object\" &&\n !Array.isArray(v) &&\n Object.keys(v).length === 0);\n if (isEmpty) {\n errors.push({\n entryId: id,\n message: `Missing required answer for question \"${q.title}\" (${id})`,\n });\n }\n }\n }\n\n return errors.length === 0 ? { ok: true } : { ok: false, errors };\n};\n","import type { ValidationIssue } from \"@ez-gform/types\";\n\n/** Thrown when `FB_PUBLIC_LOAD_DATA_` (or a caller-supplied value) doesn't match the expected shape. */\nexport class ParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ParseError\";\n }\n}\n\n/** Thrown-shaped, but returned (not thrown) by `validateValues`. */\nexport class ValidationError extends Error {\n errors: ValidationIssue[];\n\n constructor(message: string, errors: ValidationIssue[]) {\n super(message);\n this.name = \"ValidationError\";\n this.errors = errors;\n }\n}\n","import { ParseError } from \"./errors.js\";\n\nconst ASSIGNMENT_MARKER = \"FB_PUBLIC_LOAD_DATA_\";\n\n/**\n * Finds `var FB_PUBLIC_LOAD_DATA_ = [ ... ];` in a Google Forms `/viewform`\n * page and `JSON.parse`s the array. The blob is JSON-compatible (no trailing\n * commas, no JS-only syntax) so once we've sliced out exactly the array\n * literal, a plain `JSON.parse` works.\n *\n * We deliberately don't regex the whole array (it can contain `;`, nested\n * `[`/`]`, and escaped strings) — instead we find the assignment, then find\n * the matching closing bracket by walking the string and tracking bracket\n * depth while respecting JSON string-escaping rules.\n */\nexport const extractPublicLoadData = (html: string): unknown => {\n const markerIndex = html.indexOf(ASSIGNMENT_MARKER);\n if (markerIndex === -1) {\n throw new ParseError(\n \"extractPublicLoadData: FB_PUBLIC_LOAD_DATA_ not found in HTML\",\n );\n }\n\n const equalsIndex = html.indexOf(\"=\", markerIndex);\n if (equalsIndex === -1) {\n throw new ParseError(\n \"extractPublicLoadData: malformed FB_PUBLIC_LOAD_DATA_ assignment\",\n );\n }\n\n let start = equalsIndex + 1;\n while (start < html.length && /\\s/.test(html[start]!)) {\n start++;\n }\n\n if (html[start] !== \"[\") {\n throw new ParseError(\n \"extractPublicLoadData: expected array literal after FB_PUBLIC_LOAD_DATA_ =\",\n );\n }\n\n const end = findMatchingBracketEnd(html, start);\n const literal = html.slice(start, end + 1);\n\n try {\n return JSON.parse(literal);\n } catch (cause) {\n throw new ParseError(\n `extractPublicLoadData: failed to JSON.parse the extracted FB_PUBLIC_LOAD_DATA_ literal: ${\n cause instanceof Error ? cause.message : String(cause)\n }`,\n );\n }\n};\n\n/** Walks from `startIndex` (the opening `[`) and returns the index of its matching `]`. */\nconst findMatchingBracketEnd = (text: string, startIndex: number): number => {\n let depth = 0;\n let inString = false;\n let escaped = false;\n\n for (let i = startIndex; i < text.length; i++) {\n const ch = text[i];\n\n if (inString) {\n if (escaped) {\n escaped = false;\n } else if (ch === \"\\\\\") {\n escaped = true;\n } else if (ch === '\"') {\n inString = false;\n }\n continue;\n }\n\n if (ch === '\"') {\n inString = true;\n } else if (ch === \"[\") {\n depth++;\n } else if (ch === \"]\") {\n depth--;\n if (depth === 0) {\n return i;\n }\n }\n }\n\n throw new ParseError(\n \"extractPublicLoadData: unterminated array literal (no matching closing bracket)\",\n );\n};\n\n/** Extracts the `fbzx` hidden-input value from a `/viewform` page, if present. */\nexport const extractFbzx = (html: string): string | undefined => {\n const match = /name=\"fbzx\"\\s+value=\"([^\"]*)\"/.exec(html);\n return match?.[1];\n};\n","import type {\n ChoiceOption,\n FormSchema,\n GridRow,\n Question,\n QuestionType,\n Section,\n} from \"@ez-gform/types\";\nimport { ParseError } from \"./errors.js\";\nimport { extractFbzx, extractPublicLoadData } from \"./extract.js\";\n\ntype Json = unknown;\n\nconst asArray = (value: Json, context: string): Json[] => {\n if (!Array.isArray(value)) {\n throw new ParseError(\n `parseFormData: expected an array at ${context}, got ${typeof value}`,\n );\n }\n return value;\n};\n\nconst optionalString = (value: Json): string | undefined => {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n};\n\nconst toEntryId = (rawId: Json): string => {\n return `entry.${String(rawId)}`;\n};\n\nconst mapOptions = (rawOptions: Json): ChoiceOption[] | undefined => {\n if (!Array.isArray(rawOptions)) return undefined;\n return rawOptions.map((tuple) => {\n const t = Array.isArray(tuple) ? tuple : [];\n const value = typeof t[0] === \"string\" ? t[0] : \"\";\n const isOther = t[4] === 1;\n return { value, isOther };\n });\n};\n\ninterface SubQuestion {\n entryId: Json;\n options: Json;\n required: Json;\n extra: Json; // [3] - row label / scale labels / null\n rest: Json[]; // full array, for type-specific trailing flags\n}\n\nconst parseSubQuestions = (rawSubs: Json): SubQuestion[] => {\n const subs = asArray(rawSubs, \"question[4]\");\n return subs.map((rawSub) => {\n const s = asArray(rawSub, \"question[4][i]\");\n return {\n entryId: s[0],\n options: s[1],\n required: s[2],\n extra: s[3],\n rest: s,\n };\n });\n};\n\nconst TYPE_CODE_NAME: Record<\n number,\n QuestionType | \"section_header\" | \"page_break\" | \"image\" | \"video\"\n> = {\n 0: \"short_answer\",\n 1: \"paragraph\",\n 2: \"multiple_choice\",\n 3: \"dropdown\",\n 4: \"checkboxes\",\n 5: \"linear_scale\",\n 6: \"section_header\",\n 7: \"grid\", // resolved to \"grid\" | \"checkbox_grid\" below\n 8: \"page_break\",\n 9: \"date\",\n 10: \"time\",\n 11: \"image\",\n 12: \"video\",\n 13: \"file_upload\",\n // ponytail: rating (stars/hearts) has the same wire shape as a linear scale\n // (options \"1\"..\"N\", submits entry.N=<number>), so it reuses that type. Add a\n // \"rating\" QuestionType if consumers need to render the icon.\n 18: \"linear_scale\",\n};\n\n/**\n * Parses the `FB_PUBLIC_LOAD_DATA_` structure (already `JSON.parse`d, e.g. by\n * `extractPublicLoadData`) into a `FormSchema`, per the verified index map in\n * `docs/research/google-forms-internals.md` §4.\n */\nexport const parseFormData = (\n data: unknown,\n opts: { formId?: string } = {},\n): FormSchema => {\n if (!Array.isArray(data)) {\n throw new ParseError(\n \"parseFormData: expected the top-level FB_PUBLIC_LOAD_DATA_ value to be an array\",\n );\n }\n\n const container = data[1];\n if (!Array.isArray(container)) {\n throw new ParseError(\n \"parseFormData: expected data[1] (main container) to be an array\",\n );\n }\n\n const description = optionalString(container[0]);\n const rawQuestions = container[1];\n if (!Array.isArray(rawQuestions)) {\n throw new ParseError(\n \"parseFormData: expected data[1][1] (question list) to be an array\",\n );\n }\n\n // Title exists in two shapes: data[1][8] (usually a plain string, occasionally\n // the [null, \"<title>\"] wrapper shape used elsewhere) and data[3] as a plain\n // string (may differ cosmetically, e.g. trailing whitespace or truncation) —\n // prefer data[1][8], falling back to data[3].\n const container8 = container[8];\n const wrappedTitle =\n typeof container8 === \"string\"\n ? container8\n : Array.isArray(container8)\n ? container8[1]\n : undefined;\n const title =\n typeof wrappedTitle === \"string\"\n ? wrappedTitle\n : typeof data[3] === \"string\"\n ? data[3]\n : \"\";\n\n let { formId } = opts;\n if (!formId) {\n const rawFormId = data[14];\n if (typeof rawFormId === \"string\") {\n formId = rawFormId.startsWith(\"e/\") ? rawFormId.slice(2) : rawFormId;\n } else {\n formId = \"\";\n }\n }\n\n const questions: Question[] = [];\n const sections: Section[] = [{ title, description, questionIds: [] }];\n let sawPageBreak = false;\n\n for (const rawEntry of rawQuestions) {\n const entry = asArray(rawEntry, \"question entry\");\n const id = entry[0];\n const entryTitle = typeof entry[1] === \"string\" ? entry[1] : \"\";\n const entryDescription = optionalString(entry[2]);\n const typeCode = entry[3];\n\n if (typeof typeCode !== \"number\") {\n throw new ParseError(\n `parseFormData: question ${String(id)} has a non-numeric type code`,\n );\n }\n\n const kind = TYPE_CODE_NAME[typeCode];\n if (kind === undefined) {\n throw new ParseError(\n `parseFormData: unknown question type code ${typeCode} for question ${String(id)}`,\n );\n }\n\n if (kind === \"image\" || kind === \"video\") {\n continue;\n }\n\n if (kind === \"section_header\") {\n const current = sections[sections.length - 1]!;\n current.title = entryTitle;\n current.description = entryDescription;\n continue;\n }\n\n if (kind === \"page_break\") {\n sawPageBreak = true;\n sections.push({\n title: entryTitle,\n description: entryDescription,\n questionIds: [],\n });\n continue;\n }\n\n const subs = parseSubQuestions(entry[4]);\n if (subs.length === 0) {\n throw new ParseError(\n `parseFormData: question ${String(id)} (type ${typeCode}) has no sub-question entries`,\n );\n }\n const firstSub = subs[0]!;\n\n const question: Question = {\n id: String(id),\n entryId: toEntryId(firstSub.entryId),\n title: entryTitle,\n description: entryDescription,\n type: kind,\n required: firstSub.required === 1,\n };\n\n switch (kind) {\n case \"multiple_choice\":\n case \"dropdown\":\n case \"checkboxes\": {\n question.options = mapOptions(firstSub.options);\n break;\n }\n case \"linear_scale\": {\n const options = mapOptions(firstSub.options) ?? [];\n const first = options[0]?.value;\n const last = options[options.length - 1]?.value;\n const min = first !== undefined ? Number(first) : Number.NaN;\n const max = last !== undefined ? Number(last) : Number.NaN;\n const labels = Array.isArray(firstSub.extra) ? firstSub.extra : [];\n question.scale = {\n min,\n max,\n lowLabel: optionalString(labels[0]),\n highLabel: optionalString(labels[1]),\n };\n break;\n }\n case \"grid\": {\n const rows: GridRow[] = subs.map((sub) => {\n const label = Array.isArray(sub.extra)\n ? optionalString(sub.extra[0])\n : undefined;\n return { entryId: toEntryId(sub.entryId), label: label ?? \"\" };\n });\n const isCheckboxGrid = subs.some((sub) => {\n const flag = sub.rest[sub.rest.length - 1];\n return Array.isArray(flag) && flag[0] === 1;\n });\n question.type = isCheckboxGrid ? \"checkbox_grid\" : \"grid\";\n question.rows = rows;\n question.options = mapOptions(firstSub.options);\n break;\n }\n case \"date\": {\n const flags = Array.isArray(firstSub.rest[7])\n ? (firstSub.rest[7] as Json[])\n : undefined;\n question.date = {\n includeTime: flags?.[0] === 1,\n includeYear: flags ? flags[1] === 1 : true,\n };\n break;\n }\n case \"time\": {\n const flags = Array.isArray(firstSub.rest[6])\n ? (firstSub.rest[6] as Json[])\n : undefined;\n question.time = { isDuration: flags?.[0] === 1 };\n break;\n }\n case \"file_upload\":\n break;\n default:\n break;\n }\n\n questions.push(question);\n const currentSection = sections[sections.length - 1]!;\n currentSection.questionIds.push(question.id);\n }\n\n return {\n formId: formId ?? \"\",\n title,\n description,\n questions,\n sections,\n multiPage: sawPageBreak,\n };\n};\n\n/** Extracts `FB_PUBLIC_LOAD_DATA_` and `fbzx` from raw `/viewform` HTML, then parses. */\nexport const parseFormHtml = (html: string): FormSchema => {\n const data = extractPublicLoadData(html);\n const schema = parseFormData(data);\n const fbzx = extractFbzx(html);\n return fbzx ? { ...schema, fbzx } : schema;\n};\n","/**\n * A bare published form id looks like `1FAIpQLS...` (starts with `1FAIpQLS` in\n * practice, but we don't hard-require that prefix since Google hasn't\n * documented it). We only require it to be a reasonably long token of URL-safe\n * characters with no slashes, to reject obviously-wrong input like a random\n * sentence or an empty string.\n */\nconst BARE_ID_RE = /^[A-Za-z0-9_-]{10,}$/;\n\n/**\n * Accepts a bare form id, an `e/<id>` string (as stored in\n * `FB_PUBLIC_LOAD_DATA_[14]`), or any `docs.google.com/forms` URL shape\n * (including `/u/<n>/`, `/d/e/<id>/viewform`, `/d/<id>/edit`, with or without\n * query strings/trailing slashes) and returns the bare published id.\n */\nexport const normalizeFormId = (input: string): string => {\n const trimmed = input.trim();\n if (trimmed.length === 0) {\n throw new Error(\"normalizeFormId: input is empty\");\n }\n\n if (trimmed.startsWith(\"e/\")) {\n const id = trimmed.slice(2);\n return normalizeFormId(id);\n }\n\n if (/^https?:\\/\\//i.test(trimmed) || trimmed.startsWith(\"docs.google.com\")) {\n const url = new URL(\n /^https?:\\/\\//i.test(trimmed) ? trimmed : `https://${trimmed}`,\n );\n const parts = url.pathname.split(\"/\").filter(Boolean);\n // parts look like: [\"forms\", \"u\", \"0\", \"d\", \"e\", \"<id>\", \"viewform\"]\n // or: [\"forms\", \"d\", \"e\", \"<id>\", \"viewform\"]\n // or: [\"forms\", \"d\", \"<editor-id>\", \"edit\"]\n const dIndex = parts.indexOf(\"d\");\n if (dIndex === -1 || dIndex + 1 >= parts.length) {\n throw new Error(\n `normalizeFormId: could not find form id in URL \"${input}\"`,\n );\n }\n const afterD = parts[dIndex + 1];\n const id = afterD === \"e\" ? parts[dIndex + 2] : afterD;\n if (!id) {\n throw new Error(\n `normalizeFormId: could not find form id in URL \"${input}\"`,\n );\n }\n return normalizeFormId(id);\n }\n\n if (!BARE_ID_RE.test(trimmed)) {\n throw new Error(\n `normalizeFormId: \"${input}\" is not a recognizable Google Form id or URL`,\n );\n }\n return trimmed;\n};\n\nexport const formUrls = (\n formId: string,\n): {\n viewform: string;\n formResponse: string;\n} => {\n const id = normalizeFormId(formId);\n return {\n viewform: `https://docs.google.com/forms/d/e/${id}/viewform`,\n formResponse: `https://docs.google.com/forms/d/e/${id}/formResponse`,\n };\n};\n","import type { FormSchema, FormValues } from \"@ez-gform/types\";\nimport { encodeValues } from \"./encode.js\";\nimport { formUrls, normalizeFormId } from \"./url.js\";\n\n/** Builds a human-facing `/viewform?usp=pp_url&entry.N=...` prefill URL. */\nexport const buildPrefillUrl = (\n formId: string,\n values: FormValues,\n schema?: FormSchema,\n): string => {\n const { viewform } = formUrls(formId);\n const params = encodeValues(values, schema);\n const url = new URL(viewform);\n url.searchParams.set(\"usp\", \"pp_url\");\n for (const [key, value] of params) {\n url.searchParams.append(key, value);\n }\n return url.toString();\n};\n\n/**\n * A per-load anti-replay token Google embeds as a hidden `fbzx` input. When a\n * real one (captured from the page being submitted, via `schema.fbzx`) isn't\n * available, this generates a random negative ~19-digit-ish numeric string in\n * the same shape Google's own client-side JS produces, as a best-effort\n * fallback — it is not guaranteed to be accepted.\n */\nconst randomFbzx = (): string => {\n const digits = Array.from({ length: 19 }, () => {\n return Math.floor(Math.random() * 10);\n }).join(\"\");\n return `-${digits}`;\n};\n\n/** Builds the full `application/x-www-form-urlencoded` body for a `formResponse` POST. */\nexport const buildSubmitBody = (\n formId: string,\n values: FormValues,\n schema?: FormSchema,\n): URLSearchParams => {\n normalizeFormId(formId);\n const params = encodeValues(values, schema);\n\n if (schema?.multiPage) {\n const fbzx = schema.fbzx ?? randomFbzx();\n const pageCount = Math.max(schema.sections.length, 1);\n const pageHistory = Array.from({ length: pageCount }, (_, i) => {\n return i;\n }).join(\",\");\n params.set(\"fbzx\", fbzx);\n params.set(\"pageHistory\", pageHistory);\n params.set(\"partialResponse\", JSON.stringify([null, null, fbzx]));\n }\n\n params.set(\"submit\", \"Submit\");\n return params;\n};\n\nexport type SubmitResult =\n | { status: \"sent\" }\n | { status: \"ok\"; httpStatus: number }\n | { status: \"error\"; error: unknown; httpStatus?: number };\n\nexport interface SubmitOptions {\n schema?: FormSchema;\n fetch?: typeof fetch;\n mode?: \"no-cors\" | \"cors\";\n signal?: AbortSignal;\n}\n\n/**\n * POSTs `application/x-www-form-urlencoded` to the form's `formResponse`\n * endpoint. Defaults to `mode: \"no-cors\"` (required for cross-origin browser\n * submission; the response is opaque, so success can only be reported as\n * \"sent\", never confirmed) — see `docs/research/google-forms-internals.md`\n * §6. Pass `mode: \"cors\"` from Node/CLI contexts where the response can\n * actually be read.\n */\nexport const submitForm = async (\n formId: string,\n values: FormValues,\n opts: SubmitOptions = {},\n): Promise<SubmitResult> => {\n const { formResponse } = formUrls(formId);\n const body = buildSubmitBody(formId, values, opts.schema);\n const doFetch = opts.fetch ?? fetch;\n const mode = opts.mode ?? \"no-cors\";\n\n try {\n const response = await doFetch(formResponse, {\n method: \"POST\",\n mode,\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n signal: opts.signal,\n });\n\n if (mode === \"no-cors\") {\n return { status: \"sent\" };\n }\n\n if (response.ok) {\n return { status: \"ok\", httpStatus: response.status };\n }\n return {\n status: \"error\",\n error: new Error(`formResponse returned HTTP ${response.status}`),\n httpStatus: response.status,\n };\n } catch (error) {\n return { status: \"error\", error };\n }\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,93 @@
1
+ import { FormValues, FormSchema, ValidationResult, ValidationIssue } from '@ez-gform/types';
2
+ export * from '@ez-gform/types';
3
+
4
+ /**
5
+ * Encodes `FormValues` into `entry.NNN`-keyed `URLSearchParams`, per every
6
+ * rule in `docs/research/google-forms-internals.md` §3. Pure: never throws
7
+ * on invalid/unknown data, and never validates against a schema — pair with
8
+ * `validateValues` for that. The second parameter is accepted (and ignored)
9
+ * for call-site compatibility with `validateValues(values, schema)`.
10
+ */
11
+ declare const encodeValues: (values: FormValues, _?: FormSchema) => URLSearchParams;
12
+ /** Validates `values` against a parsed `FormSchema`: unknown entry ids and missing required questions. */
13
+ declare const validateValues: (values: FormValues, schema: FormSchema) => ValidationResult;
14
+
15
+ /** Thrown when `FB_PUBLIC_LOAD_DATA_` (or a caller-supplied value) doesn't match the expected shape. */
16
+ declare class ParseError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ /** Thrown-shaped, but returned (not thrown) by `validateValues`. */
20
+ declare class ValidationError extends Error {
21
+ errors: ValidationIssue[];
22
+ constructor(message: string, errors: ValidationIssue[]);
23
+ }
24
+
25
+ /**
26
+ * Finds `var FB_PUBLIC_LOAD_DATA_ = [ ... ];` in a Google Forms `/viewform`
27
+ * page and `JSON.parse`s the array. The blob is JSON-compatible (no trailing
28
+ * commas, no JS-only syntax) so once we've sliced out exactly the array
29
+ * literal, a plain `JSON.parse` works.
30
+ *
31
+ * We deliberately don't regex the whole array (it can contain `;`, nested
32
+ * `[`/`]`, and escaped strings) — instead we find the assignment, then find
33
+ * the matching closing bracket by walking the string and tracking bracket
34
+ * depth while respecting JSON string-escaping rules.
35
+ */
36
+ declare const extractPublicLoadData: (html: string) => unknown;
37
+
38
+ /**
39
+ * Parses the `FB_PUBLIC_LOAD_DATA_` structure (already `JSON.parse`d, e.g. by
40
+ * `extractPublicLoadData`) into a `FormSchema`, per the verified index map in
41
+ * `docs/research/google-forms-internals.md` §4.
42
+ */
43
+ declare const parseFormData: (data: unknown, opts?: {
44
+ formId?: string;
45
+ }) => FormSchema;
46
+ /** Extracts `FB_PUBLIC_LOAD_DATA_` and `fbzx` from raw `/viewform` HTML, then parses. */
47
+ declare const parseFormHtml: (html: string) => FormSchema;
48
+
49
+ /** Builds a human-facing `/viewform?usp=pp_url&entry.N=...` prefill URL. */
50
+ declare const buildPrefillUrl: (formId: string, values: FormValues, schema?: FormSchema) => string;
51
+ /** Builds the full `application/x-www-form-urlencoded` body for a `formResponse` POST. */
52
+ declare const buildSubmitBody: (formId: string, values: FormValues, schema?: FormSchema) => URLSearchParams;
53
+ type SubmitResult = {
54
+ status: "sent";
55
+ } | {
56
+ status: "ok";
57
+ httpStatus: number;
58
+ } | {
59
+ status: "error";
60
+ error: unknown;
61
+ httpStatus?: number;
62
+ };
63
+ interface SubmitOptions {
64
+ schema?: FormSchema;
65
+ fetch?: typeof fetch;
66
+ mode?: "no-cors" | "cors";
67
+ signal?: AbortSignal;
68
+ }
69
+ /**
70
+ * POSTs `application/x-www-form-urlencoded` to the form's `formResponse`
71
+ * endpoint. Defaults to `mode: "no-cors"` (required for cross-origin browser
72
+ * submission; the response is opaque, so success can only be reported as
73
+ * "sent", never confirmed) — see `docs/research/google-forms-internals.md`
74
+ * §6. Pass `mode: "cors"` from Node/CLI contexts where the response can
75
+ * actually be read.
76
+ */
77
+ declare const submitForm: (formId: string, values: FormValues, opts?: SubmitOptions) => Promise<SubmitResult>;
78
+
79
+ /**
80
+ * Accepts a bare form id, an `e/<id>` string (as stored in
81
+ * `FB_PUBLIC_LOAD_DATA_[14]`), or any `docs.google.com/forms` URL shape
82
+ * (including `/u/<n>/`, `/d/e/<id>/viewform`, `/d/<id>/edit`, with or without
83
+ * query strings/trailing slashes) and returns the bare published id.
84
+ */
85
+ declare const normalizeFormId: (input: string) => string;
86
+ declare const formUrls: (formId: string) => {
87
+ viewform: string;
88
+ formResponse: string;
89
+ };
90
+
91
+ declare const VERSION = "0.1.0";
92
+
93
+ export { ParseError, VERSION, ValidationError, buildPrefillUrl, buildSubmitBody, encodeValues, extractPublicLoadData, formUrls, normalizeFormId, parseFormData, parseFormHtml, submitForm, validateValues };
@@ -0,0 +1,93 @@
1
+ import { FormValues, FormSchema, ValidationResult, ValidationIssue } from '@ez-gform/types';
2
+ export * from '@ez-gform/types';
3
+
4
+ /**
5
+ * Encodes `FormValues` into `entry.NNN`-keyed `URLSearchParams`, per every
6
+ * rule in `docs/research/google-forms-internals.md` §3. Pure: never throws
7
+ * on invalid/unknown data, and never validates against a schema — pair with
8
+ * `validateValues` for that. The second parameter is accepted (and ignored)
9
+ * for call-site compatibility with `validateValues(values, schema)`.
10
+ */
11
+ declare const encodeValues: (values: FormValues, _?: FormSchema) => URLSearchParams;
12
+ /** Validates `values` against a parsed `FormSchema`: unknown entry ids and missing required questions. */
13
+ declare const validateValues: (values: FormValues, schema: FormSchema) => ValidationResult;
14
+
15
+ /** Thrown when `FB_PUBLIC_LOAD_DATA_` (or a caller-supplied value) doesn't match the expected shape. */
16
+ declare class ParseError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ /** Thrown-shaped, but returned (not thrown) by `validateValues`. */
20
+ declare class ValidationError extends Error {
21
+ errors: ValidationIssue[];
22
+ constructor(message: string, errors: ValidationIssue[]);
23
+ }
24
+
25
+ /**
26
+ * Finds `var FB_PUBLIC_LOAD_DATA_ = [ ... ];` in a Google Forms `/viewform`
27
+ * page and `JSON.parse`s the array. The blob is JSON-compatible (no trailing
28
+ * commas, no JS-only syntax) so once we've sliced out exactly the array
29
+ * literal, a plain `JSON.parse` works.
30
+ *
31
+ * We deliberately don't regex the whole array (it can contain `;`, nested
32
+ * `[`/`]`, and escaped strings) — instead we find the assignment, then find
33
+ * the matching closing bracket by walking the string and tracking bracket
34
+ * depth while respecting JSON string-escaping rules.
35
+ */
36
+ declare const extractPublicLoadData: (html: string) => unknown;
37
+
38
+ /**
39
+ * Parses the `FB_PUBLIC_LOAD_DATA_` structure (already `JSON.parse`d, e.g. by
40
+ * `extractPublicLoadData`) into a `FormSchema`, per the verified index map in
41
+ * `docs/research/google-forms-internals.md` §4.
42
+ */
43
+ declare const parseFormData: (data: unknown, opts?: {
44
+ formId?: string;
45
+ }) => FormSchema;
46
+ /** Extracts `FB_PUBLIC_LOAD_DATA_` and `fbzx` from raw `/viewform` HTML, then parses. */
47
+ declare const parseFormHtml: (html: string) => FormSchema;
48
+
49
+ /** Builds a human-facing `/viewform?usp=pp_url&entry.N=...` prefill URL. */
50
+ declare const buildPrefillUrl: (formId: string, values: FormValues, schema?: FormSchema) => string;
51
+ /** Builds the full `application/x-www-form-urlencoded` body for a `formResponse` POST. */
52
+ declare const buildSubmitBody: (formId: string, values: FormValues, schema?: FormSchema) => URLSearchParams;
53
+ type SubmitResult = {
54
+ status: "sent";
55
+ } | {
56
+ status: "ok";
57
+ httpStatus: number;
58
+ } | {
59
+ status: "error";
60
+ error: unknown;
61
+ httpStatus?: number;
62
+ };
63
+ interface SubmitOptions {
64
+ schema?: FormSchema;
65
+ fetch?: typeof fetch;
66
+ mode?: "no-cors" | "cors";
67
+ signal?: AbortSignal;
68
+ }
69
+ /**
70
+ * POSTs `application/x-www-form-urlencoded` to the form's `formResponse`
71
+ * endpoint. Defaults to `mode: "no-cors"` (required for cross-origin browser
72
+ * submission; the response is opaque, so success can only be reported as
73
+ * "sent", never confirmed) — see `docs/research/google-forms-internals.md`
74
+ * §6. Pass `mode: "cors"` from Node/CLI contexts where the response can
75
+ * actually be read.
76
+ */
77
+ declare const submitForm: (formId: string, values: FormValues, opts?: SubmitOptions) => Promise<SubmitResult>;
78
+
79
+ /**
80
+ * Accepts a bare form id, an `e/<id>` string (as stored in
81
+ * `FB_PUBLIC_LOAD_DATA_[14]`), or any `docs.google.com/forms` URL shape
82
+ * (including `/u/<n>/`, `/d/e/<id>/viewform`, `/d/<id>/edit`, with or without
83
+ * query strings/trailing slashes) and returns the bare published id.
84
+ */
85
+ declare const normalizeFormId: (input: string) => string;
86
+ declare const formUrls: (formId: string) => {
87
+ viewform: string;
88
+ formResponse: string;
89
+ };
90
+
91
+ declare const VERSION = "0.1.0";
92
+
93
+ export { ParseError, VERSION, ValidationError, buildPrefillUrl, buildSubmitBody, encodeValues, extractPublicLoadData, formUrls, normalizeFormId, parseFormData, parseFormHtml, submitForm, validateValues };