@agent-cards/checkout 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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ - Clear an earlier failure or authentication reason when the merchant confirms a completed order. The final checkout state retains the authorization, order and payment mode without carrying a stale error.
6
+
7
+ - Pass execution routing metadata through authorization and browser attachment with `executionMode`, `grantId` and `merchantOrigin`. The exported `ExecutionMetadata` type describes the result; the matching API and Vault determine whether the user approves the purchase or an existing grant applies. Autopilot execution remains limited to the configured controlled Stripe test flow, with production Autopilot disabled.
8
+ - Share generated processor metadata with the payment core while keeping the published SDK free of runtime package dependencies.
9
+
10
+ ## 0.7.0
11
+
12
+ - A company can put rules on the Vault purchases it chooses (a merchant list, a currency, a spend cap, a time window) by attaching a named preset to a stored card. A purchase the rules refuse now surfaces as `PresetRefusedError`, a typed decline: at stage `create` no authorization exists and nobody was asked; at stage `pre_replay` the authorization is `declined` with the rule's reason. It carries `code`, `presetName`, `rule`, `preset`, `attachment` (the card with its last four digits) and the rule's own statement; when several presets refuse the same purchase, `refusals` names every one and the other fields are the first. The adapters quiet the page's retry as for any decline, and a create-time refusal is never treated as a permanent misconfiguration.
13
+ - Requires the matching API and Vault release.
14
+
3
15
  ## 0.6.0
4
16
 
5
17
  - Recognize Paysafe Checkout 1.8's exact hosted tokenization endpoints and preserve its native credential and correlation headers. The matching API registry and Vault deployment are required.
package/README.md CHANGED
@@ -54,30 +54,28 @@ await attachToCdp(cdp, pageSessionId, {
54
54
  vault,
55
55
  user: 'usr_123', // whose card should pay
56
56
  merchant: 'vanman.shop',
57
- amountCents: 583, // what the user approves, smallest currency unit
57
+ amount: 583, // your hint, an integer in the currency's smallest unit (or a decimal string: '5.83')
58
58
  currency: 'usd', // "$5.83" is derived for the approval screen
59
59
  onApprovalUrl: (url) => sendToUser(url), // iMessage, SMS, push, your call
60
60
  });
61
61
  ```
62
62
 
63
- `amount: '$5.83'` (a display string) still works on its own. Pass
64
- `amountCents` + `currency` when you want the amount enforced: on a Stripe
65
- PaymentIntent confirm, Agentcard reads the intent back from Stripe at create
66
- and again right before the cardholder's device replays, a different amount is
67
- refused with nothing charged, and after the replay the charge is reconciled
68
- against the approval (`ReplayResponse.amountVerified`, with
69
- `chargedAmountCents` and `chargedKind`: `captured` for a succeeded intent's
70
- `amount_received`, `authorized` for a manual-capture intent's
71
- `amount_capturable`, `none` when nothing is collected yet; plus the
72
- `checkout_authorization.amount_mismatch` webhook to your server when it
73
- disagrees). Amounts are Stripe minor units up to 2^53-1. `onEvent` payloads
74
- name URLs by origin and path only, so a client secret in a paused request's
75
- query string never reaches your telemetry. When you pass the pair, any
76
- `amount` string you also pass is
77
- ignored: Agentcard derives the display string from the number, following
78
- Stripe's minor units, so the approval screen, the notifications and every
79
- read show one amount. Tokenization requests carry no amount, so there the
80
- pair is shown and reported (`amountAuthority: 'display_only'`), not enforced.
63
+ `amount` is your hint: an integer in the currency's smallest unit (583 for
64
+ $5.83), or a decimal string in normal units ('5.83'), with `currency`. The
65
+ processor's own amount is the higher authority: Agentcard reads it from the
66
+ paused request where the processor puts it there, or from the Stripe intent
67
+ the request names, right before the cardholder's device replays, and the
68
+ company's caps are judged on it. A hint lets a bad purchase be refused the
69
+ moment it opens; a hint more than one smallest unit away from the processor's
70
+ amount is refused with nothing charged (`AmountMismatchError`), and after the
71
+ replay the charge is reconciled against the approval
72
+ (`ReplayResponse.amountVerified`, `chargedAmount`, and `chargedKind`:
73
+ `captured` for a succeeded intent's `amount_received`, `authorized` for a
74
+ manual-capture intent's `amount_capturable`, `none` when nothing is collected
75
+ yet; plus the `checkout_authorization.amount_mismatch` webhook to your server
76
+ when the charge disagreed). Every result carries `amountAuthority`:
77
+ `processor`, `agent`, `page`, or `none`. A display string is never sent;
78
+ Agentcard derives it.
81
79
 
82
80
  Then let your agent click "Pay" like it always does. `attachToCdp` pauses the
83
81
  request for approval and resumes it only while the merchant request remains
@@ -129,7 +127,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
129
127
  | Processor | Status |
130
128
  |---|---|
131
129
  | Shopify | supported, verified end to end |
132
- | Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms with `amountCents` + `currency` use backend amount verification. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
130
+ | Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms read the intent's amount back from Stripe; a hint sent as `amount` + `currency` must agree with it. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
133
131
  | Braintree card tokenization | Prepared checkout supported; one live Haymarket Books ebook purchase with SDK `0.5.0` confirmed merchant fulfillment and SDK `completed` using a merchant receipt resolver. Independent processor capture/settlement, live 3DS and PayPal wallet flows remain unverified. |
134
132
  | Checkout.com | supported |
135
133
  | VGS Collect (Very Good Security; Wolt) | not supported: VGS's proxy aliases only submissions from its own iframe, so a replay from the cardholder's device is refused by the merchant (verified on Wolt, 2026-09-03). Not recognized, so the agent's browser is not paused there |
@@ -352,7 +350,7 @@ value continues to work. Choose `requireMerchantResult: true` for a pilot:
352
350
 
353
351
  ```ts
354
352
  const checkout = await attachToPlaywright(page, {
355
- vault, user, merchant, amountCents, currency,
353
+ vault, user, merchant, amount, currency,
356
354
  requireMerchantResult: true,
357
355
  onStateChange: state => recordState(state),
358
356
  onUserAction: action => deliverPrivatelyToUser(action),
@@ -395,7 +393,7 @@ Unrecognized merchant-server endpoints remain outside this guard unless listed
395
393
  in `paymentEndpoints`; this is not a guarantee against a merchant charging a
396
394
  saved token on its own server.
397
395
  A direct card-bearing PaymentIntent confirm remains supported with the backend's
398
- existing amount verification when `amountCents` and `currency` are supplied.
396
+ existing amount verification when `amount` and `currency` are supplied.
399
397
 
400
398
  Hosted-form submissions also always stay blocked because their payment outcome
401
399
  is unverified. `reconcile()` calls the resolver once, coalescing concurrent calls.
@@ -417,7 +415,7 @@ Prepare a Square, Braintree, Worldpay, Bambora or Mercado Pago checkout before t
417
415
  ```ts
418
416
  const checkout = await attachToPlaywright(page, {
419
417
  vault, user: 'your-user-id', merchant: 'Example merchant',
420
- amountCents: 100, currency: 'USD',
418
+ amount: 100, currency: 'USD',
421
419
  onApprovalUrl: deliverPrivatelyToCardholder,
422
420
  });
423
421
  const preparation = await checkout.prepare({
@@ -429,7 +427,7 @@ const preparation = await checkout.prepare({
429
427
  await page.getByRole('button', { name: 'Pay', exact: true }).click();
430
428
  ```
431
429
 
432
- `prepare()` is available on both Playwright and raw CDP controllers. It requires `amountCents` and `currency`, must precede the first recognized card request, and returns only when the cardholder's device is ready. It delivers the preparation URL through `onApprovalUrl` and `onUserAction`; binding the subsequent authorization sends no second approval link or SMS. The phone page must stay open. Its selected card, merchant origin, declared merchant, amount, currency, processor and environment bind one fresh request. The amount remains `display_only`; a card token does not enforce the merchant's eventual charge amount.
430
+ `prepare()` is available on both Playwright and raw CDP controllers. It requires `amount` and `currency`, must precede the first recognized card request, and returns only when the cardholder's device is ready. It delivers the preparation URL through `onApprovalUrl` and `onUserAction`; binding the subsequent authorization sends no second approval link or SMS. The phone page must stay open. Its selected card, merchant origin, declared merchant, amount, currency, processor and environment bind one fresh request. The amount's authority is `agent`; a card token does not enforce the merchant's eventual charge amount.
433
431
 
434
432
  | Processor | `environment` | Fresh native request |
435
433
  | --- | --- | --- |
@@ -519,3 +517,12 @@ coverage classifications, `recognized_traffic_share` is traffic-weighted, and
519
517
  `purchase_success_rate` stays null without observed merchant outcomes. Do not
520
518
  substitute the assessor for a browser/merchant validation run or use native
521
519
  Kernel adapter coverage as evidence for this SDK's coverage.
520
+ ### Autopilot execution metadata
521
+
522
+ When the cardholder has enabled an eligible spending rule in their vault, the same authorization can run through autopilot. The SDK keeps polling the existing authorization and returns optional `executionMode: 'autopilot' | 'user_approval'` and `grantId` metadata. Older API responses remain supported.
523
+
524
+ `authorize()` accepts optional `executionMode` and `grantId` routing hints, sent as `execution_mode` and `grant_id`. These never establish permission to spend; the protected payment service checks the cardholder's signed rule. Autopilot suppresses `onApprovalUrl` while it is executing. A definite fallback to user approval delivers the existing authorization's URL once. A lost outcome remains `PaymentOutcomeUnknownError`; it does not create or submit a second payment.
525
+
526
+ Use `executionMode: 'user_approval'` to require the existing confirmation flow. Without a selected card or grant, the backend can use exactly one eligible rule; ambiguous card selection keeps confirmation. A `grantId` restricts selection to that rule. The initial executor supports only the configured controlled Stripe test flow, and production remains disabled.
527
+
528
+ The adapters also send the observed top-level HTTPS `merchantOrigin`, such as `https://shop.example`, without a path, query or trailing slash. Direct `authorize()` callers can supply that origin explicitly. It is a routing hint; the protected adapter independently verifies the processor account, payee and amount. If a browser cannot provide its top-level URL, an explicit `merchantOrigin` option can supply the hint; otherwise the regular approval path remains available.
@@ -1,10 +1,2 @@
1
- /** Classify native Braintree GraphQL requests without changing their bytes. */
2
- export type BraintreeEnvironment = 'production' | 'sandbox';
3
- export type BraintreeRequestKind = 'configuration' | 'tokenization' | 'invalid';
4
- export declare function braintreeEnvironment(url: string): BraintreeEnvironment | undefined;
5
- /** Bounded duplicate-aware JSON for fresh-card preparation bodies. */
6
- export declare function readTokenizationJson(body: string | null): Record<string, unknown> | null;
7
- /** Undefined belongs to another processor; invalid Braintree requests stay blocked. */
8
- export declare function classifyBraintreeRequest(url: string, method: string, body: string | null): BraintreeRequestKind | undefined;
9
- /** Preparation accepts guest tokenization only; ordinary interception stays broader. */
10
- export declare function isPreparedBraintreeRequest(body: string | null): boolean;
1
+ export { braintreeEnvironment, classifyBraintreeRequest, isPreparedBraintreeRequest, readTokenizationJson } from './braintree.generated.js';
2
+ export type { BraintreeEnvironment, BraintreeRequestKind } from './braintree.generated.js';
@@ -0,0 +1,10 @@
1
+ /** Classify native Braintree GraphQL requests without changing their bytes. */
2
+ export type BraintreeEnvironment = 'production' | 'sandbox';
3
+ export type BraintreeRequestKind = 'configuration' | 'tokenization' | 'invalid';
4
+ export declare function braintreeEnvironment(url: string): BraintreeEnvironment | undefined;
5
+ /** Bounded duplicate-aware JSON for fresh-card preparation bodies. */
6
+ export declare function readTokenizationJson(body: string | null): Record<string, unknown> | null;
7
+ /** Undefined belongs to another processor; invalid Braintree requests stay blocked. */
8
+ export declare function classifyBraintreeRequest(url: string, method: string, body: string | null): BraintreeRequestKind | undefined;
9
+ /** Preparation accepts guest tokenization only; ordinary interception stays broader. */
10
+ export declare function isPreparedBraintreeRequest(body: string | null): boolean;
@@ -0,0 +1,302 @@
1
+ const PRODUCTION = 'https://payments.braintree-api.com/graphql';
2
+ const SANDBOX = 'https://payments.sandbox.braintree-api.com/graphql';
3
+ const MAX_BODY_LENGTH = 128 * 1024;
4
+ const MAX_DEPTH = 32;
5
+ const MAX_NODES = 10_000;
6
+ export function braintreeEnvironment(url) {
7
+ return url === PRODUCTION ? 'production' : url === SANDBOX ? 'sandbox' : undefined;
8
+ }
9
+ function invalid() { throw new Error('invalid_braintree_request'); }
10
+ function record(value) {
11
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
12
+ }
13
+ /** JSON.parse discards duplicate keys, so validate keys before using any value. */
14
+ class JsonReader {
15
+ text;
16
+ at = 0;
17
+ nodes = 0;
18
+ constructor(text) {
19
+ this.text = text;
20
+ }
21
+ read() {
22
+ const value = this.value(0);
23
+ this.space();
24
+ if (this.at !== this.text.length)
25
+ invalid();
26
+ return value;
27
+ }
28
+ space() {
29
+ while (' \t\r\n'.includes(this.text[this.at] ?? '\0'))
30
+ this.at++;
31
+ }
32
+ take(expected) {
33
+ this.space();
34
+ if (this.text[this.at++] !== expected)
35
+ invalid();
36
+ }
37
+ string() {
38
+ this.space();
39
+ const start = this.at;
40
+ if (this.text[this.at++] !== '"')
41
+ invalid();
42
+ while (this.at < this.text.length) {
43
+ const char = this.text[this.at++];
44
+ if (char === '\\') {
45
+ this.at++;
46
+ continue;
47
+ }
48
+ if (char === '"')
49
+ return JSON.parse(this.text.slice(start, this.at));
50
+ }
51
+ return invalid();
52
+ }
53
+ value(depth) {
54
+ if (depth > MAX_DEPTH || ++this.nodes > MAX_NODES)
55
+ invalid();
56
+ this.space();
57
+ const char = this.text[this.at];
58
+ if (char === '"')
59
+ return this.string();
60
+ if (char === '{') {
61
+ this.at++;
62
+ const result = Object.create(null);
63
+ const keys = new Set();
64
+ this.space();
65
+ if (this.text[this.at] === '}') {
66
+ this.at++;
67
+ return result;
68
+ }
69
+ while (true) {
70
+ const key = this.string();
71
+ if (keys.has(key))
72
+ invalid();
73
+ keys.add(key);
74
+ this.take(':');
75
+ result[key] = this.value(depth + 1);
76
+ this.space();
77
+ const next = this.text[this.at++];
78
+ if (next === '}')
79
+ return result;
80
+ if (next !== ',')
81
+ invalid();
82
+ }
83
+ }
84
+ if (char === '[') {
85
+ this.at++;
86
+ const result = [];
87
+ this.space();
88
+ if (this.text[this.at] === ']') {
89
+ this.at++;
90
+ return result;
91
+ }
92
+ while (true) {
93
+ result.push(this.value(depth + 1));
94
+ this.space();
95
+ const next = this.text[this.at++];
96
+ if (next === ']')
97
+ return result;
98
+ if (next !== ',')
99
+ invalid();
100
+ }
101
+ }
102
+ for (const [literal, value] of [['true', true], ['false', false], ['null', null]]) {
103
+ if (this.text.startsWith(literal, this.at)) {
104
+ this.at += literal.length;
105
+ return value;
106
+ }
107
+ }
108
+ const number = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.at));
109
+ if (!number)
110
+ invalid();
111
+ this.at += number[0].length;
112
+ const value = Number(number[0]);
113
+ if (!Number.isFinite(value))
114
+ invalid();
115
+ return value;
116
+ }
117
+ }
118
+ /** Bounded duplicate-aware JSON for fresh-card preparation bodies. */
119
+ export function readTokenizationJson(body) {
120
+ if (typeof body !== 'string' || body.length === 0 || body.length > MAX_BODY_LENGTH)
121
+ return null;
122
+ try {
123
+ const value = new JsonReader(body).read();
124
+ return record(value) ? value : null;
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ /** Native operations need names, punctuation and variables, never literals. */
131
+ function lex(query) {
132
+ const tokens = [];
133
+ let at = 0;
134
+ while (at < query.length) {
135
+ const char = query[at];
136
+ if (' \t\r\n,'.includes(char)) {
137
+ at++;
138
+ continue;
139
+ }
140
+ if ('!$():{}'.includes(char)) {
141
+ tokens.push(char);
142
+ at++;
143
+ }
144
+ else if (/[A-Za-z_]/.test(char)) {
145
+ const start = at++;
146
+ while (at < query.length && /[A-Za-z_0-9]/.test(query[at]))
147
+ at++;
148
+ tokens.push(query.slice(start, at));
149
+ }
150
+ else
151
+ invalid(); // Includes comments, strings, fragments and directives.
152
+ if (tokens.length > MAX_NODES)
153
+ invalid();
154
+ }
155
+ return tokens;
156
+ }
157
+ class GraphqlReader {
158
+ tokens;
159
+ at = 0;
160
+ constructor(tokens) {
161
+ this.tokens = tokens;
162
+ }
163
+ peek() { return this.tokens[this.at]; }
164
+ take(expected) {
165
+ if (this.tokens[this.at++] !== expected)
166
+ invalid();
167
+ }
168
+ name() {
169
+ const value = this.tokens[this.at++];
170
+ if (!value || !/^[A-Za-z_][A-Za-z_0-9]*$/.test(value))
171
+ invalid();
172
+ return value;
173
+ }
174
+ read() {
175
+ const kind = this.name();
176
+ const name = this.name();
177
+ let inputDefinition = false;
178
+ if (this.peek() === '(') {
179
+ // No defaults, alternate types, unused variables or duplicate definitions.
180
+ for (const token of ['(', '$', 'input', ':', 'TokenizeCreditCardInput', '!', ')'])
181
+ this.take(token);
182
+ inputDefinition = true;
183
+ }
184
+ const fields = this.selection(0);
185
+ if (this.at !== this.tokens.length)
186
+ invalid();
187
+ return { kind, name, inputDefinition, fields };
188
+ }
189
+ selection(depth) {
190
+ if (depth > MAX_DEPTH)
191
+ invalid();
192
+ this.take('{');
193
+ const fields = [];
194
+ const names = new Set();
195
+ while (this.peek() !== '}') {
196
+ const name = this.name();
197
+ if (names.has(name))
198
+ invalid();
199
+ names.add(name);
200
+ const args = [];
201
+ if (this.peek() === '(') {
202
+ if (depth !== 0)
203
+ invalid();
204
+ this.take('(');
205
+ const argument = this.name();
206
+ this.take(':');
207
+ this.take('$');
208
+ args.push({ name: argument, variable: this.name() });
209
+ this.take(')');
210
+ }
211
+ const children = this.peek() === '{' ? this.selection(depth + 1) : [];
212
+ fields.push({ name, arguments: args, fields: children });
213
+ // Aliases, directives and other punctuation cannot begin the next field.
214
+ }
215
+ this.take('}');
216
+ if (fields.length === 0)
217
+ invalid();
218
+ return fields;
219
+ }
220
+ }
221
+ /** Undefined belongs to another processor; invalid Braintree requests stay blocked. */
222
+ export function classifyBraintreeRequest(url, method, body) {
223
+ let hostname;
224
+ try {
225
+ hostname = new URL(url).hostname;
226
+ }
227
+ catch {
228
+ return undefined;
229
+ }
230
+ if (hostname !== 'payments.braintree-api.com' && hostname !== 'payments.sandbox.braintree-api.com')
231
+ return undefined;
232
+ if (!braintreeEnvironment(url) || method !== 'POST' || typeof body !== 'string' || body.length === 0 || body.length > MAX_BODY_LENGTH)
233
+ return 'invalid';
234
+ try {
235
+ const envelope = new JsonReader(body).read();
236
+ if (!record(envelope) || typeof envelope.query !== 'string' || typeof envelope.operationName !== 'string')
237
+ return 'invalid';
238
+ if (Object.keys(envelope).some(key => !['query', 'operationName', 'variables', 'clientSdkMetadata'].includes(key)))
239
+ return 'invalid';
240
+ if (envelope.clientSdkMetadata !== undefined && !record(envelope.clientSdkMetadata))
241
+ return 'invalid';
242
+ const operation = new GraphqlReader(lex(envelope.query)).read();
243
+ if (operation.name !== envelope.operationName || operation.fields.length !== 1)
244
+ return 'invalid';
245
+ const root = operation.fields[0];
246
+ if (root.fields.length === 0)
247
+ return 'invalid';
248
+ const variables = envelope.variables === undefined ? Object.create(null) : envelope.variables;
249
+ if (!record(variables))
250
+ return 'invalid';
251
+ if (operation.kind === 'query' && ['ClientConfiguration', 'ClientConfigurationQuery'].includes(operation.name)
252
+ && !operation.inputDefinition && root.name === 'clientConfiguration' && root.arguments.length === 0
253
+ && Object.keys(variables).length === 0)
254
+ return 'configuration';
255
+ if (operation.kind !== 'mutation' || operation.name !== 'TokenizeCreditCard' || !operation.inputDefinition
256
+ || root.name !== 'tokenizeCreditCard' || root.arguments.length !== 1
257
+ || root.arguments[0].name !== 'input' || root.arguments[0].variable !== 'input'
258
+ || Object.keys(variables).length !== 1 || !record(variables.input) || !record(variables.input.creditCard))
259
+ return 'invalid';
260
+ const card = variables.input.creditCard;
261
+ if (!['number', 'expirationMonth', 'expirationYear'].every(key => typeof card[key] === 'string' && card[key].trim().length > 0))
262
+ return 'invalid';
263
+ if (card.cvv !== undefined && (typeof card.cvv !== 'string' || card.cvv.trim().length === 0))
264
+ return 'invalid';
265
+ return 'tokenization';
266
+ }
267
+ catch {
268
+ return 'invalid';
269
+ }
270
+ }
271
+ /** Preparation accepts guest tokenization only; ordinary interception stays broader. */
272
+ export function isPreparedBraintreeRequest(body) {
273
+ if (classifyBraintreeRequest(PRODUCTION, 'POST', body) !== 'tokenization')
274
+ return false;
275
+ try {
276
+ const envelope = new JsonReader(body).read();
277
+ const variables = envelope.variables;
278
+ const input = variables.input;
279
+ if (Object.keys(input).some(key => key !== 'creditCard' && key !== 'options'))
280
+ return false;
281
+ const card = input.creditCard;
282
+ if (Object.keys(card).some(key => !['number', 'expirationMonth', 'expirationYear', 'cvv', 'cardholderName', 'billingAddress'].includes(key)))
283
+ return false;
284
+ if (Object.entries(card).some(([key, value]) => key !== 'billingAddress' && typeof value !== 'string'))
285
+ return false;
286
+ if (card.billingAddress !== undefined) {
287
+ if (!record(card.billingAddress))
288
+ return false;
289
+ const addressKeys = ['postalCode', 'firstName', 'lastName', 'company', 'streetAddress', 'extendedAddress',
290
+ 'locality', 'region', 'countryCodeNumeric', 'countryCodeAlpha2', 'countryCodeAlpha3', 'countryName'];
291
+ if (Object.entries(card.billingAddress).some(([key, value]) => !addressKeys.includes(key) || typeof value !== 'string'))
292
+ return false;
293
+ }
294
+ // Fingerprint authorization defaults validation on when the option is absent.
295
+ // Preparation permits only an explicit request for a transient card token.
296
+ return record(input.options) && Object.keys(input.options).every(key => key === 'validate')
297
+ && input.options.validate === false;
298
+ }
299
+ catch {
300
+ return false;
301
+ }
302
+ }