@marktiderman/genesis-forms 0.4.2 → 0.6.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/README.md +195 -0
- package/dist/destinations.cjs +147 -7
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +1 -1
- package/dist/destinations.d.ts +1 -1
- package/dist/destinations.js +147 -7
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-BkNuzlna.d.cts → engine-CpfOz8FN.d.ts} +33 -3
- package/dist/{engine-Dz9J5ej2.d.ts → engine-DC4YTPdr.d.cts} +33 -3
- package/dist/index.cjs +734 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +142 -75
- package/dist/index.d.ts +142 -75
- package/dist/index.js +687 -70
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +721 -26
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +201 -4
- package/dist/server.d.ts +201 -4
- package/dist/server.js +702 -27
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +494 -26
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -4
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +494 -26
- package/dist/testing.js.map +1 -1
- package/dist/{types-BBKBQ8sp.d.cts → types-oAckGR_Z.d.cts} +31 -1
- package/dist/{types-BBKBQ8sp.d.ts → types-oAckGR_Z.d.ts} +31 -1
- package/dist/validate-B5WKHbst.d.cts +322 -0
- package/dist/validate-GpTXPwyl.d.ts +322 -0
- package/package.json +1 -1
- package/dist/validate-BXXqi0fq.d.cts +0 -30
- package/dist/validate-BjNwF1UE.d.ts +0 -30
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,195 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Edit links for a sent response: `<id>.<nonce>.<expires>.<sig>`, where sig is the
|
|
7
|
+
* base64url HMAC-SHA256 of `edit-link:v2:<id>.<nonce>.<expires>` under a server-only
|
|
8
|
+
* secret of at least 32 bytes.
|
|
9
|
+
*
|
|
10
|
+
* - id: the response's uuid.
|
|
11
|
+
* - nonce: the response's current edit nonce, kept by the app. The app refuses a link
|
|
12
|
+
* whose nonce is no longer the response's, so changing the nonce revokes every link
|
|
13
|
+
* made before (change it after each edit: only the newest link works).
|
|
14
|
+
* - expires: when the link stops working, in Unix seconds, about 30 days after it was
|
|
15
|
+
* made (`EDIT_LINK_TTL_SECONDS`).
|
|
16
|
+
*
|
|
17
|
+
* Without a usable secret (missing, or shorter than `MIN_EDIT_SECRET_BYTES`) there are no
|
|
18
|
+
* edit links: signing returns null and every token fails to verify.
|
|
19
|
+
*
|
|
20
|
+
* 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.
|
|
22
|
+
*/
|
|
23
|
+
/** How long a new edit link works: 30 days. */
|
|
24
|
+
declare const EDIT_LINK_TTL_SECONDS: number;
|
|
25
|
+
/** The shortest secret edit links are signed with: 32 bytes (UTF-8), as long as the HMAC. */
|
|
26
|
+
declare const MIN_EDIT_SECRET_BYTES = 32;
|
|
27
|
+
/** The query parameter an edit link's token travels in: `?edit=<token>`. */
|
|
28
|
+
declare const EDIT_LINK_PARAM = "edit";
|
|
29
|
+
/** Whether `secret` can sign edit links: a string of at least `MIN_EDIT_SECRET_BYTES` bytes. */
|
|
30
|
+
declare function isUsableEditSecret(secret: string | null | undefined): secret is string;
|
|
31
|
+
/** What an edit link says: the response, its nonce when the link was made, and when it stops working. */
|
|
32
|
+
interface EditLinkClaims {
|
|
33
|
+
/** The response's id, in lower case. */
|
|
34
|
+
id: string;
|
|
35
|
+
nonce: string;
|
|
36
|
+
/** Unix seconds. */
|
|
37
|
+
expiresAt: number;
|
|
38
|
+
}
|
|
39
|
+
interface EditLinkOptions {
|
|
40
|
+
/** The current time in milliseconds (tests); `Date.now()` by default. */
|
|
41
|
+
now?: number | undefined;
|
|
42
|
+
/** How long the link works, in seconds; `EDIT_LINK_TTL_SECONDS` by default. */
|
|
43
|
+
ttlSeconds?: number | undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The edit token for a response and its current nonce, working for `ttlSeconds` from
|
|
47
|
+
* now; null when there is no usable secret (no edit links) or the id or nonce is malformed.
|
|
48
|
+
*/
|
|
49
|
+
declare function signEditToken(link: {
|
|
50
|
+
id: string;
|
|
51
|
+
nonce: string;
|
|
52
|
+
}, secret: string | null | undefined, options?: EditLinkOptions): Promise<string | null>;
|
|
53
|
+
/**
|
|
54
|
+
* What a token says, or null when it is malformed, its signature does not match, it has
|
|
55
|
+
* expired (or claims to last longer than `ttlSeconds` from now), or there is no usable
|
|
56
|
+
* secret. Whether the nonce is still the response's is the caller's check. The
|
|
57
|
+
* comparison is Web Crypto's HMAC verify, which runs in constant time.
|
|
58
|
+
*/
|
|
59
|
+
declare function verifyEditToken(token: string | null | undefined, secret: string | null | undefined, options?: EditLinkOptions): Promise<EditLinkClaims | null>;
|
|
60
|
+
/**
|
|
61
|
+
* A form link with the edit token added: `<formUrl>?edit=<token>` (or `&edit=` when it
|
|
62
|
+
* has a query). `param` names the parameter; `EDIT_LINK_PARAM` by default.
|
|
63
|
+
*/
|
|
64
|
+
declare function withEditToken(formUrl: string, token: string, param?: string): string;
|
|
65
|
+
interface EditLinkUrlOptions extends EditLinkOptions {
|
|
66
|
+
/** The query parameter; `EDIT_LINK_PARAM` by default. */
|
|
67
|
+
param?: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The link that reopens a response for editing: `formUrl` (the page that shows the form,
|
|
71
|
+
* with any query it needs) plus a token signed for the response's current nonce. Null
|
|
72
|
+
* when there are no edit links (no usable secret) or the id or nonce is malformed.
|
|
73
|
+
*/
|
|
74
|
+
declare function editLinkUrl(formUrl: string, link: {
|
|
75
|
+
id: string;
|
|
76
|
+
nonce: string;
|
|
77
|
+
}, secret: string | null | undefined, options?: EditLinkUrlOptions): Promise<string | null>;
|
|
78
|
+
/** Where the edit-secret reader reports a missing or short secret. */
|
|
79
|
+
type EditSecretLog = (message: string) => void;
|
|
80
|
+
/** Reads the edit-link secret from an environment, warning once when there is none. */
|
|
81
|
+
interface EditSecretReader {
|
|
82
|
+
/** The secret, or null (no edit links) when it is missing or too short; warns once, never with the secret. */
|
|
83
|
+
(env: Record<string, string | undefined>, log?: EditSecretLog): string | null;
|
|
84
|
+
/** Tests only: warn again. */
|
|
85
|
+
reset(): void;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A reader for the environment variable that holds the edit-link secret (such as
|
|
89
|
+
* `FEEDBACK_EDIT_SECRET`). Without one of at least `MIN_EDIT_SECRET_BYTES` bytes there are
|
|
90
|
+
* no edit links, and the first read says so through `log` (the call's, else the
|
|
91
|
+
* reader's, else `console.warn`); later reads stay quiet until `reset()`.
|
|
92
|
+
*/
|
|
93
|
+
declare function createEditSecretReader(options: {
|
|
94
|
+
variable: string;
|
|
95
|
+
log?: EditSecretLog | undefined;
|
|
96
|
+
}): EditSecretReader;
|
|
97
|
+
interface EditLinkRequestOptions {
|
|
98
|
+
/** The query parameter; `EDIT_LINK_PARAM` by default. */
|
|
99
|
+
param?: string | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the URL is a page that shows a form (an edit link is only honoured there,
|
|
102
|
+
* such as `/f/<slug>`). Every URL by default.
|
|
103
|
+
*/
|
|
104
|
+
isFormPage?: ((url: URL) => boolean) | undefined;
|
|
105
|
+
}
|
|
106
|
+
/** Whether this request is a form page opened with an edit link (the parameter present, even empty). */
|
|
107
|
+
declare function isEditLinkRequest(url: URL, options?: EditLinkRequestOptions): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* The response, with `Referrer-Policy: no-referrer` and `Cache-Control: private, no-store`
|
|
110
|
+
* added when the request carried an edit link; otherwise the same response, untouched.
|
|
111
|
+
* The link is a credential, so it never leaves in a Referer header (an HTTP header, not
|
|
112
|
+
* only the page's meta tag, so it holds before the page has parsed and for every request
|
|
113
|
+
* the page makes), and the page, prefilled with the response, is never cached.
|
|
114
|
+
*/
|
|
115
|
+
declare function withEditLinkHeaders(request: Request, response: Response, options?: EditLinkRequestOptions): Response;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The pieces of a submit endpoint's rate limit that aren't storage: the client's
|
|
119
|
+
* address, the part of it that names one client, a key that never holds the address,
|
|
120
|
+
* and a check that fails open. The counter itself is the app's (`RateLimitStore`), so
|
|
121
|
+
* every server instance can share it (a row in Postgres, a key in Redis).
|
|
122
|
+
*
|
|
123
|
+
* Web Crypto only. Moved up from Breakthrough's src/lib/feedback/rate-limit.server.ts;
|
|
124
|
+
* keys made here equal the ones it made, so a deploy doesn't reset anyone's count.
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* The headers a proxy reports the client's address in, most trusted first. Trust only
|
|
128
|
+
* headers your proxy sets (and overwrites): a client can send any of them itself.
|
|
129
|
+
*/
|
|
130
|
+
declare const DEFAULT_TRUSTED_IP_HEADERS: readonly string[];
|
|
131
|
+
/**
|
|
132
|
+
* How many submits, saves as they go and photo uploads one client may make per scope
|
|
133
|
+
* (a practice, an account) in a window.
|
|
134
|
+
*
|
|
135
|
+
* - submit: 60 responses per 10 minutes: a class at one gym's Wi-Fi fits.
|
|
136
|
+
* - progress: saving answers as they go. A visit counts once, against `submit`, at its
|
|
137
|
+
* first save; its later saves (each Next, the send, a reload picking up where it was)
|
|
138
|
+
* count here, in the same window: 60 people answering 10 questions each on one Wi-Fi fit.
|
|
139
|
+
* - upload: 120 photos per 10 minutes: that class, a couple of photos each.
|
|
140
|
+
*/
|
|
141
|
+
declare const RATE_LIMIT_DEFAULTS: {
|
|
142
|
+
readonly submit: {
|
|
143
|
+
readonly limit: 60;
|
|
144
|
+
readonly windowSeconds: 600;
|
|
145
|
+
};
|
|
146
|
+
readonly progress: {
|
|
147
|
+
readonly limit: 900;
|
|
148
|
+
readonly windowSeconds: 600;
|
|
149
|
+
};
|
|
150
|
+
readonly upload: {
|
|
151
|
+
readonly limit: 120;
|
|
152
|
+
readonly windowSeconds: 600;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* The client's address as the proxy reports it, used only as a rate-limit key: the first
|
|
157
|
+
* entry of the first of `trusted` that is set, trimmed, else `"unknown"`.
|
|
158
|
+
*/
|
|
159
|
+
declare function clientIp(headers: Pick<Headers, "get">, trusted?: readonly string[]): string;
|
|
160
|
+
/** The part of an address that names one client: an IPv4 address whole, an IPv6 address by its /64. */
|
|
161
|
+
declare function addressPrefix(ip: string): string;
|
|
162
|
+
/**
|
|
163
|
+
* The stored key, `<kind>:<scope>:<hex HMAC-SHA256 of the address prefix>`: what is
|
|
164
|
+
* counted (such as `submit`, `progress`, `upload`), whose count it is (a practice id),
|
|
165
|
+
* and the client, which is never stored as its address. `secret` is a server secret.
|
|
166
|
+
*/
|
|
167
|
+
declare function rateLimitKey(input: {
|
|
168
|
+
kind: string;
|
|
169
|
+
scope: string;
|
|
170
|
+
ip: string;
|
|
171
|
+
secret: string;
|
|
172
|
+
}): Promise<string>;
|
|
173
|
+
/** The app's shared counter. */
|
|
174
|
+
interface RateLimitStore {
|
|
175
|
+
/**
|
|
176
|
+
* Count one hit on `key` in its current `windowSeconds` window: true while the count is
|
|
177
|
+
* within `limit`, false past it, null when it could not count.
|
|
178
|
+
*/
|
|
179
|
+
hit(key: string, limit: number, windowSeconds: number): Promise<boolean | null>;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* True when the request may go on. Fails open: when the store can't count (it answers
|
|
183
|
+
* null or throws) the request goes on and `log` says so with `message`.
|
|
184
|
+
*/
|
|
185
|
+
declare function allow(store: RateLimitStore, check: {
|
|
186
|
+
key: string;
|
|
187
|
+
limit: number;
|
|
188
|
+
windowSeconds: number;
|
|
189
|
+
}, options?: {
|
|
190
|
+
log?: ((message: string) => void) | undefined;
|
|
191
|
+
message?: string | undefined;
|
|
192
|
+
}): Promise<boolean>;
|
|
3
193
|
|
|
4
194
|
interface SubmissionInput {
|
|
5
195
|
/** Answers keyed by field id. Ignored when `events` is given. */
|
|
@@ -35,6 +225,13 @@ interface SubmissionOptions {
|
|
|
35
225
|
* replay counts: the submit that follows it does not.
|
|
36
226
|
*/
|
|
37
227
|
maxReplayWork?: number | undefined;
|
|
228
|
+
/**
|
|
229
|
+
* The option lists the app supplies for `optionsFrom: { source: "host" }`
|
|
230
|
+
* choice fields, loaded here on the server (never taken from the client):
|
|
231
|
+
* an answer must be one of the field's own options or of these. Pass what
|
|
232
|
+
* the form was shown with, loaded afresh, such as the current staff.
|
|
233
|
+
*/
|
|
234
|
+
hostOptions?: HostOptions | undefined;
|
|
38
235
|
}
|
|
39
236
|
interface CheckedSubmission {
|
|
40
237
|
status: "draft" | "completed";
|
|
@@ -74,11 +271,11 @@ declare const RAW_EVENTS_PER_EVENT = 10;
|
|
|
74
271
|
* Check a submission against the published definition. Never throws on a
|
|
75
272
|
* malformed submission: it returns `{ ok: false, errors: { "": … } }`.
|
|
76
273
|
*/
|
|
77
|
-
declare function validateSubmission(
|
|
274
|
+
declare function validateSubmission(published: FormDefinition, submission: SubmissionInput, options?: SubmissionOptions): SubmissionResult;
|
|
78
275
|
/**
|
|
79
276
|
* The definition as respondents may see it. R1 forms hold nothing secret;
|
|
80
277
|
* assessment answer keys (R4) will be stripped here.
|
|
81
278
|
*/
|
|
82
279
|
declare function publicDefinition(definition: FormDefinition): FormDefinition;
|
|
83
280
|
|
|
84
|
-
export { type CheckedSubmission, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, RAW_EVENTS_PER_EVENT, type SubmissionInput, type SubmissionOptions, type SubmissionResult, publicDefinition, validateSubmission };
|
|
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 };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,195 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Edit links for a sent response: `<id>.<nonce>.<expires>.<sig>`, where sig is the
|
|
7
|
+
* base64url HMAC-SHA256 of `edit-link:v2:<id>.<nonce>.<expires>` under a server-only
|
|
8
|
+
* secret of at least 32 bytes.
|
|
9
|
+
*
|
|
10
|
+
* - id: the response's uuid.
|
|
11
|
+
* - nonce: the response's current edit nonce, kept by the app. The app refuses a link
|
|
12
|
+
* whose nonce is no longer the response's, so changing the nonce revokes every link
|
|
13
|
+
* made before (change it after each edit: only the newest link works).
|
|
14
|
+
* - expires: when the link stops working, in Unix seconds, about 30 days after it was
|
|
15
|
+
* made (`EDIT_LINK_TTL_SECONDS`).
|
|
16
|
+
*
|
|
17
|
+
* Without a usable secret (missing, or shorter than `MIN_EDIT_SECRET_BYTES`) there are no
|
|
18
|
+
* edit links: signing returns null and every token fails to verify.
|
|
19
|
+
*
|
|
20
|
+
* 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.
|
|
22
|
+
*/
|
|
23
|
+
/** How long a new edit link works: 30 days. */
|
|
24
|
+
declare const EDIT_LINK_TTL_SECONDS: number;
|
|
25
|
+
/** The shortest secret edit links are signed with: 32 bytes (UTF-8), as long as the HMAC. */
|
|
26
|
+
declare const MIN_EDIT_SECRET_BYTES = 32;
|
|
27
|
+
/** The query parameter an edit link's token travels in: `?edit=<token>`. */
|
|
28
|
+
declare const EDIT_LINK_PARAM = "edit";
|
|
29
|
+
/** Whether `secret` can sign edit links: a string of at least `MIN_EDIT_SECRET_BYTES` bytes. */
|
|
30
|
+
declare function isUsableEditSecret(secret: string | null | undefined): secret is string;
|
|
31
|
+
/** What an edit link says: the response, its nonce when the link was made, and when it stops working. */
|
|
32
|
+
interface EditLinkClaims {
|
|
33
|
+
/** The response's id, in lower case. */
|
|
34
|
+
id: string;
|
|
35
|
+
nonce: string;
|
|
36
|
+
/** Unix seconds. */
|
|
37
|
+
expiresAt: number;
|
|
38
|
+
}
|
|
39
|
+
interface EditLinkOptions {
|
|
40
|
+
/** The current time in milliseconds (tests); `Date.now()` by default. */
|
|
41
|
+
now?: number | undefined;
|
|
42
|
+
/** How long the link works, in seconds; `EDIT_LINK_TTL_SECONDS` by default. */
|
|
43
|
+
ttlSeconds?: number | undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The edit token for a response and its current nonce, working for `ttlSeconds` from
|
|
47
|
+
* now; null when there is no usable secret (no edit links) or the id or nonce is malformed.
|
|
48
|
+
*/
|
|
49
|
+
declare function signEditToken(link: {
|
|
50
|
+
id: string;
|
|
51
|
+
nonce: string;
|
|
52
|
+
}, secret: string | null | undefined, options?: EditLinkOptions): Promise<string | null>;
|
|
53
|
+
/**
|
|
54
|
+
* What a token says, or null when it is malformed, its signature does not match, it has
|
|
55
|
+
* expired (or claims to last longer than `ttlSeconds` from now), or there is no usable
|
|
56
|
+
* secret. Whether the nonce is still the response's is the caller's check. The
|
|
57
|
+
* comparison is Web Crypto's HMAC verify, which runs in constant time.
|
|
58
|
+
*/
|
|
59
|
+
declare function verifyEditToken(token: string | null | undefined, secret: string | null | undefined, options?: EditLinkOptions): Promise<EditLinkClaims | null>;
|
|
60
|
+
/**
|
|
61
|
+
* A form link with the edit token added: `<formUrl>?edit=<token>` (or `&edit=` when it
|
|
62
|
+
* has a query). `param` names the parameter; `EDIT_LINK_PARAM` by default.
|
|
63
|
+
*/
|
|
64
|
+
declare function withEditToken(formUrl: string, token: string, param?: string): string;
|
|
65
|
+
interface EditLinkUrlOptions extends EditLinkOptions {
|
|
66
|
+
/** The query parameter; `EDIT_LINK_PARAM` by default. */
|
|
67
|
+
param?: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The link that reopens a response for editing: `formUrl` (the page that shows the form,
|
|
71
|
+
* with any query it needs) plus a token signed for the response's current nonce. Null
|
|
72
|
+
* when there are no edit links (no usable secret) or the id or nonce is malformed.
|
|
73
|
+
*/
|
|
74
|
+
declare function editLinkUrl(formUrl: string, link: {
|
|
75
|
+
id: string;
|
|
76
|
+
nonce: string;
|
|
77
|
+
}, secret: string | null | undefined, options?: EditLinkUrlOptions): Promise<string | null>;
|
|
78
|
+
/** Where the edit-secret reader reports a missing or short secret. */
|
|
79
|
+
type EditSecretLog = (message: string) => void;
|
|
80
|
+
/** Reads the edit-link secret from an environment, warning once when there is none. */
|
|
81
|
+
interface EditSecretReader {
|
|
82
|
+
/** The secret, or null (no edit links) when it is missing or too short; warns once, never with the secret. */
|
|
83
|
+
(env: Record<string, string | undefined>, log?: EditSecretLog): string | null;
|
|
84
|
+
/** Tests only: warn again. */
|
|
85
|
+
reset(): void;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A reader for the environment variable that holds the edit-link secret (such as
|
|
89
|
+
* `FEEDBACK_EDIT_SECRET`). Without one of at least `MIN_EDIT_SECRET_BYTES` bytes there are
|
|
90
|
+
* no edit links, and the first read says so through `log` (the call's, else the
|
|
91
|
+
* reader's, else `console.warn`); later reads stay quiet until `reset()`.
|
|
92
|
+
*/
|
|
93
|
+
declare function createEditSecretReader(options: {
|
|
94
|
+
variable: string;
|
|
95
|
+
log?: EditSecretLog | undefined;
|
|
96
|
+
}): EditSecretReader;
|
|
97
|
+
interface EditLinkRequestOptions {
|
|
98
|
+
/** The query parameter; `EDIT_LINK_PARAM` by default. */
|
|
99
|
+
param?: string | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the URL is a page that shows a form (an edit link is only honoured there,
|
|
102
|
+
* such as `/f/<slug>`). Every URL by default.
|
|
103
|
+
*/
|
|
104
|
+
isFormPage?: ((url: URL) => boolean) | undefined;
|
|
105
|
+
}
|
|
106
|
+
/** Whether this request is a form page opened with an edit link (the parameter present, even empty). */
|
|
107
|
+
declare function isEditLinkRequest(url: URL, options?: EditLinkRequestOptions): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* The response, with `Referrer-Policy: no-referrer` and `Cache-Control: private, no-store`
|
|
110
|
+
* added when the request carried an edit link; otherwise the same response, untouched.
|
|
111
|
+
* The link is a credential, so it never leaves in a Referer header (an HTTP header, not
|
|
112
|
+
* only the page's meta tag, so it holds before the page has parsed and for every request
|
|
113
|
+
* the page makes), and the page, prefilled with the response, is never cached.
|
|
114
|
+
*/
|
|
115
|
+
declare function withEditLinkHeaders(request: Request, response: Response, options?: EditLinkRequestOptions): Response;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The pieces of a submit endpoint's rate limit that aren't storage: the client's
|
|
119
|
+
* address, the part of it that names one client, a key that never holds the address,
|
|
120
|
+
* and a check that fails open. The counter itself is the app's (`RateLimitStore`), so
|
|
121
|
+
* every server instance can share it (a row in Postgres, a key in Redis).
|
|
122
|
+
*
|
|
123
|
+
* Web Crypto only. Moved up from Breakthrough's src/lib/feedback/rate-limit.server.ts;
|
|
124
|
+
* keys made here equal the ones it made, so a deploy doesn't reset anyone's count.
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* The headers a proxy reports the client's address in, most trusted first. Trust only
|
|
128
|
+
* headers your proxy sets (and overwrites): a client can send any of them itself.
|
|
129
|
+
*/
|
|
130
|
+
declare const DEFAULT_TRUSTED_IP_HEADERS: readonly string[];
|
|
131
|
+
/**
|
|
132
|
+
* How many submits, saves as they go and photo uploads one client may make per scope
|
|
133
|
+
* (a practice, an account) in a window.
|
|
134
|
+
*
|
|
135
|
+
* - submit: 60 responses per 10 minutes: a class at one gym's Wi-Fi fits.
|
|
136
|
+
* - progress: saving answers as they go. A visit counts once, against `submit`, at its
|
|
137
|
+
* first save; its later saves (each Next, the send, a reload picking up where it was)
|
|
138
|
+
* count here, in the same window: 60 people answering 10 questions each on one Wi-Fi fit.
|
|
139
|
+
* - upload: 120 photos per 10 minutes: that class, a couple of photos each.
|
|
140
|
+
*/
|
|
141
|
+
declare const RATE_LIMIT_DEFAULTS: {
|
|
142
|
+
readonly submit: {
|
|
143
|
+
readonly limit: 60;
|
|
144
|
+
readonly windowSeconds: 600;
|
|
145
|
+
};
|
|
146
|
+
readonly progress: {
|
|
147
|
+
readonly limit: 900;
|
|
148
|
+
readonly windowSeconds: 600;
|
|
149
|
+
};
|
|
150
|
+
readonly upload: {
|
|
151
|
+
readonly limit: 120;
|
|
152
|
+
readonly windowSeconds: 600;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* The client's address as the proxy reports it, used only as a rate-limit key: the first
|
|
157
|
+
* entry of the first of `trusted` that is set, trimmed, else `"unknown"`.
|
|
158
|
+
*/
|
|
159
|
+
declare function clientIp(headers: Pick<Headers, "get">, trusted?: readonly string[]): string;
|
|
160
|
+
/** The part of an address that names one client: an IPv4 address whole, an IPv6 address by its /64. */
|
|
161
|
+
declare function addressPrefix(ip: string): string;
|
|
162
|
+
/**
|
|
163
|
+
* The stored key, `<kind>:<scope>:<hex HMAC-SHA256 of the address prefix>`: what is
|
|
164
|
+
* counted (such as `submit`, `progress`, `upload`), whose count it is (a practice id),
|
|
165
|
+
* and the client, which is never stored as its address. `secret` is a server secret.
|
|
166
|
+
*/
|
|
167
|
+
declare function rateLimitKey(input: {
|
|
168
|
+
kind: string;
|
|
169
|
+
scope: string;
|
|
170
|
+
ip: string;
|
|
171
|
+
secret: string;
|
|
172
|
+
}): Promise<string>;
|
|
173
|
+
/** The app's shared counter. */
|
|
174
|
+
interface RateLimitStore {
|
|
175
|
+
/**
|
|
176
|
+
* Count one hit on `key` in its current `windowSeconds` window: true while the count is
|
|
177
|
+
* within `limit`, false past it, null when it could not count.
|
|
178
|
+
*/
|
|
179
|
+
hit(key: string, limit: number, windowSeconds: number): Promise<boolean | null>;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* True when the request may go on. Fails open: when the store can't count (it answers
|
|
183
|
+
* null or throws) the request goes on and `log` says so with `message`.
|
|
184
|
+
*/
|
|
185
|
+
declare function allow(store: RateLimitStore, check: {
|
|
186
|
+
key: string;
|
|
187
|
+
limit: number;
|
|
188
|
+
windowSeconds: number;
|
|
189
|
+
}, options?: {
|
|
190
|
+
log?: ((message: string) => void) | undefined;
|
|
191
|
+
message?: string | undefined;
|
|
192
|
+
}): Promise<boolean>;
|
|
3
193
|
|
|
4
194
|
interface SubmissionInput {
|
|
5
195
|
/** Answers keyed by field id. Ignored when `events` is given. */
|
|
@@ -35,6 +225,13 @@ interface SubmissionOptions {
|
|
|
35
225
|
* replay counts: the submit that follows it does not.
|
|
36
226
|
*/
|
|
37
227
|
maxReplayWork?: number | undefined;
|
|
228
|
+
/**
|
|
229
|
+
* The option lists the app supplies for `optionsFrom: { source: "host" }`
|
|
230
|
+
* choice fields, loaded here on the server (never taken from the client):
|
|
231
|
+
* an answer must be one of the field's own options or of these. Pass what
|
|
232
|
+
* the form was shown with, loaded afresh, such as the current staff.
|
|
233
|
+
*/
|
|
234
|
+
hostOptions?: HostOptions | undefined;
|
|
38
235
|
}
|
|
39
236
|
interface CheckedSubmission {
|
|
40
237
|
status: "draft" | "completed";
|
|
@@ -74,11 +271,11 @@ declare const RAW_EVENTS_PER_EVENT = 10;
|
|
|
74
271
|
* Check a submission against the published definition. Never throws on a
|
|
75
272
|
* malformed submission: it returns `{ ok: false, errors: { "": … } }`.
|
|
76
273
|
*/
|
|
77
|
-
declare function validateSubmission(
|
|
274
|
+
declare function validateSubmission(published: FormDefinition, submission: SubmissionInput, options?: SubmissionOptions): SubmissionResult;
|
|
78
275
|
/**
|
|
79
276
|
* The definition as respondents may see it. R1 forms hold nothing secret;
|
|
80
277
|
* assessment answer keys (R4) will be stripped here.
|
|
81
278
|
*/
|
|
82
279
|
declare function publicDefinition(definition: FormDefinition): FormDefinition;
|
|
83
280
|
|
|
84
|
-
export { type CheckedSubmission, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, RAW_EVENTS_PER_EVENT, type SubmissionInput, type SubmissionOptions, type SubmissionResult, publicDefinition, validateSubmission };
|
|
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 };
|