@coherent.js/forms 1.1.2 → 2.0.0-rc.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 CHANGED
@@ -1,303 +1,222 @@
1
1
  # @coherent.js/forms
2
2
 
3
- Comprehensive forms handling and validation utilities for Coherent.js applications.
3
+ Server-rendered forms for Coherent.js: build a form on the server, validate
4
+ submissions with the same rules on both sides, and progressively enhance it in
5
+ the browser with `hydrateForm`.
6
+
7
+ - ESM-only, Node 22.12+
8
+ - The package root is isomorphic; `@coherent.js/forms/csrf` is server-only.
4
9
 
5
10
  ## Installation
6
11
 
7
12
  ```bash
8
- npm install @coherent.js/forms
9
- # or
10
13
  pnpm add @coherent.js/forms
11
- # or
12
- yarn add @coherent.js/forms
13
14
  ```
14
15
 
15
- ## Overview
16
-
17
- The `@coherent.js/forms` package provides powerful form handling capabilities including:
16
+ ## Exports
18
17
 
19
- - Form state management
20
- - Validation with built-in validators
21
- - Error handling and display
22
- - Form serialization and submission
23
- - Integration with Coherent.js components
18
+ | Import path | Exports |
19
+ | --- | --- |
20
+ | `@coherent.js/forms` | `FormBuilder`, `createFormBuilder`, `buildForm`, `DEFAULT_CLASS_NAMES`, `hydrateForm`, `validators`, `FormValidator`, `createValidator`, `validate`, `validateField`, `validateForm`, `registerValidator`, `composeValidators` |
21
+ | `@coherent.js/forms/form-builder` | `FormBuilder`, `createFormBuilder`, `buildForm`, `DEFAULT_CLASS_NAMES` |
22
+ | `@coherent.js/forms/hydration` | `hydrateForm` |
23
+ | `@coherent.js/forms/validation` | `validators`, `FormValidator`, `createValidator`, `validate` |
24
+ | `@coherent.js/forms/validators` | `validators`, `validateField`, `validateForm`, `createValidator`, `registerValidator`, `composeValidators` |
25
+ | `@coherent.js/forms/csrf` (server only) | `createCsrfToken`, `verifyCsrfToken`, `CSRF_FIELD_NAME` |
24
26
 
25
- ## Quick Start
27
+ ## Quick start
26
28
 
27
29
  ```javascript
28
- import { createForm } from '@coherent.js/forms';
29
- import { validators } from '@coherent.js/state';
30
-
31
- const contactForm = createForm({
32
- fields: {
33
- email: {
34
- value: '',
35
- validators: [validators.email('Please enter a valid email')]
36
- },
37
- message: {
38
- value: '',
39
- validators: [validators.minLength(10, 'Message must be at least 10 characters')]
40
- }
41
- }
30
+ import { render } from '@coherent.js/core';
31
+ import { createFormBuilder, validators } from '@coherent.js/forms';
32
+
33
+ // The form definition, shared by every request.
34
+ const signup = createFormBuilder({
35
+ action: '/signup',
36
+ method: 'post',
37
+ fields: [
38
+ { name: 'email', type: 'email', label: 'Email', required: true },
39
+ { name: 'password', type: 'password', label: 'Password', required: true,
40
+ validators: [validators.minLength(8)] }
41
+ ]
42
42
  });
43
43
 
44
- function ContactForm() {
45
- const handleSubmit = async (event) => {
46
- event.preventDefault();
47
-
48
- if (contactForm.validate()) {
49
- // Form is valid, submit data
50
- await submitFormData(contactForm.values);
51
- contactForm.reset();
52
- }
53
- };
54
-
55
- return {
56
- form: {
57
- onsubmit: handleSubmit,
58
- children: [
59
- {
60
- input: {
61
- type: 'email',
62
- value: contactForm.fields.email.value,
63
- oninput: (e) => contactForm.setField('email', e.target.value),
64
- className: contactForm.fields.email.error ? 'error' : ''
65
- }
66
- },
67
- {
68
- span: {
69
- text: contactForm.fields.email.error || '',
70
- className: 'error-message'
71
- }
72
- },
73
- {
74
- textarea: {
75
- value: contactForm.fields.message.value,
76
- oninput: (e) => contactForm.setField('message', e.target.value),
77
- className: contactForm.fields.message.error ? 'error' : ''
78
- }
79
- },
80
- {
81
- span: {
82
- text: contactForm.fields.message.error || '',
83
- className: 'error-message'
84
- }
85
- },
86
- {
87
- button: {
88
- type: 'submit',
89
- text: 'Send Message',
90
- disabled: contactForm.isSubmitting
91
- }
92
- }
93
- ]
94
- }
95
- };
44
+ // GET /signup
45
+ const html = render(signup.buildForm());
46
+
47
+ // POST /signup — fork() gives this request its own state
48
+ const form = signup.fork().setValues(request.body);
49
+ const errors = form.validate();
50
+ if (Object.keys(errors).length > 0) {
51
+ for (const name of Object.keys(errors)) form.touch(name);
52
+ return render(form.buildForm()); // re-render with values and errors
96
53
  }
97
54
  ```
98
55
 
99
- ## Features
56
+ In the browser, `hydrateForm('form[name="form"]')` reads the validation rules
57
+ the server rendered and validates on blur and submit.
58
+
59
+ ## Validators
100
60
 
101
- ### Form State Management
61
+ A **validator** is a function `(value, formData) => string | null`: an error
62
+ message, or `null` when the value passes. Every runner — `FormValidator`
63
+ schemas, `validateField`, `validateForm`, `FormBuilder` fields and
64
+ `hydrateForm` — calls validators that way.
102
65
 
103
- Automatically manage form state including values, errors, and submission status:
66
+ Each built-in is a **factory** that returns a validator. The last argument is
67
+ always an optional custom message:
104
68
 
105
69
  ```javascript
106
- const form = createForm({
107
- fields: {
108
- username: { value: '' },
109
- password: { value: '' }
70
+ import { validators, validateForm } from '@coherent.js/forms';
71
+
72
+ validateForm(
73
+ { name: '', email: 'nope', age: '15' },
74
+ {
75
+ name: [validators.required('Please enter your name')],
76
+ email: [validators.required, validators.email()],
77
+ age: [validators.min(18, 'You must be 18 or older')]
110
78
  }
111
- });
112
-
113
- // Access form values
114
- console.log(form.values); // { username: '', password: '' }
115
-
116
- // Update field values
117
- form.setField('username', 'john_doe');
118
-
119
- // Check form validity
120
- console.log(form.isValid); // true/false
121
-
122
- // Check submission status
123
- console.log(form.isSubmitting); // true/false
79
+ );
80
+ // → { name: 'Please enter your name', email: 'Invalid email address', age: 'You must be 18 or older' }
124
81
  ```
125
82
 
126
- ### Validation
127
-
128
- Built-in validators with custom validation support:
83
+ - A built-in may be listed without calling it (`validators.required`); it runs
84
+ with its defaults. A string names a built-in or registered validator (see
85
+ below).
86
+ - Apart from `required` and `matches`, built-ins pass empty values, so combine
87
+ them with `required`.
88
+ - For direct checks, pass the value and an options object:
89
+ `validators.minLength('abc', { min: 5 })` returns `'Minimum length is 5'`.
90
+ A lone string argument is always a message, so `validators.email('a@b.c')`
91
+ returns a validator rather than checking `'a@b.c'`.
92
+
93
+ Built-ins: `required`, `email`, `url`, `minLength(min)`, `maxLength(max)`,
94
+ `min(min)`, `max(max)`, `pattern(regex)`, `matches(field)`, `match(field)`,
95
+ `oneOf(values)`, `custom(fn)`, `number`, `integer`, `phone`, `date`, `alpha`,
96
+ `alphanumeric`, `uppercase`, `fileType(accept)`, `fileSize(maxSize)`,
97
+ `fileExtension(extensions)`. Helpers: `compose`, `when`, `chain`, `debounce`,
98
+ `cancellable`, `get`.
99
+
100
+ ### Accepted validator entries
101
+
102
+ A validator list — a `FormBuilder` field's `validators`, a `FormValidator`
103
+ schema, `validateField` / `validateForm` — accepts, in any mix:
104
+
105
+ | Entry | Example | Same as |
106
+ | --- | --- | --- |
107
+ | A validator function | `value => value ? null : 'Required'` | — |
108
+ | A built-in, called | `validators.minLength(8, 'Too short')` | — |
109
+ | A built-in, uncalled | `validators.required` | `validators.required()` |
110
+ | A name | `'required'`, `'email'`, `'noShouting'` (registered) | `validators.required()` |
111
+ | A built-in name with arguments | `'minLength:8'` | `validators.minLength(8)` |
112
+
113
+ A string with arguments is `'name:arguments'`, read by the built-in's
114
+ parameter:
115
+
116
+ - a number (`minLength`, `maxLength`, `min`, `max`, `fileSize`): the number,
117
+ then optionally a comma and a message — `'min:18'`,
118
+ `'minLength:8,Use at least 8 characters'`;
119
+ - a field name (`matches`, `match`): `'matches:password'`, optionally
120
+ followed by `,message`;
121
+ - a list (`oneOf`, `fileType`, `fileExtension`): every comma-separated value —
122
+ `'oneOf:small,medium,large'`, `'fileType:image/*,.pdf'`;
123
+ - a regular expression (`pattern`): everything after the colon, commas
124
+ included — `'pattern:^[a-z]{2,8}$'` (no flags; use `validators.pattern()`
125
+ for flags or a message);
126
+ - no parameter (`required`, `email`, …): a message — `'required:Name please'`.
127
+
128
+ Registered validators take no arguments: name them alone. An unknown name,
129
+ or arguments that do not fit (`'minLength:abc'`), is skipped.
129
130
 
130
131
  ```javascript
131
- import { validators } from '@coherent.js/state';
132
-
133
- const form = createForm({
134
- fields: {
135
- email: {
136
- value: '',
137
- validators: [
138
- validators.required('Email is required'),
139
- validators.email('Please enter a valid email')
140
- ]
141
- },
142
- age: {
143
- value: '',
144
- validators: [
145
- validators.required('Age is required'),
146
- validators.min(18, 'Must be at least 18 years old')
147
- ]
148
- }
149
- }
150
- });
151
-
152
- // Custom validator
153
- const customValidator = (value) => {
154
- if (value && value.length < 5) {
155
- return 'Value must be at least 5 characters';
156
- }
157
- return null; // null means valid
158
- };
159
-
160
- const formWithCustomValidation = createForm({
161
- fields: {
162
- customField: {
163
- value: '',
164
- validators: [customValidator]
165
- }
166
- }
132
+ createFormBuilder({
133
+ fields: [
134
+ { name: 'password', type: 'password', validators: ['required', 'minLength:8'] },
135
+ { name: 'size', validators: ['oneOf:s,m,l'] }
136
+ ]
167
137
  });
168
138
  ```
169
139
 
170
- ### Async Validation
140
+ String entries are enforced on the server and rendered into `data-validators`
141
+ exactly like the equivalent factory call, so `hydrateForm` enforces them too.
171
142
 
172
- Support for asynchronous validation (e.g., checking if username is available):
143
+ Your own validators follow the same shape:
173
144
 
174
145
  ```javascript
175
- const asyncValidator = async (value) => {
176
- if (!value) return null;
177
-
178
- const response = await fetch(`/api/check-username/${value}`);
179
- const exists = await response.json();
180
-
181
- return exists ? 'Username is already taken' : null;
182
- };
183
-
184
- const signupForm = createForm({
185
- fields: {
186
- username: {
187
- value: '',
188
- validators: [asyncValidator]
189
- }
190
- }
191
- });
192
- ```
146
+ import { registerValidator, validators, createValidator } from '@coherent.js/forms';
193
147
 
194
- ## API Reference
148
+ const noShouting = value =>
149
+ value && value === value.toUpperCase() ? 'Please stop shouting' : null;
195
150
 
196
- ### createForm(options)
151
+ registerValidator('noShouting', noShouting); // now validators.noShouting
152
+ const noSpaces = createValidator(value => /\s/.test(value), 'No spaces allowed');
153
+ ```
197
154
 
198
- Create a new form instance.
155
+ `createValidator(schema)` returns a `FormValidator`; `createValidator(fn,
156
+ message)` wraps a check function as above.
199
157
 
200
- **Parameters:**
201
- - `options.fields` - Object defining form fields and their initial state
202
- - `options.onSubmit` - Optional function to handle form submission
158
+ ### Client-side validation
203
159
 
204
- **Returns:** Form instance with methods and properties
160
+ For each field, the builder renders its validators into `data-validators` as
161
+ JSON (`[{"name":"minLength","args":[8]}]`), and `hydrateForm` rebuilds the
162
+ same rules, so the browser and the server give the same verdict and message.
163
+ Built-ins and registered validators (register the same name in the browser)
164
+ are described; anonymous functions run on the server only.
205
165
 
206
- ### Form Instance Properties
166
+ ## Forms on a server: one state per request
207
167
 
208
- - `values` - Current form values
209
- - `fields` - Field state objects with value, error, touched, etc.
210
- - `isValid` - Boolean indicating if form is valid
211
- - `isSubmitting` - Boolean indicating if form is being submitted
212
- - `errors` - Object containing field errors
168
+ A `FormBuilder` holds the values, errors and touched state of one submission.
169
+ Keep the definition at module scope, and never fill that shared instance with
170
+ request data — the next user would see it. Either fork it per request:
213
171
 
214
- ### Form Instance Methods
172
+ ```javascript
173
+ const form = signup.fork();
174
+ form.setValues(request.body);
175
+ ```
215
176
 
216
- - `setField(name, value)` - Update a field's value
217
- - `validate()` - Validate all fields, returns boolean
218
- - `reset()` - Reset form to initial state
219
- - `submit()` - Trigger form submission
177
+ or render per-request state without touching the builder:
220
178
 
221
- ## Integration with @coherent.js/state
179
+ ```javascript
180
+ render(signup.buildForm({ values: request.body, errors }));
181
+ ```
222
182
 
223
- The forms package integrates seamlessly with the reactive state system:
183
+ ## CSRF protection
224
184
 
225
- ```javascript
226
- import { createForm } from '@coherent.js/forms';
227
- import { observable } from '@coherent.js/state';
185
+ `@coherent.js/forms/csrf` (server only) issues stateless tokens bound to a
186
+ session and signed with HMAC-SHA256:
228
187
 
229
- // Create reactive form
230
- const form = createForm({
231
- fields: {
232
- search: { value: '' }
233
- }
234
- });
188
+ ```javascript
189
+ import { createCsrfToken, verifyCsrfToken } from '@coherent.js/forms/csrf';
235
190
 
236
- // Create observable for search results
237
- const searchResults = observable([]);
191
+ // GET: render the token as a hidden _csrf input
192
+ const csrfToken = createCsrfToken(process.env.CSRF_SECRET, session.id);
193
+ render(signup.buildForm({ csrfToken }));
238
194
 
239
- // Update search results when form changes
240
- form.fields.search.watch((newValue) => {
241
- if (newValue.length > 2) {
242
- performSearch(newValue).then(results => {
243
- searchResults.value = results;
244
- });
245
- }
246
- });
195
+ // POST: reject the request unless the token matches this session
196
+ if (!verifyCsrfToken(request.body._csrf, process.env.CSRF_SECRET, session.id, { maxAge: 60 * 60 * 1000 })) {
197
+ return response.status(403).end();
198
+ }
247
199
  ```
248
200
 
249
- ## Examples
250
-
251
- ### Login Form
201
+ ## Hydration
252
202
 
253
203
  ```javascript
254
- import { createForm } from '@coherent.js/forms';
255
- import { validators } from '@coherent.js/state';
256
-
257
- const loginForm = createForm({
258
- fields: {
259
- email: {
260
- value: '',
261
- validators: [
262
- validators.required('Email is required'),
263
- validators.email('Please enter a valid email')
264
- ]
265
- },
266
- password: {
267
- value: '',
268
- validators: [
269
- validators.required('Password is required'),
270
- validators.minLength(8, 'Password must be at least 8 characters')
271
- ]
272
- }
273
- },
274
- async onSubmit(values) {
275
- try {
276
- const response = await fetch('/api/login', {
277
- method: 'POST',
278
- headers: { 'Content-Type': 'application/json' },
279
- body: JSON.stringify(values)
280
- });
281
-
282
- if (response.ok) {
283
- // Handle successful login
284
- window.location.href = '/dashboard';
285
- } else {
286
- // Handle login error
287
- throw new Error('Invalid credentials');
288
- }
289
- } catch (error) {
290
- loginForm.setError('login', error.message);
291
- }
204
+ import { hydrateForm } from '@coherent.js/forms/hydration';
205
+
206
+ const controller = hydrateForm('#signup', {
207
+ onSubmit: async (values) => {
208
+ await fetch('/api/signup', { method: 'POST', body: JSON.stringify(values) });
292
209
  }
293
210
  });
294
211
  ```
295
212
 
296
- ## Related Packages
213
+ Pass the same `classNames` you gave `buildForm` if you customised them. The
214
+ controller exposes `validateField`, `validateForm`, `getValues`, `getErrors`,
215
+ `setFieldValue`, `reset` and `destroy`.
216
+
217
+ ## TypeScript
297
218
 
298
- - [@coherent.js/state](../state/README.md) - Reactive state management
299
- - [@coherent.js/core](../core/README.md) - Core framework
300
- - [@coherent.js/client](../client/README.md) - Client-side utilities
219
+ Type definitions ship with the package (`types/index.d.ts`, `types/csrf.d.ts`).
301
220
 
302
221
  ## License
303
222
 
package/dist/csrf.js ADDED
@@ -0,0 +1,57 @@
1
+ // src/csrf.js
2
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3
+ import { Buffer } from "node:buffer";
4
+ var CSRF_FIELD_NAME = "_csrf";
5
+ var TOKEN_PATTERN = /^([0-9a-z]{1,13})\.([A-Za-z0-9_-]{16,64})\.([A-Za-z0-9_-]{43})$/;
6
+ var CLOCK_SKEW_MS = 6e4;
7
+ function assertSecret(secret) {
8
+ const usable = typeof secret === "string" && secret.length > 0 || secret instanceof Uint8Array && secret.length > 0;
9
+ if (!usable) {
10
+ throw new TypeError("A CSRF secret (non-empty string or Buffer) is required");
11
+ }
12
+ }
13
+ function assertSessionId(sessionId) {
14
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
15
+ throw new TypeError("A CSRF token must be bound to a non-empty session id string");
16
+ }
17
+ }
18
+ function sign(secret, issuedAt, nonce, sessionId) {
19
+ return createHmac("sha256", secret).update(`${issuedAt}.${nonce}.${sessionId}`).digest("base64url");
20
+ }
21
+ function createCsrfToken(secret, sessionId, options = {}) {
22
+ assertSecret(secret);
23
+ assertSessionId(sessionId);
24
+ const issuedAt = Math.floor(options.now ?? Date.now()).toString(36);
25
+ const nonce = randomBytes(16).toString("base64url");
26
+ return `${issuedAt}.${nonce}.${sign(secret, issuedAt, nonce, sessionId)}`;
27
+ }
28
+ function verifyCsrfToken(token, secret, sessionId, options = {}) {
29
+ assertSecret(secret);
30
+ if (typeof sessionId !== "string" || sessionId.length === 0) return false;
31
+ if (typeof token !== "string") return false;
32
+ const match = TOKEN_PATTERN.exec(token);
33
+ if (!match) return false;
34
+ const [, issuedAt, nonce, mac] = match;
35
+ const expected = Buffer.from(sign(secret, issuedAt, nonce, sessionId));
36
+ const actual = Buffer.from(mac);
37
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
38
+ return false;
39
+ }
40
+ if (options.maxAge !== void 0) {
41
+ const age = (options.now ?? Date.now()) - parseInt(issuedAt, 36);
42
+ if (!(age >= -CLOCK_SKEW_MS && age <= options.maxAge)) return false;
43
+ }
44
+ return true;
45
+ }
46
+ var csrf_default = {
47
+ CSRF_FIELD_NAME,
48
+ createCsrfToken,
49
+ verifyCsrfToken
50
+ };
51
+ export {
52
+ CSRF_FIELD_NAME,
53
+ createCsrfToken,
54
+ csrf_default as default,
55
+ verifyCsrfToken
56
+ };
57
+ //# sourceMappingURL=csrf.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/csrf.js"],
4
+ "sourcesContent": ["/**\n * Coherent.js Forms - CSRF tokens (server only)\n *\n * Stateless, session-bound CSRF tokens signed with HMAC-SHA256. Import from\n * `@coherent.js/forms/csrf` on the server; the package root stays free of\n * `node:crypto` so it keeps working in the browser.\n *\n * ```js\n * import { createCsrfToken, verifyCsrfToken } from '@coherent.js/forms/csrf';\n *\n * // GET: render the form with a token for this session\n * const csrfToken = createCsrfToken(process.env.CSRF_SECRET, req.session.id);\n * render(form.buildForm({ csrfToken })); // adds <input type=\"hidden\" name=\"_csrf\">\n *\n * // POST: reject the request unless the token matches the session\n * if (!verifyCsrfToken(req.body._csrf, process.env.CSRF_SECRET, req.session.id, { maxAge: 3_600_000 })) {\n * return res.status(403).end();\n * }\n * ```\n *\n * A token is `<issuedAt>.<nonce>.<mac>`, where the MAC covers the issue time,\n * the nonce and the session id, so a token minted for one session is rejected\n * for any other, and `maxAge` bounds how long it stays valid.\n *\n * @module forms/csrf\n */\n\nimport { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport { Buffer } from 'node:buffer';\n\n/** Default name of the hidden field the form builder renders. */\nexport const CSRF_FIELD_NAME = '_csrf';\n\nconst TOKEN_PATTERN = /^([0-9a-z]{1,13})\\.([A-Za-z0-9_-]{16,64})\\.([A-Za-z0-9_-]{43})$/;\n\nconst CLOCK_SKEW_MS = 60_000;\n\nfunction assertSecret(secret) {\n const usable = (typeof secret === 'string' && secret.length > 0) ||\n (secret instanceof Uint8Array && secret.length > 0);\n if (!usable) {\n throw new TypeError('A CSRF secret (non-empty string or Buffer) is required');\n }\n}\n\nfunction assertSessionId(sessionId) {\n if (typeof sessionId !== 'string' || sessionId.length === 0) {\n throw new TypeError('A CSRF token must be bound to a non-empty session id string');\n }\n}\n\nfunction sign(secret, issuedAt, nonce, sessionId) {\n return createHmac('sha256', secret)\n .update(`${issuedAt}.${nonce}.${sessionId}`)\n .digest('base64url');\n}\n\n/**\n * Create a CSRF token bound to `sessionId`.\n *\n * @param {string|Buffer} secret - Server-side signing secret\n * @param {string} sessionId - The session the token belongs to\n * @param {{ now?: number }} [options] - `now` (ms) overrides the issue time\n * @returns {string} The token\n */\nexport function createCsrfToken(secret, sessionId, options = {}) {\n assertSecret(secret);\n assertSessionId(sessionId);\n\n const issuedAt = Math.floor(options.now ?? Date.now()).toString(36);\n const nonce = randomBytes(16).toString('base64url');\n return `${issuedAt}.${nonce}.${sign(secret, issuedAt, nonce, sessionId)}`;\n}\n\n/**\n * Check a submitted CSRF token. Returns `false` (never throws) for a missing,\n * malformed, forged, expired or other-session token.\n *\n * @param {unknown} token - The submitted token (e.g. `req.body._csrf`)\n * @param {string|Buffer} secret - The secret the token was created with\n * @param {string} sessionId - The current request's session id\n * @param {{ maxAge?: number, now?: number }} [options] - `maxAge` in ms\n * @returns {boolean} Whether the token is valid for this session\n */\nexport function verifyCsrfToken(token, secret, sessionId, options = {}) {\n assertSecret(secret);\n if (typeof sessionId !== 'string' || sessionId.length === 0) return false;\n if (typeof token !== 'string') return false;\n\n const match = TOKEN_PATTERN.exec(token);\n if (!match) return false;\n const [, issuedAt, nonce, mac] = match;\n\n const expected = Buffer.from(sign(secret, issuedAt, nonce, sessionId));\n const actual = Buffer.from(mac);\n if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {\n return false;\n }\n\n if (options.maxAge !== undefined) {\n const age = (options.now ?? Date.now()) - parseInt(issuedAt, 36);\n // A minute of tolerance for clocks that differ between servers.\n if (!(age >= -CLOCK_SKEW_MS && age <= options.maxAge)) return false;\n }\n\n return true;\n}\n\nexport default {\n CSRF_FIELD_NAME,\n createCsrfToken,\n verifyCsrfToken\n};\n"],
5
+ "mappings": ";AA2BA,SAAS,YAAY,aAAa,uBAAuB;AACzD,SAAS,cAAc;AAGhB,IAAM,kBAAkB;AAE/B,IAAM,gBAAgB;AAEtB,IAAM,gBAAgB;AAEtB,SAAS,aAAa,QAAQ;AAC5B,QAAM,SAAU,OAAO,WAAW,YAAY,OAAO,SAAS,KAC3D,kBAAkB,cAAc,OAAO,SAAS;AACnD,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,UAAU,wDAAwD;AAAA,EAC9E;AACF;AAEA,SAAS,gBAAgB,WAAW;AAClC,MAAI,OAAO,cAAc,YAAY,UAAU,WAAW,GAAG;AAC3D,UAAM,IAAI,UAAU,6DAA6D;AAAA,EACnF;AACF;AAEA,SAAS,KAAK,QAAQ,UAAU,OAAO,WAAW;AAChD,SAAO,WAAW,UAAU,MAAM,EAC/B,OAAO,GAAG,QAAQ,IAAI,KAAK,IAAI,SAAS,EAAE,EAC1C,OAAO,WAAW;AACvB;AAUO,SAAS,gBAAgB,QAAQ,WAAW,UAAU,CAAC,GAAG;AAC/D,eAAa,MAAM;AACnB,kBAAgB,SAAS;AAEzB,QAAM,WAAW,KAAK,MAAM,QAAQ,OAAO,KAAK,IAAI,CAAC,EAAE,SAAS,EAAE;AAClE,QAAM,QAAQ,YAAY,EAAE,EAAE,SAAS,WAAW;AAClD,SAAO,GAAG,QAAQ,IAAI,KAAK,IAAI,KAAK,QAAQ,UAAU,OAAO,SAAS,CAAC;AACzE;AAYO,SAAS,gBAAgB,OAAO,QAAQ,WAAW,UAAU,CAAC,GAAG;AACtE,eAAa,MAAM;AACnB,MAAI,OAAO,cAAc,YAAY,UAAU,WAAW,EAAG,QAAO;AACpE,MAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,QAAM,QAAQ,cAAc,KAAK,KAAK;AACtC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,CAAC,EAAE,UAAU,OAAO,GAAG,IAAI;AAEjC,QAAM,WAAW,OAAO,KAAK,KAAK,QAAQ,UAAU,OAAO,SAAS,CAAC;AACrE,QAAM,SAAS,OAAO,KAAK,GAAG;AAC9B,MAAI,OAAO,WAAW,SAAS,UAAU,CAAC,gBAAgB,QAAQ,QAAQ,GAAG;AAC3E,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,WAAW,QAAW;AAChC,UAAM,OAAO,QAAQ,OAAO,KAAK,IAAI,KAAK,SAAS,UAAU,EAAE;AAE/D,QAAI,EAAE,OAAO,CAAC,iBAAiB,OAAO,QAAQ,QAAS,QAAO;AAAA,EAChE;AAEA,SAAO;AACT;AAEA,IAAO,eAAQ;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF;",
6
+ "names": []
7
+ }