axios 1.18.0 → 1.19.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +192 -23
  3. package/dist/axios.js +410 -101
  4. package/dist/axios.min.js +3 -3
  5. package/dist/axios.min.js.map +1 -1
  6. package/dist/browser/axios.cjs +484 -119
  7. package/dist/esm/axios.js +484 -119
  8. package/dist/esm/axios.min.js +2 -2
  9. package/dist/esm/axios.min.js.map +1 -1
  10. package/dist/node/axios.cjs +589 -139
  11. package/index.d.cts +114 -74
  12. package/index.d.ts +112 -68
  13. package/lib/adapters/adapters.js +1 -1
  14. package/lib/adapters/fetch.js +27 -12
  15. package/lib/adapters/http.js +95 -34
  16. package/lib/adapters/xhr.js +1 -0
  17. package/lib/core/Axios.js +24 -6
  18. package/lib/core/AxiosError.js +46 -3
  19. package/lib/core/AxiosHeaders.js +124 -1
  20. package/lib/core/buildFullPath.js +45 -7
  21. package/lib/core/mergeConfig.js +19 -3
  22. package/lib/core/setFormDataHeaders.js +27 -0
  23. package/lib/env/data.js +1 -1
  24. package/lib/helpers/AxiosURLSearchParams.js +1 -3
  25. package/lib/helpers/HttpStatusCode.js +1 -0
  26. package/lib/helpers/buildURL.js +1 -0
  27. package/lib/helpers/combineURLs.js +11 -3
  28. package/lib/helpers/composeSignals.js +12 -1
  29. package/lib/helpers/cookies.js +5 -1
  30. package/lib/helpers/estimateDataURLDecodedBytes.js +115 -54
  31. package/lib/helpers/formDataToJSON.js +11 -5
  32. package/lib/helpers/fromDataURI.js +4 -2
  33. package/lib/helpers/parseHeaders.js +5 -3
  34. package/lib/helpers/progressEventReducer.js +3 -3
  35. package/lib/helpers/resolveConfig.js +10 -19
  36. package/lib/helpers/shouldBypassProxy.js +120 -1
  37. package/lib/helpers/toFormData.js +8 -1
  38. package/lib/helpers/validator.js +1 -1
  39. package/lib/platform/node/classes/Buffer.js +11 -0
  40. package/lib/utils.js +17 -5
  41. package/package.json +22 -20
@@ -23,12 +23,18 @@ function throwIfDepthExceeded(index) {
23
23
  * @returns An array of strings.
24
24
  */
25
25
  function parsePropPath(name) {
26
- // foo[x][y][z]
27
- // foo.x.y.z
28
- // foo-x-y-z
29
- // foo x y z
26
+ // foo[x][y][z] -> ['foo', 'x', 'y', 'z']
27
+ // foo.x.y.z -> ['foo', 'x', 'y', 'z']
28
+ // A path is split on `.` and on `[...]` groups. A segment — whether written
29
+ // in dot notation or captured inside brackets — may contain any character
30
+ // except `.`, `[` and `]`, so a key like `user-name` or `user name` is kept
31
+ // literal instead of being split (#5402). `.`, `[` and `]` keep their existing
32
+ // meaning, e.g. `foo[bar.baz]` -> ['foo', 'bar', 'baz'] and `[]` is an array push.
33
+ // Excluding `[` from the bracket group also makes the match fail fast at the
34
+ // next `[`, so a malformed name cannot rescan to the end of the string from
35
+ // every unmatched `[` — parsing stays linear in the length of the name.
30
36
  const path = [];
31
- const pattern = /\w+|\[(\w*)]/g;
37
+ const pattern = /[^.[\]]+|\[([^.[\]]*)]/g;
32
38
  let match;
33
39
 
34
40
  while ((match = pattern.exec(name)) !== null) {
@@ -42,14 +42,16 @@ export default function fromDataURI(uri, asBlob, options) {
42
42
 
43
43
  // RFC 2397 section 3: default mediatype is text/plain;charset=US-ASCII
44
44
  // Bare `data:,` leaves mime undefined; Blob normalises that to "" per spec.
45
- let mime;
45
+ let mime = '';
46
46
  if (type) {
47
47
  mime = params ? type + params : type;
48
48
  } else if (params) {
49
49
  mime = 'text/plain' + params;
50
50
  }
51
51
 
52
- const buffer = Buffer.from(decodeURIComponent(body), encoding);
52
+ const buffer = encoding === 'base64'
53
+ ? Buffer.from(body, 'base64')
54
+ : Buffer.from(decodeURIComponent(body), encoding);
53
55
 
54
56
  if (asBlob) {
55
57
  if (!_Blob) {
@@ -50,18 +50,20 @@ export default (rawHeaders) => {
50
50
  key = line.substring(0, i).trim().toLowerCase();
51
51
  val = line.substring(i + 1).trim();
52
52
 
53
- if (!key || (parsed[key] && ignoreDuplicateOf[key])) {
53
+ const hasKey = utils.hasOwnProp(parsed, key);
54
+
55
+ if (!key || (hasKey && utils.hasOwnProp(ignoreDuplicateOf, key))) {
54
56
  return;
55
57
  }
56
58
 
57
59
  if (key === 'set-cookie') {
58
- if (parsed[key]) {
60
+ if (hasKey) {
59
61
  parsed[key].push(val);
60
62
  } else {
61
63
  parsed[key] = [val];
62
64
  }
63
65
  } else {
64
- parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
66
+ parsed[key] = hasKey ? parsed[key] + ', ' + val : val;
65
67
  }
66
68
  });
67
69
 
@@ -12,7 +12,7 @@ export const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
12
12
  }
13
13
  const rawLoaded = e.loaded;
14
14
  const total = e.lengthComputable ? e.total : undefined;
15
- const loaded = total != null ? Math.min(rawLoaded, total) : rawLoaded;
15
+ const loaded = Math.max(0, total != null ? Math.min(rawLoaded, total) : rawLoaded);
16
16
  const progressBytes = Math.max(0, loaded - bytesNotified);
17
17
  const rate = _speedometer(progressBytes);
18
18
 
@@ -49,6 +49,6 @@ export const progressEventDecorator = (total, throttled) => {
49
49
  };
50
50
 
51
51
  export const asyncDecorator =
52
- (fn) =>
52
+ (fn, scheduler = utils.asap) =>
53
53
  (...args) =>
54
- utils.asap(() => fn(...args));
54
+ scheduler(() => fn(...args));
@@ -1,27 +1,14 @@
1
1
  import platform from '../platform/index.js';
2
2
  import utils from '../utils.js';
3
+ import AxiosError from '../core/AxiosError.js';
3
4
  import isURLSameOrigin from './isURLSameOrigin.js';
4
5
  import cookies from './cookies.js';
5
6
  import buildFullPath from '../core/buildFullPath.js';
6
7
  import mergeConfig from '../core/mergeConfig.js';
7
8
  import AxiosHeaders from '../core/AxiosHeaders.js';
9
+ import setFormDataHeaders from '../core/setFormDataHeaders.js';
8
10
  import buildURL from './buildURL.js';
9
11
 
10
- const FORM_DATA_CONTENT_HEADERS = ['content-type', 'content-length'];
11
-
12
- function setFormDataHeaders(headers, formHeaders, policy) {
13
- if (policy !== 'content-only') {
14
- headers.set(formHeaders);
15
- return;
16
- }
17
-
18
- Object.entries(formHeaders).forEach(([key, val]) => {
19
- if (FORM_DATA_CONTENT_HEADERS.includes(key.toLowerCase())) {
20
- headers.set(key, val);
21
- }
22
- });
23
- }
24
-
25
12
  /**
26
13
  * Encode a UTF-8 string to a Latin-1 byte string for use with btoa().
27
14
  * This is a modern replacement for the deprecated unescape(encodeURIComponent(str)) pattern.
@@ -65,10 +52,14 @@ function resolveConfig(config) {
65
52
  const username = utils.getSafeProp(auth, 'username') || '';
66
53
  const password = utils.getSafeProp(auth, 'password') || '';
67
54
 
68
- headers.set(
69
- 'Authorization',
70
- 'Basic ' + btoa(username + ':' + (password ? encodeUTF8(password) : ''))
71
- );
55
+ try {
56
+ headers.set(
57
+ 'Authorization',
58
+ 'Basic ' + btoa(username + ':' + (password ? encodeUTF8(password) : ''))
59
+ );
60
+ } catch (e) {
61
+ throw AxiosError.from(e, AxiosError.ERR_BAD_OPTION_VALUE, config);
62
+ }
72
63
  }
73
64
 
74
65
  if (utils.isFormData(data)) {
@@ -7,6 +7,112 @@ const isIPv4Loopback = (host) => {
7
7
  return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
8
8
  };
9
9
 
10
+ /**
11
+ * Canonicalize an IPv4 address written in shorthand, octal, or hex form into
12
+ * dotted-decimal. IPv6 addresses and non-IP strings are returned unchanged so
13
+ * the existing IPv4-mapped IPv6 unmap path and the isLoopback path can still
14
+ * see them.
15
+ *
16
+ * Shorthand expansion mirrors Node's URL parser: literal parts fill from the
17
+ * left, the final part fills the remaining octets from the right with
18
+ * zero-padding on the left.
19
+ * 127.1 -> 127.0.0.1
20
+ * 127.0.1 -> 127.0.0.1
21
+ * 1.2.3 -> 1.2.0.3
22
+ *
23
+ * Each octet is parsed with an explicit base: 16 for `0x`/`0X` prefix, 8 for
24
+ * zero-prefixed multi-digit all-`0-7` parts, 10 otherwise. Zero-prefixed
25
+ * decimal-looking parts that contain `8` or `9` are rejected to match Node's
26
+ * URL parser, and the comparison layer falls through to non-bypass if either
27
+ * side rejects the form (fail-safe).
28
+ *
29
+ * Returns the input unchanged on any parse failure, out-of-range octet, or
30
+ * unusual shape (1-part, 5+ parts) so the comparison layer fails closed.
31
+ */
32
+ const parseIPv4Octet = (text) => {
33
+ if (/^0[xX][0-9a-fA-F]+$/.test(text)) {
34
+ const n = parseInt(text.slice(2), 16);
35
+ return Number.isFinite(n) ? n : null;
36
+ }
37
+ if (text.length > 1 && /^0[0-7]+$/.test(text)) {
38
+ const n = parseInt(text, 8);
39
+ return Number.isFinite(n) ? n : null;
40
+ }
41
+ if (text.length > 1 && /^0[0-9]+$/.test(text)) {
42
+ return null;
43
+ }
44
+ if (/^[0-9]+$/.test(text)) {
45
+ const n = parseInt(text, 10);
46
+ return Number.isFinite(n) ? n : null;
47
+ }
48
+ return null;
49
+ };
50
+
51
+ const normalizeIPAddress = (host) => {
52
+ if (typeof host !== 'string' || !host || host.indexOf(':') !== -1) {
53
+ return host;
54
+ }
55
+
56
+ let h = host;
57
+ if (h.charAt(0) === '[' && h.charAt(h.length - 1) === ']') {
58
+ h = h.slice(1, -1);
59
+ }
60
+ h = h.replace(/\.+$/, '');
61
+
62
+ // Allowed characters for any IPv4 shape: digits, dot, 'x', 'X', hex digits.
63
+ if (!/^[0-9.xXa-fA-F]+$/.test(h)) return host;
64
+
65
+ const parts = h.split('.');
66
+
67
+ // No part may be empty (e.g. "127..0.1" or "127.0.0."). Trailing dots are
68
+ // already stripped above; this guards against the empty-middle case.
69
+ if (parts.some((p) => p === '')) return host;
70
+
71
+ if (parts.length === 4) {
72
+ // Full IPv4 form: each part is an octet.
73
+ const octets = parts.map(parseIPv4Octet);
74
+ if (octets.some((n) => n === null || n < 0 || n > 255)) return host;
75
+ return octets.join('.');
76
+ }
77
+
78
+ if (parts.length > 4) {
79
+ return host;
80
+ }
81
+
82
+ // Shorthand: 1..3 parts. Node's URL parser treats a 1-part input as a 32-bit
83
+ // integer split into octets, which has surprising semantics (e.g. "127" ->
84
+ // "0.0.0.127"). Reject 1-part inputs to keep the helper predictable: the
85
+ // fail-safe returns the input unchanged and the comparison layer falls
86
+ // through to non-bypass.
87
+ if (parts.length === 1) return host;
88
+
89
+ // 2..3 parts: literal parts fill from the left, tail fills remaining octets
90
+ // from the right with zero-padding.
91
+ const literalOctets = parts.slice(0, -1);
92
+ const tail = parts[parts.length - 1];
93
+ const tailSlots = 4 - literalOctets.length;
94
+
95
+ // Tail is parsed as a full IPv4 number (hex/octal/decimal) and packed
96
+ // low-byte-right into the remaining octets, matching Node's URL parser.
97
+ // e.g. 127.65535 (tail 0xFFFF into 3 slots) -> 127.0.255.255;
98
+ // 127.0x00ff (tail 0xFF into 3 slots) -> 127.0.0.255;
99
+ // 127.0.65535 (tail 0xFFFF into 2 slots) -> 127.0.255.255.
100
+ const tailValue = parseIPv4Octet(tail);
101
+ if (tailValue === null) return host;
102
+ const maxTail = (1 << (8 * tailSlots)) - 1;
103
+ if (tailValue < 0 || tailValue > maxTail) return host;
104
+
105
+ const tailOctets = new Array(tailSlots).fill(0);
106
+ for (let i = tailSlots - 1, v = tailValue; i >= 0; i--, v >>= 8) {
107
+ tailOctets[i] = v & 0xff;
108
+ }
109
+
110
+ const literal = literalOctets.map(parseIPv4Octet);
111
+ if (literal.some((n) => n === null || n < 0 || n > 255)) return host;
112
+
113
+ return [...literal, ...tailOctets].join('.');
114
+ };
115
+
10
116
  const isIPv6ZeroGroup = (group) => /^0{1,4}$/.test(group);
11
117
 
12
118
  // The unspecified address (IPv4 0.0.0.0 / IPv6 ::) resolves to the local host
@@ -153,7 +259,16 @@ const normalizeNoProxyHost = (hostname) => {
153
259
  hostname = hostname.slice(1, -1);
154
260
  }
155
261
 
156
- return unmapIPv4MappedIPv6(hostname.replace(/\.+$/, ''));
262
+ const trimmed = hostname.replace(/\.+$/, '');
263
+
264
+ // IPv4 shorthand/octal/hex → dotted-decimal; helper is a no-op for inputs
265
+ // containing ':' (IPv6 and IPv4-mapped IPv6) so we fall through to unmap.
266
+ const ipv4 = normalizeIPAddress(trimmed);
267
+ if (ipv4 !== trimmed) {
268
+ return ipv4;
269
+ }
270
+
271
+ return unmapIPv4MappedIPv6(trimmed);
157
272
  };
158
273
 
159
274
  export default function shouldBypassProxy(location) {
@@ -185,6 +300,10 @@ export default function shouldBypassProxy(location) {
185
300
  return false;
186
301
  }
187
302
 
303
+ if (entry === '*') {
304
+ return true;
305
+ }
306
+
188
307
  let [entryHost, entryPort] = parseNoProxyEntry(entry);
189
308
 
190
309
  entryHost = normalizeNoProxyHost(entryHost);
@@ -4,6 +4,7 @@ import utils from '../utils.js';
4
4
  import AxiosError from '../core/AxiosError.js';
5
5
  // temporary hotfix to avoid circular references until AxiosURLSearchParams is refactored
6
6
  import PlatformFormData from '../platform/node/classes/FormData.js';
7
+ import PlatformBuffer from '../platform/node/classes/Buffer.js';
7
8
 
8
9
  // Default nesting limit shared with the inverse transform (formDataToJSON) so
9
10
  // the FormData <-> JSON round-trip stays symmetric.
@@ -143,7 +144,13 @@ function toFormData(obj, formData, options) {
143
144
  }
144
145
 
145
146
  if (utils.isArrayBuffer(value) || utils.isTypedArray(value)) {
146
- return useBlob && typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value);
147
+ if (useBlob && typeof _Blob === 'function') {
148
+ return new _Blob([value]);
149
+ }
150
+ if (PlatformBuffer && PlatformBuffer.isBufferAvailable()) {
151
+ return PlatformBuffer.from(value);
152
+ }
153
+ throw new AxiosError('Blob is not supported. Use a Buffer instead.', AxiosError.ERR_NOT_SUPPORT);
147
154
  }
148
155
 
149
156
  return value;
@@ -79,7 +79,7 @@ validators.spelling = function spelling(correctSpelling) {
79
79
  */
80
80
 
81
81
  function assertOptions(options, schema, allowUnknown) {
82
- if (typeof options !== 'object') {
82
+ if (typeof options !== 'object' || options === null) {
83
83
  throw new AxiosError('options must be an object', AxiosError.ERR_BAD_OPTION_VALUE);
84
84
  }
85
85
  const keys = Object.keys(options);
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ export default {
4
+ isBufferAvailable() {
5
+ return typeof Buffer !== 'undefined';
6
+ },
7
+
8
+ from(value) {
9
+ return Buffer.from(value);
10
+ }
11
+ };
package/lib/utils.js CHANGED
@@ -282,6 +282,7 @@ const isBlob = kindOfTest('Blob');
282
282
  * @returns {boolean} True if value is a FileList, otherwise false
283
283
  */
284
284
  const isFileList = kindOfTest('FileList');
285
+ const isSet = kindOfTest('Set');
285
286
 
286
287
  /**
287
288
  * Determine if a value is a Stream
@@ -847,12 +848,23 @@ const toJSONObject = (obj) => {
847
848
  if (!('toJSON' in source)) {
848
849
  // add-on descent / delete-on-ascent: preserves path semantics, so DAG nodes serialise at every occurrence (see #7230).
849
850
  visited.add(source);
850
- const target = isArray(source) ? [] : {};
851
851
 
852
- forEach(source, (value, key) => {
853
- const reducedValue = visit(value);
854
- !isUndefined(reducedValue) && (target[key] = reducedValue);
855
- });
852
+ let target;
853
+
854
+ if (isSet(source)) {
855
+ target = [];
856
+ for (const value of source) {
857
+ const reducedValue = visit(value);
858
+ !isUndefined(reducedValue) && target.push(reducedValue);
859
+ }
860
+ } else {
861
+ target = isArray(source) ? [] : {};
862
+
863
+ forEach(source, (value, key) => {
864
+ const reducedValue = visit(value);
865
+ !isUndefined(reducedValue) && (target[key] = reducedValue);
866
+ });
867
+ }
856
868
 
857
869
  visited.delete(source);
858
870
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axios",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Promise based HTTP client for the browser and node.js",
5
5
  "main": "./dist/node/axios.cjs",
6
6
  "module": "./index.js",
@@ -51,12 +51,14 @@
51
51
  "./dist/node/axios.cjs": "./dist/browser/axios.cjs",
52
52
  "./lib/adapters/http.js": "./lib/helpers/null.js",
53
53
  "./lib/platform/node/index.js": "./lib/platform/browser/index.js",
54
+ "./lib/platform/node/classes/Buffer.js": "./lib/helpers/null.js",
54
55
  "./lib/platform/node/classes/FormData.js": "./lib/helpers/null.js"
55
56
  },
56
57
  "react-native": {
57
58
  "./dist/node/axios.cjs": "./dist/browser/axios.cjs",
58
59
  "./lib/adapters/http.js": "./lib/helpers/null.js",
59
60
  "./lib/platform/node/index.js": "./lib/platform/browser/index.js",
61
+ "./lib/platform/node/classes/Buffer.js": "./lib/helpers/null.js",
60
62
  "./lib/platform/node/classes/FormData.js": "./lib/helpers/null.js"
61
63
  },
62
64
  "repository": {
@@ -87,8 +89,8 @@
87
89
  "Martti Laine (https://github.com/codeclown)",
88
90
  "Xianming Zhong (https://github.com/chinesedfan)",
89
91
  "Shaan Majid (https://github.com/shaanmajid)",
90
- "Willian Agostini (https://github.com/WillianAgostini)",
91
92
  "Remco Haszing (https://github.com/remcohaszing)",
93
+ "Willian Agostini (https://github.com/WillianAgostini)",
92
94
  "Rikki Gibson (https://github.com/RikkiGibson)"
93
95
  ],
94
96
  "sideEffects": false,
@@ -114,7 +116,7 @@
114
116
  "dist/node/axios.cjs"
115
117
  ],
116
118
  "scripts": {
117
- "build": "gulp clear && cross-env NODE_ENV=production rollup -c -m",
119
+ "build": "gulp clear && cross-env NODE_ENV=production rollup -c",
118
120
  "version": "npm run build && git add package.json",
119
121
  "preversion": "gulp version",
120
122
  "test": "npm run test:vitest",
@@ -138,50 +140,50 @@
138
140
  },
139
141
  "dependencies": {
140
142
  "follow-redirects": "^1.16.0",
141
- "form-data": "^4.0.5",
143
+ "form-data": "^4.0.6",
142
144
  "https-proxy-agent": "^5.0.1",
143
145
  "proxy-from-env": "^2.1.0"
144
146
  },
145
147
  "devDependencies": {
146
148
  "@babel/core": "^7.29.0",
147
149
  "@babel/preset-env": "^7.29.5",
148
- "@commitlint/cli": "^21.0.1",
149
- "@commitlint/config-conventional": "^21.0.1",
150
+ "@commitlint/cli": "^21.2.0",
151
+ "@commitlint/config-conventional": "^21.2.0",
150
152
  "@eslint/js": "^10.0.1",
151
153
  "@rollup/plugin-alias": "^6.0.0",
152
- "@rollup/plugin-babel": "^7.0.0",
153
- "@rollup/plugin-commonjs": "^29.0.2",
154
+ "@rollup/plugin-babel": "^7.1.0",
155
+ "@rollup/plugin-commonjs": "^29.0.3",
154
156
  "@rollup/plugin-json": "^6.1.0",
155
157
  "@rollup/plugin-node-resolve": "^16.0.3",
156
158
  "@rollup/plugin-terser": "^1.0.0",
157
- "@vitest/browser": "^4.1.7",
158
- "@vitest/browser-playwright": "^4.1.7",
159
+ "@vitest/browser": "^4.1.9",
160
+ "@vitest/browser-playwright": "^4.1.9",
159
161
  "abortcontroller-polyfill": "^1.7.8",
160
- "acorn": "^8.16.0",
161
- "body-parser": "^2.2.2",
162
+ "acorn": "^8.17.0",
163
+ "body-parser": "^2.3.0",
162
164
  "chalk": "^5.6.2",
163
165
  "cross-env": "^10.1.0",
164
166
  "dev-null": "^0.1.1",
165
- "eslint": "^10.4.0",
167
+ "eslint": "^10.6.0",
166
168
  "express": "^5.2.1",
167
169
  "formdata-node": "^6.0.3",
168
170
  "formidable": "^3.5.4",
169
- "fs-extra": "^11.3.4",
171
+ "fs-extra": "^11.3.6",
170
172
  "get-stream": "^9.0.1",
171
- "globals": "^17.6.0",
173
+ "globals": "^17.7.0",
172
174
  "gulp": "^5.0.1",
173
175
  "husky": "^9.1.7",
174
- "lint-staged": "^17.0.5",
176
+ "lint-staged": "^17.0.8",
175
177
  "minimist": "^1.2.8",
176
- "multer": "^2.1.1",
177
- "playwright": "^1.60.0",
178
- "prettier": "^3.8.3",
178
+ "multer": "^2.2.0",
179
+ "playwright": "^1.61.1",
180
+ "prettier": "^3.9.4",
179
181
  "rollup": "^4.60.4",
180
182
  "rollup-plugin-bundle-size": "^1.0.3",
181
183
  "selfsigned": "^5.5.0",
182
184
  "stream-throttle": "^0.1.3",
183
185
  "typescript": "^5.9.3",
184
- "vitest": "^4.1.7"
186
+ "vitest": "^4.1.9"
185
187
  },
186
188
  "commitlint": {
187
189
  "rules": {