@marktiderman/genesis-forms 0.2.0 → 0.3.1

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.
Files changed (42) hide show
  1. package/README.md +113 -13
  2. package/conformance/{after-class-back-after-a-jump.scenario.json → event-feedback-back-after-a-jump.scenario.json} +21 -21
  3. package/conformance/{after-class-contact-yes-completed.scenario.json → event-feedback-contact-yes-completed.scenario.json} +21 -21
  4. package/conformance/{after-class-contact-yes-requires-name-and-email.scenario.json → event-feedback-contact-yes-requires-name-and-email.scenario.json} +19 -19
  5. package/conformance/{after-class-happy-path.scenario.json → event-feedback-happy-path.scenario.json} +24 -24
  6. package/conformance/{after-class-invalid-email-rejected.scenario.json → event-feedback-invalid-email-rejected.scenario.json} +19 -19
  7. package/conformance/{after-class-low-rating-skips-went-well.scenario.json → event-feedback-low-rating-skips-went-well.scenario.json} +21 -21
  8. package/conformance/{after-class-server-drops-unknown-and-hidden-answers.scenario.json → event-feedback-server-drops-unknown-and-hidden-answers.scenario.json} +22 -22
  9. package/conformance/{after-class-server-ignores-a-client-sent-coach.scenario.json → event-feedback-server-ignores-a-client-sent-owner.scenario.json} +24 -24
  10. package/conformance/{monthly-pulse-default-ending.scenario.json → monthly-check-in-default-ending.scenario.json} +6 -6
  11. package/conformance/{monthly-pulse-promoter-ending-via-jump.scenario.json → monthly-check-in-promoter-ending-via-jump.scenario.json} +6 -6
  12. package/dist/destinations.cjs +1235 -0
  13. package/dist/destinations.cjs.map +1 -0
  14. package/dist/destinations.d.cts +465 -0
  15. package/dist/destinations.d.ts +465 -0
  16. package/dist/destinations.js +1193 -0
  17. package/dist/destinations.js.map +1 -0
  18. package/dist/{engine-Coc-gQIB.d.ts → engine-CPLtKVew.d.cts} +2 -1
  19. package/dist/{engine-Bj8-mMVb.d.cts → engine-DOrPgFDw.d.ts} +2 -1
  20. package/dist/index.cjs +322 -24
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +102 -5
  23. package/dist/index.d.ts +102 -5
  24. package/dist/index.js +311 -25
  25. package/dist/index.js.map +1 -1
  26. package/dist/server.cjs +249 -22
  27. package/dist/server.cjs.map +1 -1
  28. package/dist/server.d.cts +3 -2
  29. package/dist/server.d.ts +3 -2
  30. package/dist/server.js +249 -22
  31. package/dist/server.js.map +1 -1
  32. package/dist/testing.cjs +253 -22
  33. package/dist/testing.cjs.map +1 -1
  34. package/dist/testing.d.cts +4 -3
  35. package/dist/testing.d.ts +4 -3
  36. package/dist/testing.js +253 -22
  37. package/dist/testing.js.map +1 -1
  38. package/dist/{validate-BbackAjB.d.cts → types-BBKBQ8sp.d.cts} +2 -29
  39. package/dist/{validate-BbackAjB.d.ts → types-BBKBQ8sp.d.ts} +2 -29
  40. package/dist/validate-BXXqi0fq.d.cts +30 -0
  41. package/dist/validate-BjNwF1UE.d.ts +30 -0
  42. package/package.json +14 -2
@@ -0,0 +1,465 @@
1
+ import { J as Json, F as FormDefinition, a as FieldBlock } from './types-BBKBQ8sp.js';
2
+
3
+ /**
4
+ * The contract every destination keeps. A destination sends one checked
5
+ * submission somewhere (an inbox, a spreadsheet, a URL) and says whether a
6
+ * failure is worth retrying. It never throws, never logs, and never puts a
7
+ * secret, an answer or contact details in an error message.
8
+ *
9
+ * Storage is the app's: it keeps the outbox, claims due deliveries, counts
10
+ * attempts and schedules retries. A destination only reports what happened.
11
+ */
12
+
13
+ /** One cell of a flat response row. */
14
+ type Cell = string | number | boolean;
15
+ /**
16
+ * What a destination delivers: a submission `validateSubmission` checked,
17
+ * plus the id the app stored it under. `{ id, ...result.submission }` fits.
18
+ */
19
+ interface DeliverySubmission {
20
+ /** The stored response's id. Every destination records it, so a resend is recognised. */
21
+ id: string;
22
+ /** Answers keyed by field id. */
23
+ answers: Record<string, Json>;
24
+ /** Untrusted URL and host inputs. */
25
+ hidden?: Record<string, Json> | undefined;
26
+ /** Trusted server inputs (account id, location…). */
27
+ context?: Record<string, Json> | undefined;
28
+ ending?: string | null | undefined;
29
+ /** When it was submitted, as the app wants it shown: an ISO time, or a day such as `2026-10-22`. */
30
+ submittedAt?: string | undefined;
31
+ /**
32
+ * Extra leading columns the app adds, in order, after `id` and `submitted_at`:
33
+ * the form's slug and version, an account id, a location. Rows and emails show them;
34
+ * the webhook sends them.
35
+ */
36
+ meta?: Record<string, Cell> | undefined;
37
+ /** Labels the app attached, such as `low_rating`. The webhook sends them; a custom email can read them. */
38
+ tags?: readonly string[] | undefined;
39
+ }
40
+ interface DeliveryOptions {
41
+ /**
42
+ * This delivery's id, stable across its retries. The email destination's
43
+ * idempotency key and the webhook's `webhook-id`. Defaults to the submission id.
44
+ */
45
+ deliveryId?: string | undefined;
46
+ /** What this destination cached last time (`DeliveryResult.state`). */
47
+ state?: Record<string, Json> | undefined;
48
+ /**
49
+ * An earlier attempt of this delivery may have gone through (it timed out,
50
+ * or its lease ran out). Destinations that can look first do: the sheet
51
+ * skips rows whose id is already there.
52
+ */
53
+ redelivery?: boolean | undefined;
54
+ /** Aborts the delivery. Each request also has its own timeout. */
55
+ signal?: AbortSignal | undefined;
56
+ }
57
+ /** One submission in a batch: each carries its own definition and delivery id. */
58
+ interface DeliveryItem {
59
+ submission: DeliverySubmission;
60
+ definition: FormDefinition;
61
+ deliveryId?: string | undefined;
62
+ }
63
+ type DeliveryResult = {
64
+ ok: true;
65
+ /** New state to keep for this destination, when it changed. */
66
+ state?: Record<string, Json>;
67
+ /** Submission ids already there (a redelivery), so nothing was sent for them. */
68
+ skipped?: string[];
69
+ } | DeliveryFailure;
70
+ interface DeliveryFailure {
71
+ ok: false;
72
+ /**
73
+ * `true`: the same request may succeed later (rate limited, the service
74
+ * down, a timeout). `false`: it will not until someone changes the config
75
+ * or credentials (a bad key, an unshared sheet, a 404 URL).
76
+ */
77
+ retryable: boolean;
78
+ /**
79
+ * Set when the service said when to come back (429, 503) or is plainly
80
+ * down: retry after this many seconds. An app may choose not to count the
81
+ * attempt, since it says nothing about the delivery itself.
82
+ */
83
+ retryAfterSeconds?: number;
84
+ /** The HTTP status that refused it, if one did. */
85
+ status?: number;
86
+ /** Safe to store and log: a status and a short reason, never secrets, answers or contact details. */
87
+ error: string;
88
+ }
89
+ /** Config is plain JSON the app stores per destination. Secrets never go in it. */
90
+ type DestinationConfig = Readonly<Record<string, unknown>>;
91
+ interface Destination {
92
+ /** `"email"`, `"sheet"`, `"webhook"`, or an app's own. */
93
+ readonly kind: string;
94
+ /** Deliver one submission. Never throws. */
95
+ deliver(submission: DeliverySubmission, definition: FormDefinition, config: DestinationConfig, options?: DeliveryOptions): Promise<DeliveryResult>;
96
+ /**
97
+ * Deliver several submissions to the same destination in one request where
98
+ * the service allows it. All succeed or all fail together. Never throws.
99
+ */
100
+ deliverBatch?(items: readonly DeliveryItem[], config: DestinationConfig, options?: Omit<DeliveryOptions, "deliveryId">): Promise<DeliveryResult>;
101
+ }
102
+ /**
103
+ * A failure with a message that is safe to store. Destinations throw it
104
+ * internally and `deliveryFailure` turns it into a result; app-written
105
+ * destinations may do the same.
106
+ */
107
+ declare class DeliveryError extends Error {
108
+ name: string;
109
+ readonly retryable: boolean;
110
+ readonly retryAfterSeconds: number | undefined;
111
+ readonly status: number | undefined;
112
+ constructor(message: string, options?: {
113
+ retryable?: boolean | undefined;
114
+ retryAfterSeconds?: number | undefined;
115
+ status?: number | undefined;
116
+ });
117
+ }
118
+ /** Every outbound request gives up after this long, unless the destination is given another `timeoutMs`. */
119
+ declare const REQUEST_TIMEOUT_MS = 10000;
120
+ /**
121
+ * Seconds to wait from a Retry-After header, else the fallback. Only the
122
+ * two forms RFC 9110 defines are read: delta-seconds (digits only) and an
123
+ * IMF-fixdate (`Wed, 21 Oct 2026 07:28:00 GMT`); anything else (`-5`,
124
+ * `1.5`, a loose date) gets the fallback. At least 1 second, at most a day.
125
+ */
126
+ declare function retryAfterSeconds(header: string | null | undefined, fallback?: number, now?: number): number;
127
+ /** Whether an HTTP status is worth retrying unchanged: 408, 425, 429 and 5xx. */
128
+ declare function isRetryableStatus(status: number): boolean;
129
+ /**
130
+ * The error for a refused request. Retryable statuses (see
131
+ * `isRetryableStatus`) wait for the Retry-After header, else 60 seconds;
132
+ * every other status is permanent.
133
+ */
134
+ declare function httpError(what: string, response: Response, detail?: string): DeliveryError;
135
+ /**
136
+ * Any error as a failed result with a safe message. A `DeliveryError` keeps
137
+ * its message; anything else is reduced to its kind ("Timed out", "Network
138
+ * error"), because a raw message may hold a URL with a token in it.
139
+ * Timeouts and aborts are retryable. A `TypeError` is permanent: it is
140
+ * almost always a bug that a retry would only repeat. `timedFetch` turns
141
+ * fetch's own network `TypeError` into a retryable `DeliveryError` first, so
142
+ * an app destination that calls `fetch` directly should do the same.
143
+ */
144
+ declare function deliveryFailure(error: unknown): DeliveryFailure;
145
+ /** Shared by the built-in destinations. */
146
+ interface HttpDeps {
147
+ /** Defaults to the global `fetch`. */
148
+ fetch?: typeof fetch | undefined;
149
+ /** Per request. Default `REQUEST_TIMEOUT_MS`. */
150
+ timeoutMs?: number | undefined;
151
+ }
152
+
153
+ interface RowOptions {
154
+ /**
155
+ * Fields marked `sensitive`: `"include"` their values (default), `"mask"`
156
+ * them as `[sensitive]`, or `"omit"` their columns.
157
+ */
158
+ sensitive?: "include" | "mask" | "omit" | undefined;
159
+ }
160
+ interface ResponseRow {
161
+ /** Cells by column key. */
162
+ row: Record<string, Cell>;
163
+ /** The column keys in order: `id`, `submitted_at`, the meta keys, then one per field key. */
164
+ columns: string[];
165
+ /** Field labels by key, recall filled in from this submission. */
166
+ labels: Record<string, string>;
167
+ }
168
+ declare const SENSITIVE_MASK = "[sensitive]";
169
+ /**
170
+ * One answer as a cell: numbers stay numbers (ratings, NPS), choices show
171
+ * their labels, a list is joined with ", ", yes/no reads "Yes"/"No", and a
172
+ * plugin type's `summarize` is used where it has one.
173
+ */
174
+ declare function answerCell(field: FieldBlock, value: Json | undefined): Cell;
175
+ /**
176
+ * The row for one submission. Columns: `id` (the response id), then
177
+ * `submitted_at` when given, then the app's `meta` columns in order, then
178
+ * one per field key in form order (blank when unanswered or hidden by
179
+ * logic). Content blocks get no column. On an anonymous form, identifying
180
+ * fields get no column at all, whatever the answers hold.
181
+ */
182
+ declare function responseRow(submission: DeliverySubmission, definition: FormDefinition, options?: RowOptions): ResponseRow;
183
+ /**
184
+ * Hidden and server inputs as recall and the webhook may show them: on an
185
+ * anonymous form identifying ones are left out, and sensitive ones are left
186
+ * out unless `sensitive` is `"include"`.
187
+ */
188
+ declare function visibleInputs(submission: DeliverySubmission, definition: FormDefinition, sensitive?: RowOptions["sensitive"]): Record<string, Json>;
189
+ /**
190
+ * A cell that a spreadsheet will never run as a formula when the text is
191
+ * opened elsewhere (a CSV export opened in Excel). Opt-in: use it when
192
+ * writing CSV, not for the sheet destination, whose RAW writes are already
193
+ * safe and would store the `'` literally. Text starting with `=`,
194
+ * `@`, a tab or a carriage return gets a leading `'`, as OWASP recommends;
195
+ * so does text starting with `+` or `-` unless it is only digits and
196
+ * number punctuation (a phone number such as `+1 (555) 010-0000`, `-2.5`),
197
+ * which cannot call a function or name a cell. Numbers and booleans are
198
+ * returned as they are.
199
+ */
200
+ declare function escapeFormula(cell: Cell): Cell;
201
+
202
+ /** One message, ready to send. Addresses are already checked. */
203
+ interface EmailMessage {
204
+ from: string;
205
+ to: string[];
206
+ replyTo?: string | undefined;
207
+ subject: string;
208
+ text: string;
209
+ html: string;
210
+ /** Stable across retries of one delivery: a provider that supports it sends once. */
211
+ idempotencyKey: string;
212
+ }
213
+ /** Sends one message. Throws a `DeliveryError` (or any error) on failure. */
214
+ interface EmailProvider {
215
+ readonly name: string;
216
+ send(message: EmailMessage, options: {
217
+ signal?: AbortSignal | undefined;
218
+ }): Promise<void>;
219
+ }
220
+ interface EmailContent {
221
+ subject: string;
222
+ text: string;
223
+ html: string;
224
+ }
225
+ interface EmailRenderInput {
226
+ submission: DeliverySubmission;
227
+ definition: FormDefinition;
228
+ config: DestinationConfig;
229
+ /** The submission as a row (sensitive values masked unless `sensitive: "include"`). */
230
+ row: ResponseRow;
231
+ }
232
+ /** What the email destination reads from a destination's config. */
233
+ interface EmailConfig {
234
+ /** One address or a list (at most 50). */
235
+ to: string | string[];
236
+ /** Overrides the default subject (still stripped of line breaks). */
237
+ subject?: string;
238
+ }
239
+ interface EmailDestinationOptions {
240
+ provider: EmailProvider;
241
+ /** `"Feedback <feedback@send.example.com>"` or a bare address. */
242
+ from: string;
243
+ /**
244
+ * The Reply-To address: the key of an email field whose answer is used, or
245
+ * a function returning an address (or null for none). Only a valid single
246
+ * address is ever used.
247
+ */
248
+ replyTo?: string | ((input: EmailRenderInput) => string | null | undefined) | undefined;
249
+ /** Builds the subject and bodies. Defaults to `renderEmail`. The subject is sanitized afterwards either way. */
250
+ render?: ((input: EmailRenderInput) => EmailContent) | undefined;
251
+ /** Sensitive fields: `"mask"` (default), `"omit"` or `"include"`. */
252
+ sensitive?: RowOptions["sensitive"];
253
+ }
254
+ /** Resend allows 50 recipients per email. */
255
+ declare const MAX_RECIPIENTS = 50;
256
+ declare function escapeHtml(text: string): string;
257
+ /**
258
+ * A header value on one line: control characters (CR, LF, NUL…) and Unicode
259
+ * line separators become spaces, runs of spaces collapse, and it is cut to
260
+ * `max` characters.
261
+ */
262
+ declare function headerSafe(text: string, max?: number): string;
263
+ /** A bare address that is safe as a header value: valid, one line, no display name. */
264
+ declare function isSafeAddress(value: unknown): value is string;
265
+ /**
266
+ * A From value: a bare address, or `Name <address>` where the name has no
267
+ * quotes, angle brackets or line breaks.
268
+ */
269
+ declare function isSafeFrom(value: string): boolean;
270
+ /** Plain text and HTML bodies from label/value lines, every value escaped. */
271
+ declare function emailBody(lines: readonly (readonly [string, Cell])[], footer?: string): {
272
+ text: string;
273
+ html: string;
274
+ };
275
+ /**
276
+ * The default notification: subject `"<form title>: new response"` (or
277
+ * `config.subject`), then the meta columns, each answered field by label,
278
+ * the ending's title when the form has several, and the response id.
279
+ */
280
+ declare function renderEmail(input: EmailRenderInput): EmailContent;
281
+ /**
282
+ * The idempotency key: the delivery id and the first 16 hex digits of
283
+ * SHA-256 of the sorted recipients, so a retried delivery never sends twice
284
+ * and one whose recipients changed meanwhile is a new send.
285
+ */
286
+ declare function idempotencyKey(deliveryId: string, to: readonly string[]): Promise<string>;
287
+ declare function emailDestination(options: EmailDestinationOptions): Destination;
288
+ declare const RESEND_API = "https://api.resend.com/emails";
289
+ interface ResendOptions extends HttpDeps {
290
+ apiKey: string;
291
+ /** Default `RESEND_API`. */
292
+ endpoint?: string | undefined;
293
+ }
294
+ /**
295
+ * Resend's REST API. Sends the idempotency key, so a retry within Resend's
296
+ * 24-hour window never sends twice. A 409 for a concurrent request with the
297
+ * same key is retried; one for a key reused with a different body is not.
298
+ */
299
+ declare function resendProvider(options: ResendOptions): EmailProvider;
300
+
301
+ /**
302
+ * An access token for a Google service account by the OAuth 2.0 JWT bearer
303
+ * flow: sign a JWT with the account's private key (RS256, WebCrypto) and
304
+ * trade it at Google's token endpoint. No googleapis dependency, so it runs
305
+ * on Node 20+, edge runtimes and Workers alike.
306
+ */
307
+
308
+ declare const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
309
+ declare const SHEETS_SCOPE = "https://www.googleapis.com/auth/spreadsheets";
310
+ interface GoogleServiceAccount {
311
+ /** The service account's `client_email`. */
312
+ clientEmail: string;
313
+ /** Its `private_key`: a PKCS#8 PEM. The `\n`-escaped form env vars often hold is accepted. */
314
+ privateKey: string;
315
+ }
316
+ /** A signed RS256 JWT. Throws a `DeliveryError` when the key is not a valid PKCS#8 RSA key. */
317
+ declare function signJwt(claims: Record<string, unknown>, privateKeyPem: string): Promise<string>;
318
+ /** Forgets cached tokens (tests, or after rotating a key). */
319
+ declare function clearGoogleTokenCache(): void;
320
+ interface GoogleTokenOptions extends HttpDeps {
321
+ scope?: string | undefined;
322
+ now?: (() => number) | undefined;
323
+ signal?: AbortSignal | undefined;
324
+ }
325
+ /** A cached or fresh access token. Throws a `DeliveryError` with a safe message on failure. */
326
+ declare function googleAccessToken(account: GoogleServiceAccount, options?: GoogleTokenOptions): Promise<string>;
327
+
328
+ declare const SHEETS_API = "https://sheets.googleapis.com/v4/spreadsheets";
329
+ declare const DEFAULT_SHEET_NAME = "Responses";
330
+ /** How many of the latest rows a redelivery checks for its id. */
331
+ declare const DEDUPE_WINDOW = 500;
332
+ /**
333
+ * How many rows the header read looks at to find the columns in use: the
334
+ * header row and the first responses under it. Bounded, so a tab with years
335
+ * of responses is never read whole just to learn its header.
336
+ */
337
+ declare const HEADER_SCAN_ROWS = 100;
338
+ /** What the sheet destination reads from a destination's config. */
339
+ interface SheetConfig {
340
+ spreadsheetId: string;
341
+ /** Default `"Responses"`. */
342
+ sheetName?: string;
343
+ }
344
+ interface SheetsDestinationOptions extends HttpDeps {
345
+ /** The service account, or a function that finds the one for a destination's config. */
346
+ account: GoogleServiceAccount | ((config: DestinationConfig) => GoogleServiceAccount | Promise<GoogleServiceAccount>);
347
+ /**
348
+ * Prefix text a spreadsheet could run as a formula with `'` (see
349
+ * `escapeFormula`), headers included. Default false: writes are RAW, so
350
+ * Sheets never runs text as a formula, and the `'` would be stored as part
351
+ * of the text. Turn it on only if the sheet is exported as CSV and opened
352
+ * in another spreadsheet. Keep one setting per sheet: header keys are
353
+ * escaped too, so switching it later adds new columns for escaped keys.
354
+ */
355
+ escapeFormulas?: boolean | undefined;
356
+ /** Sensitive fields: `"include"` (default), `"mask"` or `"omit"`. */
357
+ sensitive?: RowOptions["sensitive"];
358
+ now?: (() => number) | undefined;
359
+ }
360
+ /**
361
+ * The existing header plus any keys it does not have yet, in the order they
362
+ * arrive. A blank header cell keeps its place (dropping it would shift every
363
+ * later column away from the values stored under it), as do blank header
364
+ * cells over the `width` columns that hold responses; only blanks beyond
365
+ * those are trimmed.
366
+ */
367
+ declare function mergeHeader(existing: readonly unknown[], keys: readonly string[], width?: number): string[];
368
+ /** One cell per header column, blank where the row has no value; `escape` applies `escapeFormula` (default false). */
369
+ declare function rowFor(header: readonly string[], row: Record<string, Cell>, escape?: boolean): Cell[];
370
+ /** A1 column letters: 0 → A, 25 → Z, 26 → AA. */
371
+ declare function columnLetter(index: number): string;
372
+ /** What the destination keeps between deliveries. Opaque to the app: store and pass it back. */
373
+ interface SheetState {
374
+ spreadsheet_id: string;
375
+ sheet_name: string;
376
+ sheet_id: number;
377
+ header: string[];
378
+ columns: number;
379
+ /** The last row an append wrote (1-based), 0 when unknown. */
380
+ last_row: number;
381
+ }
382
+ declare function sheetsDestination(options: SheetsDestinationOptions): Destination;
383
+
384
+ /** What the webhook destination reads from a destination's config. */
385
+ interface WebhookConfig {
386
+ url: string;
387
+ }
388
+ interface WebhookDestinationOptions extends HttpDeps {
389
+ /**
390
+ * The signing secret: base64 of at least 16 random bytes, usually with
391
+ * the `whsec_` prefix Standard Webhooks tools issue (`whsec_` plus base64
392
+ * of 24 to 64 bytes is what the spec recommends). The prefix is optional;
393
+ * the rest is always base64-decoded, as every Standard Webhooks library
394
+ * does, so a receiver verifies with the same string. A secret that is not
395
+ * base64 fails permanently. Or a function that finds the one for a
396
+ * destination's config. Never put it in config.
397
+ */
398
+ secret: string | ((config: DestinationConfig) => string | Promise<string>);
399
+ /** Allow `http:` URLs. Default false. */
400
+ allowInsecureUrls?: boolean | undefined;
401
+ /** Allow localhost and private, loopback and link-local address literals. Default false. */
402
+ allowPrivateHosts?: boolean | undefined;
403
+ /** Sensitive fields: `"include"` (default), `"mask"` or `"omit"`. */
404
+ sensitive?: RowOptions["sensitive"];
405
+ /** The event type in the payload. Default `"form.submitted"`. */
406
+ type?: string | undefined;
407
+ now?: (() => number) | undefined;
408
+ }
409
+ /** The JSON body a webhook receives. */
410
+ interface WebhookPayload {
411
+ type: string;
412
+ /** ISO time the delivery was sent. */
413
+ timestamp: string;
414
+ data: {
415
+ form: {
416
+ id: string;
417
+ title: string;
418
+ };
419
+ submission: {
420
+ id: string;
421
+ /** Answers keyed by field id. */
422
+ answers: Record<string, Json>;
423
+ hidden: Record<string, Json>;
424
+ context: Record<string, Json>;
425
+ ending: string | null;
426
+ submittedAt: string | null;
427
+ meta: Record<string, Json>;
428
+ tags: string[];
429
+ };
430
+ /** The same submission as a flat row by column key: labels for choices, as the sheet shows it. */
431
+ row: Record<string, Json>;
432
+ };
433
+ }
434
+ /** The `webhook-signature` value for a message: `v1,<base64>`. */
435
+ declare function signWebhook(secret: string, id: string, timestamp: number, body: string): Promise<string>;
436
+ interface VerifyWebhookInput {
437
+ /** The same secret the sender signs with (base64, optionally `whsec_`-prefixed). */
438
+ secret: string;
439
+ /** The request's headers: a `Headers` or a plain object. */
440
+ headers: Headers | Record<string, string | string[] | undefined>;
441
+ /** The raw body, exactly as received. */
442
+ body: string;
443
+ /** Default 300: a timestamp further off than this is refused. */
444
+ toleranceSeconds?: number | undefined;
445
+ now?: (() => number) | undefined;
446
+ }
447
+ /**
448
+ * Checks a Standard Webhooks request: the timestamp is recent and one of the
449
+ * space-separated `v1` signatures matches. Compares in constant time (by
450
+ * WebCrypto `verify`). Never throws on a malformed request: returns false.
451
+ */
452
+ declare function verifyWebhook(input: VerifyWebhookInput): Promise<boolean>;
453
+ /** Whether a URL hostname is localhost or a private, loopback, link-local or unspecified address literal. */
454
+ declare function isPrivateHost(hostname: string): boolean;
455
+ /** The URL from config, checked. Throws a `DeliveryError` naming the rule it broke (never the URL, which may hold a token). */
456
+ declare function checkWebhookUrl(raw: string, options?: Pick<WebhookDestinationOptions, "allowInsecureUrls" | "allowPrivateHosts">): URL;
457
+ /** The payload for one submission. Identifying answers stay out on anonymous forms. */
458
+ declare function webhookPayload(submission: DeliverySubmission, definition: FormDefinition, options?: {
459
+ sensitive?: RowOptions["sensitive"];
460
+ type?: string | undefined;
461
+ now?: number | undefined;
462
+ }): WebhookPayload;
463
+ declare function webhookDestination(options: WebhookDestinationOptions): Destination;
464
+
465
+ export { type Cell, DEDUPE_WINDOW, DEFAULT_SHEET_NAME, DeliveryError, type DeliveryFailure, type DeliveryItem, type DeliveryOptions, type DeliveryResult, type DeliverySubmission, type Destination, type DestinationConfig, type EmailConfig, type EmailContent, type EmailDestinationOptions, type EmailMessage, type EmailProvider, type EmailRenderInput, GOOGLE_TOKEN_URL, type GoogleServiceAccount, type GoogleTokenOptions, HEADER_SCAN_ROWS, type HttpDeps, MAX_RECIPIENTS, REQUEST_TIMEOUT_MS, RESEND_API, type ResendOptions, type ResponseRow, type RowOptions, SENSITIVE_MASK, SHEETS_API, SHEETS_SCOPE, type SheetConfig, type SheetState, type SheetsDestinationOptions, type VerifyWebhookInput, type WebhookConfig, type WebhookDestinationOptions, type WebhookPayload, answerCell, checkWebhookUrl, clearGoogleTokenCache, columnLetter, deliveryFailure, emailBody, emailDestination, escapeFormula, escapeHtml, googleAccessToken, headerSafe, httpError, idempotencyKey, isPrivateHost, isRetryableStatus, isSafeAddress, isSafeFrom, mergeHeader, renderEmail, resendProvider, responseRow, retryAfterSeconds, rowFor, sheetsDestination, signJwt, signWebhook, verifyWebhook, visibleInputs, webhookDestination, webhookPayload };