@marktiderman/genesis-forms 0.6.0 → 0.8.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/server.d.cts CHANGED
@@ -1,6 +1,8 @@
1
- import { F as FieldValidator, H as HostOptions } from './validate-B5WKHbst.cjs';
2
- export { A as AnswerSnapshot, d as OptionSnapshot, f as RestoredAnswers, h as answerSnapshots, i as answersFromSnapshots, $ as withHostOptions } from './validate-B5WKHbst.cjs';
3
- import { J as Json, e as FormEvent, F as FormDefinition } from './types-oAckGR_Z.cjs';
1
+ import { F as FieldValidator, H as HostOptions } from './validate-irIPubxc.cjs';
2
+ export { A as AnswerSnapshot, d as OptionSnapshot, f as RestoredAnswers, h as answerSnapshots, i as answersFromSnapshots, $ as withHostOptions } from './validate-irIPubxc.cjs';
3
+ import { J as Json, F as FormDefinition, a as FieldBlock, e as FormEvent } from './types-jqDKcmP4.cjs';
4
+ import { S as SignedUpload } from './upload-protocol-BOe018kL.cjs';
5
+ export { f as MAX_UPLOAD_REQUEST_BYTES, j as UploadGrant, k as UploadRefusal, l as UploadRequest } from './upload-protocol-BOe018kL.cjs';
4
6
 
5
7
  /**
6
8
  * Edit links for a sent response: `<id>.<nonce>.<expires>.<sig>`, where sig is the
@@ -18,7 +20,7 @@ import { J as Json, e as FormEvent, F as FormDefinition } from './types-oAckGR_Z
18
20
  * edit links: signing returns null and every token fails to verify.
19
21
  *
20
22
  * Web Crypto only, no storage: the app keeps the nonce and checks it. Moved up from
21
- * Breakthrough's src/lib/forms/edit-link.ts unchanged, so links it signed still verify.
23
+ * The host app's src/lib/forms/edit-link.ts unchanged, so links it signed still verify.
22
24
  */
23
25
  /** How long a new edit link works: 30 days. */
24
26
  declare const EDIT_LINK_TTL_SECONDS: number;
@@ -120,7 +122,7 @@ declare function withEditLinkHeaders(request: Request, response: Response, optio
120
122
  * and a check that fails open. The counter itself is the app's (`RateLimitStore`), so
121
123
  * every server instance can share it (a row in Postgres, a key in Redis).
122
124
  *
123
- * Web Crypto only. Moved up from Breakthrough's src/lib/feedback/rate-limit.server.ts;
125
+ * Web Crypto only. Moved up from the host app's src/lib/feedback/rate-limit.server.ts;
124
126
  * keys made here equal the ones it made, so a deploy doesn't reset anyone's count.
125
127
  */
126
128
  /**
@@ -191,6 +193,197 @@ declare function allow(store: RateLimitStore, check: {
191
193
  message?: string | undefined;
192
194
  }): Promise<boolean>;
193
195
 
196
+ /** What the store records when an upload begins. */
197
+ interface BeginUploadInput {
198
+ formId: string;
199
+ fieldId: string;
200
+ name: string;
201
+ contentType: string;
202
+ size: number;
203
+ /** A new response's attempt (`attemptHash` of its attempt id), or… */
204
+ attemptHash: string | null;
205
+ /** …the id of the sent response an edit link is changing. Exactly one of the two. */
206
+ editOf: string | null;
207
+ }
208
+ /** One upload as the store reads it back, with what storage holds for it. */
209
+ interface StoredUpload {
210
+ id: string;
211
+ fieldId: string;
212
+ attemptHash: string | null;
213
+ editOf: string | null;
214
+ /** The response that took it, once one did. */
215
+ responseId: string | null;
216
+ name: string;
217
+ contentType: string;
218
+ size: number;
219
+ /** Whether the file is in storage (the respondent's browser finished sending it). */
220
+ stored: boolean;
221
+ /** The type and size storage holds, which are trusted over what the browser said. */
222
+ storedType: string | null;
223
+ storedSize: number | null;
224
+ }
225
+ /** What uploads need from the app's database and storage; the app's service-role client, say. */
226
+ interface UploadStore {
227
+ /**
228
+ * Records one upload and names its object in storage; `{ tooMany: true }` once the
229
+ * attempt or edit has as many as the app allows (the host app: 20).
230
+ */
231
+ beginUpload(input: BeginUploadInput): Promise<{
232
+ id: string;
233
+ objectName: string;
234
+ } | {
235
+ tooMany: true;
236
+ }>;
237
+ /** Signs an upload of that object, sent to the browser as `upload` (`SignedUpload`). */
238
+ signUpload(objectName: string): Promise<SignedUpload>;
239
+ /** This form's uploads with these ids; others are left out. */
240
+ uploadRefs(formId: string, ids: string[]): Promise<StoredUpload[]>;
241
+ }
242
+ /** Who may use an upload: a new response's attempt, or an edit of a sent response. */
243
+ type UploadOwner = {
244
+ attemptHash: string;
245
+ } | {
246
+ responseId: string;
247
+ };
248
+ /** The form's upload fields (`UPLOAD_FIELD_TYPES`: photo). */
249
+ declare function uploadFields(definition: FormDefinition): FieldBlock[];
250
+ /** An upload field type's limits and its words for a file it doesn't take. */
251
+ interface UploadRules {
252
+ maxFiles: number;
253
+ maxBytes: number;
254
+ mimeTypes: readonly string[];
255
+ typeMessage: string;
256
+ sizeMessage: string;
257
+ countMessage: string;
258
+ }
259
+ /** The rules for an upload field, or null when the field doesn't take files. */
260
+ declare function uploadRules(field: FieldBlock): UploadRules | null;
261
+ /** Said when a reference isn't a file this respondent finished uploading here. */
262
+ declare const NOT_UPLOADED_MESSAGE = "A photo didn't finish uploading. Remove it and add it again.";
263
+ interface CheckUploadsOptions {
264
+ /** The form's id, as the store keys its uploads. */
265
+ formId: string;
266
+ owner: UploadOwner;
267
+ /** Needed only when the answers name a file. */
268
+ store?: Pick<UploadStore, "uploadRefs"> | undefined;
269
+ }
270
+ type CheckedUploads = {
271
+ ok: true;
272
+ answers: Record<string, Json>;
273
+ } | {
274
+ ok: false;
275
+ errors: Record<string, string>;
276
+ };
277
+ /**
278
+ * Checks every file the answers name: uploaded by this attempt (or for this edit) to this
279
+ * form's field, in storage, of a type and size the field takes, and no more files than it
280
+ * takes. The stored name, type and size replace what the browser sent. Answers without
281
+ * files are returned as they are, without asking the store. Errors are keyed by field id;
282
+ * a store that throws throws (answer 503: nothing can be checked).
283
+ */
284
+ declare function checkUploads(definition: FormDefinition, answers: Record<string, Json>, options: CheckUploadsOptions): Promise<CheckedUploads>;
285
+ /**
286
+ * The stored hash of a visit's attempt id: hex SHA-256 (Web Crypto). It equals
287
+ * The host app's `resumeTokenHash`, so uploads recorded before the move still match.
288
+ */
289
+ declare function attemptHash(attemptId: string): Promise<string>;
290
+
291
+ /** The form an upload is for, as the app found it from the request. */
292
+ interface UploadForm {
293
+ /** The form's id, as the store keys uploads. */
294
+ id: string;
295
+ /** The published definition respondents answer. */
296
+ definition: FormDefinition;
297
+ /** Whether it takes responses now (not paused, closed, full…). */
298
+ open: boolean;
299
+ /** Whose rate-limit count this is: a practice, an account. */
300
+ scope: string;
301
+ /** Whether its edit links work. Default false: no uploads for an edit. */
302
+ allowsEdits?: boolean | undefined;
303
+ /** Names the form in logs, such as its slug. */
304
+ label?: string | undefined;
305
+ }
306
+ /** A sent response an edit link may change, as the app loads it. */
307
+ interface EditableResponse {
308
+ id: string;
309
+ /** The response's current nonce: only a link made with it works. */
310
+ editNonce: string;
311
+ }
312
+ /** The handler's words; each default is what the host app said. */
313
+ interface UploadMessages {
314
+ /** 415 / 400: not a JSON object. */
315
+ notJson: string;
316
+ /** 413: past `maxRequestBytes`. */
317
+ tooLong: string;
318
+ /** 400: no attempt id, or not a UUID. */
319
+ reload: string;
320
+ /** 404: `resolveForm` found no form. */
321
+ unavailable: string;
322
+ /** 409: the form isn't open. */
323
+ notOpen: string;
324
+ /** 409 (`code: "closed"`): an edit while the form isn't open. */
325
+ notTakingChanges: string;
326
+ /** 403 (`code: "bad_link"`): an edit link that doesn't check out, or a form without edits. */
327
+ badEditLink: string;
328
+ /** 400: the field isn't an upload field of this form. */
329
+ notUploadField: string;
330
+ /** 429: the rate limit. */
331
+ rateLimited: string;
332
+ /** 429 (`code: "too_many_photos"`): the store has as many uploads for this attempt or edit as it allows. */
333
+ tooMany: string;
334
+ /** 503 / 500: something failed on the server; nothing more is said. */
335
+ failed: string;
336
+ }
337
+ declare const UPLOAD_MESSAGES: UploadMessages;
338
+ type Secret = string | null | undefined;
339
+ interface UploadHandlerOptions<P = void> {
340
+ /**
341
+ * The form the request is for (from its host, path or `params`), or null (404). A throw
342
+ * answers 503 and logs the error's message.
343
+ */
344
+ resolveForm(request: Request, params: P): Promise<UploadForm | null>;
345
+ store: UploadStore;
346
+ /**
347
+ * Uploads for an edit link (`edit` in the request). Without it, or without a usable
348
+ * secret, every edit link is refused (403).
349
+ */
350
+ edit?: {
351
+ /** The edit-link secret, or a reader of it (`createEditSecretReader`). */
352
+ secret: Secret | (() => Secret);
353
+ /**
354
+ * The sent response with this id, of this form (and its scope), or null (403).
355
+ * A throw answers 503.
356
+ */
357
+ response(id: string, form: UploadForm): Promise<EditableResponse | null>;
358
+ } | undefined;
359
+ /** Counted per scope and client address, as `kind: "upload"`. Without it, nothing is counted. */
360
+ rateLimit?: {
361
+ store: RateLimitStore;
362
+ /** The server secret the address is hashed with (`rateLimitKey`). */
363
+ secret: string;
364
+ /** Default `RATE_LIMIT_DEFAULTS.upload`: 120 per 600 seconds. */
365
+ limit?: number | undefined;
366
+ windowSeconds?: number | undefined;
367
+ /** Default `DEFAULT_TRUSTED_IP_HEADERS`. */
368
+ trustedIpHeaders?: readonly string[] | undefined;
369
+ } | undefined;
370
+ /** The stored hash of an attempt id. Default `attemptHash` (hex SHA-256). */
371
+ hashAttempt?: ((attemptId: string) => string | Promise<string>) | undefined;
372
+ /** Default `MAX_UPLOAD_REQUEST_BYTES` (4,000). */
373
+ maxRequestBytes?: number | undefined;
374
+ /** The current time (tests): edit links expire. */
375
+ now?: (() => Date) | undefined;
376
+ /** Where failures are said; `console.error` with an `[upload]` prefix by default. Never sent to the browser. */
377
+ log?: ((message: string) => void) | undefined;
378
+ messages?: Partial<UploadMessages> | undefined;
379
+ }
380
+ /**
381
+ * The upload endpoint: `(request, params) => Response`, for a POST route. `params` is
382
+ * whatever the route has (a form slug, say), passed to `resolveForm`. See the README's
383
+ * "Uploads" for the JSON and every status.
384
+ */
385
+ declare function createUploadHandler<P = void>(options: UploadHandlerOptions<P>): (request: Request, params: P) => Promise<Response>;
386
+
194
387
  interface SubmissionInput {
195
388
  /** Answers keyed by field id. Ignored when `events` is given. */
196
389
  answers?: Record<string, Json> | undefined;
@@ -278,4 +471,4 @@ declare function validateSubmission(published: FormDefinition, submission: Submi
278
471
  */
279
472
  declare function publicDefinition(definition: FormDefinition): FormDefinition;
280
473
 
281
- export { type CheckedSubmission, DEFAULT_TRUSTED_IP_HEADERS, EDIT_LINK_PARAM, EDIT_LINK_TTL_SECONDS, type EditLinkClaims, type EditLinkOptions, type EditLinkRequestOptions, type EditLinkUrlOptions, type EditSecretLog, type EditSecretReader, HostOptions, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, MIN_EDIT_SECRET_BYTES, RATE_LIMIT_DEFAULTS, RAW_EVENTS_PER_EVENT, type RateLimitStore, type SubmissionInput, type SubmissionOptions, type SubmissionResult, addressPrefix, allow, clientIp, createEditSecretReader, editLinkUrl, isEditLinkRequest, isUsableEditSecret, publicDefinition, rateLimitKey, signEditToken, validateSubmission, verifyEditToken, withEditLinkHeaders, withEditToken };
474
+ export { type BeginUploadInput, type CheckUploadsOptions, type CheckedSubmission, type CheckedUploads, DEFAULT_TRUSTED_IP_HEADERS, EDIT_LINK_PARAM, EDIT_LINK_TTL_SECONDS, type EditLinkClaims, type EditLinkOptions, type EditLinkRequestOptions, type EditLinkUrlOptions, type EditSecretLog, type EditSecretReader, type EditableResponse, HostOptions, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, MIN_EDIT_SECRET_BYTES, NOT_UPLOADED_MESSAGE, RATE_LIMIT_DEFAULTS, RAW_EVENTS_PER_EVENT, type RateLimitStore, SignedUpload, type StoredUpload, type SubmissionInput, type SubmissionOptions, type SubmissionResult, UPLOAD_MESSAGES, type UploadForm, type UploadHandlerOptions, type UploadMessages, type UploadOwner, type UploadRules, type UploadStore, addressPrefix, allow, attemptHash, checkUploads, clientIp, createEditSecretReader, createUploadHandler, editLinkUrl, isEditLinkRequest, isUsableEditSecret, publicDefinition, rateLimitKey, signEditToken, uploadFields, uploadRules, validateSubmission, verifyEditToken, withEditLinkHeaders, withEditToken };
package/dist/server.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { F as FieldValidator, H as HostOptions } from './validate-GpTXPwyl.js';
2
- export { A as AnswerSnapshot, d as OptionSnapshot, f as RestoredAnswers, h as answerSnapshots, i as answersFromSnapshots, $ as withHostOptions } from './validate-GpTXPwyl.js';
3
- import { J as Json, e as FormEvent, F as FormDefinition } from './types-oAckGR_Z.js';
1
+ import { F as FieldValidator, H as HostOptions } from './validate-DvfQHYnu.js';
2
+ export { A as AnswerSnapshot, d as OptionSnapshot, f as RestoredAnswers, h as answerSnapshots, i as answersFromSnapshots, $ as withHostOptions } from './validate-DvfQHYnu.js';
3
+ import { J as Json, F as FormDefinition, a as FieldBlock, e as FormEvent } from './types-jqDKcmP4.js';
4
+ import { S as SignedUpload } from './upload-protocol-BUO74WkS.js';
5
+ export { f as MAX_UPLOAD_REQUEST_BYTES, j as UploadGrant, k as UploadRefusal, l as UploadRequest } from './upload-protocol-BUO74WkS.js';
4
6
 
5
7
  /**
6
8
  * Edit links for a sent response: `<id>.<nonce>.<expires>.<sig>`, where sig is the
@@ -18,7 +20,7 @@ import { J as Json, e as FormEvent, F as FormDefinition } from './types-oAckGR_Z
18
20
  * edit links: signing returns null and every token fails to verify.
19
21
  *
20
22
  * Web Crypto only, no storage: the app keeps the nonce and checks it. Moved up from
21
- * Breakthrough's src/lib/forms/edit-link.ts unchanged, so links it signed still verify.
23
+ * The host app's src/lib/forms/edit-link.ts unchanged, so links it signed still verify.
22
24
  */
23
25
  /** How long a new edit link works: 30 days. */
24
26
  declare const EDIT_LINK_TTL_SECONDS: number;
@@ -120,7 +122,7 @@ declare function withEditLinkHeaders(request: Request, response: Response, optio
120
122
  * and a check that fails open. The counter itself is the app's (`RateLimitStore`), so
121
123
  * every server instance can share it (a row in Postgres, a key in Redis).
122
124
  *
123
- * Web Crypto only. Moved up from Breakthrough's src/lib/feedback/rate-limit.server.ts;
125
+ * Web Crypto only. Moved up from the host app's src/lib/feedback/rate-limit.server.ts;
124
126
  * keys made here equal the ones it made, so a deploy doesn't reset anyone's count.
125
127
  */
126
128
  /**
@@ -191,6 +193,197 @@ declare function allow(store: RateLimitStore, check: {
191
193
  message?: string | undefined;
192
194
  }): Promise<boolean>;
193
195
 
196
+ /** What the store records when an upload begins. */
197
+ interface BeginUploadInput {
198
+ formId: string;
199
+ fieldId: string;
200
+ name: string;
201
+ contentType: string;
202
+ size: number;
203
+ /** A new response's attempt (`attemptHash` of its attempt id), or… */
204
+ attemptHash: string | null;
205
+ /** …the id of the sent response an edit link is changing. Exactly one of the two. */
206
+ editOf: string | null;
207
+ }
208
+ /** One upload as the store reads it back, with what storage holds for it. */
209
+ interface StoredUpload {
210
+ id: string;
211
+ fieldId: string;
212
+ attemptHash: string | null;
213
+ editOf: string | null;
214
+ /** The response that took it, once one did. */
215
+ responseId: string | null;
216
+ name: string;
217
+ contentType: string;
218
+ size: number;
219
+ /** Whether the file is in storage (the respondent's browser finished sending it). */
220
+ stored: boolean;
221
+ /** The type and size storage holds, which are trusted over what the browser said. */
222
+ storedType: string | null;
223
+ storedSize: number | null;
224
+ }
225
+ /** What uploads need from the app's database and storage; the app's service-role client, say. */
226
+ interface UploadStore {
227
+ /**
228
+ * Records one upload and names its object in storage; `{ tooMany: true }` once the
229
+ * attempt or edit has as many as the app allows (the host app: 20).
230
+ */
231
+ beginUpload(input: BeginUploadInput): Promise<{
232
+ id: string;
233
+ objectName: string;
234
+ } | {
235
+ tooMany: true;
236
+ }>;
237
+ /** Signs an upload of that object, sent to the browser as `upload` (`SignedUpload`). */
238
+ signUpload(objectName: string): Promise<SignedUpload>;
239
+ /** This form's uploads with these ids; others are left out. */
240
+ uploadRefs(formId: string, ids: string[]): Promise<StoredUpload[]>;
241
+ }
242
+ /** Who may use an upload: a new response's attempt, or an edit of a sent response. */
243
+ type UploadOwner = {
244
+ attemptHash: string;
245
+ } | {
246
+ responseId: string;
247
+ };
248
+ /** The form's upload fields (`UPLOAD_FIELD_TYPES`: photo). */
249
+ declare function uploadFields(definition: FormDefinition): FieldBlock[];
250
+ /** An upload field type's limits and its words for a file it doesn't take. */
251
+ interface UploadRules {
252
+ maxFiles: number;
253
+ maxBytes: number;
254
+ mimeTypes: readonly string[];
255
+ typeMessage: string;
256
+ sizeMessage: string;
257
+ countMessage: string;
258
+ }
259
+ /** The rules for an upload field, or null when the field doesn't take files. */
260
+ declare function uploadRules(field: FieldBlock): UploadRules | null;
261
+ /** Said when a reference isn't a file this respondent finished uploading here. */
262
+ declare const NOT_UPLOADED_MESSAGE = "A photo didn't finish uploading. Remove it and add it again.";
263
+ interface CheckUploadsOptions {
264
+ /** The form's id, as the store keys its uploads. */
265
+ formId: string;
266
+ owner: UploadOwner;
267
+ /** Needed only when the answers name a file. */
268
+ store?: Pick<UploadStore, "uploadRefs"> | undefined;
269
+ }
270
+ type CheckedUploads = {
271
+ ok: true;
272
+ answers: Record<string, Json>;
273
+ } | {
274
+ ok: false;
275
+ errors: Record<string, string>;
276
+ };
277
+ /**
278
+ * Checks every file the answers name: uploaded by this attempt (or for this edit) to this
279
+ * form's field, in storage, of a type and size the field takes, and no more files than it
280
+ * takes. The stored name, type and size replace what the browser sent. Answers without
281
+ * files are returned as they are, without asking the store. Errors are keyed by field id;
282
+ * a store that throws throws (answer 503: nothing can be checked).
283
+ */
284
+ declare function checkUploads(definition: FormDefinition, answers: Record<string, Json>, options: CheckUploadsOptions): Promise<CheckedUploads>;
285
+ /**
286
+ * The stored hash of a visit's attempt id: hex SHA-256 (Web Crypto). It equals
287
+ * The host app's `resumeTokenHash`, so uploads recorded before the move still match.
288
+ */
289
+ declare function attemptHash(attemptId: string): Promise<string>;
290
+
291
+ /** The form an upload is for, as the app found it from the request. */
292
+ interface UploadForm {
293
+ /** The form's id, as the store keys uploads. */
294
+ id: string;
295
+ /** The published definition respondents answer. */
296
+ definition: FormDefinition;
297
+ /** Whether it takes responses now (not paused, closed, full…). */
298
+ open: boolean;
299
+ /** Whose rate-limit count this is: a practice, an account. */
300
+ scope: string;
301
+ /** Whether its edit links work. Default false: no uploads for an edit. */
302
+ allowsEdits?: boolean | undefined;
303
+ /** Names the form in logs, such as its slug. */
304
+ label?: string | undefined;
305
+ }
306
+ /** A sent response an edit link may change, as the app loads it. */
307
+ interface EditableResponse {
308
+ id: string;
309
+ /** The response's current nonce: only a link made with it works. */
310
+ editNonce: string;
311
+ }
312
+ /** The handler's words; each default is what the host app said. */
313
+ interface UploadMessages {
314
+ /** 415 / 400: not a JSON object. */
315
+ notJson: string;
316
+ /** 413: past `maxRequestBytes`. */
317
+ tooLong: string;
318
+ /** 400: no attempt id, or not a UUID. */
319
+ reload: string;
320
+ /** 404: `resolveForm` found no form. */
321
+ unavailable: string;
322
+ /** 409: the form isn't open. */
323
+ notOpen: string;
324
+ /** 409 (`code: "closed"`): an edit while the form isn't open. */
325
+ notTakingChanges: string;
326
+ /** 403 (`code: "bad_link"`): an edit link that doesn't check out, or a form without edits. */
327
+ badEditLink: string;
328
+ /** 400: the field isn't an upload field of this form. */
329
+ notUploadField: string;
330
+ /** 429: the rate limit. */
331
+ rateLimited: string;
332
+ /** 429 (`code: "too_many_photos"`): the store has as many uploads for this attempt or edit as it allows. */
333
+ tooMany: string;
334
+ /** 503 / 500: something failed on the server; nothing more is said. */
335
+ failed: string;
336
+ }
337
+ declare const UPLOAD_MESSAGES: UploadMessages;
338
+ type Secret = string | null | undefined;
339
+ interface UploadHandlerOptions<P = void> {
340
+ /**
341
+ * The form the request is for (from its host, path or `params`), or null (404). A throw
342
+ * answers 503 and logs the error's message.
343
+ */
344
+ resolveForm(request: Request, params: P): Promise<UploadForm | null>;
345
+ store: UploadStore;
346
+ /**
347
+ * Uploads for an edit link (`edit` in the request). Without it, or without a usable
348
+ * secret, every edit link is refused (403).
349
+ */
350
+ edit?: {
351
+ /** The edit-link secret, or a reader of it (`createEditSecretReader`). */
352
+ secret: Secret | (() => Secret);
353
+ /**
354
+ * The sent response with this id, of this form (and its scope), or null (403).
355
+ * A throw answers 503.
356
+ */
357
+ response(id: string, form: UploadForm): Promise<EditableResponse | null>;
358
+ } | undefined;
359
+ /** Counted per scope and client address, as `kind: "upload"`. Without it, nothing is counted. */
360
+ rateLimit?: {
361
+ store: RateLimitStore;
362
+ /** The server secret the address is hashed with (`rateLimitKey`). */
363
+ secret: string;
364
+ /** Default `RATE_LIMIT_DEFAULTS.upload`: 120 per 600 seconds. */
365
+ limit?: number | undefined;
366
+ windowSeconds?: number | undefined;
367
+ /** Default `DEFAULT_TRUSTED_IP_HEADERS`. */
368
+ trustedIpHeaders?: readonly string[] | undefined;
369
+ } | undefined;
370
+ /** The stored hash of an attempt id. Default `attemptHash` (hex SHA-256). */
371
+ hashAttempt?: ((attemptId: string) => string | Promise<string>) | undefined;
372
+ /** Default `MAX_UPLOAD_REQUEST_BYTES` (4,000). */
373
+ maxRequestBytes?: number | undefined;
374
+ /** The current time (tests): edit links expire. */
375
+ now?: (() => Date) | undefined;
376
+ /** Where failures are said; `console.error` with an `[upload]` prefix by default. Never sent to the browser. */
377
+ log?: ((message: string) => void) | undefined;
378
+ messages?: Partial<UploadMessages> | undefined;
379
+ }
380
+ /**
381
+ * The upload endpoint: `(request, params) => Response`, for a POST route. `params` is
382
+ * whatever the route has (a form slug, say), passed to `resolveForm`. See the README's
383
+ * "Uploads" for the JSON and every status.
384
+ */
385
+ declare function createUploadHandler<P = void>(options: UploadHandlerOptions<P>): (request: Request, params: P) => Promise<Response>;
386
+
194
387
  interface SubmissionInput {
195
388
  /** Answers keyed by field id. Ignored when `events` is given. */
196
389
  answers?: Record<string, Json> | undefined;
@@ -278,4 +471,4 @@ declare function validateSubmission(published: FormDefinition, submission: Submi
278
471
  */
279
472
  declare function publicDefinition(definition: FormDefinition): FormDefinition;
280
473
 
281
- export { type CheckedSubmission, DEFAULT_TRUSTED_IP_HEADERS, EDIT_LINK_PARAM, EDIT_LINK_TTL_SECONDS, type EditLinkClaims, type EditLinkOptions, type EditLinkRequestOptions, type EditLinkUrlOptions, type EditSecretLog, type EditSecretReader, HostOptions, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, MIN_EDIT_SECRET_BYTES, RATE_LIMIT_DEFAULTS, RAW_EVENTS_PER_EVENT, type RateLimitStore, type SubmissionInput, type SubmissionOptions, type SubmissionResult, addressPrefix, allow, clientIp, createEditSecretReader, editLinkUrl, isEditLinkRequest, isUsableEditSecret, publicDefinition, rateLimitKey, signEditToken, validateSubmission, verifyEditToken, withEditLinkHeaders, withEditToken };
474
+ export { type BeginUploadInput, type CheckUploadsOptions, type CheckedSubmission, type CheckedUploads, DEFAULT_TRUSTED_IP_HEADERS, EDIT_LINK_PARAM, EDIT_LINK_TTL_SECONDS, type EditLinkClaims, type EditLinkOptions, type EditLinkRequestOptions, type EditLinkUrlOptions, type EditSecretLog, type EditSecretReader, type EditableResponse, HostOptions, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, MIN_EDIT_SECRET_BYTES, NOT_UPLOADED_MESSAGE, RATE_LIMIT_DEFAULTS, RAW_EVENTS_PER_EVENT, type RateLimitStore, SignedUpload, type StoredUpload, type SubmissionInput, type SubmissionOptions, type SubmissionResult, UPLOAD_MESSAGES, type UploadForm, type UploadHandlerOptions, type UploadMessages, type UploadOwner, type UploadRules, type UploadStore, addressPrefix, allow, attemptHash, checkUploads, clientIp, createEditSecretReader, createUploadHandler, editLinkUrl, isEditLinkRequest, isUsableEditSecret, publicDefinition, rateLimitKey, signEditToken, uploadFields, uploadRules, validateSubmission, verifyEditToken, withEditLinkHeaders, withEditToken };