Index: frontend/node_modules/qs/.editorconfig
===================================================================
--- frontend/node_modules/qs/.editorconfig	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/.editorconfig	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,46 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+max_line_length = 180
+quote_type = single
+
+[test/*]
+max_line_length = off
+
+[LICENSE.md]
+indent_size = off
+
+[*.md]
+max_line_length = off
+
+[*.json]
+max_line_length = off
+
+[Makefile]
+max_line_length = off
+
+[CHANGELOG.md]
+indent_style = space
+indent_size = 2
+
+[LICENSE]
+indent_size = 2
+max_line_length = off
+
+[coverage/**/*]
+indent_size = off
+indent_style = off
+indent = off
+max_line_length = off
+
+[.nycrc]
+indent_style = tab
+
+[tea.yaml]
+indent_size = 2
Index: frontend/node_modules/qs/.github/FUNDING.yml
===================================================================
--- frontend/node_modules/qs/.github/FUNDING.yml	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/.github/FUNDING.yml	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: [ljharb]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: npm/qs
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with a single custom sponsorship URL
Index: frontend/node_modules/qs/.github/SECURITY.md
===================================================================
--- frontend/node_modules/qs/.github/SECURITY.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/.github/SECURITY.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,11 @@
+# Security
+
+Please file a private vulnerability report via GitHub, email [@ljharb](https://github.com/ljharb), or see https://tidelift.com/security if you have a potential security vulnerability to report.
+
+## Incident Response Plan
+
+Please see our [Incident Response Plan](https://github.com/ljharb/.github/blob/main/INCIDENT_RESPONSE_PLAN.md).
+
+## Threat Model
+
+Please see [THREAT_MODEL.md](./THREAT_MODEL.md).
Index: frontend/node_modules/qs/.github/THREAT_MODEL.md
===================================================================
--- frontend/node_modules/qs/.github/THREAT_MODEL.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/.github/THREAT_MODEL.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,78 @@
+## Threat Model for qs (querystring parsing library)
+
+### 1. Library Overview
+
+- **Library Name:** qs
+- **Brief Description:** A JavaScript library for parsing and stringifying URL query strings, supporting nested objects and arrays. It is widely used in Node.js and web applications for processing query parameters[2][6][8].
+- **Key Public APIs/Functions:** `qs.parse()`, `qs.stringify()`
+
+### 2. Define Scope
+
+This threat model focuses on the core parsing and stringifying functionality, specifically the handling of nested objects and arrays, option validation, and cycle management in stringification.
+
+### 3. Conceptual System Diagram
+
+```
+Caller Application → qs.parse(input, options) → Parsing Engine → Output Object
+                           │
+                           └→ Options Handling
+
+Caller Application → qs.stringify(obj, options) → Stringifying Engine → Output String
+                           │
+                           └→ Options Handling
+                           └→ Cycle Tracking
+```
+
+**Trust Boundaries:**
+- **Input string (parse):** May come from untrusted sources (e.g., user input, network requests)
+- **Input object (stringify):** May contain cycles, which can lead to infinite loops during stringification
+- **Options:** Provided by the caller
+- **Cycle Tracking:** Used only during stringification to detect and handle circular references
+
+### 4. Identify Assets
+
+- **Integrity of parsed output:** Prevent malicious manipulation of the output object structure, especially ensuring builtins/globals are not modified as a result of parse[3][4][8].
+- **Confidentiality of processed data:** Avoid leaking sensitive information through errors or output.
+- **Availability/performance for host application:** Prevent crashes or resource exhaustion in the consuming application.
+- **Security of host application:** Prevent the library from being a vector for attacks (e.g., prototype pollution, DoS).
+- **Reputation of library:** Maintain trust by avoiding supply chain attacks and vulnerabilities[1].
+
+### 5. Identify Threats
+
+| Component / API / Interaction         | S  | T  | R  | I  | D  | E  |
+|---------------------------------------|----|----|----|----|----|----|
+| Public API Call (`parse`)             | –  | ✓  | –  | ✓  | ✓  | ✓  |
+| Public API Call (`stringify`)         | –  | ✓  | –  | ✓  | ✓  | –  |
+| Options Handling                      | ✓  | ✓  | –  | ✓  | –  | ✓  |
+| Dependency Interaction                | –  | –  | –  | –  | ✓  | –  |
+
+**Key Threats:**
+- **Tampering:** Malicious input can, if not prevented, alter parsed output (e.g., prototype pollution via `__proto__`, modification of builtins/globals)[3][4][8].
+- **Information Disclosure:** Error messages may expose internal details or sensitive data.
+- **Denial of Service:** Large or malformed input can exhaust memory or CPU.
+- **Elevation of Privilege:** Prototype pollution can lead to unintended privilege escalation in the host application[3][4][8].
+
+### 6. Mitigation/Countermeasures
+
+| Threat Identified                                 | Proposed Mitigation |
+|---------------------------------------------------|---------------------|
+| Tampering (malicious input, prototype pollution)  | Strict input validation; keep `allowPrototypes: false` by default; use `plainObjects` for output; ensure builtins/globals are never modified by parse[4][8]. |
+| Information Disclosure (error messages)           | Generic error messages without stack traces or internal paths. |
+| Denial of Service (memory/CPU exhaustion)         | Enforce `arrayLimit` and `parameterLimit` with safe defaults; enable `throwOnLimitExceeded`; limit nesting depth[7]. |
+| Elevation of Privilege (prototype pollution)      | Keep `allowPrototypes: false`; validate options against allowlist; use `plainObjects` to avoid prototype pollution[4][8]. |
+
+### 7. Risk Ranking
+
+- **High:** Denial of Service via array parsing or malformed input (historical vulnerability)
+- **Medium:** Prototype pollution via options or input (if `allowPrototypes` enabled)
+- **Low:** Information disclosure in errors
+
+### 8. Next Steps & Review
+
+1. **Audit option validation logic.**
+2. **Add depth limiting to nested parsing and stringification.**
+3. **Implement fuzz testing for parser and stringifier edge cases.**
+4. **Regularly review dependencies for vulnerabilities.**
+5. **Keep documentation and threat model up to date.**
+6. **Ensure builtins/globals are never modified as a result of parse.**
+7. **Support round-trip consistency between parse and stringify as a non-security goal, with the right options[5][9].**
Index: frontend/node_modules/qs/.nycrc
===================================================================
--- frontend/node_modules/qs/.nycrc	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/.nycrc	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,13 @@
+{
+	"all": true,
+	"check-coverage": false,
+	"reporter": ["text-summary", "text", "html", "json"],
+	"lines": 86,
+	"statements": 85.93,
+	"functions": 82.43,
+	"branches": 76.06,
+	"exclude": [
+		"coverage",
+		"dist"
+	]
+}
Index: frontend/node_modules/qs/CHANGELOG.md
===================================================================
--- frontend/node_modules/qs/CHANGELOG.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/CHANGELOG.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,822 @@
+## **6.15.2**
+- [Fix] `stringify`: skip null/undefined entries in `arrayFormat: 'comma'` + `encodeValuesOnly` instead of crashing in `encoder`
+- [Fix] `stringify`: use configured `delimiter` after `charsetSentinel` (#555)
+- [Fix] `stringify`: apply `formatter` to encoded key under `strictNullHandling` (#554)
+- [Fix] `stringify`: skip null/undefined filter-array entries instead of crashing in `encoder` (#551)
+- [Fix] `parse`: handle nested bracket groups and add regression tests (#530)
+- [readme] fix grammar (#550)
+- [Dev Deps] update `@ljharb/eslint-config`
+- [Tests] add regression tests for keys containing percent-encoded bracket text
+
+## **6.15.1**
+- [Fix] `parse`: `parameterLimit: Infinity` with `throwOnLimitExceeded: true` silently drops all parameters
+- [Deps] update `@ljharb/eslint-config`
+- [Dev Deps] update `@ljharb/eslint-config`, `iconv-lite`
+- [Tests] increase coverage
+
+## **6.15.0**
+- [New] `parse`: add `strictMerge` option to wrap object/primitive conflicts in an array (#425, #122)
+- [Fix] `duplicates` option should not apply to bracket notation keys (#514)
+
+## **6.14.2**
+- [Fix] `parse`: mark overflow objects for indexed notation exceeding `arrayLimit` (#546)
+- [Fix] `arrayLimit` means max count, not max index, in `combine`/`merge`/`parseArrayValue`
+- [Fix] `parse`: throw on `arrayLimit` exceeded with indexed notation when `throwOnLimitExceeded` is true (#529)
+- [Fix] `parse`: enforce `arrayLimit` on `comma`-parsed values
+- [Fix] `parse`: fix error message to reflect arrayLimit as max index; remove extraneous comments (#545)
+- [Robustness] avoid `.push`, use `void`
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [meta] fix changelog typo (`arrayLength` → `arrayLimit`)
+- [actions] fix rebase workflow permissions
+
+## **6.14.1**
+- [Fix] ensure `arrayLimit` applies to `[]` notation as well
+- [Fix] `parse`: when a custom decoder returns `null` for a key, ignore that key
+- [Refactor] `parse`: extract key segment splitting helper
+- [meta] add threat model
+- [actions] add workflow permissions
+- [Tests] `stringify`: increase coverage
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `npmignore`, `es-value-fixtures`, `for-each`, `object-inspect`
+
+## **6.14.0**
+- [New] `parse`: add `throwOnParameterLimitExceeded` option (#517)
+- [Refactor] `parse`: use `utils.combine` more
+- [patch] `parse`: add explicit `throwOnLimitExceeded` default
+- [actions] use shared action; re-add finishers
+- [meta] Fix changelog formatting bug
+- [Deps] update `side-channel`
+- [Dev Deps] update `es-value-fixtures`, `has-bigints`, `has-proto`, `has-symbols`
+- [Tests] increase coverage
+
+## **6.13.3**
+[Fix] fix regressions from robustness refactor
+[actions] update reusable workflows
+
+## **6.13.2**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.13.1**
+- [Fix] `stringify`: avoid a crash when a `filter` key is `null`
+- [Fix] `utils.merge`: functions should not be stringified into keys
+- [Fix] `parse`: avoid a crash with interpretNumericEntities: true, comma: true, and iso charset
+- [Fix] `stringify`: ensure a non-string `filter` does not crash
+- [Refactor] use `__proto__` syntax instead of `Object.create` for null objects
+- [Refactor] misc cleanup
+- [Tests] `utils.merge`: add some coverage
+- [Tests] fix a test case
+- [actions] split out node 10-20, and 20+
+- [Dev Deps] update `es-value-fixtures`, `mock-property`, `object-inspect`, `tape`
+
+## **6.13.0**
+- [New] `parse`: add `strictDepth` option (#511)
+- [Tests] use `npm audit` instead of `aud`
+
+## **6.12.5**
+- [Fix] fix regressions from robustness refactor
+- [actions] update reusable workflows
+
+## **6.12.4**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.12.3**
+- [Fix] `parse`: properly account for `strictNullHandling` when `allowEmptyArrays`
+- [meta] fix changelog indentation
+
+## **6.12.2**
+- [Fix] `parse`: parse encoded square brackets (#506)
+- [readme] add CII best practices badge
+
+## **6.12.1**
+- [Fix] `parse`: Disable `decodeDotInKeys` by default to restore previous behavior (#501)
+- [Performance] `utils`: Optimize performance under large data volumes, reduce memory usage, and speed up processing (#502)
+- [Refactor] `utils`: use `+=`
+- [Tests] increase coverage
+
+## **6.12.0**
+
+- [New] `parse`/`stringify`: add `decodeDotInKeys`/`encodeDotKeys` options (#488)
+- [New] `parse`: add `duplicates` option
+- [New] `parse`/`stringify`: add `allowEmptyArrays` option to allow [] in object values (#487)
+- [Refactor] `parse`/`stringify`: move allowDots config logic to its own variable
+- [Refactor] `stringify`: move option-handling code into `normalizeStringifyOptions`
+- [readme] update readme, add logos (#484)
+- [readme] `stringify`: clarify default `arrayFormat` behavior
+- [readme] fix line wrapping
+- [readme] remove dead badges
+- [Deps] update `side-channel`
+- [meta] make the dist build 50% smaller
+- [meta] add `sideEffects` flag
+- [meta] run build in prepack, not prepublish
+- [Tests] `parse`: remove useless tests; add coverage
+- [Tests] `stringify`: increase coverage
+- [Tests] use `mock-property`
+- [Tests] `stringify`: improve coverage
+- [Dev Deps] update `@ljharb/eslint-config `, `aud`, `has-override-mistake`, `has-property-descriptors`, `mock-property`, `npmignore`, `object-inspect`, `tape`
+- [Dev Deps] pin `glob`, since v10.3.8+ requires a broken `jackspeak`
+- [Dev Deps] pin `jackspeak` since 2.1.2+ depends on npm aliases, which kill the install process in npm < 6
+
+## **6.11.4**
+- [Fix] fix regressions from robustness refactor
+- [actions] update reusable workflows
+
+## **6.11.3**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.11.2**
+- [Fix] `parse`: Fix parsing when the global Object prototype is frozen (#473)
+- [Tests] add passing test cases with empty keys (#473)
+
+## **6.11.1**
+- [Fix] `stringify`: encode comma values more consistently (#463)
+- [readme] add usage of `filter` option for injecting custom serialization, i.e. of custom types (#447)
+- [meta] remove extraneous code backticks (#457)
+- [meta] fix changelog markdown
+- [actions] update checkout action
+- [actions] restrict action permissions
+- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `object-inspect`, `tape`
+
+## **6.11.0**
+- [New] [Fix] `stringify`: revert 0e903c0; add `commaRoundTrip` option (#442)
+- [readme] fix version badge
+
+## **6.10.7**
+- [Fix] fix regressions from robustness refactor
+- [actions] update reusable workflows
+
+## **6.10.6**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.10.5**
+- [Fix] `stringify`: with `arrayFormat: comma`, properly include an explicit `[]` on a single-item array (#434)
+
+## **6.10.4**
+- [Fix] `stringify`: with `arrayFormat: comma`, include an explicit `[]` on a single-item array (#441)
+- [meta] use `npmignore` to autogenerate an npmignore file
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `has-symbol`, `object-inspect`, `tape`
+
+## **6.10.3**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [actions] reuse common workflows
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `object-inspect`, `tape`
+
+## **6.10.2**
+- [Fix] `stringify`: actually fix cyclic references (#426)
+- [Fix] `stringify`: avoid encoding arrayformat comma when `encodeValuesOnly = true` (#424)
+- [readme] remove travis badge; add github actions/codecov badges; update URLs
+- [Docs] add note and links for coercing primitive values (#408)
+- [actions] update codecov uploader
+- [actions] update workflows
+- [Tests] clean up stringify tests slightly
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `object-inspect`, `safe-publish-latest`, `tape`
+
+## **6.10.1**
+- [Fix] `stringify`: avoid exception on repeated object values (#402)
+
+## **6.10.0**
+- [New] `stringify`: throw on cycles, instead of an infinite loop (#395, #394, #393)
+- [New] `parse`: add `allowSparse` option for collapsing arrays with missing indices (#312)
+- [meta] fix README.md (#399)
+- [meta] only run `npm run dist` in publish, not install
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `has-symbols`, `tape`
+- [Tests] fix tests on node v0.6
+- [Tests] use `ljharb/actions/node/install` instead of `ljharb/actions/node/run`
+- [Tests] Revert "[meta] ignore eclint transitive audit warning"
+
+## **6.9.9**
+- [Fix] fix regressions from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.9.8**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.9.7**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Fix] `stringify`: avoid encoding arrayformat comma when `encodeValuesOnly = true` (#424)
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [readme] remove travis badge; add github actions/codecov badges; update URLs
+- [Docs] add note and links for coercing primitive values (#408)
+- [Tests] clean up stringify tests slightly
+- [meta] fix README.md (#399)
+- Revert "[meta] ignore eclint transitive audit warning"
+- [actions] backport actions from main
+- [Dev Deps] backport updates from main
+
+## **6.9.6**
+- [Fix] restore `dist` dir; mistakenly removed in d4f6c32
+
+## **6.9.5**
+- [Fix] `stringify`: do not encode parens for RFC1738
+- [Fix] `stringify`: fix arrayFormat comma with empty array/objects (#350)
+- [Refactor] `format`: remove `util.assign` call
+- [meta] add "Allow Edits" workflow; update rebase workflow
+- [actions] switch Automatic Rebase workflow to `pull_request_target` event
+- [Tests] `stringify`: add tests for #378
+- [Tests] migrate tests to Github Actions
+- [Tests] run `nyc` on all tests; use `tape` runner
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `browserify`, `mkdirp`, `object-inspect`, `tape`; add `aud`
+
+## **6.9.4**
+- [Fix] `stringify`: when `arrayFormat` is `comma`, respect `serializeDate` (#364)
+- [Refactor] `stringify`: reduce branching (part of #350)
+- [Refactor] move `maybeMap` to `utils`
+- [Dev Deps] update `browserify`, `tape`
+
+## **6.9.3**
+- [Fix] proper comma parsing of URL-encoded commas (#361)
+- [Fix] parses comma delimited array while having percent-encoded comma treated as normal text (#336)
+
+## **6.9.2**
+- [Fix] `parse`: Fix parsing array from object with `comma` true (#359)
+- [Fix] `parse`: throw a TypeError instead of an Error for bad charset (#349)
+- [meta] ignore eclint transitive audit warning
+- [meta] fix indentation in package.json
+- [meta] add tidelift marketing copy
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `object-inspect`, `has-symbols`, `tape`, `mkdirp`, `iconv-lite`
+- [actions] add automatic rebasing / merge commit blocking
+
+## **6.9.1**
+- [Fix] `parse`: with comma true, handle field that holds an array of arrays (#335)
+- [Fix] `parse`: with comma true, do not split non-string values (#334)
+- [meta] add `funding` field
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`
+- [Tests] use shared travis-ci config
+
+## **6.9.0**
+- [New] `parse`/`stringify`: Pass extra key/value argument to `decoder` (#333)
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `evalmd`
+- [Tests] `parse`: add passing `arrayFormat` tests
+- [Tests] add `posttest` using `npx aud` to run `npm audit` without a lockfile
+- [Tests] up to `node` `v12.10`, `v11.15`, `v10.16`, `v8.16`
+- [Tests] `Buffer.from` in node v5.0-v5.9 and v4.0-v4.4 requires a TypedArray
+
+## **6.8.5**
+- [Fix] fix regressions from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.8.4**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.8.3**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [Fix] `stringify`: avoid encoding arrayformat comma when `encodeValuesOnly = true` (#424)
+- [readme] remove travis badge; add github actions/codecov badges; update URLs
+- [Tests] clean up stringify tests slightly
+- [Docs] add note and links for coercing primitive values (#408)
+- [meta] fix README.md (#399)
+- [actions] backport actions from main
+- [Dev Deps] backport updates from main
+- [Refactor] `stringify`: reduce branching
+- [meta] do not publish workflow files
+
+## **6.8.2**
+- [Fix] proper comma parsing of URL-encoded commas (#361)
+- [Fix] parses comma delimited array while having percent-encoded comma treated as normal text (#336)
+
+## **6.8.1**
+- [Fix] `parse`: Fix parsing array from object with `comma` true (#359)
+- [Fix] `parse`: throw a TypeError instead of an Error for bad charset (#349)
+- [Fix] `parse`: with comma true, handle field that holds an array of arrays (#335)
+- [fix] `parse`: with comma true, do not split non-string values (#334)
+- [meta] add tidelift marketing copy
+- [meta] add `funding` field
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `tape`, `safe-publish-latest`, `evalmd`, `has-symbols`, `iconv-lite`, `mkdirp`, `object-inspect`
+- [Tests] `parse`: add passing `arrayFormat` tests
+- [Tests] use shared travis-ci configs
+- [Tests] `Buffer.from` in node v5.0-v5.9 and v4.0-v4.4 requires a TypedArray
+- [actions] add automatic rebasing / merge commit blocking
+
+## **6.8.0**
+- [New] add `depth=false` to preserve the original key; [Fix] `depth=0` should preserve the original key (#326)
+- [New] [Fix] stringify symbols and bigints
+- [Fix] ensure node 0.12 can stringify Symbols
+- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
+- [Refactor] `formats`: tiny bit of cleanup.
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `browserify`, `safe-publish-latest`, `iconv-lite`, `tape`
+- [Tests] add tests for `depth=0` and `depth=false` behavior, both current and intuitive/intended (#326)
+- [Tests] use `eclint` instead of `editorconfig-tools`
+- [docs] readme: add security note
+- [meta] add github sponsorship
+- [meta] add FUNDING.yml
+- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
+
+## **6.7.5**
+- [Fix] fix regressions from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.7.4**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.7.3**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Fix] `stringify`: avoid encoding arrayformat comma when `encodeValuesOnly = true` (#424)
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [readme] remove travis badge; add github actions/codecov badges; update URLs
+- [Docs] add note and links for coercing primitive values (#408)
+- [meta] fix README.md (#399)
+- [meta] do not publish workflow files
+- [actions] backport actions from main
+- [Dev Deps] backport updates from main
+- [Tests] use `nyc` for coverage
+- [Tests] clean up stringify tests slightly
+
+## **6.7.2**
+- [Fix] proper comma parsing of URL-encoded commas (#361)
+- [Fix] parses comma delimited array while having percent-encoded comma treated as normal text (#336)
+
+## **6.7.1**
+- [Fix] `parse`: Fix parsing array from object with `comma` true (#359)
+- [Fix] `parse`: with comma true, handle field that holds an array of arrays (#335)
+- [fix] `parse`: with comma true, do not split non-string values (#334)
+- [Fix] `parse`: throw a TypeError instead of an Error for bad charset (#349)
+- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
+- [Refactor] `formats`: tiny bit of cleanup.
+- readme: add security note
+- [meta] add tidelift marketing copy
+- [meta] add `funding` field
+- [meta] add FUNDING.yml
+- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `tape`, `safe-publish-latest`, `evalmd`, `iconv-lite`, `mkdirp`, `object-inspect`, `browserify`
+- [Tests] `parse`: add passing `arrayFormat` tests
+- [Tests] use shared travis-ci configs
+- [Tests] `Buffer.from` in node v5.0-v5.9 and v4.0-v4.4 requires a TypedArray
+- [Tests] add tests for `depth=0` and `depth=false` behavior, both current and intuitive/intended
+- [Tests] use `eclint` instead of `editorconfig-tools`
+- [actions] add automatic rebasing / merge commit blocking
+
+## **6.7.0**
+- [New] `stringify`/`parse`: add `comma` as an `arrayFormat` option (#276, #219)
+- [Fix] correctly parse nested arrays (#212)
+- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source, also with an array source
+- [Robustness] `stringify`: cache `Object.prototype.hasOwnProperty`
+- [Refactor] `utils`: `isBuffer`: small tweak; add tests
+- [Refactor] use cached `Array.isArray`
+- [Refactor] `parse`/`stringify`: make a function to normalize the options
+- [Refactor] `utils`: reduce observable [[Get]]s
+- [Refactor] `stringify`/`utils`: cache `Array.isArray`
+- [Tests] always use `String(x)` over `x.toString()`
+- [Tests] fix Buffer tests to work in node < 4.5 and node < 5.10
+- [Tests] temporarily allow coverage to fail
+
+## **6.6.3**
+- [Fix] fix regressions from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.6.2**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.6.1**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
+- [Fix] `utils.merge`: avoid a crash with a null target and an array source
+- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
+- [Fix] correctly parse nested arrays
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [Robustness] `stringify`: cache `Object.prototype.hasOwnProperty`
+- [Refactor] `formats`: tiny bit of cleanup.
+- [Refactor] `utils`: `isBuffer`: small tweak; add tests
+- [Refactor]: `stringify`/`utils`: cache `Array.isArray`
+- [Refactor] `utils`: reduce observable [[Get]]s
+- [Refactor] use cached `Array.isArray`
+- [Refactor] `parse`/`stringify`: make a function to normalize the options
+- [readme] remove travis badge; add github actions/codecov badges; update URLs
+- [Docs] Clarify the need for "arrayLimit" option
+- [meta] fix README.md (#399)
+- [meta] do not publish workflow files
+- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
+- [meta] add FUNDING.yml
+- [meta] Fixes typo in CHANGELOG.md
+- [actions] backport actions from main
+- [Tests] fix Buffer tests to work in node < 4.5 and node < 5.10
+- [Tests] always use `String(x)` over `x.toString()`
+- [Dev Deps] backport from main
+
+## **6.6.0**
+- [New] Add support for iso-8859-1, utf8 "sentinel" and numeric entities (#268)
+- [New] move two-value combine to a `utils` function (#189)
+- [Fix] `stringify`: fix a crash with `strictNullHandling` and a custom `filter`/`serializeDate` (#279)
+- [Fix] when `parseArrays` is false, properly handle keys ending in `[]` (#260)
+- [Fix] `stringify`: do not crash in an obscure combo of `interpretNumericEntities`, a bad custom `decoder`, & `iso-8859-1`
+- [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided
+- [refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
+- [Refactor] `parse`: only need to reassign the var once
+- [Refactor] `parse`/`stringify`: clean up `charset` options checking; fix defaults
+- [Refactor] add missing defaults
+- [Refactor] `parse`: one less `concat` call
+- [Refactor] `utils`: `compactQueue`: make it explicitly side-effecting
+- [Dev Deps] update `browserify`, `eslint`, `@ljharb/eslint-config`, `iconv-lite`, `safe-publish-latest`, `tape`
+- [Tests] up to `node` `v10.10`, `v9.11`, `v8.12`, `v6.14`, `v4.9`; pin included builds to LTS
+
+## **6.5.5**
+- [Fix] fix regressions from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.5.4**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] document that `addQueryPrefix` does not add `?` to empty output (#418)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.5.3**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
+- [Fix] correctly parse nested arrays
+- [Fix] `stringify`: fix a crash with `strictNullHandling` and a custom `filter`/`serializeDate` (#279)
+- [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided
+- [Fix] when `parseArrays` is false, properly handle keys ending in `[]`
+- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
+- [Fix] `utils.merge`: avoid a crash with a null target and an array source
+- [Refactor] `utils`: reduce observable [[Get]]s
+- [Refactor] use cached `Array.isArray`
+- [Refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
+- [Refactor] `parse`: only need to reassign the var once
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [readme] remove travis badge; add github actions/codecov badges; update URLs
+- [Docs] Clean up license text so it’s properly detected as BSD-3-Clause
+- [Docs] Clarify the need for "arrayLimit" option
+- [meta] fix README.md (#399)
+- [meta] add FUNDING.yml
+- [actions] backport actions from main
+- [Tests] always use `String(x)` over `x.toString()`
+- [Tests] remove nonexistent tape option
+- [Dev Deps] backport from main
+
+## **6.5.2**
+- [Fix] use `safer-buffer` instead of `Buffer` constructor
+- [Refactor] utils: `module.exports` one thing, instead of mutating `exports` (#230)
+- [Dev Deps] update `browserify`, `eslint`, `iconv-lite`, `safer-buffer`, `tape`, `browserify`
+
+## **6.5.1**
+- [Fix] Fix parsing & compacting very deep objects (#224)
+- [Refactor] name utils functions
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `tape`
+- [Tests] up to `node` `v8.4`; use `nvm install-latest-npm` so newer npm doesn’t break older node
+- [Tests] Use precise dist for Node.js 0.6 runtime (#225)
+- [Tests] make 0.6 required, now that it’s passing
+- [Tests] on `node` `v8.2`; fix npm on node 0.6
+
+## **6.5.0**
+- [New] add `utils.assign`
+- [New] pass default encoder/decoder to custom encoder/decoder functions (#206)
+- [New] `parse`/`stringify`: add `ignoreQueryPrefix`/`addQueryPrefix` options, respectively (#213)
+- [Fix] Handle stringifying empty objects with addQueryPrefix (#217)
+- [Fix] do not mutate `options` argument (#207)
+- [Refactor] `parse`: cache index to reuse in else statement (#182)
+- [Docs] add various badges to readme (#208)
+- [Dev Deps] update `eslint`, `browserify`, `iconv-lite`, `tape`
+- [Tests] up to `node` `v8.1`, `v7.10`, `v6.11`; npm v4.6 breaks on node < v1; npm v5+ breaks on node < v4
+- [Tests] add `editorconfig-tools`
+
+## **6.4.3**
+- [Fix] fix regressions from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.4.2**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] replace runkit CI badge with shields.io check-runs badge
+- [readme] replace travis CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.4.1**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
+- [Fix] use `safer-buffer` instead of `Buffer` constructor
+- [Fix] `utils.merge`: avoid a crash with a null target and an array source
+- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
+- [Fix] `stringify`: fix a crash with `strictNullHandling` and a custom `filter`/`serializeDate` (#279)
+- [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided
+- [Fix] when `parseArrays` is false, properly handle keys ending in `[]`
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [Refactor] use cached `Array.isArray`
+- [Refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
+- [readme] remove travis badge; add github actions/codecov badges; update URLs
+- [Docs] Clarify the need for "arrayLimit" option
+- [meta] fix README.md (#399)
+- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
+- [meta] add FUNDING.yml
+- [actions] backport actions from main
+- [Tests] remove nonexistent tape option
+- [Dev Deps] backport from main
+
+## **6.4.0**
+- [New] `qs.stringify`: add `encodeValuesOnly` option
+- [Fix] follow `allowPrototypes` option during merge (#201, #201)
+- [Fix] support keys starting with brackets (#202, #200)
+- [Fix] chmod a-x
+- [Dev Deps] update `eslint`
+- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
+- [eslint] reduce warnings
+
+## **6.3.5**
+- [Fix] fix regressions from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.3.4**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] replace travis CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.3.3**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Fix] fix for an impossible situation: when the formatter is called with a non-string value
+- [Fix] `utils.merge`: avoid a crash with a null target and an array source
+- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
+- [Fix] `stringify`: fix a crash with `strictNullHandling` and a custom `filter`/`serializeDate` (#279)
+- [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided
+- [Fix] when `parseArrays` is false, properly handle keys ending in `[]`
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [Refactor] use cached `Array.isArray`
+- [Refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269)
+- [Docs] Clarify the need for "arrayLimit" option
+- [meta] fix README.md (#399)
+- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
+- [meta] add FUNDING.yml
+- [actions] backport actions from main
+- [Tests] use `safer-buffer` instead of `Buffer` constructor
+- [Tests] remove nonexistent tape option
+- [Dev Deps] backport from main
+
+## **6.3.2**
+- [Fix] follow `allowPrototypes` option during merge (#201, #200)
+- [Dev Deps] update `eslint`
+- [Fix] chmod a-x
+- [Fix] support keys starting with brackets (#202, #200)
+- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
+
+## **6.3.1**
+- [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties (thanks, @snyk!)
+- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `browserify`, `iconv-lite`, `qs-iconv`, `tape`
+- [Tests] on all node minors; improve test matrix
+- [Docs] document stringify option `allowDots` (#195)
+- [Docs] add empty object and array values example (#195)
+- [Docs] Fix minor inconsistency/typo (#192)
+- [Docs] document stringify option `sort` (#191)
+- [Refactor] `stringify`: throw faster with an invalid encoder
+- [Refactor] remove unnecessary escapes (#184)
+- Remove contributing.md, since `qs` is no longer part of `hapi` (#183)
+
+## **6.3.0**
+- [New] Add support for RFC 1738 (#174, #173)
+- [New] `stringify`: Add `serializeDate` option to customize Date serialization (#159)
+- [Fix] ensure `utils.merge` handles merging two arrays
+- [Refactor] only constructors should be capitalized
+- [Refactor] capitalized var names are for constructors only
+- [Refactor] avoid using a sparse array
+- [Robustness] `formats`: cache `String#replace`
+- [Dev Deps] update `browserify`, `eslint`, `@ljharb/eslint-config`; add `safe-publish-latest`
+- [Tests] up to `node` `v6.8`, `v4.6`; improve test matrix
+- [Tests] flesh out arrayLimit/arrayFormat tests (#107)
+- [Tests] skip Object.create tests when null objects are not available
+- [Tests] Turn on eslint for test files (#175)
+
+## **6.2.6**
+- [Fix] fix regression from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.2.5**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] replace travis CI badge with shields.io check-runs badge
+- [actions] fix rebase workflow permissions
+
+## **6.2.4**
+- [Fix] `parse`: ignore `__proto__` keys (#428)
+- [Fix] `utils.merge`: avoid a crash with a null target and an array source
+- [Fix] `utils.merge`: avoid a crash with a null target and a truthy non-array source
+- [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided
+- [Fix] when `parseArrays` is false, properly handle keys ending in `[]`
+- [Robustness] `stringify`: avoid relying on a global `undefined` (#427)
+- [Refactor] use cached `Array.isArray`
+- [Docs] Clarify the need for "arrayLimit" option
+- [meta] fix README.md (#399)
+- [meta] Clean up license text so it’s properly detected as BSD-3-Clause
+- [meta] add FUNDING.yml
+- [actions] backport actions from main
+- [Tests] use `safer-buffer` instead of `Buffer` constructor
+- [Tests] remove nonexistent tape option
+- [Dev Deps] backport from main
+
+## **6.2.3**
+- [Fix] follow `allowPrototypes` option during merge (#201, #200)
+- [Fix] chmod a-x
+- [Fix] support keys starting with brackets (#202, #200)
+- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
+
+## **6.2.2**
+- [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
+
+## **6.2.1**
+- [Fix] ensure `key[]=x&key[]&key[]=y` results in 3, not 2, values
+- [Refactor] Be explicit and use `Object.prototype.hasOwnProperty.call`
+- [Tests] remove `parallelshell` since it does not reliably report failures
+- [Tests] up to `node` `v6.3`, `v5.12`
+- [Dev Deps] update `tape`, `eslint`, `@ljharb/eslint-config`, `qs-iconv`
+
+## [**6.2.0**](https://github.com/ljharb/qs/issues?milestone=36&state=closed)
+- [New] pass Buffers to the encoder/decoder directly (#161)
+- [New] add "encoder" and "decoder" options, for custom param encoding/decoding (#160)
+- [Fix] fix compacting of nested sparse arrays (#150)
+
+## **6.1.4**
+- [Fix] fix regression from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.1.3**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] replace travis CI badge with shields.io check-runs badge
+
+## **6.1.2**
+- [Fix] follow `allowPrototypes` option during merge (#201, #200)
+- [Fix] chmod a-x
+- [Fix] support keys starting with brackets (#202, #200)
+- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
+
+## **6.1.1**
+- [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
+
+## [**6.1.0**](https://github.com/ljharb/qs/issues?milestone=35&state=closed)
+- [New] allowDots option for `stringify` (#151)
+- [Fix] "sort" option should work at a depth of 3 or more (#151)
+- [Fix] Restore `dist` directory; will be removed in v7 (#148)
+
+## **6.0.6**
+- [Fix] fix regression from robustness refactor
+- [meta] add `npmignore` to autogenerate an npmignore file
+- [actions] update reusable workflows
+
+## **6.0.5**
+- [Robustness] avoid `.push`, use `void`
+- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
+- [readme] replace travis CI badge with shields.io check-runs badge
+
+## **6.0.4**
+- [Fix] follow `allowPrototypes` option during merge (#201, #200)
+- [Fix] chmod a-x
+- [Fix] support keys starting with brackets (#202, #200)
+- [Tests] up to `node` `v7.7`, `v6.10`,` v4.8`; disable osx builds since they block linux builds
+
+## **6.0.3**
+- [Fix] ensure that `allowPrototypes: false` does not ever shadow Object.prototype properties
+- [Fix] Restore `dist` directory; will be removed in v7 (#148)
+
+## [**6.0.2**](https://github.com/ljharb/qs/issues?milestone=33&state=closed)
+- Revert ES6 requirement and restore support for node down to v0.8.
+
+## [**6.0.1**](https://github.com/ljharb/qs/issues?milestone=32&state=closed)
+- [**#127**](https://github.com/ljharb/qs/pull/127) Fix engines definition in package.json
+
+## [**6.0.0**](https://github.com/ljharb/qs/issues?milestone=31&state=closed)
+- [**#124**](https://github.com/ljharb/qs/issues/124) Use ES6 and drop support for node < v4
+
+## **5.2.1**
+- [Fix] ensure `key[]=x&key[]&key[]=y` results in 3, not 2, values
+
+## [**5.2.0**](https://github.com/ljharb/qs/issues?milestone=30&state=closed)
+- [**#64**](https://github.com/ljharb/qs/issues/64) Add option to sort object keys in the query string
+
+## [**5.1.0**](https://github.com/ljharb/qs/issues?milestone=29&state=closed)
+- [**#117**](https://github.com/ljharb/qs/issues/117) make URI encoding stringified results optional
+- [**#106**](https://github.com/ljharb/qs/issues/106) Add flag `skipNulls` to optionally skip null values in stringify
+
+## [**5.0.0**](https://github.com/ljharb/qs/issues?milestone=28&state=closed)
+- [**#114**](https://github.com/ljharb/qs/issues/114) default allowDots to false
+- [**#100**](https://github.com/ljharb/qs/issues/100) include dist to npm
+
+## [**4.0.0**](https://github.com/ljharb/qs/issues?milestone=26&state=closed)
+- [**#98**](https://github.com/ljharb/qs/issues/98) make returning plain objects and allowing prototype overwriting properties optional
+
+## [**3.1.0**](https://github.com/ljharb/qs/issues?milestone=24&state=closed)
+- [**#89**](https://github.com/ljharb/qs/issues/89) Add option to disable "Transform dot notation to bracket notation"
+
+## [**3.0.0**](https://github.com/ljharb/qs/issues?milestone=23&state=closed)
+- [**#80**](https://github.com/ljharb/qs/issues/80) qs.parse silently drops properties
+- [**#77**](https://github.com/ljharb/qs/issues/77) Perf boost
+- [**#60**](https://github.com/ljharb/qs/issues/60) Add explicit option to disable array parsing
+- [**#74**](https://github.com/ljharb/qs/issues/74) Bad parse when turning array into object
+- [**#81**](https://github.com/ljharb/qs/issues/81) Add a `filter` option
+- [**#68**](https://github.com/ljharb/qs/issues/68) Fixed issue with recursion and passing strings into objects.
+- [**#66**](https://github.com/ljharb/qs/issues/66) Add mixed array and object dot notation support Closes: #47
+- [**#76**](https://github.com/ljharb/qs/issues/76) RFC 3986
+- [**#85**](https://github.com/ljharb/qs/issues/85) No equal sign
+- [**#84**](https://github.com/ljharb/qs/issues/84) update license attribute
+
+## [**2.4.1**](https://github.com/ljharb/qs/issues?milestone=20&state=closed)
+- [**#73**](https://github.com/ljharb/qs/issues/73) Property 'hasOwnProperty' of object #<Object> is not a function
+
+## [**2.4.0**](https://github.com/ljharb/qs/issues?milestone=19&state=closed)
+- [**#70**](https://github.com/ljharb/qs/issues/70) Add arrayFormat option
+
+## [**2.3.3**](https://github.com/ljharb/qs/issues?milestone=18&state=closed)
+- [**#59**](https://github.com/ljharb/qs/issues/59) make sure array indexes are >= 0, closes #57
+- [**#58**](https://github.com/ljharb/qs/issues/58) make qs usable for browser loader
+
+## [**2.3.2**](https://github.com/ljharb/qs/issues?milestone=17&state=closed)
+- [**#55**](https://github.com/ljharb/qs/issues/55) allow merging a string into an object
+
+## [**2.3.1**](https://github.com/ljharb/qs/issues?milestone=16&state=closed)
+- [**#52**](https://github.com/ljharb/qs/issues/52) Return "undefined" and "false" instead of throwing "TypeError".
+
+## [**2.3.0**](https://github.com/ljharb/qs/issues?milestone=15&state=closed)
+- [**#50**](https://github.com/ljharb/qs/issues/50) add option to omit array indices, closes #46
+
+## [**2.2.5**](https://github.com/ljharb/qs/issues?milestone=14&state=closed)
+- [**#39**](https://github.com/ljharb/qs/issues/39) Is there an alternative to Buffer.isBuffer?
+- [**#49**](https://github.com/ljharb/qs/issues/49) refactor utils.merge, fixes #45
+- [**#41**](https://github.com/ljharb/qs/issues/41) avoid browserifying Buffer, for #39
+
+## [**2.2.4**](https://github.com/ljharb/qs/issues?milestone=13&state=closed)
+- [**#38**](https://github.com/ljharb/qs/issues/38) how to handle object keys beginning with a number
+
+## [**2.2.3**](https://github.com/ljharb/qs/issues?milestone=12&state=closed)
+- [**#37**](https://github.com/ljharb/qs/issues/37) parser discards first empty value in array
+- [**#36**](https://github.com/ljharb/qs/issues/36) Update to lab 4.x
+
+## [**2.2.2**](https://github.com/ljharb/qs/issues?milestone=11&state=closed)
+- [**#33**](https://github.com/ljharb/qs/issues/33) Error when plain object in a value
+- [**#34**](https://github.com/ljharb/qs/issues/34) use Object.prototype.hasOwnProperty.call instead of obj.hasOwnProperty
+- [**#24**](https://github.com/ljharb/qs/issues/24) Changelog? Semver?
+
+## [**2.2.1**](https://github.com/ljharb/qs/issues?milestone=10&state=closed)
+- [**#32**](https://github.com/ljharb/qs/issues/32) account for circular references properly, closes #31
+- [**#31**](https://github.com/ljharb/qs/issues/31) qs.parse stackoverflow on circular objects
+
+## [**2.2.0**](https://github.com/ljharb/qs/issues?milestone=9&state=closed)
+- [**#26**](https://github.com/ljharb/qs/issues/26) Don't use Buffer global if it's not present
+- [**#30**](https://github.com/ljharb/qs/issues/30) Bug when merging non-object values into arrays
+- [**#29**](https://github.com/ljharb/qs/issues/29) Don't call Utils.clone at the top of Utils.merge
+- [**#23**](https://github.com/ljharb/qs/issues/23) Ability to not limit parameters?
+
+## [**2.1.0**](https://github.com/ljharb/qs/issues?milestone=8&state=closed)
+- [**#22**](https://github.com/ljharb/qs/issues/22) Enable using a RegExp as delimiter
+
+## [**2.0.0**](https://github.com/ljharb/qs/issues?milestone=7&state=closed)
+- [**#18**](https://github.com/ljharb/qs/issues/18) Why is there arrayLimit?
+- [**#20**](https://github.com/ljharb/qs/issues/20) Configurable parametersLimit
+- [**#21**](https://github.com/ljharb/qs/issues/21) make all limits optional, for #18, for #20
+
+## [**1.2.2**](https://github.com/ljharb/qs/issues?milestone=6&state=closed)
+- [**#19**](https://github.com/ljharb/qs/issues/19) Don't overwrite null values
+
+## [**1.2.1**](https://github.com/ljharb/qs/issues?milestone=5&state=closed)
+- [**#16**](https://github.com/ljharb/qs/issues/16) ignore non-string delimiters
+- [**#15**](https://github.com/ljharb/qs/issues/15) Close code block
+
+## [**1.2.0**](https://github.com/ljharb/qs/issues?milestone=4&state=closed)
+- [**#12**](https://github.com/ljharb/qs/issues/12) Add optional delim argument
+- [**#13**](https://github.com/ljharb/qs/issues/13) fix #11: flattened keys in array are now correctly parsed
+
+## [**1.1.0**](https://github.com/ljharb/qs/issues?milestone=3&state=closed)
+- [**#7**](https://github.com/ljharb/qs/issues/7) Empty values of a POST array disappear after being submitted
+- [**#9**](https://github.com/ljharb/qs/issues/9) Should not omit equals signs (=) when value is null
+- [**#6**](https://github.com/ljharb/qs/issues/6) Minor grammar fix in README
+
+## [**1.0.2**](https://github.com/ljharb/qs/issues?milestone=2&state=closed)
+- [**#5**](https://github.com/ljharb/qs/issues/5) array holes incorrectly copied into object on large index
Index: frontend/node_modules/qs/LICENSE.md
===================================================================
--- frontend/node_modules/qs/LICENSE.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/LICENSE.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Index: frontend/node_modules/qs/README.md
===================================================================
--- frontend/node_modules/qs/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,758 @@
+<p align="center">
+    <img alt="qs" src="./logos/banner_default.png" width="800" />
+</p>
+
+# qs <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
+
+[![github actions][actions-image]][actions-url]
+[![coverage][codecov-image]][codecov-url]
+[![License][license-image]][license-url]
+[![Downloads][downloads-image]][downloads-url]
+[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/9058/badge)](https://bestpractices.coreinfrastructure.org/projects/9058)
+
+[![npm badge][npm-badge-png]][package-url]
+
+A querystring parsing and stringifying library with some added security.
+
+Lead Maintainer: [Jordan Harband](https://github.com/ljharb)
+
+The **qs** module was originally created and maintained by [TJ Holowaychuk](https://github.com/visionmedia/node-querystring).
+
+## Usage
+
+```javascript
+var qs = require('qs');
+var assert = require('assert');
+
+var obj = qs.parse('a=c');
+assert.deepEqual(obj, { a: 'c' });
+
+var str = qs.stringify(obj);
+assert.equal(str, 'a=c');
+```
+
+### Parsing Objects
+
+[](#preventEval)
+```javascript
+qs.parse(string, [options]);
+```
+
+**qs** allows you to create nested objects within your query strings, by surrounding the name of sub-keys with square brackets `[]`.
+For example, the string `'foo[bar]=baz'` converts to:
+
+```javascript
+assert.deepEqual(qs.parse('foo[bar]=baz'), {
+    foo: {
+        bar: 'baz'
+    }
+});
+```
+
+When using the `plainObjects` option the parsed value is returned as a null object, created via `{ __proto__: null }` and as such you should be aware that prototype methods will not exist on it and a user may set those names to whatever value they like:
+
+```javascript
+var nullObject = qs.parse('a[hasOwnProperty]=b', { plainObjects: true });
+assert.deepEqual(nullObject, { a: { hasOwnProperty: 'b' } });
+```
+
+By default parameters that would overwrite properties on the object prototype are ignored, if you wish to keep the data from those fields either use `plainObjects` as mentioned above, or set `allowPrototypes` to `true` which will allow user input to overwrite those properties.
+*WARNING* It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten.
+Always be careful with this option.
+
+```javascript
+var protoObject = qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true });
+assert.deepEqual(protoObject, { a: { hasOwnProperty: 'b' } });
+```
+
+URI encoded strings work too:
+
+```javascript
+assert.deepEqual(qs.parse('a%5Bb%5D=c'), {
+    a: { b: 'c' }
+});
+```
+
+You can also nest your objects, like `'foo[bar][baz]=foobarbaz'`:
+
+```javascript
+assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), {
+    foo: {
+        bar: {
+            baz: 'foobarbaz'
+        }
+    }
+});
+```
+
+By default, when nesting objects **qs** will only parse up to 5 children deep.
+This means if you attempt to parse a string like `'a[b][c][d][e][f][g][h][i]=j'` your resulting object will be:
+
+```javascript
+var expected = {
+    a: {
+        b: {
+            c: {
+                d: {
+                    e: {
+                        f: {
+                            '[g][h][i]': 'j'
+                        }
+                    }
+                }
+            }
+        }
+    }
+};
+var string = 'a[b][c][d][e][f][g][h][i]=j';
+assert.deepEqual(qs.parse(string), expected);
+```
+
+This depth can be overridden by passing a `depth` option to `qs.parse(string, [options])`:
+
+```javascript
+var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
+assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } });
+```
+
+You can configure **qs** to throw an error when parsing nested input beyond this depth using the `strictDepth` option (defaulted to false):
+
+```javascript
+try {
+    qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
+} catch (err) {
+    assert(err instanceof RangeError);
+    assert.strictEqual(err.message, 'Input depth exceeded depth option of 1 and strictDepth is true');
+}
+```
+
+The depth limit helps mitigate abuse when **qs** is used to parse user input, and it is recommended to keep it a reasonably small number. The strictDepth option adds a layer of protection by throwing an error when the limit is exceeded, allowing you to catch and handle such cases.
+
+For similar reasons, by default **qs** will only parse up to 1000 parameters. This can be overridden by passing a `parameterLimit` option:
+
+```javascript
+var limited = qs.parse('a=b&c=d', { parameterLimit: 1 });
+assert.deepEqual(limited, { a: 'b' });
+```
+
+If you want an error to be thrown whenever the a limit is exceeded (eg, `parameterLimit`, `arrayLimit`), set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit.
+```javascript
+try {
+    qs.parse('a=1&b=2&c=3&d=4', { parameterLimit: 3, throwOnLimitExceeded: true });
+} catch (err) {
+    assert(err instanceof Error);
+    assert.strictEqual(err.message, 'Parameter limit exceeded. Only 3 parameters allowed.');
+}
+```
+
+When `throwOnLimitExceeded` is set to `false` (default), **qs** will parse up to the specified `parameterLimit` and ignore the rest without throwing an error.
+
+To bypass the leading question mark, use `ignoreQueryPrefix`:
+
+```javascript
+var prefixed = qs.parse('?a=b&c=d', { ignoreQueryPrefix: true });
+assert.deepEqual(prefixed, { a: 'b', c: 'd' });
+```
+
+An optional delimiter can also be passed:
+
+```javascript
+var delimited = qs.parse('a=b;c=d', { delimiter: ';' });
+assert.deepEqual(delimited, { a: 'b', c: 'd' });
+```
+
+Delimiters can be a regular expression too:
+
+```javascript
+var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
+assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' });
+```
+
+Option `allowDots` can be used to enable dot notation:
+
+```javascript
+var withDots = qs.parse('a.b=c', { allowDots: true });
+assert.deepEqual(withDots, { a: { b: 'c' } });
+```
+
+Option `decodeDotInKeys` can be used to decode dots in keys
+Note: it implies `allowDots`, so `parse` will error if you set `decodeDotInKeys` to `true`, and `allowDots` to `false`.
+
+```javascript
+var withDots = qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { decodeDotInKeys: true });
+assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' }});
+```
+
+Option `allowEmptyArrays` can be used to allow empty array values in an object
+```javascript
+var withEmptyArrays = qs.parse('foo[]&bar=baz', { allowEmptyArrays: true });
+assert.deepEqual(withEmptyArrays, { foo: [], bar: 'baz' });
+```
+
+Option `duplicates` can be used to change the behavior when duplicate keys are encountered
+```javascript
+assert.deepEqual(qs.parse('foo=bar&foo=baz'), { foo: ['bar', 'baz'] });
+assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'combine' }), { foo: ['bar', 'baz'] });
+assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'first' }), { foo: 'bar' });
+assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'last' }), { foo: 'baz' });
+```
+
+Note that keys with bracket notation (`[]`) always combine into arrays, regardless of the `duplicates` setting:
+```javascript
+assert.deepEqual(qs.parse('a=1&a=2&b[]=1&b[]=2', { duplicates: 'last' }), { a: '2', b: ['1', '2'] });
+```
+
+If you have to deal with legacy browsers or services, there's also support for decoding percent-encoded octets as iso-8859-1:
+
+```javascript
+var oldCharset = qs.parse('a=%A7', { charset: 'iso-8859-1' });
+assert.deepEqual(oldCharset, { a: '§' });
+```
+
+Some services add an initial `utf8=✓` value to forms so that old Internet Explorer versions are more likely to submit the form as utf-8.
+Additionally, the server can check the value against wrong encodings of the checkmark character and detect that a query string or `application/x-www-form-urlencoded` body was *not* sent as utf-8, eg. if the form had an `accept-charset` parameter or the containing page had a different character set.
+
+**qs** supports this mechanism via the `charsetSentinel` option.
+If specified, the `utf8` parameter will be omitted from the returned object.
+It will be used to switch to `iso-8859-1`/`utf-8` mode depending on how the checkmark is encoded.
+
+**Important**: When you specify both the `charset` option and the `charsetSentinel` option, the `charset` will be overridden when the request contains a `utf8` parameter from which the actual charset can be deduced.
+In that sense the `charset` will behave as the default charset rather than the authoritative charset.
+
+```javascript
+var detectedAsUtf8 = qs.parse('utf8=%E2%9C%93&a=%C3%B8', {
+    charset: 'iso-8859-1',
+    charsetSentinel: true
+});
+assert.deepEqual(detectedAsUtf8, { a: 'ø' });
+
+// Browsers encode the checkmark as &#10003; when submitting as iso-8859-1:
+var detectedAsIso8859_1 = qs.parse('utf8=%26%2310003%3B&a=%F8', {
+    charset: 'utf-8',
+    charsetSentinel: true
+});
+assert.deepEqual(detectedAsIso8859_1, { a: 'ø' });
+```
+
+If you want to decode the `&#...;` syntax to the actual character, you can specify the `interpretNumericEntities` option as well:
+
+```javascript
+var detectedAsIso8859_1 = qs.parse('a=%26%239786%3B', {
+    charset: 'iso-8859-1',
+    interpretNumericEntities: true
+});
+assert.deepEqual(detectedAsIso8859_1, { a: '☺' });
+```
+
+It also works when the charset has been detected in `charsetSentinel` mode.
+
+### Parsing Arrays
+
+**qs** can also parse arrays using a similar `[]` notation:
+
+```javascript
+var withArray = qs.parse('a[]=b&a[]=c');
+assert.deepEqual(withArray, { a: ['b', 'c'] });
+```
+
+You may specify an index as well:
+
+```javascript
+var withIndexes = qs.parse('a[1]=c&a[0]=b');
+assert.deepEqual(withIndexes, { a: ['b', 'c'] });
+```
+
+Note that the only difference between an index in an array and a key in an object is that the value between the brackets must be a number to create an array.
+When creating arrays with specific indices, **qs** will compact a sparse array to only the existing values preserving their order:
+
+```javascript
+var noSparse = qs.parse('a[1]=b&a[15]=c');
+assert.deepEqual(noSparse, { a: ['b', 'c'] });
+```
+
+You may also use `allowSparse` option to parse sparse arrays:
+
+```javascript
+var sparseArray = qs.parse('a[1]=2&a[3]=5', { allowSparse: true });
+assert.deepEqual(sparseArray, { a: [, '2', , '5'] });
+```
+
+Note that an empty string is also a value, and will be preserved:
+
+```javascript
+var withEmptyString = qs.parse('a[]=&a[]=b');
+assert.deepEqual(withEmptyString, { a: ['', 'b'] });
+
+var withIndexedEmptyString = qs.parse('a[0]=b&a[1]=&a[2]=c');
+assert.deepEqual(withIndexedEmptyString, { a: ['b', '', 'c'] });
+```
+
+**qs** will also limit arrays to a maximum of `20` elements.
+Any array members with an index of `20` or greater will instead be converted to an object with the index as the key.
+This is needed to handle cases when someone sent, for example, `a[999999999]` and it will take significant time to iterate over this huge array.
+
+```javascript
+var withMaxIndex = qs.parse('a[100]=b');
+assert.deepEqual(withMaxIndex, { a: { '100': 'b' } });
+```
+
+This limit can be overridden by passing an `arrayLimit` option:
+
+```javascript
+var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 });
+assert.deepEqual(withArrayLimit, { a: { '1': 'b' } });
+```
+
+If you want to throw an error whenever the array limit is exceeded, set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit.
+```javascript
+try {
+    qs.parse('a[1]=b', { arrayLimit: 0, throwOnLimitExceeded: true });
+} catch (err) {
+    assert(err instanceof Error);
+    assert.strictEqual(err.message, 'Array limit exceeded. Only 0 elements allowed in an array.');
+}
+```
+
+When `throwOnLimitExceeded` is set to `false` (default), **qs** will parse up to the specified `arrayLimit` and if the limit is exceeded, the array will instead be converted to an object with the index as the key
+
+To prevent array syntax (`a[]`, `a[0]`) from being parsed as arrays, set `parseArrays` to `false`.
+Note that duplicate keys (e.g. `a=b&a=c`) may still produce arrays when `duplicates` is `'combine'` (the default).
+
+```javascript
+var noParsingArrays = qs.parse('a[]=b', { parseArrays: false });
+assert.deepEqual(noParsingArrays, { a: { '0': 'b' } });
+```
+
+If you mix notations, **qs** will merge the two items into an object:
+
+```javascript
+var mixedNotation = qs.parse('a[0]=b&a[b]=c');
+assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } });
+```
+
+When a key appears as both a plain value and an object, **qs** will by default wrap the conflicting values in an array (`strictMerge` defaults to `true`):
+
+```javascript
+assert.deepEqual(qs.parse('a[b]=c&a=d'), { a: [{ b: 'c' }, 'd'] });
+assert.deepEqual(qs.parse('a=d&a[b]=c'), { a: ['d', { b: 'c' }] });
+```
+
+To restore the legacy behavior (where the primitive is used as a key with value `true`), set `strictMerge` to `false`:
+
+```javascript
+assert.deepEqual(qs.parse('a[b]=c&a=d', { strictMerge: false }), { a: { b: 'c', d: true } });
+```
+
+You can also create arrays of objects:
+
+```javascript
+var arraysOfObjects = qs.parse('a[][b]=c');
+assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] });
+```
+
+Some people use comma to join array, **qs** can parse it:
+```javascript
+var arraysOfObjects = qs.parse('a=b,c', { comma: true })
+assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] })
+```
+(_this cannot convert nested objects, such as `a={b:1},{c:d}`_)
+
+### Parsing primitive/scalar values (numbers, booleans, null, etc)
+
+By default, all values are parsed as strings.
+This behavior will not change and is explained in [issue #91](https://github.com/ljharb/qs/issues/91).
+
+```javascript
+var primitiveValues = qs.parse('a=15&b=true&c=null');
+assert.deepEqual(primitiveValues, { a: '15', b: 'true', c: 'null' });
+```
+
+If you wish to auto-convert values which look like numbers, booleans, and other values into their primitive counterparts, you can use the [query-types Express JS middleware](https://github.com/xpepermint/query-types) which will auto-convert all request query parameters.
+
+### Stringifying
+
+[](#preventEval)
+```javascript
+qs.stringify(object, [options]);
+```
+
+When stringifying, **qs** by default URI encodes output. Objects are stringified as you would expect:
+
+```javascript
+assert.equal(qs.stringify({ a: 'b' }), 'a=b');
+assert.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
+```
+
+This encoding can be disabled by setting the `encode` option to `false`:
+
+```javascript
+var unencoded = qs.stringify({ a: { b: 'c' } }, { encode: false });
+assert.equal(unencoded, 'a[b]=c');
+```
+
+Encoding can be disabled for keys by setting the `encodeValuesOnly` option to `true`:
+```javascript
+var encodedValues = qs.stringify(
+    { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
+    { encodeValuesOnly: true }
+);
+assert.equal(encodedValues,'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h');
+```
+
+This encoding can also be replaced by a custom encoding method set as `encoder` option:
+
+```javascript
+var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) {
+    // Passed in values `a`, `b`, `c`
+    return // Return encoded string
+}})
+```
+
+_(Note: the `encoder` option does not apply if `encode` is `false`)_
+
+Analogue to the `encoder` there is a `decoder` option for `parse` to override decoding of properties and values:
+
+```javascript
+var decoded = qs.parse('x=z', { decoder: function (str) {
+    // Passed in values `x`, `z`
+    return // Return decoded string
+}})
+```
+
+You can encode keys and values using different logic by using the type argument provided to the encoder:
+
+```javascript
+var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str, defaultEncoder, charset, type) {
+    if (type === 'key') {
+        return // Encoded key
+    } else if (type === 'value') {
+        return // Encoded value
+    }
+}})
+```
+
+The type argument is also provided to the decoder:
+
+```javascript
+var decoded = qs.parse('x=z', { decoder: function (str, defaultDecoder, charset, type) {
+    if (type === 'key') {
+        return // Decoded key
+    } else if (type === 'value') {
+        return // Decoded value
+    }
+}})
+```
+
+Examples beyond this point will be shown as though the output is not URI encoded for clarity.
+Please note that the return values in these cases *will* be URI encoded during real usage.
+
+When arrays are stringified, they follow the `arrayFormat` option, which defaults to `indices`:
+
+```javascript
+qs.stringify({ a: ['b', 'c', 'd'] });
+// 'a[0]=b&a[1]=c&a[2]=d'
+```
+
+You may override this by setting the `indices` option to `false`, or to be more explicit, the `arrayFormat` option to `repeat`:
+
+```javascript
+qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false });
+// 'a=b&a=c&a=d'
+```
+
+You may use the `arrayFormat` option to specify the format of the output array:
+
+```javascript
+qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
+// 'a[0]=b&a[1]=c'
+qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
+// 'a[]=b&a[]=c'
+qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
+// 'a=b&a=c'
+qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
+// 'a=b,c'
+```
+
+Note: when using `arrayFormat` set to `'comma'`, you can also pass the `commaRoundTrip` option set to `true` or `false`, to append `[]` on single-item arrays, so that they can round trip through a parse.
+
+When objects are stringified, by default they use bracket notation:
+
+```javascript
+qs.stringify({ a: { b: { c: 'd', e: 'f' } } });
+// 'a[b][c]=d&a[b][e]=f'
+```
+
+You may override this to use dot notation by setting the `allowDots` option to `true`:
+
+```javascript
+qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true });
+// 'a.b.c=d&a.b.e=f'
+```
+
+You may encode the dot notation in the keys of object with option `encodeDotInKeys` by setting it to `true`:
+Note: it implies `allowDots`, so `stringify` will error if you set `decodeDotInKeys` to `true`, and `allowDots` to `false`.
+Caveat: when `encodeValuesOnly` is `true` as well as `encodeDotInKeys`, only dots in keys and nothing else will be encoded.
+```javascript
+qs.stringify({ "name.obj": { "first": "John", "last": "Doe" } }, { allowDots: true, encodeDotInKeys: true })
+// 'name%252Eobj.first=John&name%252Eobj.last=Doe'
+```
+
+You may allow empty array values by setting the `allowEmptyArrays` option to `true`:
+```javascript
+qs.stringify({ foo: [], bar: 'baz' }, { allowEmptyArrays: true });
+// 'foo[]&bar=baz'
+```
+
+Empty strings and null values will omit the value, but the equals sign (=) remains in place:
+
+```javascript
+assert.equal(qs.stringify({ a: '' }), 'a=');
+```
+
+Key with no values (such as an empty object or array) will return nothing:
+
+```javascript
+assert.equal(qs.stringify({ a: [] }), '');
+assert.equal(qs.stringify({ a: {} }), '');
+assert.equal(qs.stringify({ a: [{}] }), '');
+assert.equal(qs.stringify({ a: { b: []} }), '');
+assert.equal(qs.stringify({ a: { b: {}} }), '');
+```
+
+Properties that are set to `undefined` will be omitted entirely:
+
+```javascript
+assert.equal(qs.stringify({ a: null, b: undefined }), 'a=');
+```
+
+The query string may optionally be prepended with a question mark:
+
+```javascript
+assert.equal(qs.stringify({ a: 'b', c: 'd' }, { addQueryPrefix: true }), '?a=b&c=d');
+```
+
+Note that when the output is an empty string, the prefix will not be added:
+
+```javascript
+assert.equal(qs.stringify({}, { addQueryPrefix: true }), '');
+```
+
+The delimiter may be overridden with stringify as well:
+
+```javascript
+assert.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
+```
+
+If you only want to override the serialization of `Date` objects, you can provide a `serializeDate` option:
+
+```javascript
+var date = new Date(7);
+assert.equal(qs.stringify({ a: date }), 'a=1970-01-01T00:00:00.007Z'.replace(/:/g, '%3A'));
+assert.equal(
+    qs.stringify({ a: date }, { serializeDate: function (d) { return d.getTime(); } }),
+    'a=7'
+);
+```
+
+You may use the `sort` option to affect the order of parameter keys:
+
+```javascript
+function alphabeticalSort(a, b) {
+    return a.localeCompare(b);
+}
+assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y');
+```
+
+Finally, you can use the `filter` option to restrict which keys will be included in the stringified output.
+If you pass a function, it will be called for each key to obtain the replacement value.
+Otherwise, if you pass an array, it will be used to select properties and array indices for stringification:
+
+```javascript
+function filterFunc(prefix, value) {
+    if (prefix == 'b') {
+        // Return an `undefined` value to omit a property.
+        return;
+    }
+    if (prefix == 'e[f]') {
+        return value.getTime();
+    }
+    if (prefix == 'e[g][0]') {
+        return value * 2;
+    }
+    return value;
+}
+qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc });
+// 'a=b&c=d&e[f]=123&e[g][0]=4'
+qs.stringify({ a: 'b', c: 'd', e: 'f' }, { filter: ['a', 'e'] });
+// 'a=b&e=f'
+qs.stringify({ a: ['b', 'c', 'd'], e: 'f' }, { filter: ['a', 0, 2] });
+// 'a[0]=b&a[2]=d'
+```
+
+You could also use `filter` to inject custom serialization for user defined types.
+Consider you're working with some api that expects query strings of the format for ranges:
+
+```
+https://domain.com/endpoint?range=30...70
+```
+
+For which you model as:
+
+```javascript
+class Range {
+    constructor(from, to) {
+        this.from = from;
+        this.to = to;
+    }
+}
+```
+
+You could _inject_ a custom serializer to handle values of this type:
+
+```javascript
+qs.stringify(
+    {
+        range: new Range(30, 70),
+    },
+    {
+        filter: (prefix, value) => {
+            if (value instanceof Range) {
+                return `${value.from}...${value.to}`;
+            }
+            // serialize the usual way
+            return value;
+        },
+    }
+);
+// range=30...70
+```
+
+### Handling of `null` values
+
+By default, `null` values are treated like empty strings:
+
+```javascript
+var withNull = qs.stringify({ a: null, b: '' });
+assert.equal(withNull, 'a=&b=');
+```
+
+Parsing does not distinguish between parameters with and without equal signs.
+Both are converted to empty strings.
+
+```javascript
+var equalsInsensitive = qs.parse('a&b=');
+assert.deepEqual(equalsInsensitive, { a: '', b: '' });
+```
+
+To distinguish between `null` values and empty strings use the `strictNullHandling` flag. In the result string the `null`
+values have no `=` sign:
+
+```javascript
+var strictNull = qs.stringify({ a: null, b: '' }, { strictNullHandling: true });
+assert.equal(strictNull, 'a&b=');
+```
+
+To parse values without `=` back to `null` use the `strictNullHandling` flag:
+
+```javascript
+var parsedStrictNull = qs.parse('a&b=', { strictNullHandling: true });
+assert.deepEqual(parsedStrictNull, { a: null, b: '' });
+```
+
+To completely skip rendering keys with `null` values, use the `skipNulls` flag:
+
+```javascript
+var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true });
+assert.equal(nullsSkipped, 'a=b');
+```
+
+If you're communicating with legacy systems, you can switch to `iso-8859-1` using the `charset` option:
+
+```javascript
+var iso = qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' });
+assert.equal(iso, '%E6=%E6');
+```
+
+Characters that don't exist in `iso-8859-1` will be converted to numeric entities, similar to what browsers do:
+
+```javascript
+var numeric = qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' });
+assert.equal(numeric, 'a=%26%239786%3B');
+```
+
+You can use the `charsetSentinel` option to announce the character by including an `utf8=✓` parameter with the proper encoding if the checkmark, similar to what Ruby on Rails and others do when submitting forms.
+
+```javascript
+var sentinel = qs.stringify({ a: '☺' }, { charsetSentinel: true });
+assert.equal(sentinel, 'utf8=%E2%9C%93&a=%E2%98%BA');
+
+var isoSentinel = qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' });
+assert.equal(isoSentinel, 'utf8=%26%2310003%3B&a=%E6');
+```
+
+### Dealing with special character sets
+
+By default the encoding and decoding of characters is done in `utf-8`, and `iso-8859-1` support is also built in via the `charset` parameter.
+
+If you wish to encode querystrings to a different character set (i.e.
+[Shift JIS](https://en.wikipedia.org/wiki/Shift_JIS)) you can use the
+[`qs-iconv`](https://github.com/martinheidegger/qs-iconv) library:
+
+```javascript
+var encoder = require('qs-iconv/encoder')('shift_jis');
+var shiftJISEncoded = qs.stringify({ a: 'こんにちは！' }, { encoder: encoder });
+assert.equal(shiftJISEncoded, 'a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I');
+```
+
+This also works for decoding of query strings:
+
+```javascript
+var decoder = require('qs-iconv/decoder')('shift_jis');
+var obj = qs.parse('a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I', { decoder: decoder });
+assert.deepEqual(obj, { a: 'こんにちは！' });
+```
+
+### RFC 3986 and RFC 1738 space encoding
+
+RFC3986 used as default option and encodes ' ' to *%20* which is backward compatible.
+In the same time, output can be stringified as per RFC1738 with ' ' equal to '+'.
+
+```
+assert.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
+assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC3986' }), 'a=b%20c');
+assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC1738' }), 'a=b+c');
+```
+
+## Security
+
+Please email [@ljharb](https://github.com/ljharb) or see https://tidelift.com/security if you have a potential security vulnerability to report.
+
+## qs for enterprise
+
+Available as part of the Tidelift Subscription
+
+The maintainers of qs and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications.
+Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use.
+[Learn more.](https://tidelift.com/subscription/pkg/npm-qs?utm_source=npm-qs&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
+
+[package-url]: https://npmjs.org/package/qs
+[npm-version-svg]: https://versionbadg.es/ljharb/qs.svg
+[deps-svg]: https://david-dm.org/ljharb/qs.svg
+[deps-url]: https://david-dm.org/ljharb/qs
+[dev-deps-svg]: https://david-dm.org/ljharb/qs/dev-status.svg
+[dev-deps-url]: https://david-dm.org/ljharb/qs#info=devDependencies
+[npm-badge-png]: https://nodei.co/npm/qs.png?downloads=true&stars=true
+[license-image]: https://img.shields.io/npm/l/qs.svg
+[license-url]: LICENSE
+[downloads-image]: https://img.shields.io/npm/dm/qs.svg
+[downloads-url]: https://npm-stat.com/charts.html?package=qs
+[codecov-image]: https://codecov.io/gh/ljharb/qs/branch/main/graphs/badge.svg
+[codecov-url]: https://app.codecov.io/gh/ljharb/qs/
+[actions-image]: https://img.shields.io/github/check-runs/ljharb/qs/main
+[actions-url]: https://github.com/ljharb/qs/actions
+
+## Acknowledgements
+
+qs logo by [NUMI](https://github.com/numi-hq/open-design):
+
+[<img src="https://raw.githubusercontent.com/numi-hq/open-design/main/assets/numi-lockup.png" alt="NUMI Logo" style="width: 200px;"/>](https://numi.tech/?ref=qs)
Index: frontend/node_modules/qs/dist/qs.js
===================================================================
--- frontend/node_modules/qs/dist/qs.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/dist/qs.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,141 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Qs = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
+"use strict";var replace=String.prototype.replace,percentTwenties=/%20/g,Format={RFC1738:"RFC1738",RFC3986:"RFC3986"};module.exports={default:Format.RFC3986,formatters:{RFC1738:function(e){return replace.call(e,percentTwenties,"+")},RFC3986:function(e){return String(e)}},RFC1738:Format.RFC1738,RFC3986:Format.RFC3986};
+
+},{}],2:[function(require,module,exports){
+"use strict";var stringify=require(4),parse=require(3),formats=require(1);module.exports={formats:formats,parse:parse,stringify:stringify};
+
+},{"1":1,"3":3,"4":4}],3:[function(require,module,exports){
+"use strict";var utils=require(5),has=Object.prototype.hasOwnProperty,isArray=Array.isArray,defaults={allowDots:!1,allowEmptyArrays:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decodeDotInKeys:!1,decoder:utils.decode,delimiter:"&",depth:5,duplicates:"combine",ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictDepth:!1,strictMerge:!0,strictNullHandling:!1,throwOnLimitExceeded:!1},interpretNumericEntities=function(e){return e.replace(/&#(\d+);/g,function(e,t){return String.fromCharCode(parseInt(t,10))})},parseArrayValue=function(e,t,r){if(e&&"string"==typeof e&&t.comma&&e.indexOf(",")>-1)return e.split(",");if(t.throwOnLimitExceeded&&r>=t.arrayLimit)throw new RangeError("Array limit exceeded. Only "+t.arrayLimit+" element"+(1===t.arrayLimit?"":"s")+" allowed in an array.");return e},isoSentinel="utf8=%26%2310003%3B",charsetSentinel="utf8=%E2%9C%93",parseValues=function parseQueryStringValues(e,t){var r={__proto__:null},i=t.ignoreQueryPrefix?e.replace(/^\?/,""):e;i=i.replace(/%5B/gi,"[").replace(/%5D/gi,"]");var a=t.parameterLimit===1/0?void 0:t.parameterLimit,o=i.split(t.delimiter,t.throwOnLimitExceeded&&void 0!==a?a+1:a);if(t.throwOnLimitExceeded&&void 0!==a&&o.length>a)throw new RangeError("Parameter limit exceeded. Only "+a+" parameter"+(1===a?"":"s")+" allowed.");var l,n=-1,s=t.charset;if(t.charsetSentinel)for(l=0;l<o.length;++l)0===o[l].indexOf("utf8=")&&(o[l]===charsetSentinel?s="utf-8":o[l]===isoSentinel&&(s="iso-8859-1"),n=l,l=o.length);for(l=0;l<o.length;++l)if(l!==n){var d,c,p=o[l],u=p.indexOf("]="),y=-1===u?p.indexOf("="):u+1;if(-1===y?(d=t.decoder(p,defaults.decoder,s,"key"),c=t.strictNullHandling?null:""):null!==(d=t.decoder(p.slice(0,y),defaults.decoder,s,"key"))&&(c=utils.maybeMap(parseArrayValue(p.slice(y+1),t,isArray(r[d])?r[d].length:0),function(e){return t.decoder(e,defaults.decoder,s,"value")})),c&&t.interpretNumericEntities&&"iso-8859-1"===s&&(c=interpretNumericEntities(String(c))),p.indexOf("[]=")>-1&&(c=isArray(c)?[c]:c),t.comma&&isArray(c)&&c.length>t.arrayLimit){if(t.throwOnLimitExceeded)throw new RangeError("Array limit exceeded. Only "+t.arrayLimit+" element"+(1===t.arrayLimit?"":"s")+" allowed in an array.");c=utils.combine([],c,t.arrayLimit,t.plainObjects)}if(null!==d){var f=has.call(r,d);f&&("combine"===t.duplicates||p.indexOf("[]=")>-1)?r[d]=utils.combine(r[d],c,t.arrayLimit,t.plainObjects):f&&"last"!==t.duplicates||(r[d]=c)}}return r},parseObject=function(e,t,r,i){var a=0;if(e.length>0&&"[]"===e[e.length-1]){var o=e.slice(0,-1).join("");a=Array.isArray(t)&&t[o]?t[o].length:0}for(var l=i?t:parseArrayValue(t,r,a),n=e.length-1;n>=0;--n){var s,d=e[n];if("[]"===d&&r.parseArrays)s=utils.isOverflow(l)?l:r.allowEmptyArrays&&(""===l||r.strictNullHandling&&null===l)?[]:utils.combine([],l,r.arrayLimit,r.plainObjects);else{s=r.plainObjects?{__proto__:null}:{};var c="["===d.charAt(0)&&"]"===d.charAt(d.length-1)?d.slice(1,-1):d,p=r.decodeDotInKeys?c.replace(/%2E/g,"."):c,u=parseInt(p,10),y=!isNaN(u)&&d!==p&&String(u)===p&&u>=0&&r.parseArrays;if(r.parseArrays||""!==p)if(y&&u<r.arrayLimit)(s=[])[u]=l;else{if(y&&r.throwOnLimitExceeded)throw new RangeError("Array limit exceeded. Only "+r.arrayLimit+" element"+(1===r.arrayLimit?"":"s")+" allowed in an array.");y?(s[u]=l,utils.markOverflow(s,u)):"__proto__"!==p&&(s[p]=l)}else s={0:l}}l=s}return l},splitKeyIntoSegments=function splitKeyIntoSegments(e,t){var r=t.allowDots?e.replace(/\.([^.[]+)/g,"[$1]"):e;if(t.depth<=0){if(!t.plainObjects&&has.call(Object.prototype,r)&&!t.allowPrototypes)return;return[r]}var i=[],a=r.indexOf("["),o=a>=0?r.slice(0,a):r;if(o){if(!t.plainObjects&&has.call(Object.prototype,o)&&!t.allowPrototypes)return;i[i.length]=o}for(var l=r.length,n=a,s=0;n>=0&&s<t.depth;){for(var d=1,c=n+1,p=-1;c<l&&p<0;){var u=r.charCodeAt(c);91===u?d+=1:93===u&&0==(d-=1)&&(p=c),c+=1}if(p<0)return i[i.length]="["+r.slice(n)+"]",i;var y=r.slice(n,p+1),f=y.slice(1,-1);if(!t.plainObjects&&has.call(Object.prototype,f)&&!t.allowPrototypes)return;i[i.length]=y,s+=1,n=r.indexOf("[",p+1)}if(n>=0){if(!0===t.strictDepth)throw new RangeError("Input depth exceeded depth option of "+t.depth+" and strictDepth is true");i[i.length]="["+r.slice(n)+"]"}return i},parseKeys=function parseQueryStringKeys(e,t,r,i){if(e){var a=splitKeyIntoSegments(e,r);if(a)return parseObject(a,t,r,i)}},normalizeParseOptions=function normalizeParseOptions(e){if(!e)return defaults;if(void 0!==e.allowEmptyArrays&&"boolean"!=typeof e.allowEmptyArrays)throw new TypeError("`allowEmptyArrays` option can only be `true` or `false`, when provided");if(void 0!==e.decodeDotInKeys&&"boolean"!=typeof e.decodeDotInKeys)throw new TypeError("`decodeDotInKeys` option can only be `true` or `false`, when provided");if(null!==e.decoder&&void 0!==e.decoder&&"function"!=typeof e.decoder)throw new TypeError("Decoder has to be a function.");if(void 0!==e.charset&&"utf-8"!==e.charset&&"iso-8859-1"!==e.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");if(void 0!==e.throwOnLimitExceeded&&"boolean"!=typeof e.throwOnLimitExceeded)throw new TypeError("`throwOnLimitExceeded` option must be a boolean");var t=void 0===e.charset?defaults.charset:e.charset,r=void 0===e.duplicates?defaults.duplicates:e.duplicates;if("combine"!==r&&"first"!==r&&"last"!==r)throw new TypeError("The duplicates option must be either combine, first, or last");return{allowDots:void 0===e.allowDots?!0===e.decodeDotInKeys||defaults.allowDots:!!e.allowDots,allowEmptyArrays:"boolean"==typeof e.allowEmptyArrays?!!e.allowEmptyArrays:defaults.allowEmptyArrays,allowPrototypes:"boolean"==typeof e.allowPrototypes?e.allowPrototypes:defaults.allowPrototypes,allowSparse:"boolean"==typeof e.allowSparse?e.allowSparse:defaults.allowSparse,arrayLimit:"number"==typeof e.arrayLimit?e.arrayLimit:defaults.arrayLimit,charset:t,charsetSentinel:"boolean"==typeof e.charsetSentinel?e.charsetSentinel:defaults.charsetSentinel,comma:"boolean"==typeof e.comma?e.comma:defaults.comma,decodeDotInKeys:"boolean"==typeof e.decodeDotInKeys?e.decodeDotInKeys:defaults.decodeDotInKeys,decoder:"function"==typeof e.decoder?e.decoder:defaults.decoder,delimiter:"string"==typeof e.delimiter||utils.isRegExp(e.delimiter)?e.delimiter:defaults.delimiter,depth:"number"==typeof e.depth||!1===e.depth?+e.depth:defaults.depth,duplicates:r,ignoreQueryPrefix:!0===e.ignoreQueryPrefix,interpretNumericEntities:"boolean"==typeof e.interpretNumericEntities?e.interpretNumericEntities:defaults.interpretNumericEntities,parameterLimit:"number"==typeof e.parameterLimit?e.parameterLimit:defaults.parameterLimit,parseArrays:!1!==e.parseArrays,plainObjects:"boolean"==typeof e.plainObjects?e.plainObjects:defaults.plainObjects,strictDepth:"boolean"==typeof e.strictDepth?!!e.strictDepth:defaults.strictDepth,strictMerge:"boolean"==typeof e.strictMerge?!!e.strictMerge:defaults.strictMerge,strictNullHandling:"boolean"==typeof e.strictNullHandling?e.strictNullHandling:defaults.strictNullHandling,throwOnLimitExceeded:"boolean"==typeof e.throwOnLimitExceeded&&e.throwOnLimitExceeded}};module.exports=function(e,t){var r=normalizeParseOptions(t);if(""===e||null==e)return r.plainObjects?{__proto__:null}:{};for(var i="string"==typeof e?parseValues(e,r):e,a=r.plainObjects?{__proto__:null}:{},o=Object.keys(i),l=0;l<o.length;++l){var n=o[l],s=parseKeys(n,i[n],r,"string"==typeof e);a=utils.merge(a,s,r)}return!0===r.allowSparse?a:utils.compact(a)};
+
+},{"5":5}],4:[function(require,module,exports){
+"use strict";var getSideChannel=require(46),utils=require(5),formats=require(1),has=Object.prototype.hasOwnProperty,arrayPrefixGenerators={brackets:function brackets(e){return e+"[]"},comma:"comma",indices:function indices(e,r){return e+"["+r+"]"},repeat:function repeat(e){return e}},isArray=Array.isArray,push=Array.prototype.push,pushToArray=function(e,r){push.apply(e,isArray(r)?r:[r])},toISO=Date.prototype.toISOString,defaultFormat=formats.default,defaults={addQueryPrefix:!1,allowDots:!1,allowEmptyArrays:!1,arrayFormat:"indices",charset:"utf-8",charsetSentinel:!1,commaRoundTrip:!1,delimiter:"&",encode:!0,encodeDotInKeys:!1,encoder:utils.encode,encodeValuesOnly:!1,filter:void 0,format:defaultFormat,formatter:formats.formatters[defaultFormat],indices:!1,serializeDate:function serializeDate(e){return toISO.call(e)},skipNulls:!1,strictNullHandling:!1},isNonNullishPrimitive=function isNonNullishPrimitive(e){return"string"==typeof e||"number"==typeof e||"boolean"==typeof e||"symbol"==typeof e||"bigint"==typeof e},sentinel={},stringify=function stringify(e,r,t,o,a,n,i,l,s,f,u,d,y,c,p,m,h,v){for(var g=e,w=v,b=0,A=!1;void 0!==(w=w.get(sentinel))&&!A;){var D=w.get(e);if(b+=1,void 0!==D){if(D===b)throw new RangeError("Cyclic object value");A=!0}void 0===w.get(sentinel)&&(b=0)}if("function"==typeof f?g=f(r,g):g instanceof Date?g=y(g):"comma"===t&&isArray(g)&&(g=utils.maybeMap(g,function(e){return e instanceof Date?y(e):e})),null===g){if(n)return p(s&&!m?s(r,defaults.encoder,h,"key",c):r);g=""}if(isNonNullishPrimitive(g)||utils.isBuffer(g))return s?[p(m?r:s(r,defaults.encoder,h,"key",c))+"="+p(s(g,defaults.encoder,h,"value",c))]:[p(r)+"="+p(String(g))];var S,E=[];if(void 0===g)return E;if("comma"===t&&isArray(g))m&&s&&(g=utils.maybeMap(g,function(e){return null==e?e:s(e)})),S=[{value:g.length>0?g.join(",")||null:void 0}];else if(isArray(f))S=f;else{var N=Object.keys(g);S=u?N.sort(u):N}var T=l?String(r).replace(/\./g,"%2E"):String(r),O=o&&isArray(g)&&1===g.length?T+"[]":T;if(a&&isArray(g)&&0===g.length)return O+"[]";for(var k=0;k<S.length;++k){var I=S[k],P="object"==typeof I&&I&&void 0!==I.value?I.value:g[I];if(!i||null!==P){var x=d&&l?String(I).replace(/\./g,"%2E"):String(I),z=isArray(g)?"function"==typeof t?t(O,x):O:O+(d?"."+x:"["+x+"]");v.set(e,b);var K=getSideChannel();K.set(sentinel,v),pushToArray(E,stringify(P,z,t,o,a,n,i,l,"comma"===t&&m&&isArray(g)?null:s,f,u,d,y,c,p,m,h,K))}}return E},normalizeStringifyOptions=function normalizeStringifyOptions(e){if(!e)return defaults;if(void 0!==e.allowEmptyArrays&&"boolean"!=typeof e.allowEmptyArrays)throw new TypeError("`allowEmptyArrays` option can only be `true` or `false`, when provided");if(void 0!==e.encodeDotInKeys&&"boolean"!=typeof e.encodeDotInKeys)throw new TypeError("`encodeDotInKeys` option can only be `true` or `false`, when provided");if(null!==e.encoder&&void 0!==e.encoder&&"function"!=typeof e.encoder)throw new TypeError("Encoder has to be a function.");var r=e.charset||defaults.charset;if(void 0!==e.charset&&"utf-8"!==e.charset&&"iso-8859-1"!==e.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var t=formats.default;if(void 0!==e.format){if(!has.call(formats.formatters,e.format))throw new TypeError("Unknown format option provided.");t=e.format}var o,a=formats.formatters[t],n=defaults.filter;if(("function"==typeof e.filter||isArray(e.filter))&&(n=e.filter),o=e.arrayFormat in arrayPrefixGenerators?e.arrayFormat:"indices"in e?e.indices?"indices":"repeat":defaults.arrayFormat,"commaRoundTrip"in e&&"boolean"!=typeof e.commaRoundTrip)throw new TypeError("`commaRoundTrip` must be a boolean, or absent");var i=void 0===e.allowDots?!0===e.encodeDotInKeys||defaults.allowDots:!!e.allowDots;return{addQueryPrefix:"boolean"==typeof e.addQueryPrefix?e.addQueryPrefix:defaults.addQueryPrefix,allowDots:i,allowEmptyArrays:"boolean"==typeof e.allowEmptyArrays?!!e.allowEmptyArrays:defaults.allowEmptyArrays,arrayFormat:o,charset:r,charsetSentinel:"boolean"==typeof e.charsetSentinel?e.charsetSentinel:defaults.charsetSentinel,commaRoundTrip:!!e.commaRoundTrip,delimiter:void 0===e.delimiter?defaults.delimiter:e.delimiter,encode:"boolean"==typeof e.encode?e.encode:defaults.encode,encodeDotInKeys:"boolean"==typeof e.encodeDotInKeys?e.encodeDotInKeys:defaults.encodeDotInKeys,encoder:"function"==typeof e.encoder?e.encoder:defaults.encoder,encodeValuesOnly:"boolean"==typeof e.encodeValuesOnly?e.encodeValuesOnly:defaults.encodeValuesOnly,filter:n,format:t,formatter:a,serializeDate:"function"==typeof e.serializeDate?e.serializeDate:defaults.serializeDate,skipNulls:"boolean"==typeof e.skipNulls?e.skipNulls:defaults.skipNulls,sort:"function"==typeof e.sort?e.sort:null,strictNullHandling:"boolean"==typeof e.strictNullHandling?e.strictNullHandling:defaults.strictNullHandling}};module.exports=function(e,r){var t,o=e,a=normalizeStringifyOptions(r);"function"==typeof a.filter?o=(0,a.filter)("",o):isArray(a.filter)&&(t=a.filter);var n=[];if("object"!=typeof o||null===o)return"";var i=arrayPrefixGenerators[a.arrayFormat],l="comma"===i&&a.commaRoundTrip;t||(t=Object.keys(o)),a.sort&&t.sort(a.sort);for(var s=getSideChannel(),f=0;f<t.length;++f){var u=t[f];if(null!=u){var d=o[u];a.skipNulls&&null===d||pushToArray(n,stringify(d,u,i,l,a.allowEmptyArrays,a.strictNullHandling,a.skipNulls,a.encodeDotInKeys,a.encode?a.encoder:null,a.filter,a.sort,a.allowDots,a.serializeDate,a.format,a.formatter,a.encodeValuesOnly,a.charset,s))}}var y=n.join(a.delimiter),c=!0===a.addQueryPrefix?"?":"";return a.charsetSentinel&&("iso-8859-1"===a.charset?c+="utf8=%26%2310003%3B"+a.delimiter:c+="utf8=%E2%9C%93"+a.delimiter),y.length>0?c+y:""};
+
+},{"1":1,"46":46,"5":5}],5:[function(require,module,exports){
+"use strict";var formats=require(1),getSideChannel=require(46),has=Object.prototype.hasOwnProperty,isArray=Array.isArray,overflowChannel=getSideChannel(),markOverflow=function markOverflow(e,r){return overflowChannel.set(e,r),e},isOverflow=function isOverflow(e){return overflowChannel.has(e)},getMaxIndex=function getMaxIndex(e){return overflowChannel.get(e)},setMaxIndex=function setMaxIndex(e,r){overflowChannel.set(e,r)},hexTable=function(){for(var e=[],r=0;r<256;++r)e[e.length]="%"+((r<16?"0":"")+r.toString(16)).toUpperCase();return e}(),compactQueue=function compactQueue(e){for(;e.length>1;){var r=e.pop(),t=r.obj[r.prop];if(isArray(t)){for(var n=[],o=0;o<t.length;++o)void 0!==t[o]&&(n[n.length]=t[o]);r.obj[r.prop]=n}}},arrayToObject=function arrayToObject(e,r){for(var t=r&&r.plainObjects?{__proto__:null}:{},n=0;n<e.length;++n)void 0!==e[n]&&(t[n]=e[n]);return t},merge=function merge(e,r,t){if(!r)return e;if("object"!=typeof r&&"function"!=typeof r){if(isArray(e)){var n=e.length;if(t&&"number"==typeof t.arrayLimit&&n>t.arrayLimit)return markOverflow(arrayToObject(e.concat(r),t),n);e[n]=r}else{if(!e||"object"!=typeof e)return[e,r];if(isOverflow(e)){var o=getMaxIndex(e)+1;e[o]=r,setMaxIndex(e,o)}else{if(t&&t.strictMerge)return[e,r];(t&&(t.plainObjects||t.allowPrototypes)||!has.call(Object.prototype,r))&&(e[r]=!0)}}return e}if(!e||"object"!=typeof e){if(isOverflow(r)){for(var a=Object.keys(r),i=t&&t.plainObjects?{__proto__:null,0:e}:{0:e},c=0;c<a.length;c++)i[parseInt(a[c],10)+1]=r[a[c]];return markOverflow(i,getMaxIndex(r)+1)}var l=[e].concat(r);return t&&"number"==typeof t.arrayLimit&&l.length>t.arrayLimit?markOverflow(arrayToObject(l,t),l.length-1):l}var f=e;return isArray(e)&&!isArray(r)&&(f=arrayToObject(e,t)),isArray(e)&&isArray(r)?(r.forEach(function(r,n){if(has.call(e,n)){var o=e[n];o&&"object"==typeof o&&r&&"object"==typeof r?e[n]=merge(o,r,t):e[e.length]=r}else e[n]=r}),e):Object.keys(r).reduce(function(e,n){var o=r[n];if(has.call(e,n)?e[n]=merge(e[n],o,t):e[n]=o,isOverflow(r)&&!isOverflow(e)&&markOverflow(e,getMaxIndex(r)),isOverflow(e)){var a=parseInt(n,10);String(a)===n&&a>=0&&a>getMaxIndex(e)&&setMaxIndex(e,a)}return e},f)},assign=function assignSingleSource(e,r){return Object.keys(r).reduce(function(e,t){return e[t]=r[t],e},e)},decode=function(e,r,t){var n=e.replace(/\+/g," ");if("iso-8859-1"===t)return n.replace(/%[0-9a-f]{2}/gi,unescape);try{return decodeURIComponent(n)}catch(e){return n}},limit=1024,encode=function encode(e,r,t,n,o){if(0===e.length)return e;var a=e;if("symbol"==typeof e?a=Symbol.prototype.toString.call(e):"string"!=typeof e&&(a=String(e)),"iso-8859-1"===t)return escape(a).replace(/%u[0-9a-f]{4}/gi,function(e){return"%26%23"+parseInt(e.slice(2),16)+"%3B"});for(var i="",c=0;c<a.length;c+=limit){for(var l=a.length>=limit?a.slice(c,c+limit):a,f=[],s=0;s<l.length;++s){var u=l.charCodeAt(s);45===u||46===u||95===u||126===u||u>=48&&u<=57||u>=65&&u<=90||u>=97&&u<=122||o===formats.RFC1738&&(40===u||41===u)?f[f.length]=l.charAt(s):u<128?f[f.length]=hexTable[u]:u<2048?f[f.length]=hexTable[192|u>>6]+hexTable[128|63&u]:u<55296||u>=57344?f[f.length]=hexTable[224|u>>12]+hexTable[128|u>>6&63]+hexTable[128|63&u]:(s+=1,u=65536+((1023&u)<<10|1023&l.charCodeAt(s)),f[f.length]=hexTable[240|u>>18]+hexTable[128|u>>12&63]+hexTable[128|u>>6&63]+hexTable[128|63&u])}i+=f.join("")}return i},compact=function compact(e){for(var r=[{obj:{o:e},prop:"o"}],t=[],n=0;n<r.length;++n)for(var o=r[n],a=o.obj[o.prop],i=Object.keys(a),c=0;c<i.length;++c){var l=i[c],f=a[l];"object"==typeof f&&null!==f&&-1===t.indexOf(f)&&(r[r.length]={obj:a,prop:l},t[t.length]=f)}return compactQueue(r),e},isRegExp=function isRegExp(e){return"[object RegExp]"===Object.prototype.toString.call(e)},isBuffer=function isBuffer(e){return!(!e||"object"!=typeof e||!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e)))},combine=function combine(e,r,t,n){if(isOverflow(e)){var o=getMaxIndex(e)+1;return e[o]=r,setMaxIndex(e,o),e}var a=[].concat(e,r);return a.length>t?markOverflow(arrayToObject(a,{plainObjects:n}),a.length-1):a},maybeMap=function maybeMap(e,r){if(isArray(e)){for(var t=[],n=0;n<e.length;n+=1)t[t.length]=r(e[n]);return t}return r(e)};module.exports={/* common-shake removed: arrayToObject:arrayToObject *//* common-shake removed: assign:assign */combine:combine,compact:compact,decode:decode,encode:encode,isBuffer:isBuffer,isOverflow:isOverflow,isRegExp:isRegExp,markOverflow:markOverflow,maybeMap:maybeMap,merge:merge};
+
+},{"1":1,"46":46}],46:[function(require,module,exports){
+"use strict";var $TypeError=require(20),inspect=require(42),getSideChannelList=require(43),getSideChannelMap=require(44),getSideChannelWeakMap=require(45),makeChannel=getSideChannelWeakMap||getSideChannelMap||getSideChannelList;module.exports=function getSideChannel(){var e,n={assert:function(e){if(!n.has(e))throw new $TypeError("Side channel does not contain "+inspect(e))},delete:function(n){return!!e&&e.delete(n)},get:function(n){return e&&e.get(n)},has:function(n){return!!e&&e.has(n)},set:function(n,t){e||(e=makeChannel()),e.set(n,t)}};return n};
+
+},{"20":20,"42":42,"43":43,"44":44,"45":45}],6:[function(require,module,exports){
+
+},{}],7:[function(require,module,exports){
+"use strict";var bind=require(24),$apply=require(8),$call=require(9),$reflectApply=require(11);module.exports=$reflectApply||bind.call($call,$apply);
+
+},{"11":11,"24":24,"8":8,"9":9}],9:[function(require,module,exports){
+"use strict";module.exports=Function.prototype.call;
+
+},{}],8:[function(require,module,exports){
+"use strict";module.exports=Function.prototype.apply;
+
+},{}],24:[function(require,module,exports){
+"use strict";var implementation=require(23);module.exports=Function.prototype.bind||implementation;
+
+},{"23":23}],11:[function(require,module,exports){
+"use strict";module.exports="undefined"!=typeof Reflect&&Reflect&&Reflect.apply;
+
+},{}],10:[function(require,module,exports){
+"use strict";var bind=require(24),$TypeError=require(20),$call=require(9),$actualApply=require(7);module.exports=function callBindBasic(r){if(r.length<1||"function"!=typeof r[0])throw new $TypeError("a function is required");return $actualApply(bind,$call,r)};
+
+},{"20":20,"24":24,"7":7,"9":9}],20:[function(require,module,exports){
+"use strict";module.exports=TypeError;
+
+},{}],12:[function(require,module,exports){
+"use strict";var GetIntrinsic=require(25),callBindBasic=require(10),$indexOf=callBindBasic([GetIntrinsic("%String.prototype.indexOf%")]);module.exports=function callBoundIntrinsic(i,n){var t=GetIntrinsic(i,!!n);return"function"==typeof t&&$indexOf(i,".prototype.")>-1?callBindBasic([t]):t};
+
+},{"10":10,"25":25}],25:[function(require,module,exports){
+"use strict";var undefined,$Object=require(22),$Error=require(16),$EvalError=require(15),$RangeError=require(17),$ReferenceError=require(18),$SyntaxError=require(19),$TypeError=require(20),$URIError=require(21),abs=require(34),floor=require(35),max=require(37),min=require(38),pow=require(39),round=require(40),sign=require(41),$Function=Function,getEvalledConstructor=function(r){try{return $Function('"use strict"; return ('+r+").constructor;")()}catch(r){}},$gOPD=require(30),$defineProperty=require(14),throwTypeError=function(){throw new $TypeError},ThrowTypeError=$gOPD?function(){try{return throwTypeError}catch(r){try{return $gOPD(arguments,"callee").get}catch(r){return throwTypeError}}}():throwTypeError,hasSymbols=require(31)(),getProto=require(28),$ObjectGPO=require(26),$ReflectGPO=require(27),$apply=require(8),$call=require(9),needsEval={},TypedArray="undefined"!=typeof Uint8Array&&getProto?getProto(Uint8Array):undefined,INTRINSICS={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?undefined:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?undefined:ArrayBuffer,"%ArrayIteratorPrototype%":hasSymbols&&getProto?getProto([][Symbol.iterator]()):undefined,"%AsyncFromSyncIteratorPrototype%":undefined,"%AsyncFunction%":needsEval,"%AsyncGenerator%":needsEval,"%AsyncGeneratorFunction%":needsEval,"%AsyncIteratorPrototype%":needsEval,"%Atomics%":"undefined"==typeof Atomics?undefined:Atomics,"%BigInt%":"undefined"==typeof BigInt?undefined:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?undefined:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?undefined:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?undefined:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":$Error,"%eval%":eval,"%EvalError%":$EvalError,"%Float16Array%":"undefined"==typeof Float16Array?undefined:Float16Array,"%Float32Array%":"undefined"==typeof Float32Array?undefined:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?undefined:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?undefined:FinalizationRegistry,"%Function%":$Function,"%GeneratorFunction%":needsEval,"%Int8Array%":"undefined"==typeof Int8Array?undefined:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?undefined:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?undefined:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":hasSymbols&&getProto?getProto(getProto([][Symbol.iterator]())):undefined,"%JSON%":"object"==typeof JSON?JSON:undefined,"%Map%":"undefined"==typeof Map?undefined:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&hasSymbols&&getProto?getProto((new Map)[Symbol.iterator]()):undefined,"%Math%":Math,"%Number%":Number,"%Object%":$Object,"%Object.getOwnPropertyDescriptor%":$gOPD,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?undefined:Promise,"%Proxy%":"undefined"==typeof Proxy?undefined:Proxy,"%RangeError%":$RangeError,"%ReferenceError%":$ReferenceError,"%Reflect%":"undefined"==typeof Reflect?undefined:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?undefined:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&hasSymbols&&getProto?getProto((new Set)[Symbol.iterator]()):undefined,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?undefined:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":hasSymbols&&getProto?getProto(""[Symbol.iterator]()):undefined,"%Symbol%":hasSymbols?Symbol:undefined,"%SyntaxError%":$SyntaxError,"%ThrowTypeError%":ThrowTypeError,"%TypedArray%":TypedArray,"%TypeError%":$TypeError,"%Uint8Array%":"undefined"==typeof Uint8Array?undefined:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?undefined:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?undefined:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?undefined:Uint32Array,"%URIError%":$URIError,"%WeakMap%":"undefined"==typeof WeakMap?undefined:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?undefined:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?undefined:WeakSet,"%Function.prototype.call%":$call,"%Function.prototype.apply%":$apply,"%Object.defineProperty%":$defineProperty,"%Object.getPrototypeOf%":$ObjectGPO,"%Math.abs%":abs,"%Math.floor%":floor,"%Math.max%":max,"%Math.min%":min,"%Math.pow%":pow,"%Math.round%":round,"%Math.sign%":sign,"%Reflect.getPrototypeOf%":$ReflectGPO};if(getProto)try{null.error}catch(r){var errorProto=getProto(getProto(r));INTRINSICS["%Error.prototype%"]=errorProto}var doEval=function doEval(r){var e;if("%AsyncFunction%"===r)e=getEvalledConstructor("async function () {}");else if("%GeneratorFunction%"===r)e=getEvalledConstructor("function* () {}");else if("%AsyncGeneratorFunction%"===r)e=getEvalledConstructor("async function* () {}");else if("%AsyncGenerator%"===r){var t=doEval("%AsyncGeneratorFunction%");t&&(e=t.prototype)}else if("%AsyncIteratorPrototype%"===r){var o=doEval("%AsyncGenerator%");o&&getProto&&(e=getProto(o.prototype))}return INTRINSICS[r]=e,e},LEGACY_ALIASES={__proto__:null,"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},bind=require(24),hasOwn=require(33),$concat=bind.call($call,Array.prototype.concat),$spliceApply=bind.call($apply,Array.prototype.splice),$replace=bind.call($call,String.prototype.replace),$strSlice=bind.call($call,String.prototype.slice),$exec=bind.call($call,RegExp.prototype.exec),rePropName=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,reEscapeChar=/\\(\\)?/g,stringToPath=function stringToPath(r){var e=$strSlice(r,0,1),t=$strSlice(r,-1);if("%"===e&&"%"!==t)throw new $SyntaxError("invalid intrinsic syntax, expected closing `%`");if("%"===t&&"%"!==e)throw new $SyntaxError("invalid intrinsic syntax, expected opening `%`");var o=[];return $replace(r,rePropName,function(r,e,t,n){o[o.length]=t?$replace(n,reEscapeChar,"$1"):e||r}),o},getBaseIntrinsic=function getBaseIntrinsic(r,e){var t,o=r;if(hasOwn(LEGACY_ALIASES,o)&&(o="%"+(t=LEGACY_ALIASES[o])[0]+"%"),hasOwn(INTRINSICS,o)){var n=INTRINSICS[o];if(n===needsEval&&(n=doEval(o)),void 0===n&&!e)throw new $TypeError("intrinsic "+r+" exists, but is not available. Please file an issue!");return{alias:t,name:o,value:n}}throw new $SyntaxError("intrinsic "+r+" does not exist!")};module.exports=function GetIntrinsic(r,e){if("string"!=typeof r||0===r.length)throw new $TypeError("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof e)throw new $TypeError('"allowMissing" argument must be a boolean');if(null===$exec(/^%?[^%]*%?$/,r))throw new $SyntaxError("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var t=stringToPath(r),o=t.length>0?t[0]:"",n=getBaseIntrinsic("%"+o+"%",e),a=n.name,i=n.value,y=!1,p=n.alias;p&&(o=p[0],$spliceApply(t,$concat([0,1],p)));for(var d=1,s=!0;d<t.length;d+=1){var f=t[d],l=$strSlice(f,0,1),u=$strSlice(f,-1);if(('"'===l||"'"===l||"`"===l||'"'===u||"'"===u||"`"===u)&&l!==u)throw new $SyntaxError("property names with quotes must have matching quotes");if("constructor"!==f&&s||(y=!0),hasOwn(INTRINSICS,a="%"+(o+="."+f)+"%"))i=INTRINSICS[a];else if(null!=i){if(!(f in i)){if(!e)throw new $TypeError("base intrinsic for "+r+" exists, but the property is not available.");return}if($gOPD&&d+1>=t.length){var c=$gOPD(i,f);i=(s=!!c)&&"get"in c&&!("originalValue"in c.get)?c.get:i[f]}else s=hasOwn(i,f),i=i[f];s&&!y&&(INTRINSICS[a]=i)}}return i};
+
+},{"14":14,"15":15,"16":16,"17":17,"18":18,"19":19,"20":20,"21":21,"22":22,"24":24,"26":26,"27":27,"28":28,"30":30,"31":31,"33":33,"34":34,"35":35,"37":37,"38":38,"39":39,"40":40,"41":41,"8":8,"9":9}],13:[function(require,module,exports){
+"use strict";var hasProtoAccessor,callBind=require(10),gOPD=require(30);try{hasProtoAccessor=[].__proto__===Array.prototype}catch(t){if(!t||"object"!=typeof t||!("code"in t)||"ERR_PROTO_ACCESS"!==t.code)throw t}var desc=!!hasProtoAccessor&&gOPD&&gOPD(Object.prototype,"__proto__"),$Object=Object,$getPrototypeOf=$Object.getPrototypeOf;module.exports=desc&&"function"==typeof desc.get?callBind([desc.get]):"function"==typeof $getPrototypeOf&&function getDunder(t){return $getPrototypeOf(null==t?t:$Object(t))};
+
+},{"10":10,"30":30}],30:[function(require,module,exports){
+"use strict";var $gOPD=require(29);if($gOPD)try{$gOPD([],"length")}catch(g){$gOPD=null}module.exports=$gOPD;
+
+},{"29":29}],14:[function(require,module,exports){
+"use strict";var $defineProperty=Object.defineProperty||!1;if($defineProperty)try{$defineProperty({},"a",{value:1})}catch(e){$defineProperty=!1}module.exports=$defineProperty;
+
+},{}],15:[function(require,module,exports){
+"use strict";module.exports=EvalError;
+
+},{}],16:[function(require,module,exports){
+"use strict";module.exports=Error;
+
+},{}],17:[function(require,module,exports){
+"use strict";module.exports=RangeError;
+
+},{}],18:[function(require,module,exports){
+"use strict";module.exports=ReferenceError;
+
+},{}],19:[function(require,module,exports){
+"use strict";module.exports=SyntaxError;
+
+},{}],21:[function(require,module,exports){
+"use strict";module.exports=URIError;
+
+},{}],22:[function(require,module,exports){
+"use strict";module.exports=Object;
+
+},{}],23:[function(require,module,exports){
+"use strict";var ERROR_MESSAGE="Function.prototype.bind called on incompatible ",toStr=Object.prototype.toString,max=Math.max,funcType="[object Function]",concatty=function concatty(t,n){for(var r=[],o=0;o<t.length;o+=1)r[o]=t[o];for(var e=0;e<n.length;e+=1)r[e+t.length]=n[e];return r},slicy=function slicy(t,n){for(var r=[],o=n||0,e=0;o<t.length;o+=1,e+=1)r[e]=t[o];return r},joiny=function(t,n){for(var r="",o=0;o<t.length;o+=1)r+=t[o],o+1<t.length&&(r+=n);return r};module.exports=function bind(t){var n=this;if("function"!=typeof n||toStr.apply(n)!==funcType)throw new TypeError(ERROR_MESSAGE+n);for(var r,o=slicy(arguments,1),e=max(0,n.length-o.length),i=[],c=0;c<e;c++)i[c]="$"+c;if(r=Function("binder","return function ("+joiny(i,",")+"){ return binder.apply(this,arguments); }")(function(){if(this instanceof r){var e=n.apply(this,concatty(o,arguments));return Object(e)===e?e:this}return n.apply(t,concatty(o,arguments))}),n.prototype){var p=function Empty(){};p.prototype=n.prototype,r.prototype=new p,p.prototype=null}return r};
+
+},{}],35:[function(require,module,exports){
+"use strict";module.exports=Math.floor;
+
+},{}],34:[function(require,module,exports){
+"use strict";module.exports=Math.abs;
+
+},{}],38:[function(require,module,exports){
+"use strict";module.exports=Math.min;
+
+},{}],40:[function(require,module,exports){
+"use strict";module.exports=Math.round;
+
+},{}],37:[function(require,module,exports){
+"use strict";module.exports=Math.max;
+
+},{}],39:[function(require,module,exports){
+"use strict";module.exports=Math.pow;
+
+},{}],27:[function(require,module,exports){
+"use strict";module.exports="undefined"!=typeof Reflect&&Reflect.getPrototypeOf||null;
+
+},{}],26:[function(require,module,exports){
+"use strict";var $Object=require(22);module.exports=$Object.getPrototypeOf||null;
+
+},{"22":22}],33:[function(require,module,exports){
+"use strict";var call=Function.prototype.call,$hasOwn=Object.prototype.hasOwnProperty,bind=require(24);module.exports=bind.call(call,$hasOwn);
+
+},{"24":24}],41:[function(require,module,exports){
+"use strict";var $isNaN=require(36);module.exports=function sign(i){return $isNaN(i)||0===i?i:i<0?-1:1};
+
+},{"36":36}],31:[function(require,module,exports){
+"use strict";var origSymbol="undefined"!=typeof Symbol&&Symbol,hasSymbolSham=require(32);module.exports=function hasNativeSymbols(){return"function"==typeof origSymbol&&"function"==typeof Symbol&&"symbol"==typeof origSymbol("foo")&&"symbol"==typeof Symbol("bar")&&hasSymbolSham()};
+
+},{"32":32}],28:[function(require,module,exports){
+"use strict";var reflectGetProto=require(27),originalGetProto=require(26),getDunderProto=require(13);module.exports=reflectGetProto?function getProto(t){return reflectGetProto(t)}:originalGetProto?function getProto(t){if(!t||"object"!=typeof t&&"function"!=typeof t)throw new TypeError("getProto: not an object");return originalGetProto(t)}:getDunderProto?function getProto(t){return getDunderProto(t)}:null;
+
+},{"13":13,"26":26,"27":27}],29:[function(require,module,exports){
+"use strict";module.exports=Object.getOwnPropertyDescriptor;
+
+},{}],32:[function(require,module,exports){
+"use strict";module.exports=function hasSymbols(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var t={},e=Symbol("test"),r=Object(e);if("string"==typeof e)return!1;if("[object Symbol]"!==Object.prototype.toString.call(e))return!1;if("[object Symbol]"!==Object.prototype.toString.call(r))return!1;for(var o in t[e]=42,t)return!1;if("function"==typeof Object.keys&&0!==Object.keys(t).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(t).length)return!1;var n=Object.getOwnPropertySymbols(t);if(1!==n.length||n[0]!==e)return!1;if(!Object.prototype.propertyIsEnumerable.call(t,e))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var y=Object.getOwnPropertyDescriptor(t,e);if(42!==y.value||!0!==y.enumerable)return!1}return!0};
+
+},{}],36:[function(require,module,exports){
+"use strict";module.exports=Number.isNaN||function isNaN(e){return e!=e};
+
+},{}],42:[function(require,module,exports){
+(function (global){(function (){
+var hasMap="function"==typeof Map&&Map.prototype,mapSizeDescriptor=Object.getOwnPropertyDescriptor&&hasMap?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,mapSize=hasMap&&mapSizeDescriptor&&"function"==typeof mapSizeDescriptor.get?mapSizeDescriptor.get:null,mapForEach=hasMap&&Map.prototype.forEach,hasSet="function"==typeof Set&&Set.prototype,setSizeDescriptor=Object.getOwnPropertyDescriptor&&hasSet?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,setSize=hasSet&&setSizeDescriptor&&"function"==typeof setSizeDescriptor.get?setSizeDescriptor.get:null,setForEach=hasSet&&Set.prototype.forEach,hasWeakMap="function"==typeof WeakMap&&WeakMap.prototype,weakMapHas=hasWeakMap?WeakMap.prototype.has:null,hasWeakSet="function"==typeof WeakSet&&WeakSet.prototype,weakSetHas=hasWeakSet?WeakSet.prototype.has:null,hasWeakRef="function"==typeof WeakRef&&WeakRef.prototype,weakRefDeref=hasWeakRef?WeakRef.prototype.deref:null,booleanValueOf=Boolean.prototype.valueOf,objectToString=Object.prototype.toString,functionToString=Function.prototype.toString,$match=String.prototype.match,$slice=String.prototype.slice,$replace=String.prototype.replace,$toUpperCase=String.prototype.toUpperCase,$toLowerCase=String.prototype.toLowerCase,$test=RegExp.prototype.test,$concat=Array.prototype.concat,$join=Array.prototype.join,$arrSlice=Array.prototype.slice,$floor=Math.floor,bigIntValueOf="function"==typeof BigInt?BigInt.prototype.valueOf:null,gOPS=Object.getOwnPropertySymbols,symToString="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,hasShammedSymbols="function"==typeof Symbol&&"object"==typeof Symbol.iterator,toStringTag="function"==typeof Symbol&&Symbol.toStringTag&&(Symbol.toStringTag,1)?Symbol.toStringTag:null,isEnumerable=Object.prototype.propertyIsEnumerable,gPO=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(t){return t.__proto__}:null);function addNumericSeparator(t,e){if(t===1/0||t===-1/0||t!=t||t&&t>-1e3&&t<1e3||$test.call(/e/,e))return e;var r=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof t){var n=t<0?-$floor(-t):$floor(t);if(n!==t){var o=String(n),i=$slice.call(e,o.length+1);return $replace.call(o,r,"$&_")+"."+$replace.call($replace.call(i,/([0-9]{3})/g,"$&_"),/_$/,"")}}return $replace.call(e,r,"$&_")}var utilInspect=require(6),inspectCustom=utilInspect.custom,inspectSymbol=isSymbol(inspectCustom)?inspectCustom:null,quotes={__proto__:null,double:'"',single:"'"},quoteREs={__proto__:null,double:/(["\\])/g,single:/(['\\])/g};function wrapQuotes(t,e,r){var n=r.quoteStyle||e,o=quotes[n];return o+t+o}function quote(t){return $replace.call(String(t),/"/g,"&quot;")}function canTrustToString(t){return!toStringTag||!("object"==typeof t&&(toStringTag in t||void 0!==t[toStringTag]))}function isArray(t){return"[object Array]"===toStr(t)&&canTrustToString(t)}function isDate(t){return"[object Date]"===toStr(t)&&canTrustToString(t)}function isRegExp(t){return"[object RegExp]"===toStr(t)&&canTrustToString(t)}function isError(t){return"[object Error]"===toStr(t)&&canTrustToString(t)}function isString(t){return"[object String]"===toStr(t)&&canTrustToString(t)}function isNumber(t){return"[object Number]"===toStr(t)&&canTrustToString(t)}function isBoolean(t){return"[object Boolean]"===toStr(t)&&canTrustToString(t)}function isSymbol(t){if(hasShammedSymbols)return t&&"object"==typeof t&&t instanceof Symbol;if("symbol"==typeof t)return!0;if(!t||"object"!=typeof t||!symToString)return!1;try{return symToString.call(t),!0}catch(t){}return!1}function isBigInt(t){if(!t||"object"!=typeof t||!bigIntValueOf)return!1;try{return bigIntValueOf.call(t),!0}catch(t){}return!1}module.exports=function inspect_(t,e,r,n){var o=e||{};if(has(o,"quoteStyle")&&!has(quotes,o.quoteStyle))throw new TypeError('option "quoteStyle" must be "single" or "double"');if(has(o,"maxStringLength")&&("number"==typeof o.maxStringLength?o.maxStringLength<0&&o.maxStringLength!==1/0:null!==o.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var i=!has(o,"customInspect")||o.customInspect;if("boolean"!=typeof i&&"symbol"!==i)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(has(o,"indent")&&null!==o.indent&&"\t"!==o.indent&&!(parseInt(o.indent,10)===o.indent&&o.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(has(o,"numericSeparator")&&"boolean"!=typeof o.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var a=o.numericSeparator;if(void 0===t)return"undefined";if(null===t)return"null";if("boolean"==typeof t)return t?"true":"false";if("string"==typeof t)return inspectString(t,o);if("number"==typeof t){if(0===t)return 1/0/t>0?"0":"-0";var c=String(t);return a?addNumericSeparator(t,c):c}if("bigint"==typeof t){var l=String(t)+"n";return a?addNumericSeparator(t,l):l}var u=void 0===o.depth?5:o.depth;if(void 0===r&&(r=0),r>=u&&u>0&&"object"==typeof t)return isArray(t)?"[Array]":"[Object]";var p=getIndent(o,r);if(void 0===n)n=[];else if(indexOf(n,t)>=0)return"[Circular]";function inspect(t,e,i){if(e&&(n=$arrSlice.call(n)).push(e),i){var a={depth:o.depth};return has(o,"quoteStyle")&&(a.quoteStyle=o.quoteStyle),inspect_(t,a,r+1,n)}return inspect_(t,o,r+1,n)}if("function"==typeof t&&!isRegExp(t)){var s=nameOf(t),f=arrObjKeys(t,inspect);return"[Function"+(s?": "+s:" (anonymous)")+"]"+(f.length>0?" { "+$join.call(f,", ")+" }":"")}if(isSymbol(t)){var y=hasShammedSymbols?$replace.call(String(t),/^(Symbol\(.*\))_[^)]*$/,"$1"):symToString.call(t);return"object"!=typeof t||hasShammedSymbols?y:markBoxed(y)}if(isElement(t)){for(var S="<"+$toLowerCase.call(String(t.nodeName)),g=t.attributes||[],m=0;m<g.length;m++)S+=" "+g[m].name+"="+wrapQuotes(quote(g[m].value),"double",o);return S+=">",t.childNodes&&t.childNodes.length&&(S+="..."),S+"</"+$toLowerCase.call(String(t.nodeName))+">"}if(isArray(t)){if(0===t.length)return"[]";var b=arrObjKeys(t,inspect);return p&&!singleLineValues(b)?"["+indentedJoin(b,p)+"]":"[ "+$join.call(b,", ")+" ]"}if(isError(t)){var h=arrObjKeys(t,inspect);return"cause"in Error.prototype||!("cause"in t)||isEnumerable.call(t,"cause")?0===h.length?"["+String(t)+"]":"{ ["+String(t)+"] "+$join.call(h,", ")+" }":"{ ["+String(t)+"] "+$join.call($concat.call("[cause]: "+inspect(t.cause),h),", ")+" }"}if("object"==typeof t&&i){if(inspectSymbol&&"function"==typeof t[inspectSymbol]&&utilInspect)return utilInspect(t,{depth:u-r});if("symbol"!==i&&"function"==typeof t.inspect)return t.inspect()}if(isMap(t)){var d=[];return mapForEach&&mapForEach.call(t,function(e,r){d.push(inspect(r,t,!0)+" => "+inspect(e,t))}),collectionOf("Map",mapSize.call(t),d,p)}if(isSet(t)){var O=[];return setForEach&&setForEach.call(t,function(e){O.push(inspect(e,t))}),collectionOf("Set",setSize.call(t),O,p)}if(isWeakMap(t))return weakCollectionOf("WeakMap");if(isWeakSet(t))return weakCollectionOf("WeakSet");if(isWeakRef(t))return weakCollectionOf("WeakRef");if(isNumber(t))return markBoxed(inspect(Number(t)));if(isBigInt(t))return markBoxed(inspect(bigIntValueOf.call(t)));if(isBoolean(t))return markBoxed(booleanValueOf.call(t));if(isString(t))return markBoxed(inspect(String(t)));if("undefined"!=typeof window&&t===window)return"{ [object Window] }";if("undefined"!=typeof globalThis&&t===globalThis||"undefined"!=typeof global&&t===global)return"{ [object globalThis] }";if(!isDate(t)&&!isRegExp(t)){var j=arrObjKeys(t,inspect),w=gPO?gPO(t)===Object.prototype:t instanceof Object||t.constructor===Object,$=t instanceof Object?"":"null prototype",v=!w&&toStringTag&&Object(t)===t&&toStringTag in t?$slice.call(toStr(t),8,-1):$?"Object":"",k=(w||"function"!=typeof t.constructor?"":t.constructor.name?t.constructor.name+" ":"")+(v||$?"["+$join.call($concat.call([],v||[],$||[]),": ")+"] ":"");return 0===j.length?k+"{}":p?k+"{"+indentedJoin(j,p)+"}":k+"{ "+$join.call(j,", ")+" }"}return String(t)};var hasOwn=Object.prototype.hasOwnProperty||function(t){return t in this};function has(t,e){return hasOwn.call(t,e)}function toStr(t){return objectToString.call(t)}function nameOf(t){if(t.name)return t.name;var e=$match.call(functionToString.call(t),/^function\s*([\w$]+)/);return e?e[1]:null}function indexOf(t,e){if(t.indexOf)return t.indexOf(e);for(var r=0,n=t.length;r<n;r++)if(t[r]===e)return r;return-1}function isMap(t){if(!mapSize||!t||"object"!=typeof t)return!1;try{mapSize.call(t);try{setSize.call(t)}catch(t){return!0}return t instanceof Map}catch(t){}return!1}function isWeakMap(t){if(!weakMapHas||!t||"object"!=typeof t)return!1;try{weakMapHas.call(t,weakMapHas);try{weakSetHas.call(t,weakSetHas)}catch(t){return!0}return t instanceof WeakMap}catch(t){}return!1}function isWeakRef(t){if(!weakRefDeref||!t||"object"!=typeof t)return!1;try{return weakRefDeref.call(t),!0}catch(t){}return!1}function isSet(t){if(!setSize||!t||"object"!=typeof t)return!1;try{setSize.call(t);try{mapSize.call(t)}catch(t){return!0}return t instanceof Set}catch(t){}return!1}function isWeakSet(t){if(!weakSetHas||!t||"object"!=typeof t)return!1;try{weakSetHas.call(t,weakSetHas);try{weakMapHas.call(t,weakMapHas)}catch(t){return!0}return t instanceof WeakSet}catch(t){}return!1}function isElement(t){return!(!t||"object"!=typeof t)&&("undefined"!=typeof HTMLElement&&t instanceof HTMLElement||"string"==typeof t.nodeName&&"function"==typeof t.getAttribute)}function inspectString(t,e){if(t.length>e.maxStringLength){var r=t.length-e.maxStringLength,n="... "+r+" more character"+(r>1?"s":"");return inspectString($slice.call(t,0,e.maxStringLength),e)+n}var o=quoteREs[e.quoteStyle||"single"];return o.lastIndex=0,wrapQuotes($replace.call($replace.call(t,o,"\\$1"),/[\x00-\x1f]/g,lowbyte),"single",e)}function lowbyte(t){var e=t.charCodeAt(0),r={8:"b",9:"t",10:"n",12:"f",13:"r"}[e];return r?"\\"+r:"\\x"+(e<16?"0":"")+$toUpperCase.call(e.toString(16))}function markBoxed(t){return"Object("+t+")"}function weakCollectionOf(t){return t+" { ? }"}function collectionOf(t,e,r,n){return t+" ("+e+") {"+(n?indentedJoin(r,n):$join.call(r,", "))+"}"}function singleLineValues(t){for(var e=0;e<t.length;e++)if(indexOf(t[e],"\n")>=0)return!1;return!0}function getIndent(t,e){var r;if("\t"===t.indent)r="\t";else{if(!("number"==typeof t.indent&&t.indent>0))return null;r=$join.call(Array(t.indent+1)," ")}return{base:r,prev:$join.call(Array(e+1),r)}}function indentedJoin(t,e){if(0===t.length)return"";var r="\n"+e.prev+e.base;return r+$join.call(t,","+r)+"\n"+e.prev}function arrObjKeys(t,e){var r=isArray(t),n=[];if(r){n.length=t.length;for(var o=0;o<t.length;o++)n[o]=has(t,o)?e(t[o],t):""}var i,a="function"==typeof gOPS?gOPS(t):[];if(hasShammedSymbols){i={};for(var c=0;c<a.length;c++)i["$"+a[c]]=a[c]}for(var l in t)has(t,l)&&(r&&String(Number(l))===l&&l<t.length||hasShammedSymbols&&i["$"+l]instanceof Symbol||($test.call(/[^\w$]/,l)?n.push(e(l,t)+": "+e(t[l],t)):n.push(l+": "+e(t[l],t))));if("function"==typeof gOPS)for(var u=0;u<a.length;u++)isEnumerable.call(t,a[u])&&n.push("["+e(a[u])+"]: "+e(t[a[u]],t));return n}
+
+}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"6":6}],43:[function(require,module,exports){
+"use strict";var inspect=require(42),$TypeError=require(20),listGetNode=function(e,t,n){for(var i,r=e;null!=(i=r.next);r=i)if(i.key===t)return r.next=i.next,n||(i.next=e.next,e.next=i),i},listGet=function(e,t){if(e){var n=listGetNode(e,t);return n&&n.value}},listSet=function(e,t,n){var i=listGetNode(e,t);i?i.value=n:e.next={key:t,next:e.next,value:n}},listHas=function(e,t){return!!e&&!!listGetNode(e,t)},listDelete=function(e,t){if(e)return listGetNode(e,t,!0)};module.exports=function getSideChannelList(){var e,t={assert:function(e){if(!t.has(e))throw new $TypeError("Side channel does not contain "+inspect(e))},delete:function(t){var n=listDelete(e,t);return n&&e&&!e.next&&(e=void 0),!!n},get:function(t){return listGet(e,t)},has:function(t){return listHas(e,t)},set:function(t,n){e||(e={next:void 0}),listSet(e,t,n)}};return t};
+
+},{"20":20,"42":42}],44:[function(require,module,exports){
+"use strict";var GetIntrinsic=require(25),callBound=require(12),inspect=require(42),$TypeError=require(20),$Map=GetIntrinsic("%Map%",!0),$mapGet=callBound("Map.prototype.get",!0),$mapSet=callBound("Map.prototype.set",!0),$mapHas=callBound("Map.prototype.has",!0),$mapDelete=callBound("Map.prototype.delete",!0),$mapSize=callBound("Map.prototype.size",!0);module.exports=!!$Map&&function getSideChannelMap(){var e,t={assert:function(e){if(!t.has(e))throw new $TypeError("Side channel does not contain "+inspect(e))},delete:function(t){if(e){var n=$mapDelete(e,t);return 0===$mapSize(e)&&(e=void 0),n}return!1},get:function(t){if(e)return $mapGet(e,t)},has:function(t){return!!e&&$mapHas(e,t)},set:function(t,n){e||(e=new $Map),$mapSet(e,t,n)}};return t};
+
+},{"12":12,"20":20,"25":25,"42":42}],45:[function(require,module,exports){
+"use strict";var GetIntrinsic=require(25),callBound=require(12),inspect=require(42),getSideChannelMap=require(44),$TypeError=require(20),$WeakMap=GetIntrinsic("%WeakMap%",!0),$weakMapGet=callBound("WeakMap.prototype.get",!0),$weakMapSet=callBound("WeakMap.prototype.set",!0),$weakMapHas=callBound("WeakMap.prototype.has",!0),$weakMapDelete=callBound("WeakMap.prototype.delete",!0);module.exports=$WeakMap?function getSideChannelWeakMap(){var e,t,a={assert:function(e){if(!a.has(e))throw new $TypeError("Side channel does not contain "+inspect(e))},delete:function(a){if($WeakMap&&a&&("object"==typeof a||"function"==typeof a)){if(e)return $weakMapDelete(e,a)}else if(getSideChannelMap&&t)return t.delete(a);return!1},get:function(a){return $WeakMap&&a&&("object"==typeof a||"function"==typeof a)&&e?$weakMapGet(e,a):t&&t.get(a)},has:function(a){return $WeakMap&&a&&("object"==typeof a||"function"==typeof a)&&e?$weakMapHas(e,a):!!t&&t.has(a)},set:function(a,n){$WeakMap&&a&&("object"==typeof a||"function"==typeof a)?(e||(e=new $WeakMap),$weakMapSet(e,a,n)):getSideChannelMap&&(t||(t=getSideChannelMap()),t.set(a,n))}};return a}:getSideChannelMap;
+
+},{"12":12,"20":20,"25":25,"42":42,"44":44}]},{},[2])(2)
+});
Index: frontend/node_modules/qs/eslint.config.mjs
===================================================================
--- frontend/node_modules/qs/eslint.config.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/eslint.config.mjs	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,57 @@
+import ljharbConfig from '@ljharb/eslint-config/flat';
+
+export default [
+    {
+        ignores: ['dist/'],
+    },
+
+    ...ljharbConfig,
+
+    {
+        rules: {
+            complexity: 'off',
+            'consistent-return': 'warn',
+            eqeqeq: ['error', 'allow-null'],
+            'func-name-matching': 'off',
+            'id-length': [
+                'error',
+                {
+                    max: 25,
+                    min: 1,
+                    properties: 'never',
+                },
+            ],
+            indent: ['error', 4],
+            'max-lines': 'off',
+            'max-lines-per-function': [
+                'error',
+                { max: 150 },
+            ],
+            'max-params': ['error', 18],
+            'max-statements': ['error', 100],
+            'multiline-comment-style': 'off',
+            'no-continue': 'warn',
+            'no-magic-numbers': 'off',
+            'no-restricted-syntax': [
+                'error',
+                'BreakStatement',
+                'DebuggerStatement',
+                'ForInStatement',
+                'LabeledStatement',
+                'WithStatement',
+            ],
+        },
+    },
+
+    {
+        files: ['test/**'],
+        rules: {
+            'function-paren-newline': 'off',
+            'max-lines-per-function': 'off',
+            'max-statements': 'off',
+            'no-buffer-constructor': 'off',
+            'no-extend-native': 'off',
+            'no-throw-literal': 'off',
+        },
+    },
+];
Index: frontend/node_modules/qs/lib/formats.js
===================================================================
--- frontend/node_modules/qs/lib/formats.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/lib/formats.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,23 @@
+'use strict';
+
+var replace = String.prototype.replace;
+var percentTwenties = /%20/g;
+
+var Format = {
+    RFC1738: 'RFC1738',
+    RFC3986: 'RFC3986'
+};
+
+module.exports = {
+    'default': Format.RFC3986,
+    formatters: {
+        RFC1738: function (value) {
+            return replace.call(value, percentTwenties, '+');
+        },
+        RFC3986: function (value) {
+            return String(value);
+        }
+    },
+    RFC1738: Format.RFC1738,
+    RFC3986: Format.RFC3986
+};
Index: frontend/node_modules/qs/lib/index.js
===================================================================
--- frontend/node_modules/qs/lib/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/lib/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,11 @@
+'use strict';
+
+var stringify = require('./stringify');
+var parse = require('./parse');
+var formats = require('./formats');
+
+module.exports = {
+    formats: formats,
+    parse: parse,
+    stringify: stringify
+};
Index: frontend/node_modules/qs/lib/parse.js
===================================================================
--- frontend/node_modules/qs/lib/parse.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/lib/parse.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,403 @@
+'use strict';
+
+var utils = require('./utils');
+
+var has = Object.prototype.hasOwnProperty;
+var isArray = Array.isArray;
+
+var defaults = {
+    allowDots: false,
+    allowEmptyArrays: false,
+    allowPrototypes: false,
+    allowSparse: false,
+    arrayLimit: 20,
+    charset: 'utf-8',
+    charsetSentinel: false,
+    comma: false,
+    decodeDotInKeys: false,
+    decoder: utils.decode,
+    delimiter: '&',
+    depth: 5,
+    duplicates: 'combine',
+    ignoreQueryPrefix: false,
+    interpretNumericEntities: false,
+    parameterLimit: 1000,
+    parseArrays: true,
+    plainObjects: false,
+    strictDepth: false,
+    strictMerge: true,
+    strictNullHandling: false,
+    throwOnLimitExceeded: false
+};
+
+var interpretNumericEntities = function (str) {
+    return str.replace(/&#(\d+);/g, function ($0, numberStr) {
+        return String.fromCharCode(parseInt(numberStr, 10));
+    });
+};
+
+var parseArrayValue = function (val, options, currentArrayLength) {
+    if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) {
+        return val.split(',');
+    }
+
+    if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) {
+        throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
+    }
+
+    return val;
+};
+
+// This is what browsers will submit when the ✓ character occurs in an
+// application/x-www-form-urlencoded body and the encoding of the page containing
+// the form is iso-8859-1, or when the submitted form has an accept-charset
+// attribute of iso-8859-1. Presumably also with other charsets that do not contain
+// the ✓ character, such as us-ascii.
+var isoSentinel = 'utf8=%26%2310003%3B'; // encodeURIComponent('&#10003;')
+
+// These are the percent-encoded utf-8 octets representing a checkmark, indicating that the request actually is utf-8 encoded.
+var charsetSentinel = 'utf8=%E2%9C%93'; // encodeURIComponent('✓')
+
+var parseValues = function parseQueryStringValues(str, options) {
+    var obj = { __proto__: null };
+
+    var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str;
+    cleanStr = cleanStr.replace(/%5B/gi, '[').replace(/%5D/gi, ']');
+
+    var limit = options.parameterLimit === Infinity ? void undefined : options.parameterLimit;
+    var parts = cleanStr.split(
+        options.delimiter,
+        options.throwOnLimitExceeded && typeof limit !== 'undefined' ? limit + 1 : limit
+    );
+
+    if (options.throwOnLimitExceeded && typeof limit !== 'undefined' && parts.length > limit) {
+        throw new RangeError('Parameter limit exceeded. Only ' + limit + ' parameter' + (limit === 1 ? '' : 's') + ' allowed.');
+    }
+
+    var skipIndex = -1; // Keep track of where the utf8 sentinel was found
+    var i;
+
+    var charset = options.charset;
+    if (options.charsetSentinel) {
+        for (i = 0; i < parts.length; ++i) {
+            if (parts[i].indexOf('utf8=') === 0) {
+                if (parts[i] === charsetSentinel) {
+                    charset = 'utf-8';
+                } else if (parts[i] === isoSentinel) {
+                    charset = 'iso-8859-1';
+                }
+                skipIndex = i;
+                i = parts.length; // The eslint settings do not allow break;
+            }
+        }
+    }
+
+    for (i = 0; i < parts.length; ++i) {
+        if (i === skipIndex) {
+            continue;
+        }
+        var part = parts[i];
+
+        var bracketEqualsPos = part.indexOf(']=');
+        var pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1;
+
+        var key;
+        var val;
+        if (pos === -1) {
+            key = options.decoder(part, defaults.decoder, charset, 'key');
+            val = options.strictNullHandling ? null : '';
+        } else {
+            key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key');
+
+            if (key !== null) {
+                val = utils.maybeMap(
+                    parseArrayValue(
+                        part.slice(pos + 1),
+                        options,
+                        isArray(obj[key]) ? obj[key].length : 0
+                    ),
+                    function (encodedVal) {
+                        return options.decoder(encodedVal, defaults.decoder, charset, 'value');
+                    }
+                );
+            }
+        }
+
+        if (val && options.interpretNumericEntities && charset === 'iso-8859-1') {
+            val = interpretNumericEntities(String(val));
+        }
+
+        if (part.indexOf('[]=') > -1) {
+            val = isArray(val) ? [val] : val;
+        }
+
+        if (options.comma && isArray(val) && val.length > options.arrayLimit) {
+            if (options.throwOnLimitExceeded) {
+                throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
+            }
+            val = utils.combine([], val, options.arrayLimit, options.plainObjects);
+        }
+
+        if (key !== null) {
+            var existing = has.call(obj, key);
+            if (existing && (options.duplicates === 'combine' || part.indexOf('[]=') > -1)) {
+                obj[key] = utils.combine(
+                    obj[key],
+                    val,
+                    options.arrayLimit,
+                    options.plainObjects
+                );
+            } else if (!existing || options.duplicates === 'last') {
+                obj[key] = val;
+            }
+        }
+    }
+
+    return obj;
+};
+
+var parseObject = function (chain, val, options, valuesParsed) {
+    var currentArrayLength = 0;
+    if (chain.length > 0 && chain[chain.length - 1] === '[]') {
+        var parentKey = chain.slice(0, -1).join('');
+        currentArrayLength = Array.isArray(val) && val[parentKey] ? val[parentKey].length : 0;
+    }
+
+    var leaf = valuesParsed ? val : parseArrayValue(val, options, currentArrayLength);
+
+    for (var i = chain.length - 1; i >= 0; --i) {
+        var obj;
+        var root = chain[i];
+
+        if (root === '[]' && options.parseArrays) {
+            if (utils.isOverflow(leaf)) {
+                // leaf is already an overflow object, preserve it
+                obj = leaf;
+            } else {
+                obj = options.allowEmptyArrays && (leaf === '' || (options.strictNullHandling && leaf === null))
+                    ? []
+                    : utils.combine(
+                        [],
+                        leaf,
+                        options.arrayLimit,
+                        options.plainObjects
+                    );
+            }
+        } else {
+            obj = options.plainObjects ? { __proto__: null } : {};
+            var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
+            var decodedRoot = options.decodeDotInKeys ? cleanRoot.replace(/%2E/g, '.') : cleanRoot;
+            var index = parseInt(decodedRoot, 10);
+            var isValidArrayIndex = !isNaN(index)
+                && root !== decodedRoot
+                && String(index) === decodedRoot
+                && index >= 0
+                && options.parseArrays;
+            if (!options.parseArrays && decodedRoot === '') {
+                obj = { 0: leaf };
+            } else if (isValidArrayIndex && index < options.arrayLimit) {
+                obj = [];
+                obj[index] = leaf;
+            } else if (isValidArrayIndex && options.throwOnLimitExceeded) {
+                throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
+            } else if (isValidArrayIndex) {
+                obj[index] = leaf;
+                utils.markOverflow(obj, index);
+            } else if (decodedRoot !== '__proto__') {
+                obj[decodedRoot] = leaf;
+            }
+        }
+
+        leaf = obj;
+    }
+
+    return leaf;
+};
+
+// Split a key like "a[b][c[]]" into ['a', '[b]', '[c[]]'] while preserving
+// qs parse semantics for depth/prototype guards.
+var splitKeyIntoSegments = function splitKeyIntoSegments(originalKey, options) {
+    var key = options.allowDots ? originalKey.replace(/\.([^.[]+)/g, '[$1]') : originalKey;
+
+    // depth <= 0 keeps the whole key as one segment
+    if (options.depth <= 0) {
+        if (!options.plainObjects && has.call(Object.prototype, key)) {
+            if (!options.allowPrototypes) {
+                return;
+            }
+        }
+
+        return [key];
+    }
+
+    var segments = [];
+
+    // parent before the first '[' (may be empty if key starts with '[')
+    var first = key.indexOf('[');
+    var parent = first >= 0 ? key.slice(0, first) : key;
+    if (parent) {
+        if (!options.plainObjects && has.call(Object.prototype, parent)) {
+            if (!options.allowPrototypes) {
+                return;
+            }
+        }
+
+        segments[segments.length] = parent;
+    }
+
+    var n = key.length;
+    var open = first;
+    var collected = 0;
+
+    while (open >= 0 && collected < options.depth) {
+        var level = 1;
+        var i = open + 1;
+        var close = -1;
+
+        // balance nested '[' and ']' inside this bracket group using a nesting level counter
+        while (i < n && close < 0) {
+            var cu = key.charCodeAt(i);
+            if (cu === 0x5B) { // '['
+                level += 1;
+            } else if (cu === 0x5D) { // ']'
+                level -= 1;
+                if (level === 0) {
+                    close = i; // found matching close; loop will exit by condition
+                }
+            }
+            i += 1;
+        }
+
+        if (close < 0) {
+            // Unterminated group: wrap the raw remainder in one bracket pair so it stays
+            // a single literal segment (e.g. "[[]b" -> "[[]b]"); we do not infer missing ']'.
+            segments[segments.length] = '[' + key.slice(open) + ']';
+            return segments;
+        }
+
+        var seg = key.slice(open, close + 1);
+        // prototype guard for the content of this group
+        var content = seg.slice(1, -1);
+        if (!options.plainObjects && has.call(Object.prototype, content) && !options.allowPrototypes) {
+            return;
+        }
+
+        segments[segments.length] = seg;
+        collected += 1;
+
+        // find the next '[' after this balanced group
+        open = key.indexOf('[', close + 1);
+    }
+
+    if (open >= 0) {
+        if (options.strictDepth === true) {
+            throw new RangeError('Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true');
+        }
+
+        segments[segments.length] = '[' + key.slice(open) + ']';
+    }
+
+    return segments;
+};
+
+var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) {
+    if (!givenKey) {
+        return;
+    }
+
+    var keys = splitKeyIntoSegments(givenKey, options);
+
+    if (!keys) {
+        return;
+    }
+
+    return parseObject(keys, val, options, valuesParsed);
+};
+
+var normalizeParseOptions = function normalizeParseOptions(opts) {
+    if (!opts) {
+        return defaults;
+    }
+
+    if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') {
+        throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided');
+    }
+
+    if (typeof opts.decodeDotInKeys !== 'undefined' && typeof opts.decodeDotInKeys !== 'boolean') {
+        throw new TypeError('`decodeDotInKeys` option can only be `true` or `false`, when provided');
+    }
+
+    if (opts.decoder !== null && typeof opts.decoder !== 'undefined' && typeof opts.decoder !== 'function') {
+        throw new TypeError('Decoder has to be a function.');
+    }
+
+    if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
+        throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
+    }
+
+    if (typeof opts.throwOnLimitExceeded !== 'undefined' && typeof opts.throwOnLimitExceeded !== 'boolean') {
+        throw new TypeError('`throwOnLimitExceeded` option must be a boolean');
+    }
+
+    var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset;
+
+    var duplicates = typeof opts.duplicates === 'undefined' ? defaults.duplicates : opts.duplicates;
+
+    if (duplicates !== 'combine' && duplicates !== 'first' && duplicates !== 'last') {
+        throw new TypeError('The duplicates option must be either combine, first, or last');
+    }
+
+    var allowDots = typeof opts.allowDots === 'undefined' ? opts.decodeDotInKeys === true ? true : defaults.allowDots : !!opts.allowDots;
+
+    return {
+        allowDots: allowDots,
+        allowEmptyArrays: typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays,
+        allowPrototypes: typeof opts.allowPrototypes === 'boolean' ? opts.allowPrototypes : defaults.allowPrototypes,
+        allowSparse: typeof opts.allowSparse === 'boolean' ? opts.allowSparse : defaults.allowSparse,
+        arrayLimit: typeof opts.arrayLimit === 'number' ? opts.arrayLimit : defaults.arrayLimit,
+        charset: charset,
+        charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
+        comma: typeof opts.comma === 'boolean' ? opts.comma : defaults.comma,
+        decodeDotInKeys: typeof opts.decodeDotInKeys === 'boolean' ? opts.decodeDotInKeys : defaults.decodeDotInKeys,
+        decoder: typeof opts.decoder === 'function' ? opts.decoder : defaults.decoder,
+        delimiter: typeof opts.delimiter === 'string' || utils.isRegExp(opts.delimiter) ? opts.delimiter : defaults.delimiter,
+        // eslint-disable-next-line no-implicit-coercion, no-extra-parens
+        depth: (typeof opts.depth === 'number' || opts.depth === false) ? +opts.depth : defaults.depth,
+        duplicates: duplicates,
+        ignoreQueryPrefix: opts.ignoreQueryPrefix === true,
+        interpretNumericEntities: typeof opts.interpretNumericEntities === 'boolean' ? opts.interpretNumericEntities : defaults.interpretNumericEntities,
+        parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit,
+        parseArrays: opts.parseArrays !== false,
+        plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
+        strictDepth: typeof opts.strictDepth === 'boolean' ? !!opts.strictDepth : defaults.strictDepth,
+        strictMerge: typeof opts.strictMerge === 'boolean' ? !!opts.strictMerge : defaults.strictMerge,
+        strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling,
+        throwOnLimitExceeded: typeof opts.throwOnLimitExceeded === 'boolean' ? opts.throwOnLimitExceeded : false
+    };
+};
+
+module.exports = function (str, opts) {
+    var options = normalizeParseOptions(opts);
+
+    if (str === '' || str === null || typeof str === 'undefined') {
+        return options.plainObjects ? { __proto__: null } : {};
+    }
+
+    var tempObj = typeof str === 'string' ? parseValues(str, options) : str;
+    var obj = options.plainObjects ? { __proto__: null } : {};
+
+    // Iterate over the keys and setup the new object
+
+    var keys = Object.keys(tempObj);
+    for (var i = 0; i < keys.length; ++i) {
+        var key = keys[i];
+        var newObj = parseKeys(key, tempObj[key], options, typeof str === 'string');
+        obj = utils.merge(obj, newObj, options);
+    }
+
+    if (options.allowSparse === true) {
+        return obj;
+    }
+
+    return utils.compact(obj);
+};
Index: frontend/node_modules/qs/lib/stringify.js
===================================================================
--- frontend/node_modules/qs/lib/stringify.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/lib/stringify.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,363 @@
+'use strict';
+
+var getSideChannel = require('side-channel');
+var utils = require('./utils');
+var formats = require('./formats');
+var has = Object.prototype.hasOwnProperty;
+
+var arrayPrefixGenerators = {
+    brackets: function brackets(prefix) {
+        return prefix + '[]';
+    },
+    comma: 'comma',
+    indices: function indices(prefix, key) {
+        return prefix + '[' + key + ']';
+    },
+    repeat: function repeat(prefix) {
+        return prefix;
+    }
+};
+
+var isArray = Array.isArray;
+var push = Array.prototype.push;
+var pushToArray = function (arr, valueOrArray) {
+    push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]);
+};
+
+var toISO = Date.prototype.toISOString;
+
+var defaultFormat = formats['default'];
+var defaults = {
+    addQueryPrefix: false,
+    allowDots: false,
+    allowEmptyArrays: false,
+    arrayFormat: 'indices',
+    charset: 'utf-8',
+    charsetSentinel: false,
+    commaRoundTrip: false,
+    delimiter: '&',
+    encode: true,
+    encodeDotInKeys: false,
+    encoder: utils.encode,
+    encodeValuesOnly: false,
+    filter: void undefined,
+    format: defaultFormat,
+    formatter: formats.formatters[defaultFormat],
+    // deprecated
+    indices: false,
+    serializeDate: function serializeDate(date) {
+        return toISO.call(date);
+    },
+    skipNulls: false,
+    strictNullHandling: false
+};
+
+var isNonNullishPrimitive = function isNonNullishPrimitive(v) {
+    return typeof v === 'string'
+        || typeof v === 'number'
+        || typeof v === 'boolean'
+        || typeof v === 'symbol'
+        || typeof v === 'bigint';
+};
+
+var sentinel = {};
+
+var stringify = function stringify(
+    object,
+    prefix,
+    generateArrayPrefix,
+    commaRoundTrip,
+    allowEmptyArrays,
+    strictNullHandling,
+    skipNulls,
+    encodeDotInKeys,
+    encoder,
+    filter,
+    sort,
+    allowDots,
+    serializeDate,
+    format,
+    formatter,
+    encodeValuesOnly,
+    charset,
+    sideChannel
+) {
+    var obj = object;
+
+    var tmpSc = sideChannel;
+    var step = 0;
+    var findFlag = false;
+    while ((tmpSc = tmpSc.get(sentinel)) !== void undefined && !findFlag) {
+        // Where object last appeared in the ref tree
+        var pos = tmpSc.get(object);
+        step += 1;
+        if (typeof pos !== 'undefined') {
+            if (pos === step) {
+                throw new RangeError('Cyclic object value');
+            } else {
+                findFlag = true; // Break while
+            }
+        }
+        if (typeof tmpSc.get(sentinel) === 'undefined') {
+            step = 0;
+        }
+    }
+
+    if (typeof filter === 'function') {
+        obj = filter(prefix, obj);
+    } else if (obj instanceof Date) {
+        obj = serializeDate(obj);
+    } else if (generateArrayPrefix === 'comma' && isArray(obj)) {
+        obj = utils.maybeMap(obj, function (value) {
+            if (value instanceof Date) {
+                return serializeDate(value);
+            }
+            return value;
+        });
+    }
+
+    if (obj === null) {
+        if (strictNullHandling) {
+            return formatter(encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix);
+        }
+
+        obj = '';
+    }
+
+    if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) {
+        if (encoder) {
+            var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset, 'key', format);
+            return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset, 'value', format))];
+        }
+        return [formatter(prefix) + '=' + formatter(String(obj))];
+    }
+
+    var values = [];
+
+    if (typeof obj === 'undefined') {
+        return values;
+    }
+
+    var objKeys;
+    if (generateArrayPrefix === 'comma' && isArray(obj)) {
+        // we need to join elements in
+        if (encodeValuesOnly && encoder) {
+            obj = utils.maybeMap(obj, function (v) {
+                return v == null ? v : encoder(v);
+            });
+        }
+        objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
+    } else if (isArray(filter)) {
+        objKeys = filter;
+    } else {
+        var keys = Object.keys(obj);
+        objKeys = sort ? keys.sort(sort) : keys;
+    }
+
+    var encodedPrefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix);
+
+    var adjustedPrefix = commaRoundTrip && isArray(obj) && obj.length === 1 ? encodedPrefix + '[]' : encodedPrefix;
+
+    if (allowEmptyArrays && isArray(obj) && obj.length === 0) {
+        return adjustedPrefix + '[]';
+    }
+
+    for (var j = 0; j < objKeys.length; ++j) {
+        var key = objKeys[j];
+        var value = typeof key === 'object' && key && typeof key.value !== 'undefined'
+            ? key.value
+            : obj[key];
+
+        if (skipNulls && value === null) {
+            continue;
+        }
+
+        var encodedKey = allowDots && encodeDotInKeys ? String(key).replace(/\./g, '%2E') : String(key);
+        var keyPrefix = isArray(obj)
+            ? typeof generateArrayPrefix === 'function' ? generateArrayPrefix(adjustedPrefix, encodedKey) : adjustedPrefix
+            : adjustedPrefix + (allowDots ? '.' + encodedKey : '[' + encodedKey + ']');
+
+        sideChannel.set(object, step);
+        var valueSideChannel = getSideChannel();
+        valueSideChannel.set(sentinel, sideChannel);
+        pushToArray(values, stringify(
+            value,
+            keyPrefix,
+            generateArrayPrefix,
+            commaRoundTrip,
+            allowEmptyArrays,
+            strictNullHandling,
+            skipNulls,
+            encodeDotInKeys,
+            generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder,
+            filter,
+            sort,
+            allowDots,
+            serializeDate,
+            format,
+            formatter,
+            encodeValuesOnly,
+            charset,
+            valueSideChannel
+        ));
+    }
+
+    return values;
+};
+
+var normalizeStringifyOptions = function normalizeStringifyOptions(opts) {
+    if (!opts) {
+        return defaults;
+    }
+
+    if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') {
+        throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided');
+    }
+
+    if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') {
+        throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided');
+    }
+
+    if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') {
+        throw new TypeError('Encoder has to be a function.');
+    }
+
+    var charset = opts.charset || defaults.charset;
+    if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
+        throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
+    }
+
+    var format = formats['default'];
+    if (typeof opts.format !== 'undefined') {
+        if (!has.call(formats.formatters, opts.format)) {
+            throw new TypeError('Unknown format option provided.');
+        }
+        format = opts.format;
+    }
+    var formatter = formats.formatters[format];
+
+    var filter = defaults.filter;
+    if (typeof opts.filter === 'function' || isArray(opts.filter)) {
+        filter = opts.filter;
+    }
+
+    var arrayFormat;
+    if (opts.arrayFormat in arrayPrefixGenerators) {
+        arrayFormat = opts.arrayFormat;
+    } else if ('indices' in opts) {
+        arrayFormat = opts.indices ? 'indices' : 'repeat';
+    } else {
+        arrayFormat = defaults.arrayFormat;
+    }
+
+    if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') {
+        throw new TypeError('`commaRoundTrip` must be a boolean, or absent');
+    }
+
+    var allowDots = typeof opts.allowDots === 'undefined' ? opts.encodeDotInKeys === true ? true : defaults.allowDots : !!opts.allowDots;
+
+    return {
+        addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix,
+        allowDots: allowDots,
+        allowEmptyArrays: typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays,
+        arrayFormat: arrayFormat,
+        charset: charset,
+        charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
+        commaRoundTrip: !!opts.commaRoundTrip,
+        delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter,
+        encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode,
+        encodeDotInKeys: typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys,
+        encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder,
+        encodeValuesOnly: typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly,
+        filter: filter,
+        format: format,
+        formatter: formatter,
+        serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate,
+        skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls,
+        sort: typeof opts.sort === 'function' ? opts.sort : null,
+        strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
+    };
+};
+
+module.exports = function (object, opts) {
+    var obj = object;
+    var options = normalizeStringifyOptions(opts);
+
+    var objKeys;
+    var filter;
+
+    if (typeof options.filter === 'function') {
+        filter = options.filter;
+        obj = filter('', obj);
+    } else if (isArray(options.filter)) {
+        filter = options.filter;
+        objKeys = filter;
+    }
+
+    var keys = [];
+
+    if (typeof obj !== 'object' || obj === null) {
+        return '';
+    }
+
+    var generateArrayPrefix = arrayPrefixGenerators[options.arrayFormat];
+    var commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip;
+
+    if (!objKeys) {
+        objKeys = Object.keys(obj);
+    }
+
+    if (options.sort) {
+        objKeys.sort(options.sort);
+    }
+
+    var sideChannel = getSideChannel();
+    for (var i = 0; i < objKeys.length; ++i) {
+        var key = objKeys[i];
+
+        if (typeof key === 'undefined' || key === null) {
+            continue;
+        }
+
+        var value = obj[key];
+
+        if (options.skipNulls && value === null) {
+            continue;
+        }
+        pushToArray(keys, stringify(
+            value,
+            key,
+            generateArrayPrefix,
+            commaRoundTrip,
+            options.allowEmptyArrays,
+            options.strictNullHandling,
+            options.skipNulls,
+            options.encodeDotInKeys,
+            options.encode ? options.encoder : null,
+            options.filter,
+            options.sort,
+            options.allowDots,
+            options.serializeDate,
+            options.format,
+            options.formatter,
+            options.encodeValuesOnly,
+            options.charset,
+            sideChannel
+        ));
+    }
+
+    var joined = keys.join(options.delimiter);
+    var prefix = options.addQueryPrefix === true ? '?' : '';
+
+    if (options.charsetSentinel) {
+        if (options.charset === 'iso-8859-1') {
+            // encodeURIComponent('&#10003;'), the "numeric entity" representation of a checkmark
+            prefix += 'utf8=%26%2310003%3B' + options.delimiter;
+        } else {
+            // encodeURIComponent('✓')
+            prefix += 'utf8=%E2%9C%93' + options.delimiter;
+        }
+    }
+
+    return joined.length > 0 ? prefix + joined : '';
+};
Index: frontend/node_modules/qs/lib/utils.js
===================================================================
--- frontend/node_modules/qs/lib/utils.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/lib/utils.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,342 @@
+'use strict';
+
+var formats = require('./formats');
+var getSideChannel = require('side-channel');
+
+var has = Object.prototype.hasOwnProperty;
+var isArray = Array.isArray;
+
+// Track objects created from arrayLimit overflow using side-channel
+// Stores the current max numeric index for O(1) lookup
+var overflowChannel = getSideChannel();
+
+var markOverflow = function markOverflow(obj, maxIndex) {
+    overflowChannel.set(obj, maxIndex);
+    return obj;
+};
+
+var isOverflow = function isOverflow(obj) {
+    return overflowChannel.has(obj);
+};
+
+var getMaxIndex = function getMaxIndex(obj) {
+    return overflowChannel.get(obj);
+};
+
+var setMaxIndex = function setMaxIndex(obj, maxIndex) {
+    overflowChannel.set(obj, maxIndex);
+};
+
+var hexTable = (function () {
+    var array = [];
+    for (var i = 0; i < 256; ++i) {
+        array[array.length] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
+    }
+
+    return array;
+}());
+
+var compactQueue = function compactQueue(queue) {
+    while (queue.length > 1) {
+        var item = queue.pop();
+        var obj = item.obj[item.prop];
+
+        if (isArray(obj)) {
+            var compacted = [];
+
+            for (var j = 0; j < obj.length; ++j) {
+                if (typeof obj[j] !== 'undefined') {
+                    compacted[compacted.length] = obj[j];
+                }
+            }
+
+            item.obj[item.prop] = compacted;
+        }
+    }
+};
+
+var arrayToObject = function arrayToObject(source, options) {
+    var obj = options && options.plainObjects ? { __proto__: null } : {};
+    for (var i = 0; i < source.length; ++i) {
+        if (typeof source[i] !== 'undefined') {
+            obj[i] = source[i];
+        }
+    }
+
+    return obj;
+};
+
+var merge = function merge(target, source, options) {
+    /* eslint no-param-reassign: 0 */
+    if (!source) {
+        return target;
+    }
+
+    if (typeof source !== 'object' && typeof source !== 'function') {
+        if (isArray(target)) {
+            var nextIndex = target.length;
+            if (options && typeof options.arrayLimit === 'number' && nextIndex > options.arrayLimit) {
+                return markOverflow(arrayToObject(target.concat(source), options), nextIndex);
+            }
+            target[nextIndex] = source;
+        } else if (target && typeof target === 'object') {
+            if (isOverflow(target)) {
+                // Add at next numeric index for overflow objects
+                var newIndex = getMaxIndex(target) + 1;
+                target[newIndex] = source;
+                setMaxIndex(target, newIndex);
+            } else if (options && options.strictMerge) {
+                return [target, source];
+            } else if (
+                (options && (options.plainObjects || options.allowPrototypes))
+                || !has.call(Object.prototype, source)
+            ) {
+                target[source] = true;
+            }
+        } else {
+            return [target, source];
+        }
+
+        return target;
+    }
+
+    if (!target || typeof target !== 'object') {
+        if (isOverflow(source)) {
+            // Create new object with target at 0, source values shifted by 1
+            var sourceKeys = Object.keys(source);
+            var result = options && options.plainObjects
+                ? { __proto__: null, 0: target }
+                : { 0: target };
+            for (var m = 0; m < sourceKeys.length; m++) {
+                var oldKey = parseInt(sourceKeys[m], 10);
+                result[oldKey + 1] = source[sourceKeys[m]];
+            }
+            return markOverflow(result, getMaxIndex(source) + 1);
+        }
+        var combined = [target].concat(source);
+        if (options && typeof options.arrayLimit === 'number' && combined.length > options.arrayLimit) {
+            return markOverflow(arrayToObject(combined, options), combined.length - 1);
+        }
+        return combined;
+    }
+
+    var mergeTarget = target;
+    if (isArray(target) && !isArray(source)) {
+        mergeTarget = arrayToObject(target, options);
+    }
+
+    if (isArray(target) && isArray(source)) {
+        source.forEach(function (item, i) {
+            if (has.call(target, i)) {
+                var targetItem = target[i];
+                if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
+                    target[i] = merge(targetItem, item, options);
+                } else {
+                    target[target.length] = item;
+                }
+            } else {
+                target[i] = item;
+            }
+        });
+        return target;
+    }
+
+    return Object.keys(source).reduce(function (acc, key) {
+        var value = source[key];
+
+        if (has.call(acc, key)) {
+            acc[key] = merge(acc[key], value, options);
+        } else {
+            acc[key] = value;
+        }
+
+        if (isOverflow(source) && !isOverflow(acc)) {
+            markOverflow(acc, getMaxIndex(source));
+        }
+        if (isOverflow(acc)) {
+            var keyNum = parseInt(key, 10);
+            if (String(keyNum) === key && keyNum >= 0 && keyNum > getMaxIndex(acc)) {
+                setMaxIndex(acc, keyNum);
+            }
+        }
+
+        return acc;
+    }, mergeTarget);
+};
+
+var assign = function assignSingleSource(target, source) {
+    return Object.keys(source).reduce(function (acc, key) {
+        acc[key] = source[key];
+        return acc;
+    }, target);
+};
+
+var decode = function (str, defaultDecoder, charset) {
+    var strWithoutPlus = str.replace(/\+/g, ' ');
+    if (charset === 'iso-8859-1') {
+        // unescape never throws, no try...catch needed:
+        return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape);
+    }
+    // utf-8
+    try {
+        return decodeURIComponent(strWithoutPlus);
+    } catch (e) {
+        return strWithoutPlus;
+    }
+};
+
+var limit = 1024;
+
+/* eslint operator-linebreak: [2, "before"] */
+
+var encode = function encode(str, defaultEncoder, charset, kind, format) {
+    // This code was originally written by Brian White (mscdex) for the io.js core querystring library.
+    // It has been adapted here for stricter adherence to RFC 3986
+    if (str.length === 0) {
+        return str;
+    }
+
+    var string = str;
+    if (typeof str === 'symbol') {
+        string = Symbol.prototype.toString.call(str);
+    } else if (typeof str !== 'string') {
+        string = String(str);
+    }
+
+    if (charset === 'iso-8859-1') {
+        return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) {
+            return '%26%23' + parseInt($0.slice(2), 16) + '%3B';
+        });
+    }
+
+    var out = '';
+    for (var j = 0; j < string.length; j += limit) {
+        var segment = string.length >= limit ? string.slice(j, j + limit) : string;
+        var arr = [];
+
+        for (var i = 0; i < segment.length; ++i) {
+            var c = segment.charCodeAt(i);
+            if (
+                c === 0x2D // -
+                || c === 0x2E // .
+                || c === 0x5F // _
+                || c === 0x7E // ~
+                || (c >= 0x30 && c <= 0x39) // 0-9
+                || (c >= 0x41 && c <= 0x5A) // a-z
+                || (c >= 0x61 && c <= 0x7A) // A-Z
+                || (format === formats.RFC1738 && (c === 0x28 || c === 0x29)) // ( )
+            ) {
+                arr[arr.length] = segment.charAt(i);
+                continue;
+            }
+
+            if (c < 0x80) {
+                arr[arr.length] = hexTable[c];
+                continue;
+            }
+
+            if (c < 0x800) {
+                arr[arr.length] = hexTable[0xC0 | (c >> 6)]
+                    + hexTable[0x80 | (c & 0x3F)];
+                continue;
+            }
+
+            if (c < 0xD800 || c >= 0xE000) {
+                arr[arr.length] = hexTable[0xE0 | (c >> 12)]
+                    + hexTable[0x80 | ((c >> 6) & 0x3F)]
+                    + hexTable[0x80 | (c & 0x3F)];
+                continue;
+            }
+
+            i += 1;
+            c = 0x10000 + (((c & 0x3FF) << 10) | (segment.charCodeAt(i) & 0x3FF));
+
+            arr[arr.length] = hexTable[0xF0 | (c >> 18)]
+                + hexTable[0x80 | ((c >> 12) & 0x3F)]
+                + hexTable[0x80 | ((c >> 6) & 0x3F)]
+                + hexTable[0x80 | (c & 0x3F)];
+        }
+
+        out += arr.join('');
+    }
+
+    return out;
+};
+
+var compact = function compact(value) {
+    var queue = [{ obj: { o: value }, prop: 'o' }];
+    var refs = [];
+
+    for (var i = 0; i < queue.length; ++i) {
+        var item = queue[i];
+        var obj = item.obj[item.prop];
+
+        var keys = Object.keys(obj);
+        for (var j = 0; j < keys.length; ++j) {
+            var key = keys[j];
+            var val = obj[key];
+            if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
+                queue[queue.length] = { obj: obj, prop: key };
+                refs[refs.length] = val;
+            }
+        }
+    }
+
+    compactQueue(queue);
+
+    return value;
+};
+
+var isRegExp = function isRegExp(obj) {
+    return Object.prototype.toString.call(obj) === '[object RegExp]';
+};
+
+var isBuffer = function isBuffer(obj) {
+    if (!obj || typeof obj !== 'object') {
+        return false;
+    }
+
+    return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
+};
+
+var combine = function combine(a, b, arrayLimit, plainObjects) {
+    // If 'a' is already an overflow object, add to it
+    if (isOverflow(a)) {
+        var newIndex = getMaxIndex(a) + 1;
+        a[newIndex] = b;
+        setMaxIndex(a, newIndex);
+        return a;
+    }
+
+    var result = [].concat(a, b);
+    if (result.length > arrayLimit) {
+        return markOverflow(arrayToObject(result, { plainObjects: plainObjects }), result.length - 1);
+    }
+    return result;
+};
+
+var maybeMap = function maybeMap(val, fn) {
+    if (isArray(val)) {
+        var mapped = [];
+        for (var i = 0; i < val.length; i += 1) {
+            mapped[mapped.length] = fn(val[i]);
+        }
+        return mapped;
+    }
+    return fn(val);
+};
+
+module.exports = {
+    arrayToObject: arrayToObject,
+    assign: assign,
+    combine: combine,
+    compact: compact,
+    decode: decode,
+    encode: encode,
+    isBuffer: isBuffer,
+    isOverflow: isOverflow,
+    isRegExp: isRegExp,
+    markOverflow: markOverflow,
+    maybeMap: maybeMap,
+    merge: merge
+};
Index: frontend/node_modules/qs/package.json
===================================================================
--- frontend/node_modules/qs/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,94 @@
+{
+    "name": "qs",
+    "description": "A querystring parser that supports nesting and arrays, with a depth limit",
+    "homepage": "https://github.com/ljharb/qs",
+    "version": "6.15.2",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/ljharb/qs.git"
+    },
+    "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+    },
+    "main": "lib/index.js",
+    "sideEffects": false,
+    "contributors": [
+        {
+            "name": "Jordan Harband",
+            "email": "ljharb@gmail.com",
+            "url": "http://ljharb.codes"
+        }
+    ],
+    "keywords": [
+        "querystring",
+        "qs",
+        "query",
+        "url",
+        "parse",
+        "stringify"
+    ],
+    "engines": {
+        "node": ">=0.6"
+    },
+    "dependencies": {
+        "side-channel": "^1.1.0"
+    },
+    "devDependencies": {
+        "@browserify/envify": "^6.0.0",
+        "@browserify/uglifyify": "^6.0.0",
+        "@ljharb/eslint-config": "^22.2.3",
+        "browserify": "^16.5.2",
+        "bundle-collapser": "^1.4.0",
+        "common-shakeify": "~1.0.0",
+        "eclint": "^2.8.1",
+        "es-value-fixtures": "^1.7.1",
+        "eslint": "^9.39.2",
+        "evalmd": "^0.0.19",
+        "for-each": "^0.3.5",
+        "glob": "=10.3.7",
+        "has-bigints": "^1.1.0",
+        "has-override-mistake": "^1.0.1",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "iconv-lite": "^0.5.2",
+        "in-publish": "^2.0.1",
+        "jackspeak": "=2.1.1",
+        "jiti": "^0.0.0",
+        "mkdirp": "^0.5.5",
+        "mock-property": "^1.1.0",
+        "module-deps": "^6.2.3",
+        "npmignore": "^0.3.5",
+        "nyc": "^10.3.2",
+        "object-inspect": "^1.13.4",
+        "qs-iconv": "^1.0.4",
+        "safe-publish-latest": "^2.0.0",
+        "safer-buffer": "^2.1.2",
+        "tape": "^5.9.0",
+        "unassertify": "^3.0.1"
+    },
+    "scripts": {
+        "prepack": "npmignore --auto --commentLines=autogenerated && npm run dist",
+        "prepublishOnly": "safe-publish-latest",
+        "prepublish": "not-in-publish || npm run prepublishOnly",
+        "pretest": "npm run --silent readme && npm run --silent lint",
+        "test": "npm run tests-only",
+        "tests-only": "nyc tape 'test/**/*.js'",
+        "posttest": "npx npm@'>=10.2' audit --production",
+        "readme": "evalmd README.md",
+        "postlint": "eclint check $(git ls-files | xargs find 2> /dev/null | grep -vE 'node_modules|\\.git' | grep -v dist/)",
+        "lint": "eslint .",
+        "dist": "mkdirp dist && browserify --standalone Qs -g unassertify -g @browserify/envify -g [@browserify/uglifyify --mangle.keep_fnames --compress.keep_fnames --format.indent_level=1 --compress.arrows=false --compress.passes=4 --compress.typeofs=false] -p common-shakeify -p bundle-collapser/plugin lib/index.js > dist/qs.js"
+    },
+    "license": "BSD-3-Clause",
+    "publishConfig": {
+        "ignore": [
+            "!dist/*",
+            "bower.json",
+            "component.json",
+            ".github/workflows",
+            "logos",
+            "tea.yaml"
+        ]
+    }
+}
Index: frontend/node_modules/qs/test/empty-keys-cases.js
===================================================================
--- frontend/node_modules/qs/test/empty-keys-cases.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/test/empty-keys-cases.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,267 @@
+'use strict';
+
+module.exports = {
+    emptyTestCases: [
+        {
+            input: '&',
+            withEmptyKeys: {},
+            stringifyOutput: {
+                brackets: '',
+                indices: '',
+                repeat: ''
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '&&',
+            withEmptyKeys: {},
+            stringifyOutput: {
+                brackets: '',
+                indices: '',
+                repeat: ''
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '&=',
+            withEmptyKeys: { '': '' },
+            stringifyOutput: {
+                brackets: '=',
+                indices: '=',
+                repeat: '='
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '&=&',
+            withEmptyKeys: { '': '' },
+            stringifyOutput: {
+                brackets: '=',
+                indices: '=',
+                repeat: '='
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '&=&=',
+            withEmptyKeys: { '': ['', ''] },
+            stringifyOutput: {
+                brackets: '[]=&[]=',
+                indices: '[0]=&[1]=',
+                repeat: '=&='
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '&=&=&',
+            withEmptyKeys: { '': ['', ''] },
+            stringifyOutput: {
+                brackets: '[]=&[]=',
+                indices: '[0]=&[1]=',
+                repeat: '=&='
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '=',
+            withEmptyKeys: { '': '' },
+            noEmptyKeys: {},
+            stringifyOutput: {
+                brackets: '=',
+                indices: '=',
+                repeat: '='
+            }
+        },
+        {
+            input: '=&',
+            withEmptyKeys: { '': '' },
+            stringifyOutput: {
+                brackets: '=',
+                indices: '=',
+                repeat: '='
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '=&&&',
+            withEmptyKeys: { '': '' },
+            stringifyOutput: {
+                brackets: '=',
+                indices: '=',
+                repeat: '='
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '=&=&=&',
+            withEmptyKeys: { '': ['', '', ''] },
+            stringifyOutput: {
+                brackets: '[]=&[]=&[]=',
+                indices: '[0]=&[1]=&[2]=',
+                repeat: '=&=&='
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '=&a[]=b&a[1]=c',
+            withEmptyKeys: { '': '', a: ['b', 'c'] },
+            stringifyOutput: {
+                brackets: '=&a[]=b&a[]=c',
+                indices: '=&a[0]=b&a[1]=c',
+                repeat: '=&a=b&a=c'
+            },
+            noEmptyKeys: { a: ['b', 'c'] }
+        },
+        {
+            input: '=a',
+            withEmptyKeys: { '': 'a' },
+            noEmptyKeys: {},
+            stringifyOutput: {
+                brackets: '=a',
+                indices: '=a',
+                repeat: '=a'
+            }
+        },
+        {
+            input: 'a==a',
+            withEmptyKeys: { a: '=a' },
+            noEmptyKeys: { a: '=a' },
+            stringifyOutput: {
+                brackets: 'a==a',
+                indices: 'a==a',
+                repeat: 'a==a'
+            }
+        },
+        {
+            input: '=&a[]=b',
+            withEmptyKeys: { '': '', a: ['b'] },
+            stringifyOutput: {
+                brackets: '=&a[]=b',
+                indices: '=&a[0]=b',
+                repeat: '=&a=b'
+            },
+            noEmptyKeys: { a: ['b'] }
+        },
+        {
+            input: '=&a[]=b&a[]=c&a[2]=d',
+            withEmptyKeys: { '': '', a: ['b', 'c', 'd'] },
+            stringifyOutput: {
+                brackets: '=&a[]=b&a[]=c&a[]=d',
+                indices: '=&a[0]=b&a[1]=c&a[2]=d',
+                repeat: '=&a=b&a=c&a=d'
+            },
+            noEmptyKeys: { a: ['b', 'c', 'd'] }
+        },
+        {
+            input: '=a&=b',
+            withEmptyKeys: { '': ['a', 'b'] },
+            stringifyOutput: {
+                brackets: '[]=a&[]=b',
+                indices: '[0]=a&[1]=b',
+                repeat: '=a&=b'
+            },
+            noEmptyKeys: {}
+        },
+        {
+            input: '=a&foo=b',
+            withEmptyKeys: { '': 'a', foo: 'b' },
+            noEmptyKeys: { foo: 'b' },
+            stringifyOutput: {
+                brackets: '=a&foo=b',
+                indices: '=a&foo=b',
+                repeat: '=a&foo=b'
+            }
+        },
+        {
+            input: 'a[]=b&a=c&=',
+            withEmptyKeys: { '': '', a: ['b', 'c'] },
+            stringifyOutput: {
+                brackets: '=&a[]=b&a[]=c',
+                indices: '=&a[0]=b&a[1]=c',
+                repeat: '=&a=b&a=c'
+            },
+            noEmptyKeys: { a: ['b', 'c'] }
+        },
+        {
+            input: 'a[]=b&a=c&=',
+            withEmptyKeys: { '': '', a: ['b', 'c'] },
+            stringifyOutput: {
+                brackets: '=&a[]=b&a[]=c',
+                indices: '=&a[0]=b&a[1]=c',
+                repeat: '=&a=b&a=c'
+            },
+            noEmptyKeys: { a: ['b', 'c'] }
+        },
+        {
+            input: 'a[0]=b&a=c&=',
+            withEmptyKeys: { '': '', a: ['b', 'c'] },
+            stringifyOutput: {
+                brackets: '=&a[]=b&a[]=c',
+                indices: '=&a[0]=b&a[1]=c',
+                repeat: '=&a=b&a=c'
+            },
+            noEmptyKeys: { a: ['b', 'c'] }
+        },
+        {
+            input: 'a=b&a[]=c&=',
+            withEmptyKeys: { '': '', a: ['b', 'c'] },
+            stringifyOutput: {
+                brackets: '=&a[]=b&a[]=c',
+                indices: '=&a[0]=b&a[1]=c',
+                repeat: '=&a=b&a=c'
+            },
+            noEmptyKeys: { a: ['b', 'c'] }
+        },
+        {
+            input: 'a=b&a[0]=c&=',
+            withEmptyKeys: { '': '', a: ['b', 'c'] },
+            stringifyOutput: {
+                brackets: '=&a[]=b&a[]=c',
+                indices: '=&a[0]=b&a[1]=c',
+                repeat: '=&a=b&a=c'
+            },
+            noEmptyKeys: { a: ['b', 'c'] }
+        },
+        {
+            input: '[]=a&[]=b& []=1',
+            withEmptyKeys: { '': ['a', 'b'], ' ': ['1'] },
+            stringifyOutput: {
+                brackets: '[]=a&[]=b& []=1',
+                indices: '[0]=a&[1]=b& [0]=1',
+                repeat: '=a&=b& =1'
+            },
+            noEmptyKeys: { 0: 'a', 1: 'b', ' ': ['1'] }
+        },
+        {
+            input: '[0]=a&[1]=b&a[0]=1&a[1]=2',
+            withEmptyKeys: { '': ['a', 'b'], a: ['1', '2'] },
+            noEmptyKeys: { 0: 'a', 1: 'b', a: ['1', '2'] },
+            stringifyOutput: {
+                brackets: '[]=a&[]=b&a[]=1&a[]=2',
+                indices: '[0]=a&[1]=b&a[0]=1&a[1]=2',
+                repeat: '=a&=b&a=1&a=2'
+            }
+        },
+        {
+            input: '[deep]=a&[deep]=2',
+            withEmptyKeys: { '': { deep: ['a', '2'] }
+            },
+            stringifyOutput: {
+                brackets: '[deep][]=a&[deep][]=2',
+                indices: '[deep][0]=a&[deep][1]=2',
+                repeat: '[deep]=a&[deep]=2'
+            },
+            noEmptyKeys: { deep: ['a', '2'] }
+        },
+        {
+            input: '%5B0%5D=a&%5B1%5D=b',
+            withEmptyKeys: { '': ['a', 'b'] },
+            stringifyOutput: {
+                brackets: '[]=a&[]=b',
+                indices: '[0]=a&[1]=b',
+                repeat: '=a&=b'
+            },
+            noEmptyKeys: { 0: 'a', 1: 'b' }
+        }
+    ]
+};
Index: frontend/node_modules/qs/test/parse.js
===================================================================
--- frontend/node_modules/qs/test/parse.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/test/parse.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1703 @@
+'use strict';
+
+var test = require('tape');
+var hasPropertyDescriptors = require('has-property-descriptors')();
+var iconv = require('iconv-lite');
+var mockProperty = require('mock-property');
+var hasOverrideMistake = require('has-override-mistake')();
+var SaferBuffer = require('safer-buffer').Buffer;
+var v = require('es-value-fixtures');
+var inspect = require('object-inspect');
+var emptyTestCases = require('./empty-keys-cases').emptyTestCases;
+var hasProto = require('has-proto')();
+
+var qs = require('../');
+var utils = require('../lib/utils');
+
+test('parse()', function (t) {
+    t.test('parses a simple string', function (st) {
+        st.deepEqual(qs.parse('0=foo'), { 0: 'foo' });
+        st.deepEqual(qs.parse('foo=c++'), { foo: 'c  ' });
+        st.deepEqual(qs.parse('a[>=]=23'), { a: { '>=': '23' } });
+        st.deepEqual(qs.parse('a[<=>]==23'), { a: { '<=>': '=23' } });
+        st.deepEqual(qs.parse('a[==]=23'), { a: { '==': '23' } });
+        st.deepEqual(qs.parse('foo', { strictNullHandling: true }), { foo: null });
+        st.deepEqual(qs.parse('foo'), { foo: '' });
+        st.deepEqual(qs.parse('foo='), { foo: '' });
+        st.deepEqual(qs.parse('foo=bar'), { foo: 'bar' });
+        st.deepEqual(qs.parse(' foo = bar = baz '), { ' foo ': ' bar = baz ' });
+        st.deepEqual(qs.parse('foo=bar=baz'), { foo: 'bar=baz' });
+        st.deepEqual(qs.parse('foo=bar&bar=baz'), { foo: 'bar', bar: 'baz' });
+        st.deepEqual(qs.parse('foo2=bar2&baz2='), { foo2: 'bar2', baz2: '' });
+        st.deepEqual(qs.parse('foo=bar&baz', { strictNullHandling: true }), { foo: 'bar', baz: null });
+        st.deepEqual(qs.parse('foo=bar&baz'), { foo: 'bar', baz: '' });
+        st.deepEqual(qs.parse('cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'), {
+            cht: 'p3',
+            chd: 't:60,40',
+            chs: '250x100',
+            chl: 'Hello|World'
+        });
+        st.end();
+    });
+
+    t.test('comma: false', function (st) {
+        st.deepEqual(qs.parse('a[]=b&a[]=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b,c'), { a: 'b,c' });
+        st.deepEqual(qs.parse('a=b&a=c'), { a: ['b', 'c'] });
+        st.end();
+    });
+
+    t.test('comma: true', function (st) {
+        st.deepEqual(qs.parse('a[]=b&a[]=c', { comma: true }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { comma: true }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b,c', { comma: true }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b&a=c', { comma: true }), { a: ['b', 'c'] });
+        st.end();
+    });
+
+    t.test('allows enabling dot notation', function (st) {
+        st.deepEqual(qs.parse('a.b=c'), { 'a.b': 'c' });
+        st.deepEqual(qs.parse('a.b=c', { allowDots: true }), { a: { b: 'c' } });
+
+        st.end();
+    });
+
+    t.test('decode dot keys correctly', function (st) {
+        st.deepEqual(
+            qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { allowDots: false, decodeDotInKeys: false }),
+            { 'name%2Eobj.first': 'John', 'name%2Eobj.last': 'Doe' },
+            'with allowDots false and decodeDotInKeys false'
+        );
+        st.deepEqual(
+            qs.parse('name.obj.first=John&name.obj.last=Doe', { allowDots: true, decodeDotInKeys: false }),
+            { name: { obj: { first: 'John', last: 'Doe' } } },
+            'with allowDots false and decodeDotInKeys false'
+        );
+        st.deepEqual(
+            qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { allowDots: true, decodeDotInKeys: false }),
+            { 'name%2Eobj': { first: 'John', last: 'Doe' } },
+            'with allowDots true and decodeDotInKeys false'
+        );
+        st.deepEqual(
+            qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { allowDots: true, decodeDotInKeys: true }),
+            { 'name.obj': { first: 'John', last: 'Doe' } },
+            'with allowDots true and decodeDotInKeys true'
+        );
+
+        st.deepEqual(
+            qs.parse(
+                'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
+                { allowDots: false, decodeDotInKeys: false }
+            ),
+            { 'name%2Eobj%2Esubobject.first%2Egodly%2Ename': 'John', 'name%2Eobj%2Esubobject.last': 'Doe' },
+            'with allowDots false and decodeDotInKeys false'
+        );
+        st.deepEqual(
+            qs.parse(
+                'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe',
+                { allowDots: true, decodeDotInKeys: false }
+            ),
+            { name: { obj: { subobject: { first: { godly: { name: 'John' } }, last: 'Doe' } } } },
+            'with allowDots true and decodeDotInKeys false'
+        );
+        st.deepEqual(
+            qs.parse(
+                'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
+                { allowDots: true, decodeDotInKeys: true }
+            ),
+            { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
+            'with allowDots true and decodeDotInKeys true'
+        );
+        st.deepEqual(
+            qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe'),
+            { 'name%2Eobj.first': 'John', 'name%2Eobj.last': 'Doe' },
+            'with allowDots and decodeDotInKeys undefined'
+        );
+
+        st.end();
+    });
+
+    t.test('decodes dot in key of object, and allow enabling dot notation when decodeDotInKeys is set to true and allowDots is undefined', function (st) {
+        st.deepEqual(
+            qs.parse(
+                'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
+                { decodeDotInKeys: true }
+            ),
+            { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
+            'with allowDots undefined and decodeDotInKeys true'
+        );
+
+        st.end();
+    });
+
+    t.test('throws when decodeDotInKeys is not of type boolean', function (st) {
+        st['throws'](
+            function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: 'foobar' }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: 0 }); },
+            TypeError
+        );
+        st['throws'](
+            function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: NaN }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.parse('foo[]&bar=baz', { decodeDotInKeys: null }); },
+            TypeError
+        );
+
+        st.end();
+    });
+
+    t.test('allows empty arrays in obj values', function (st) {
+        st.deepEqual(qs.parse('foo[]&bar=baz', { allowEmptyArrays: true }), { foo: [], bar: 'baz' });
+        st.deepEqual(qs.parse('foo[]&bar=baz', { allowEmptyArrays: false }), { foo: [''], bar: 'baz' });
+
+        st.end();
+    });
+
+    t.test('throws when allowEmptyArrays is not of type boolean', function (st) {
+        st['throws'](
+            function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: 'foobar' }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: 0 }); },
+            TypeError
+        );
+        st['throws'](
+            function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: NaN }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: null }); },
+            TypeError
+        );
+
+        st.end();
+    });
+
+    t.test('allowEmptyArrays + strictNullHandling', function (st) {
+        st.deepEqual(
+            qs.parse('testEmptyArray[]', { strictNullHandling: true, allowEmptyArrays: true }),
+            { testEmptyArray: [] }
+        );
+
+        st.end();
+    });
+
+    t.deepEqual(qs.parse('a[b]=c'), { a: { b: 'c' } }, 'parses a single nested string');
+    t.deepEqual(qs.parse('a[b][c]=d'), { a: { b: { c: 'd' } } }, 'parses a double nested string');
+    t.deepEqual(
+        qs.parse('a[b][c][d][e][f][g][h]=i'),
+        { a: { b: { c: { d: { e: { f: { '[g][h]': 'i' } } } } } } },
+        'defaults to a depth of 5'
+    );
+
+    t.test('only parses one level when depth = 1', function (st) {
+        st.deepEqual(qs.parse('a[b][c]=d', { depth: 1 }), { a: { b: { '[c]': 'd' } } });
+        st.deepEqual(qs.parse('a[b][c][d]=e', { depth: 1 }), { a: { b: { '[c][d]': 'e' } } });
+        st.end();
+    });
+
+    t.test('uses original key when depth = 0', function (st) {
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { 'a[0]': 'b', 'a[1]': 'c' });
+        st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: 0 }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
+        st.deepEqual(qs.parse('a.b=c', { depth: 0, allowDots: true }), { 'a[b]': 'c' }, 'normalizes dots before applying depth-0 behavior');
+        st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {}, 'respects prototype guard at depth 0');
+        st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' }, 'allows prototypes at depth 0 when enabled');
+        st.end();
+    });
+
+    t.test('ignores prototype keys when depth = 0 and allowPrototypes is false', function (st) {
+        st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {});
+        st.deepEqual(qs.parse('hasOwnProperty=bar', { depth: 0 }), {});
+        st.deepEqual(qs.parse('toString=foo&a=b', { depth: 0 }), { a: 'b' });
+        st.end();
+    });
+
+    t.test('allows prototype keys when depth = 0 and allowPrototypes is true', function (st) {
+        st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' });
+        st.end();
+    });
+
+    t.test('uses original key when depth = false', function (st) {
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: false }), { 'a[0]': 'b', 'a[1]': 'c' });
+        st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: false }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
+        st.end();
+    });
+
+    t.deepEqual(qs.parse('a=b&a=c'), { a: ['b', 'c'] }, 'parses a simple array');
+
+    t.test('parses an explicit array', function (st) {
+        st.deepEqual(qs.parse('a[]=b'), { a: ['b'] });
+        st.deepEqual(qs.parse('a[]=b&a[]=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[]=b&a[]=c&a[]=d'), { a: ['b', 'c', 'd'] });
+        st.end();
+    });
+
+    t.test('parses a mix of simple and explicit arrays', function (st) {
+        st.deepEqual(qs.parse('a=b&a[]=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[]=b&a=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[0]=b&a=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b&a[0]=c'), { a: ['b', 'c'] });
+
+        st.deepEqual(qs.parse('a[1]=b&a=c', { arrayLimit: 20 }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[]=b&a=c', { arrayLimit: 0 }), { a: { 0: 'b', 1: 'c' } });
+        st.deepEqual(qs.parse('a[]=b&a=c'), { a: ['b', 'c'] });
+
+        st.deepEqual(qs.parse('a=b&a[1]=c', { arrayLimit: 20 }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b&a[]=c', { arrayLimit: 0 }), { a: { 0: 'b', 1: 'c' } });
+        st.deepEqual(qs.parse('a=b&a[]=c'), { a: ['b', 'c'] });
+
+        st.end();
+    });
+
+    t.test('parses a nested array', function (st) {
+        st.deepEqual(qs.parse('a[b][]=c&a[b][]=d'), { a: { b: ['c', 'd'] } });
+        st.deepEqual(qs.parse('a[>=]=25'), { a: { '>=': '25' } });
+        st.end();
+    });
+
+    t.test('parses keys with literal [] inside a bracket group (#493)', function (st) {
+        // A bracket pair inside a bracket group should be treated literally as part of the key
+        st.deepEqual(
+            qs.parse('search[withbracket[]]=foobar'),
+            { search: { 'withbracket[]': 'foobar' } },
+            'treats inner [] literally when inside a bracket group'
+        );
+
+        // Single-level variant
+        st.deepEqual(
+            qs.parse('a[b[]]=c'),
+            { a: { 'b[]': 'c' } },
+            'keeps "b[]" as a literal key'
+        );
+
+        // Nested with an array push on the outer level
+        st.deepEqual(
+            qs.parse('list[][x[]]=y'),
+            { list: [{ 'x[]': 'y' }] },
+            'preserves inner [] while still treating outer [] as array push'
+        );
+
+        // Multiple nested bracket pairs: inner [] remains literal as part of the key
+        st.deepEqual(
+            qs.parse('a[b[c[]]]=d'),
+            { a: { 'b[c[]]': 'd' } },
+            'treats "b[c[]]" as a literal key inside the bracket group'
+        );
+
+        // Depth limits with literal brackets: preserve inner [] while limiting bracket-group parsing
+        st.deepEqual(
+            qs.parse('a[b[c[]]][d]=e', { depth: 1 }),
+            { a: { 'b[c[]]': { '[d]': 'e' } } },
+            'respects depth: 1 and preserves literal inner [] in the parsed key'
+        );
+
+        // Unterminated inner bracket group is wrapped as a literal remainder segment
+        st.deepEqual(
+            qs.parse('a[[]b=c'),
+            { a: { '[[]b': 'c' } },
+            'handles unterminated inner bracket groups without throwing'
+        );
+
+        st.end();
+    });
+
+    t.test('allows to specify array indices', function (st) {
+        st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
+        st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[1]=c', { arrayLimit: 20 }), { a: ['c'] });
+        st.deepEqual(qs.parse('a[1]=c', { arrayLimit: 0 }), { a: { 1: 'c' } });
+        st.deepEqual(qs.parse('a[1]=c'), { a: ['c'] });
+        st.end();
+    });
+
+    t.test('limits specific array indices to arrayLimit', function (st) {
+        st.deepEqual(qs.parse('a[19]=a', { arrayLimit: 20 }), { a: ['a'] });
+        st.deepEqual(qs.parse('a[20]=a', { arrayLimit: 20 }), { a: { 20: 'a' } });
+
+        st.deepEqual(qs.parse('a[19]=a'), { a: ['a'] });
+        st.deepEqual(qs.parse('a[20]=a'), { a: { 20: 'a' } });
+        st.end();
+    });
+
+    t.deepEqual(qs.parse('a[12b]=c'), { a: { '12b': 'c' } }, 'supports keys that begin with a number');
+
+    t.test('supports encoded = signs', function (st) {
+        st.deepEqual(qs.parse('he%3Dllo=th%3Dere'), { 'he=llo': 'th=ere' });
+        st.end();
+    });
+
+    t.test('is ok with url encoded strings', function (st) {
+        st.deepEqual(qs.parse('a[b%20c]=d'), { a: { 'b c': 'd' } });
+        st.deepEqual(qs.parse('a[b]=c%20d'), { a: { b: 'c d' } });
+        st.end();
+    });
+
+    t.test('allows brackets in the value', function (st) {
+        st.deepEqual(qs.parse('pets=["tobi"]'), { pets: '["tobi"]' });
+        st.deepEqual(qs.parse('operators=[">=", "<="]'), { operators: '[">=", "<="]' });
+        st.end();
+    });
+
+    t.test('allows empty values', function (st) {
+        st.deepEqual(qs.parse(''), {});
+        st.deepEqual(qs.parse(null), {});
+        st.deepEqual(qs.parse(undefined), {});
+        st.end();
+    });
+
+    t.test('transforms arrays to objects', function (st) {
+        st.deepEqual(qs.parse('foo[0]=bar&foo[bad]=baz'), { foo: { 0: 'bar', bad: 'baz' } });
+        st.deepEqual(qs.parse('foo[bad]=baz&foo[0]=bar'), { foo: { bad: 'baz', 0: 'bar' } });
+        st.deepEqual(qs.parse('foo[bad]=baz&foo[]=bar'), { foo: { bad: 'baz', 0: 'bar' } });
+        st.deepEqual(qs.parse('foo[]=bar&foo[bad]=baz'), { foo: { 0: 'bar', bad: 'baz' } });
+        st.deepEqual(qs.parse('foo[bad]=baz&foo[]=bar&foo[]=foo'), { foo: { bad: 'baz', 0: 'bar', 1: 'foo' } });
+        st.deepEqual(qs.parse('foo[0][a]=a&foo[0][b]=b&foo[1][a]=aa&foo[1][b]=bb'), { foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] });
+
+        st.deepEqual(qs.parse('a[]=b&a[t]=u&a[hasOwnProperty]=c', { allowPrototypes: false }), { a: { 0: 'b', t: 'u' } });
+        st.deepEqual(qs.parse('a[]=b&a[t]=u&a[hasOwnProperty]=c', { allowPrototypes: true }), { a: { 0: 'b', t: 'u', hasOwnProperty: 'c' } });
+        st.deepEqual(qs.parse('a[]=b&a[hasOwnProperty]=c&a[x]=y', { allowPrototypes: false }), { a: { 0: 'b', x: 'y' } });
+        st.deepEqual(qs.parse('a[]=b&a[hasOwnProperty]=c&a[x]=y', { allowPrototypes: true }), { a: { 0: 'b', hasOwnProperty: 'c', x: 'y' } });
+        st.end();
+    });
+
+    t.test('transforms arrays to objects (dot notation)', function (st) {
+        st.deepEqual(qs.parse('foo[0].baz=bar&fool.bad=baz', { allowDots: true }), { foo: [{ baz: 'bar' }], fool: { bad: 'baz' } });
+        st.deepEqual(qs.parse('foo[0].baz=bar&fool.bad.boo=baz', { allowDots: true }), { foo: [{ baz: 'bar' }], fool: { bad: { boo: 'baz' } } });
+        st.deepEqual(qs.parse('foo[0][0].baz=bar&fool.bad=baz', { allowDots: true }), { foo: [[{ baz: 'bar' }]], fool: { bad: 'baz' } });
+        st.deepEqual(qs.parse('foo[0].baz[0]=15&foo[0].bar=2', { allowDots: true }), { foo: [{ baz: ['15'], bar: '2' }] });
+        st.deepEqual(qs.parse('foo[0].baz[0]=15&foo[0].baz[1]=16&foo[0].bar=2', { allowDots: true }), { foo: [{ baz: ['15', '16'], bar: '2' }] });
+        st.deepEqual(qs.parse('foo.bad=baz&foo[0]=bar', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar' } });
+        st.deepEqual(qs.parse('foo.bad=baz&foo[]=bar', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar' } });
+        st.deepEqual(qs.parse('foo[]=bar&foo.bad=baz', { allowDots: true }), { foo: { 0: 'bar', bad: 'baz' } });
+        st.deepEqual(qs.parse('foo.bad=baz&foo[]=bar&foo[]=foo', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar', 1: 'foo' } });
+        st.deepEqual(qs.parse('foo[0].a=a&foo[0].b=b&foo[1].a=aa&foo[1].b=bb', { allowDots: true }), { foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] });
+        st.end();
+    });
+
+    t.test('correctly prunes undefined values when converting an array to an object', function (st) {
+        st.deepEqual(qs.parse('a[2]=b&a[99999999]=c'), { a: { 2: 'b', 99999999: 'c' } });
+        st.end();
+    });
+
+    t.test('supports malformed uri characters', function (st) {
+        st.deepEqual(qs.parse('{%:%}', { strictNullHandling: true }), { '{%:%}': null });
+        st.deepEqual(qs.parse('{%:%}='), { '{%:%}': '' });
+        st.deepEqual(qs.parse('foo=%:%}'), { foo: '%:%}' });
+        st.end();
+    });
+
+    t.test('doesn\'t produce empty keys', function (st) {
+        st.deepEqual(qs.parse('_r=1&'), { _r: '1' });
+        st.end();
+    });
+
+    t.test('cannot access Object prototype', function (st) {
+        qs.parse('constructor[prototype][bad]=bad');
+        qs.parse('bad[constructor][prototype][bad]=bad');
+        st.equal(typeof Object.prototype.bad, 'undefined');
+        st.end();
+    });
+
+    t.test('parses arrays of objects', function (st) {
+        st.deepEqual(qs.parse('a[][b]=c'), { a: [{ b: 'c' }] });
+        st.deepEqual(qs.parse('a[0][b]=c'), { a: [{ b: 'c' }] });
+        st.end();
+    });
+
+    t.test('allows for empty strings in arrays', function (st) {
+        st.deepEqual(qs.parse('a[]=b&a[]=&a[]=c'), { a: ['b', '', 'c'] });
+
+        st.deepEqual(
+            qs.parse('a[0]=b&a[1]&a[2]=c&a[19]=', { strictNullHandling: true, arrayLimit: 20 }),
+            { a: ['b', null, 'c', ''] },
+            'with arrayLimit 20 + array indices: null then empty string works'
+        );
+        st.deepEqual(
+            qs.parse('a[]=b&a[]&a[]=c&a[]=', { strictNullHandling: true, arrayLimit: 0 }),
+            { a: { 0: 'b', 1: null, 2: 'c', 3: '' } },
+            'with arrayLimit 0 + array brackets: null then empty string works'
+        );
+
+        st.deepEqual(
+            qs.parse('a[0]=b&a[1]=&a[2]=c&a[19]', { strictNullHandling: true, arrayLimit: 20 }),
+            { a: ['b', '', 'c', null] },
+            'with arrayLimit 20 + array indices: empty string then null works'
+        );
+        st.deepEqual(
+            qs.parse('a[]=b&a[]=&a[]=c&a[]', { strictNullHandling: true, arrayLimit: 0 }),
+            { a: { 0: 'b', 1: '', 2: 'c', 3: null } },
+            'with arrayLimit 0 + array brackets: empty string then null works'
+        );
+
+        st.deepEqual(
+            qs.parse('a[]=&a[]=b&a[]=c'),
+            { a: ['', 'b', 'c'] },
+            'array brackets: empty strings work'
+        );
+        st.end();
+    });
+
+    t.test('compacts sparse arrays', function (st) {
+        st.deepEqual(qs.parse('a[10]=1&a[2]=2', { arrayLimit: 20 }), { a: ['2', '1'] });
+        st.deepEqual(qs.parse('a[1][b][2][c]=1', { arrayLimit: 20 }), { a: [{ b: [{ c: '1' }] }] });
+        st.deepEqual(qs.parse('a[1][2][3][c]=1', { arrayLimit: 20 }), { a: [[[{ c: '1' }]]] });
+        st.deepEqual(qs.parse('a[1][2][3][c][1]=1', { arrayLimit: 20 }), { a: [[[{ c: ['1'] }]]] });
+        st.end();
+    });
+
+    t.test('parses sparse arrays', function (st) {
+        /* eslint no-sparse-arrays: 0 */
+        st.deepEqual(qs.parse('a[4]=1&a[1]=2', { allowSparse: true }), { a: [, '2', , , '1'] });
+        st.deepEqual(qs.parse('a[1][b][2][c]=1', { allowSparse: true }), { a: [, { b: [, , { c: '1' }] }] });
+        st.deepEqual(qs.parse('a[1][2][3][c]=1', { allowSparse: true }), { a: [, [, , [, , , { c: '1' }]]] });
+        st.deepEqual(qs.parse('a[1][2][3][c][1]=1', { allowSparse: true }), { a: [, [, , [, , , { c: [, '1'] }]]] });
+        st.end();
+    });
+
+    t.test('parses semi-parsed strings', function (st) {
+        st.deepEqual(qs.parse({ 'a[b]': 'c' }), { a: { b: 'c' } });
+        st.deepEqual(qs.parse({ 'a[b]': 'c', 'a[d]': 'e' }), { a: { b: 'c', d: 'e' } });
+        st.end();
+    });
+
+    t.test('parses buffers correctly', function (st) {
+        var b = SaferBuffer.from('test');
+        st.deepEqual(qs.parse({ a: b }), { a: b });
+        st.end();
+    });
+
+    t.test('parses jquery-param strings', function (st) {
+        // readable = 'filter[0][]=int1&filter[0][]==&filter[0][]=77&filter[]=and&filter[2][]=int2&filter[2][]==&filter[2][]=8'
+        var encoded = 'filter%5B0%5D%5B%5D=int1&filter%5B0%5D%5B%5D=%3D&filter%5B0%5D%5B%5D=77&filter%5B%5D=and&filter%5B2%5D%5B%5D=int2&filter%5B2%5D%5B%5D=%3D&filter%5B2%5D%5B%5D=8';
+        var expected = { filter: [['int1', '=', '77'], 'and', ['int2', '=', '8']] };
+        st.deepEqual(qs.parse(encoded), expected);
+        st.end();
+    });
+
+    t.test('continues parsing when no parent is found', function (st) {
+        st.deepEqual(qs.parse('[]=&a=b'), { 0: '', a: 'b' });
+        st.deepEqual(qs.parse('[]&a=b', { strictNullHandling: true }), { 0: null, a: 'b' });
+        st.deepEqual(qs.parse('[foo]=bar'), { foo: 'bar' });
+        st.end();
+    });
+
+    t.test('does not error when parsing a very long array', function (st) {
+        var str = 'a[]=a';
+        while (Buffer.byteLength(str) < 128 * 1024) {
+            str = str + '&' + str;
+        }
+
+        st.doesNotThrow(function () {
+            qs.parse(str);
+        });
+
+        st.end();
+    });
+
+    t.test('does not throw when a native prototype has an enumerable property', function (st) {
+        st.intercept(Object.prototype, 'crash', { value: '' });
+        st.intercept(Array.prototype, 'crash', { value: '' });
+
+        st.doesNotThrow(qs.parse.bind(null, 'a=b'));
+        st.deepEqual(qs.parse('a=b'), { a: 'b' });
+        st.doesNotThrow(qs.parse.bind(null, 'a[][b]=c'));
+        st.deepEqual(qs.parse('a[][b]=c'), { a: [{ b: 'c' }] });
+
+        st.end();
+    });
+
+    t.test('parses a string with an alternative string delimiter', function (st) {
+        st.deepEqual(qs.parse('a=b;c=d', { delimiter: ';' }), { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('parses a string with an alternative RegExp delimiter', function (st) {
+        st.deepEqual(qs.parse('a=b; c=d', { delimiter: /[;,] */ }), { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('does not use non-splittable objects as delimiters', function (st) {
+        st.deepEqual(qs.parse('a=b&c=d', { delimiter: true }), { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('allows overriding parameter limit', function (st) {
+        st.deepEqual(qs.parse('a=b&c=d', { parameterLimit: 1 }), { a: 'b' });
+        st.end();
+    });
+
+    t.test('allows setting the parameter limit to Infinity', function (st) {
+        st.deepEqual(qs.parse('a=b&c=d', { parameterLimit: Infinity }), { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('allows overriding array limit', function (st) {
+        st.deepEqual(qs.parse('a[0]=b', { arrayLimit: -1 }), { a: { 0: 'b' } });
+        st.deepEqual(qs.parse('a[0]=b', { arrayLimit: 0 }), { a: { 0: 'b' } });
+
+        st.deepEqual(qs.parse('a[-1]=b', { arrayLimit: -1 }), { a: { '-1': 'b' } });
+        st.deepEqual(qs.parse('a[-1]=b', { arrayLimit: 0 }), { a: { '-1': 'b' } });
+
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayLimit: -1 }), { a: { 0: 'b', 1: 'c' } });
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayLimit: 0 }), { a: { 0: 'b', 1: 'c' } });
+
+        st.end();
+    });
+
+    t.test('allows disabling array parsing', function (st) {
+        var indices = qs.parse('a[0]=b&a[1]=c', { parseArrays: false });
+        st.deepEqual(indices, { a: { 0: 'b', 1: 'c' } });
+        st.equal(Array.isArray(indices.a), false, 'parseArrays:false, indices case is not an array');
+
+        var emptyBrackets = qs.parse('a[]=b', { parseArrays: false });
+        st.deepEqual(emptyBrackets, { a: { 0: 'b' } });
+        st.equal(Array.isArray(emptyBrackets.a), false, 'parseArrays:false, empty brackets case is not an array');
+
+        st.end();
+    });
+
+    t.test('allows for query string prefix', function (st) {
+        st.deepEqual(qs.parse('?foo=bar', { ignoreQueryPrefix: true }), { foo: 'bar' });
+        st.deepEqual(qs.parse('foo=bar', { ignoreQueryPrefix: true }), { foo: 'bar' });
+        st.deepEqual(qs.parse('?foo=bar', { ignoreQueryPrefix: false }), { '?foo': 'bar' });
+
+        st.end();
+    });
+
+    t.test('parses an object', function (st) {
+        var input = {
+            'user[name]': { 'pop[bob]': 3 },
+            'user[email]': null
+        };
+
+        var expected = {
+            user: {
+                name: { 'pop[bob]': 3 },
+                email: null
+            }
+        };
+
+        var result = qs.parse(input);
+
+        st.deepEqual(result, expected);
+        st.end();
+    });
+
+    t.test('parses string with comma as array divider', function (st) {
+        st.deepEqual(qs.parse('foo=bar,tee', { comma: true }), { foo: ['bar', 'tee'] });
+        st.deepEqual(qs.parse('foo[bar]=coffee,tee', { comma: true }), { foo: { bar: ['coffee', 'tee'] } });
+        st.deepEqual(qs.parse('foo=', { comma: true }), { foo: '' });
+        st.deepEqual(qs.parse('foo', { comma: true }), { foo: '' });
+        st.deepEqual(qs.parse('foo', { comma: true, strictNullHandling: true }), { foo: null });
+
+        // test cases inversed from from stringify tests
+        st.deepEqual(qs.parse('a[0]=c'), { a: ['c'] });
+        st.deepEqual(qs.parse('a[]=c'), { a: ['c'] });
+        st.deepEqual(qs.parse('a[]=c', { comma: true }), { a: ['c'] });
+
+        st.deepEqual(qs.parse('a[0]=c&a[1]=d'), { a: ['c', 'd'] });
+        st.deepEqual(qs.parse('a[]=c&a[]=d'), { a: ['c', 'd'] });
+        st.deepEqual(qs.parse('a=c,d', { comma: true }), { a: ['c', 'd'] });
+
+        st.end();
+    });
+
+    t.test('parses values with comma as array divider', function (st) {
+        st.deepEqual(qs.parse({ foo: 'bar,tee' }, { comma: false }), { foo: 'bar,tee' });
+        st.deepEqual(qs.parse({ foo: 'bar,tee' }, { comma: true }), { foo: ['bar', 'tee'] });
+        st.end();
+    });
+
+    t.test('use number decoder, parses string that has one number with comma option enabled', function (st) {
+        var decoder = function (str, defaultDecoder, charset, type) {
+            if (!isNaN(Number(str))) {
+                return parseFloat(str);
+            }
+            return defaultDecoder(str, defaultDecoder, charset, type);
+        };
+
+        st.deepEqual(qs.parse('foo=1', { comma: true, decoder: decoder }), { foo: 1 });
+        st.deepEqual(qs.parse('foo=0', { comma: true, decoder: decoder }), { foo: 0 });
+
+        st.end();
+    });
+
+    t.test('parses brackets holds array of arrays when having two parts of strings with comma as array divider', function (st) {
+        st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=4,5,6', { comma: true }), { foo: [['1', '2', '3'], ['4', '5', '6']] });
+        st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=', { comma: true }), { foo: [['1', '2', '3'], ''] });
+        st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=,', { comma: true }), { foo: [['1', '2', '3'], ['', '']] });
+        st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=a', { comma: true }), { foo: [['1', '2', '3'], 'a'] });
+
+        st.end();
+    });
+
+    t.test('parses url-encoded brackets holds array of arrays when having two parts of strings with comma as array divider', function (st) {
+        st.deepEqual(qs.parse('foo%5B%5D=1,2,3&foo%5B%5D=4,5,6', { comma: true }), { foo: [['1', '2', '3'], ['4', '5', '6']] });
+        st.deepEqual(qs.parse('foo%5B%5D=1,2,3&foo%5B%5D=', { comma: true }), { foo: [['1', '2', '3'], ''] });
+        st.deepEqual(qs.parse('foo%5B%5D=1,2,3&foo%5B%5D=,', { comma: true }), { foo: [['1', '2', '3'], ['', '']] });
+        st.deepEqual(qs.parse('foo%5B%5D=1,2,3&foo%5B%5D=a', { comma: true }), { foo: [['1', '2', '3'], 'a'] });
+
+        st.end();
+    });
+
+    t.test('parses comma delimited array while having percent-encoded comma treated as normal text', function (st) {
+        st.deepEqual(qs.parse('foo=a%2Cb', { comma: true }), { foo: 'a,b' });
+        st.deepEqual(qs.parse('foo=a%2C%20b,d', { comma: true }), { foo: ['a, b', 'd'] });
+        st.deepEqual(qs.parse('foo=a%2C%20b,c%2C%20d', { comma: true }), { foo: ['a, b', 'c, d'] });
+
+        st.end();
+    });
+
+    t.test('parses an object in dot notation', function (st) {
+        var input = {
+            'user.name': { 'pop[bob]': 3 },
+            'user.email.': null
+        };
+
+        var expected = {
+            user: {
+                name: { 'pop[bob]': 3 },
+                email: null
+            }
+        };
+
+        var result = qs.parse(input, { allowDots: true });
+
+        st.deepEqual(result, expected);
+        st.end();
+    });
+
+    t.test('parses an object and not child values', function (st) {
+        var input = {
+            'user[name]': { 'pop[bob]': { test: 3 } },
+            'user[email]': null
+        };
+
+        var expected = {
+            user: {
+                name: { 'pop[bob]': { test: 3 } },
+                email: null
+            }
+        };
+
+        var result = qs.parse(input);
+
+        st.deepEqual(result, expected);
+        st.end();
+    });
+
+    t.test('does not blow up when Buffer global is missing', function (st) {
+        var restore = mockProperty(global, 'Buffer', { 'delete': true });
+
+        var result = qs.parse('a=b&c=d');
+
+        restore();
+
+        st.deepEqual(result, { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('does not crash when parsing circular references', function (st) {
+        var a = {};
+        a.b = a;
+
+        var parsed;
+
+        st.doesNotThrow(function () {
+            parsed = qs.parse({ 'foo[bar]': 'baz', 'foo[baz]': a });
+        });
+
+        st.equal('foo' in parsed, true, 'parsed has "foo" property');
+        st.equal('bar' in parsed.foo, true);
+        st.equal('baz' in parsed.foo, true);
+        st.equal(parsed.foo.bar, 'baz');
+        st.deepEqual(parsed.foo.baz, a);
+        st.end();
+    });
+
+    t.test('does not crash when parsing deep objects', function (st) {
+        var parsed;
+        var str = 'foo';
+
+        for (var i = 0; i < 5000; i++) {
+            str += '[p]';
+        }
+
+        str += '=bar';
+
+        st.doesNotThrow(function () {
+            parsed = qs.parse(str, { depth: 5000 });
+        });
+
+        st.equal('foo' in parsed, true, 'parsed has "foo" property');
+
+        var depth = 0;
+        var ref = parsed.foo;
+        while ((ref = ref.p)) {
+            depth += 1;
+        }
+
+        st.equal(depth, 5000, 'parsed is 5000 properties deep');
+
+        st.end();
+    });
+
+    t.test('parses null objects correctly', { skip: !hasProto }, function (st) {
+        var a = { __proto__: null, b: 'c' };
+
+        st.deepEqual(qs.parse(a), { b: 'c' });
+        var result = qs.parse({ a: a });
+        st.equal('a' in result, true, 'result has "a" property');
+        st.deepEqual(result.a, a);
+        st.end();
+    });
+
+    t.test('parses dates correctly', function (st) {
+        var now = new Date();
+        st.deepEqual(qs.parse({ a: now }), { a: now });
+        st.end();
+    });
+
+    t.test('parses regular expressions correctly', function (st) {
+        var re = /^test$/;
+        st.deepEqual(qs.parse({ a: re }), { a: re });
+        st.end();
+    });
+
+    t.test('does not allow overwriting prototype properties', function (st) {
+        st.deepEqual(qs.parse('a[hasOwnProperty]=b', { allowPrototypes: false }), {});
+        st.deepEqual(qs.parse('hasOwnProperty=b', { allowPrototypes: false }), {});
+
+        st.deepEqual(
+            qs.parse('toString', { allowPrototypes: false }),
+            {},
+            'bare "toString" results in {}'
+        );
+
+        st.end();
+    });
+
+    t.test('can allow overwriting prototype properties', function (st) {
+        st.deepEqual(qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true }), { a: { hasOwnProperty: 'b' } });
+        st.deepEqual(qs.parse('hasOwnProperty=b', { allowPrototypes: true }), { hasOwnProperty: 'b' });
+
+        st.deepEqual(
+            qs.parse('toString', { allowPrototypes: true }),
+            { toString: '' },
+            'bare "toString" results in { toString: "" }'
+        );
+
+        st.end();
+    });
+
+    t.test('does not crash when the global Object prototype is frozen', { skip: !hasPropertyDescriptors || !hasOverrideMistake }, function (st) {
+        // We can't actually freeze the global Object prototype as that will interfere with other tests, and once an object is frozen, it
+        // can't be unfrozen. Instead, we add a new non-writable property to simulate this.
+        st.teardown(mockProperty(Object.prototype, 'frozenProp', { value: 'foo', nonWritable: true, nonEnumerable: true }));
+
+        st['throws'](
+            function () {
+                var obj = {};
+                obj.frozenProp = 'bar';
+            },
+            // node < 6 has a different error message
+            /^TypeError: Cannot assign to read only property 'frozenProp' of (?:object '#<Object>'|#<Object>)/,
+            'regular assignment of an inherited non-writable property throws'
+        );
+
+        var parsed;
+        st.doesNotThrow(
+            function () {
+                parsed = qs.parse('frozenProp', { allowPrototypes: false });
+            },
+            'parsing a nonwritable Object.prototype property does not throw'
+        );
+
+        st.deepEqual(parsed, {}, 'bare "frozenProp" results in {}');
+
+        st.end();
+    });
+
+    t.test('params starting with a closing bracket', function (st) {
+        st.deepEqual(qs.parse(']=toString'), { ']': 'toString' });
+        st.deepEqual(qs.parse(']]=toString'), { ']]': 'toString' });
+        st.deepEqual(qs.parse(']hello]=toString'), { ']hello]': 'toString' });
+        st.end();
+    });
+
+    t.test('params starting with a starting bracket', function (st) {
+        st.deepEqual(qs.parse('[=toString'), { '[': 'toString' });
+        st.deepEqual(qs.parse('[[=toString'), { '[[': 'toString' });
+        st.deepEqual(qs.parse('[hello[=toString'), { '[hello[': 'toString' });
+        st.end();
+    });
+
+    t.test('add keys to objects', function (st) {
+        st.deepEqual(
+            qs.parse('a[b]=c&a=d', { strictMerge: false }),
+            { a: { b: 'c', d: true } },
+            'can add keys to objects'
+        );
+
+        st.deepEqual(
+            qs.parse('a[b]=c&a=toString', { strictMerge: false }),
+            { a: { b: 'c' } },
+            'can not overwrite prototype'
+        );
+
+        st.deepEqual(
+            qs.parse('a[b]=c&a=toString', { strictMerge: false, allowPrototypes: true }),
+            { a: { b: 'c', toString: true } },
+            'can overwrite prototype with allowPrototypes true'
+        );
+
+        st.deepEqual(
+            qs.parse('a[b]=c&a=toString', { strictMerge: false, plainObjects: true }),
+            { __proto__: null, a: { __proto__: null, b: 'c', toString: true } },
+            'can overwrite prototype with plainObjects true'
+        );
+
+        st.end();
+    });
+
+    t.test('strictMerge wraps object and primitive into an array', function (st) {
+        st.deepEqual(
+            qs.parse('a[b]=c&a=d'),
+            { a: [{ b: 'c' }, 'd'] },
+            'object then primitive produces array'
+        );
+
+        st.deepEqual(
+            qs.parse('a=d&a[b]=c'),
+            { a: ['d', { b: 'c' }] },
+            'primitive then object produces array'
+        );
+
+        st.deepEqual(
+            qs.parse('a[b]=c&a=toString'),
+            { a: [{ b: 'c' }, 'toString'] },
+            'prototype-colliding value is preserved in array'
+        );
+
+        st.deepEqual(
+            qs.parse('a[b]=c&a=toString', { plainObjects: true }),
+            { __proto__: null, a: [{ __proto__: null, b: 'c' }, 'toString'] },
+            'plainObjects preserved in array wrapping'
+        );
+
+        st.end();
+    });
+
+    t.test('dunder proto is ignored', function (st) {
+        var payload = 'categories[__proto__]=login&categories[__proto__]&categories[length]=42';
+        var result = qs.parse(payload, { allowPrototypes: true });
+
+        st.deepEqual(
+            result,
+            {
+                categories: {
+                    length: '42'
+                }
+            },
+            'silent [[Prototype]] payload'
+        );
+
+        var plainResult = qs.parse(payload, { allowPrototypes: true, plainObjects: true });
+
+        st.deepEqual(
+            plainResult,
+            {
+                __proto__: null,
+                categories: {
+                    __proto__: null,
+                    length: '42'
+                }
+            },
+            'silent [[Prototype]] payload: plain objects'
+        );
+
+        var query = qs.parse('categories[__proto__]=cats&categories[__proto__]=dogs&categories[some][json]=toInject', { allowPrototypes: true });
+
+        st.notOk(Array.isArray(query.categories), 'is not an array');
+        st.notOk(query.categories instanceof Array, 'is not instanceof an array');
+        st.deepEqual(query.categories, { some: { json: 'toInject' } });
+        st.equal(JSON.stringify(query.categories), '{"some":{"json":"toInject"}}', 'stringifies as a non-array');
+
+        st.deepEqual(
+            qs.parse('foo[__proto__][hidden]=value&foo[bar]=stuffs', { allowPrototypes: true }),
+            {
+                foo: {
+                    bar: 'stuffs'
+                }
+            },
+            'hidden values'
+        );
+
+        st.deepEqual(
+            qs.parse('foo[__proto__][hidden]=value&foo[bar]=stuffs', { allowPrototypes: true, plainObjects: true }),
+            {
+                __proto__: null,
+                foo: {
+                    __proto__: null,
+                    bar: 'stuffs'
+                }
+            },
+            'hidden values: plain objects'
+        );
+
+        st.end();
+    });
+
+    t.test('can return null objects', { skip: !hasProto }, function (st) {
+        var expected = {
+            __proto__: null,
+            a: {
+                __proto__: null,
+                b: 'c',
+                hasOwnProperty: 'd'
+            }
+        };
+        st.deepEqual(qs.parse('a[b]=c&a[hasOwnProperty]=d', { plainObjects: true }), expected);
+        st.deepEqual(qs.parse(null, { plainObjects: true }), { __proto__: null });
+        var expectedArray = {
+            __proto__: null,
+            a: {
+                __proto__: null,
+                0: 'b',
+                c: 'd'
+            }
+        };
+        st.deepEqual(qs.parse('a[]=b&a[c]=d', { plainObjects: true }), expectedArray);
+        st.end();
+    });
+
+    t.test('can parse with custom encoding', function (st) {
+        st.deepEqual(qs.parse('%8c%a7=%91%e5%8d%e3%95%7b', {
+            decoder: function (str) {
+                var reg = /%([0-9A-F]{2})/ig;
+                var result = [];
+                var parts = reg.exec(str);
+                while (parts) {
+                    result.push(parseInt(parts[1], 16));
+                    parts = reg.exec(str);
+                }
+                return String(iconv.decode(SaferBuffer.from(result), 'shift_jis'));
+            }
+        }), { 県: '大阪府' });
+        st.end();
+    });
+
+    t.test('receives the default decoder as a second argument', function (st) {
+        st.plan(1);
+        qs.parse('a', {
+            decoder: function (str, defaultDecoder) {
+                st.equal(defaultDecoder, utils.decode);
+            }
+        });
+        st.end();
+    });
+
+    t.test('throws error with wrong decoder', function (st) {
+        st['throws'](function () {
+            qs.parse({}, { decoder: 'string' });
+        }, new TypeError('Decoder has to be a function.'));
+        st.end();
+    });
+
+    t.test('does not mutate the options argument', function (st) {
+        var options = {};
+        qs.parse('a[b]=true', options);
+        st.deepEqual(options, {});
+        st.end();
+    });
+
+    t.test('throws if an invalid charset is specified', function (st) {
+        st['throws'](function () {
+            qs.parse('a=b', { charset: 'foobar' });
+        }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'));
+        st.end();
+    });
+
+    t.test('parses an iso-8859-1 string if asked to', function (st) {
+        st.deepEqual(qs.parse('%A2=%BD', { charset: 'iso-8859-1' }), { '¢': '½' });
+        st.end();
+    });
+
+    var urlEncodedCheckmarkInUtf8 = '%E2%9C%93';
+    var urlEncodedOSlashInUtf8 = '%C3%B8';
+    var urlEncodedNumCheckmark = '%26%2310003%3B';
+    var urlEncodedNumSmiley = '%26%239786%3B';
+
+    t.test('prefers an utf-8 charset specified by the utf8 sentinel to a default charset of iso-8859-1', function (st) {
+        st.deepEqual(qs.parse('utf8=' + urlEncodedCheckmarkInUtf8 + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'iso-8859-1' }), { ø: 'ø' });
+        st.end();
+    });
+
+    t.test('prefers an iso-8859-1 charset specified by the utf8 sentinel to a default charset of utf-8', function (st) {
+        st.deepEqual(qs.parse('utf8=' + urlEncodedNumCheckmark + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'utf-8' }), { 'Ã¸': 'Ã¸' });
+        st.end();
+    });
+
+    t.test('does not require the utf8 sentinel to be defined before the parameters whose decoding it affects', function (st) {
+        st.deepEqual(qs.parse('a=' + urlEncodedOSlashInUtf8 + '&utf8=' + urlEncodedNumCheckmark, { charsetSentinel: true, charset: 'utf-8' }), { a: 'Ã¸' });
+        st.end();
+    });
+
+    t.test('ignores an utf8 sentinel with an unknown value', function (st) {
+        st.deepEqual(qs.parse('utf8=foo&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'utf-8' }), { ø: 'ø' });
+        st.end();
+    });
+
+    t.test('uses the utf8 sentinel to switch to utf-8 when no default charset is given', function (st) {
+        st.deepEqual(qs.parse('utf8=' + urlEncodedCheckmarkInUtf8 + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true }), { ø: 'ø' });
+        st.end();
+    });
+
+    t.test('uses the utf8 sentinel to switch to iso-8859-1 when no default charset is given', function (st) {
+        st.deepEqual(qs.parse('utf8=' + urlEncodedNumCheckmark + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true }), { 'Ã¸': 'Ã¸' });
+        st.end();
+    });
+
+    t.test('interprets numeric entities in iso-8859-1 when `interpretNumericEntities`', function (st) {
+        st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'iso-8859-1', interpretNumericEntities: true }), { foo: '☺' });
+        st.end();
+    });
+
+    t.test('handles a custom decoder returning `null`, in the `iso-8859-1` charset, when `interpretNumericEntities`', function (st) {
+        st.deepEqual(qs.parse('foo=&bar=' + urlEncodedNumSmiley, {
+            charset: 'iso-8859-1',
+            decoder: function (str, defaultDecoder, charset) {
+                return str ? defaultDecoder(str, defaultDecoder, charset) : null;
+            },
+            interpretNumericEntities: true
+        }), { foo: null, bar: '☺' });
+        st.end();
+    });
+
+    t.test('handles a custom decoder returning `null`, with a string key of `null`', function (st) {
+        st.deepEqual(
+            qs.parse('null=1&ToNull=2', {
+                decoder: function (str, defaultDecoder, charset) {
+                    return str === 'ToNull' ? null : defaultDecoder(str, defaultDecoder, charset);
+                }
+            }),
+            { 'null': '1' },
+            '"null" key is not overridden by `null` decoder result'
+        );
+
+        st.end();
+    });
+
+    t.test('does not interpret numeric entities in iso-8859-1 when `interpretNumericEntities` is absent', function (st) {
+        st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'iso-8859-1' }), { foo: '&#9786;' });
+        st.end();
+    });
+
+    t.test('does not interpret numeric entities when the charset is utf-8, even when `interpretNumericEntities`', function (st) {
+        st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'utf-8', interpretNumericEntities: true }), { foo: '&#9786;' });
+        st.end();
+    });
+
+    t.test('interpretNumericEntities with comma:true and iso charset does not crash', function (st) {
+        st.deepEqual(
+            qs.parse('b&a[]=1,' + urlEncodedNumSmiley, { comma: true, charset: 'iso-8859-1', interpretNumericEntities: true }),
+            { b: '', a: ['1,☺'] }
+        );
+
+        st.end();
+    });
+
+    t.test('does not interpret %uXXXX syntax in iso-8859-1 mode', function (st) {
+        st.deepEqual(qs.parse('%u263A=%u263A', { charset: 'iso-8859-1' }), { '%u263A': '%u263A' });
+        st.end();
+    });
+
+    t.test('allows for decoding keys and values differently', function (st) {
+        var decoder = function (str, defaultDecoder, charset, type) {
+            if (type === 'key') {
+                return defaultDecoder(str, defaultDecoder, charset, type).toLowerCase();
+            }
+            if (type === 'value') {
+                return defaultDecoder(str, defaultDecoder, charset, type).toUpperCase();
+            }
+            throw 'this should never happen! type: ' + type;
+        };
+
+        st.deepEqual(qs.parse('KeY=vAlUe', { decoder: decoder }), { key: 'VALUE' });
+
+        var noopDecoder = function () { return 'x'; };
+        noopDecoder();
+        st['throws'](
+            function () { decoder('x', noopDecoder, 'utf-8', 'unknown'); },
+            'this should never happen! type: unknown',
+            'decoder throws for unexpected type'
+        );
+
+        st.end();
+    });
+
+    t.test('parameter limit tests', function (st) {
+        st.test('does not throw error when within parameter limit', function (sst) {
+            var result = qs.parse('a=1&b=2&c=3', { parameterLimit: 5, throwOnLimitExceeded: true });
+            sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses without errors');
+            sst.end();
+        });
+
+        st.test('throws error when throwOnLimitExceeded is present but not boolean', function (sst) {
+            sst['throws'](
+                function () {
+                    qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: 3, throwOnLimitExceeded: 'true' });
+                },
+                new TypeError('`throwOnLimitExceeded` option must be a boolean'),
+                'throws error when throwOnLimitExceeded is present and not boolean'
+            );
+            sst.end();
+        });
+
+        st.test('throws error when parameter limit exceeded', function (sst) {
+            sst['throws'](
+                function () {
+                    qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: 3, throwOnLimitExceeded: true });
+                },
+                new RangeError('Parameter limit exceeded. Only 3 parameters allowed.'),
+                'throws error when parameter limit is exceeded'
+            );
+
+            sst['throws'](
+                function () {
+                    qs.parse('a=1&b=2', { parameterLimit: 1, throwOnLimitExceeded: true });
+                },
+                new RangeError('Parameter limit exceeded. Only 1 parameter allowed.'),
+                'throws error with singular "parameter" when parameterLimit is 1'
+            );
+            sst.end();
+        });
+
+        st.test('silently truncates when throwOnLimitExceeded is not given', function (sst) {
+            var result = qs.parse('a=1&b=2&c=3&d=4&e=5', { parameterLimit: 3 });
+            sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses and truncates silently');
+            sst.end();
+        });
+
+        st.test('silently truncates when parameter limit exceeded without error', function (sst) {
+            var result = qs.parse('a=1&b=2&c=3&d=4&e=5', { parameterLimit: 3, throwOnLimitExceeded: false });
+            sst.deepEqual(result, { a: '1', b: '2', c: '3' }, 'parses and truncates silently');
+            sst.end();
+        });
+
+        st.test('allows unlimited parameters when parameterLimit set to Infinity', function (sst) {
+            var result = qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: Infinity });
+            sst.deepEqual(result, { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6' }, 'parses all parameters without truncation');
+            sst.end();
+        });
+
+        st.test('allows unlimited parameters when parameterLimit is Infinity and throwOnLimitExceeded is true', function (sst) {
+            var result = qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: Infinity, throwOnLimitExceeded: true });
+            sst.deepEqual(result, { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6' }, 'parses all parameters without truncation or throwing');
+            sst.end();
+        });
+
+        st.end();
+    });
+
+    t.test('array limit tests', function (st) {
+        st.test('does not throw error when array is within limit', function (sst) {
+            var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 5, throwOnLimitExceeded: true });
+            sst.deepEqual(result, { a: ['1', '2', '3'] }, 'parses array without errors');
+            sst.end();
+        });
+
+        st.test('throws error when throwOnLimitExceeded is present but not boolean for array limit', function (sst) {
+            sst['throws'](
+                function () {
+                    qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3, throwOnLimitExceeded: 'true' });
+                },
+                new TypeError('`throwOnLimitExceeded` option must be a boolean'),
+                'throws error when throwOnLimitExceeded is present and not boolean for array limit'
+            );
+            sst.end();
+        });
+
+        st.test('throws error when array limit exceeded', function (sst) {
+            // 4 elements exceeds limit of 3
+            sst['throws'](
+                function () {
+                    qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3, throwOnLimitExceeded: true });
+                },
+                new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
+                'throws error when array limit is exceeded'
+            );
+            sst.end();
+        });
+
+        st.test('does not throw when at limit', function (sst) {
+            // 3 elements = limit of 3, should not throw
+            var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 3, throwOnLimitExceeded: true });
+            sst.ok(Array.isArray(result.a), 'result is an array');
+            sst.deepEqual(result.a, ['1', '2', '3'], 'all values present');
+            sst.end();
+        });
+
+        st.test('converts array to object if length is greater than limit', function (sst) {
+            var result = qs.parse('a[1]=1&a[2]=2&a[3]=3&a[4]=4&a[5]=5&a[6]=6', { arrayLimit: 5 });
+
+            sst.deepEqual(result, { a: { 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6' } }, 'parses into object if array length is greater than limit');
+            sst.end();
+        });
+
+        st.test('throws error when indexed notation exceeds arrayLimit with throwOnLimitExceeded', function (sst) {
+            sst['throws'](
+                function () {
+                    qs.parse('a[1001]=b', { arrayLimit: 1000, throwOnLimitExceeded: true });
+                },
+                new RangeError('Array limit exceeded. Only 1000 elements allowed in an array.'),
+                'throws error for a single index exceeding arrayLimit'
+            );
+
+            sst['throws'](
+                function () {
+                    qs.parse('a[0]=1&a[1]=2&a[2]=3&a[10]=4', { arrayLimit: 6, throwOnLimitExceeded: true, allowSparse: true });
+                },
+                new RangeError('Array limit exceeded. Only 6 elements allowed in an array.'),
+                'throws error when a sparse index exceeds arrayLimit'
+            );
+
+            sst['throws'](
+                function () {
+                    qs.parse('a[2]=b', { arrayLimit: 1, throwOnLimitExceeded: true });
+                },
+                new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
+                'throws error with singular "element" when arrayLimit is 1'
+            );
+
+            sst.end();
+        });
+
+        st.test('does not throw for indexed notation within arrayLimit with throwOnLimitExceeded', function (sst) {
+            var result = qs.parse('a[4]=b', { arrayLimit: 5, throwOnLimitExceeded: true, allowSparse: true });
+            sst.ok(Array.isArray(result.a), 'result is an array');
+            sst.equal(result.a.length, 5, 'array has correct length');
+            sst.equal(result.a[4], 'b', 'value at index 4 is correct');
+            sst.end();
+        });
+
+        st.test('silently converts to object for indexed notation exceeding arrayLimit without throwOnLimitExceeded', function (sst) {
+            var result = qs.parse('a[1001]=b', { arrayLimit: 1000 });
+            sst.deepEqual(result, { a: { 1001: 'b' } }, 'converts to object without throwing');
+            sst.end();
+        });
+
+        st.test('throws when duplicate bracket keys exceed arrayLimit with throwOnLimitExceeded', function (sst) {
+            sst['throws'](
+                function () {
+                    qs.parse('a[]=1&a[]=2&a[]=3&a[]=4&a[]=5&a[]=6', { arrayLimit: 5, throwOnLimitExceeded: true });
+                },
+                new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
+                'throws error when duplicate bracket notation exceeds array limit'
+            );
+            sst.end();
+        });
+
+        st.end();
+    });
+
+    t.end();
+});
+
+test('parses empty keys', function (t) {
+    emptyTestCases.forEach(function (testCase) {
+        t.test('skips empty string key with ' + testCase.input, function (st) {
+            st.deepEqual(qs.parse(testCase.input), testCase.noEmptyKeys);
+
+            st.end();
+        });
+    });
+});
+
+test('`duplicates` option', function (t) {
+    v.nonStrings.concat('not a valid option').forEach(function (invalidOption) {
+        if (typeof invalidOption !== 'undefined') {
+            t['throws'](
+                function () { qs.parse('', { duplicates: invalidOption }); },
+                TypeError,
+                'throws on invalid option: ' + inspect(invalidOption)
+            );
+        }
+    });
+
+    t.deepEqual(
+        qs.parse('foo=bar&foo=baz'),
+        { foo: ['bar', 'baz'] },
+        'duplicates: default, combine'
+    );
+
+    t.deepEqual(
+        qs.parse('foo=bar&foo=baz', { duplicates: 'combine' }),
+        { foo: ['bar', 'baz'] },
+        'duplicates: combine'
+    );
+
+    t.deepEqual(
+        qs.parse('foo=bar&foo=baz', { duplicates: 'first' }),
+        { foo: 'bar' },
+        'duplicates: first'
+    );
+
+    t.deepEqual(
+        qs.parse('foo=bar&foo=baz', { duplicates: 'last' }),
+        { foo: 'baz' },
+        'duplicates: last'
+    );
+
+    t.test('bracket notation always combines regardless of duplicates', function (st) {
+        st.deepEqual(
+            qs.parse('a=1&a=2&b[]=1&b[]=2', { duplicates: 'last' }),
+            { a: '2', b: ['1', '2'] },
+            'duplicates last: unbracketed takes last, bracketed combines'
+        );
+
+        st.deepEqual(
+            qs.parse('b[]=1&b[]=2', { duplicates: 'last' }),
+            { b: ['1', '2'] },
+            'duplicates last: bracketed always combines'
+        );
+
+        st.deepEqual(
+            qs.parse('b[]=1&b[]=2', { duplicates: 'first' }),
+            { b: ['1', '2'] },
+            'duplicates first: bracketed always combines'
+        );
+
+        st.deepEqual(
+            qs.parse('a=1&a=2&b[]=1&b[]=2', { duplicates: 'first' }),
+            { a: '1', b: ['1', '2'] },
+            'duplicates first: unbracketed takes first, bracketed combines'
+        );
+
+        st.end();
+    });
+
+    t.end();
+});
+
+test('qs strictDepth option - throw cases', function (t) {
+    t.test('throws an exception when depth exceeds the limit with strictDepth: true', function (st) {
+        st['throws'](
+            function () {
+                qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true });
+            },
+            RangeError,
+            'throws RangeError'
+        );
+        st.end();
+    });
+
+    t.test('throws an exception for multiple nested arrays with strictDepth: true', function (st) {
+        st['throws'](
+            function () {
+                qs.parse('a[0][1][2][3][4]=b', { depth: 3, strictDepth: true });
+            },
+            RangeError,
+            'throws RangeError'
+        );
+        st.end();
+    });
+
+    t.test('throws an exception for nested objects and arrays with strictDepth: true', function (st) {
+        st['throws'](
+            function () {
+                qs.parse('a[b][c][0][d][e]=f', { depth: 3, strictDepth: true });
+            },
+            RangeError,
+            'throws RangeError'
+        );
+        st.end();
+    });
+
+    t.test('throws an exception for different types of values with strictDepth: true', function (st) {
+        st['throws'](
+            function () {
+                qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 3, strictDepth: true });
+            },
+            RangeError,
+            'throws RangeError'
+        );
+        st.end();
+    });
+
+});
+
+test('qs strictDepth option - non-throw cases', function (t) {
+    t.test('when depth is 0 and strictDepth true, do not throw', function (st) {
+        st.doesNotThrow(
+            function () {
+                qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 0, strictDepth: true });
+            },
+            RangeError,
+            'does not throw RangeError'
+        );
+        st.end();
+    });
+
+    t.test('parses successfully when depth is within the limit with strictDepth: true', function (st) {
+        st.doesNotThrow(
+            function () {
+                var result = qs.parse('a[b]=c', { depth: 1, strictDepth: true });
+                st.deepEqual(result, { a: { b: 'c' } }, 'parses correctly');
+            }
+        );
+        st.end();
+    });
+
+    t.test('does not throw an exception when depth exceeds the limit with strictDepth: false', function (st) {
+        st.doesNotThrow(
+            function () {
+                var result = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
+                st.deepEqual(result, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }, 'parses with depth limit');
+            }
+        );
+        st.end();
+    });
+
+    t.test('parses successfully when depth is within the limit with strictDepth: false', function (st) {
+        st.doesNotThrow(
+            function () {
+                var result = qs.parse('a[b]=c', { depth: 1 });
+                st.deepEqual(result, { a: { b: 'c' } }, 'parses correctly');
+            }
+        );
+        st.end();
+    });
+
+    t.test('does not throw when depth is exactly at the limit with strictDepth: true', function (st) {
+        st.doesNotThrow(
+            function () {
+                var result = qs.parse('a[b][c]=d', { depth: 2, strictDepth: true });
+                st.deepEqual(result, { a: { b: { c: 'd' } } }, 'parses correctly');
+            }
+        );
+        st.end();
+    });
+});
+
+test('DOS', function (t) {
+    var arr = [];
+    for (var i = 0; i < 105; i++) {
+        arr[arr.length] = 'x';
+    }
+    var attack = 'a[]=' + arr.join('&a[]=');
+    var result = qs.parse(attack, { arrayLimit: 100 });
+
+    t.notOk(Array.isArray(result.a), 'arrayLimit is respected: result is an object, not an array');
+    t.equal(Object.keys(result.a).length, 105, 'all values are preserved');
+
+    t.end();
+});
+
+test('arrayLimit boundary conditions', function (t) {
+    // arrayLimit is the max number of elements allowed in an array
+    t.test('exactly at the limit stays as array', function (st) {
+        // 3 elements = limit of 3
+        var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 3 });
+        st.ok(Array.isArray(result.a), 'result is an array when count equals limit');
+        st.deepEqual(result.a, ['1', '2', '3'], 'all values present');
+        st.end();
+    });
+
+    t.test('one over the limit converts to object', function (st) {
+        // 4 elements exceeds limit of 3
+        var result = qs.parse('a[]=1&a[]=2&a[]=3&a[]=4', { arrayLimit: 3 });
+        st.notOk(Array.isArray(result.a), 'result is not an array when over limit');
+        st.deepEqual(result.a, { 0: '1', 1: '2', 2: '3', 3: '4' }, 'all values preserved as object');
+        st.end();
+    });
+
+    t.test('arrayLimit 1 with one value', function (st) {
+        // 1 element = limit of 1
+        var result = qs.parse('a[]=1', { arrayLimit: 1 });
+        st.ok(Array.isArray(result.a), 'result is an array when count equals limit');
+        st.deepEqual(result.a, ['1'], 'value preserved as array');
+        st.end();
+    });
+
+    t.test('arrayLimit 1 with two values converts to object', function (st) {
+        // 2 elements exceeds limit of 1
+        var result = qs.parse('a[]=1&a[]=2', { arrayLimit: 1 });
+        st.notOk(Array.isArray(result.a), 'result is not an array');
+        st.deepEqual(result.a, { 0: '1', 1: '2' }, 'all values preserved as object');
+        st.end();
+    });
+
+    t.end();
+});
+
+test('comma + arrayLimit', function (t) {
+    t.test('comma-separated values within arrayLimit stay as array', function (st) {
+        var result = qs.parse('a=1,2,3', { comma: true, arrayLimit: 5 });
+        st.ok(Array.isArray(result.a), 'result is an array');
+        st.deepEqual(result.a, ['1', '2', '3'], 'all values present');
+        st.end();
+    });
+
+    t.test('comma-separated values exceeding arrayLimit convert to object', function (st) {
+        var result = qs.parse('a=1,2,3,4', { comma: true, arrayLimit: 3 });
+        st.notOk(Array.isArray(result.a), 'result is not an array when over limit');
+        st.deepEqual(result.a, { 0: '1', 1: '2', 2: '3', 3: '4' }, 'all values preserved as object');
+        st.end();
+    });
+
+    t.test('comma-separated values exceeding arrayLimit with throwOnLimitExceeded throws', function (st) {
+        st['throws'](
+            function () {
+                qs.parse('a=1,2,3,4', { comma: true, arrayLimit: 3, throwOnLimitExceeded: true });
+            },
+            new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
+            'throws error when comma-split exceeds array limit'
+        );
+
+        st['throws'](
+            function () {
+                qs.parse('a=1,2,3', { comma: true, arrayLimit: 1, throwOnLimitExceeded: true });
+            },
+            new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
+            'throws error with singular "element" when arrayLimit is 1'
+        );
+        st.end();
+    });
+
+    t.test('comma-separated values at exactly arrayLimit stay as array', function (st) {
+        var result = qs.parse('a=1,2,3', { comma: true, arrayLimit: 3 });
+        st.ok(Array.isArray(result.a), 'result is an array when exactly at limit');
+        st.deepEqual(result.a, ['1', '2', '3'], 'all values present');
+        st.end();
+    });
+
+    t.end();
+});
+
+test('mixed array and object notation', function (t) {
+    t.test('array brackets with object key - under limit', function (st) {
+        st.deepEqual(
+            qs.parse('a[]=b&a[c]=d'),
+            { a: { 0: 'b', c: 'd' } },
+            'mixing [] and [key] converts to object'
+        );
+        st.end();
+    });
+
+    t.test('array index with object key - under limit', function (st) {
+        st.deepEqual(
+            qs.parse('a[0]=b&a[c]=d'),
+            { a: { 0: 'b', c: 'd' } },
+            'mixing [0] and [key] produces object'
+        );
+        st.end();
+    });
+
+    t.test('plain value with array brackets - under limit', function (st) {
+        st.deepEqual(
+            qs.parse('a=b&a[]=c', { arrayLimit: 20 }),
+            { a: ['b', 'c'] },
+            'plain value combined with [] stays as array under limit'
+        );
+        st.end();
+    });
+
+    t.test('array brackets with plain value - under limit', function (st) {
+        st.deepEqual(
+            qs.parse('a[]=b&a=c', { arrayLimit: 20 }),
+            { a: ['b', 'c'] },
+            '[] combined with plain value stays as array under limit'
+        );
+        st.end();
+    });
+
+    t.test('plain value with array index - under limit', function (st) {
+        st.deepEqual(
+            qs.parse('a=b&a[0]=c', { arrayLimit: 20 }),
+            { a: ['b', 'c'] },
+            'plain value combined with [0] stays as array under limit'
+        );
+        st.end();
+    });
+
+    t.test('multiple plain values with duplicates combine', function (st) {
+        st.deepEqual(
+            qs.parse('a=b&a=c&a=d', { arrayLimit: 20 }),
+            { a: ['b', 'c', 'd'] },
+            'duplicate plain keys combine into array'
+        );
+        st.end();
+    });
+
+    t.test('multiple plain values exceeding limit', function (st) {
+        // 3 elements (indices 0-2), max index 2 > limit 1
+        st.deepEqual(
+            qs.parse('a=b&a=c&a=d', { arrayLimit: 1 }),
+            { a: { 0: 'b', 1: 'c', 2: 'd' } },
+            'duplicate plain keys convert to object when exceeding limit'
+        );
+        st.end();
+    });
+
+    t.test('mixed notation produces consistent results when arrayLimit is exceeded', function (st) {
+        var expected = { a: { 0: 'b', 1: 'c', 2: 'd' } };
+
+        st.deepEqual(
+            qs.parse('a[]=b&a[1]=c&a=d', { arrayLimit: -1 }),
+            expected,
+            'arrayLimit -1'
+        );
+
+        st.deepEqual(
+            qs.parse('a[]=b&a[1]=c&a=d', { arrayLimit: 0 }),
+            expected,
+            'arrayLimit 0'
+        );
+
+        st.deepEqual(
+            qs.parse('a[]=b&a[1]=c&a=d', { arrayLimit: 1 }),
+            expected,
+            'arrayLimit 1'
+        );
+
+        st.end();
+    });
+
+    t.test('uses existing array length for currentArrayLength when parsing object input with bracket keys', function (st) {
+        var input = {};
+        var arr = ['x', 'y'];
+        arr.a = ['z', 'w'];
+        input['a[]'] = arr;
+        st.deepEqual(qs.parse(input), { a: ['x', 'y'] }, 'parses object input with bracket keys using existing array values');
+        st.end();
+    });
+
+    t.test('throws with singular message when object input bracket key exceeds arrayLimit of 1', function (st) {
+        var input = {};
+        var arr = ['x'];
+        arr.a = ['z', 'w'];
+        input['a[]'] = arr;
+        st['throws'](
+            function () {
+                qs.parse(input, { throwOnLimitExceeded: true, arrayLimit: 1 });
+            },
+            new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
+            'throws singular error for object input exceeding arrayLimit 1'
+        );
+        st.end();
+    });
+
+    t.end();
+});
Index: frontend/node_modules/qs/test/stringify.js
===================================================================
--- frontend/node_modules/qs/test/stringify.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/test/stringify.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1448 @@
+'use strict';
+
+var test = require('tape');
+var qs = require('../');
+var utils = require('../lib/utils');
+var iconv = require('iconv-lite');
+var SaferBuffer = require('safer-buffer').Buffer;
+var hasSymbols = require('has-symbols');
+var mockProperty = require('mock-property');
+var emptyTestCases = require('./empty-keys-cases').emptyTestCases;
+var hasProto = require('has-proto')();
+var hasBigInt = require('has-bigints')();
+
+test('stringify()', function (t) {
+    t.test('stringifies a querystring object', function (st) {
+        st.equal(qs.stringify({ a: 'b' }), 'a=b');
+        st.equal(qs.stringify({ a: 1 }), 'a=1');
+        st.equal(qs.stringify({ a: 1, b: 2 }), 'a=1&b=2');
+        st.equal(qs.stringify({ a: 'A_Z' }), 'a=A_Z');
+        st.equal(qs.stringify({ a: '€' }), 'a=%E2%82%AC');
+        st.equal(qs.stringify({ a: '' }), 'a=%EE%80%80');
+        st.equal(qs.stringify({ a: 'א' }), 'a=%D7%90');
+        st.equal(qs.stringify({ a: '𐐷' }), 'a=%F0%90%90%B7');
+        st.end();
+    });
+
+    t.test('stringifies falsy values', function (st) {
+        st.equal(qs.stringify(undefined), '');
+        st.equal(qs.stringify(null), '');
+        st.equal(qs.stringify(null, { strictNullHandling: true }), '');
+        st.equal(qs.stringify(false), '');
+        st.equal(qs.stringify(0), '');
+        st.end();
+    });
+
+    t.test('stringifies symbols', { skip: !hasSymbols() }, function (st) {
+        st.equal(qs.stringify(Symbol.iterator), '');
+        st.equal(qs.stringify([Symbol.iterator]), '0=Symbol%28Symbol.iterator%29');
+        st.equal(qs.stringify({ a: Symbol.iterator }), 'a=Symbol%28Symbol.iterator%29');
+        st.equal(
+            qs.stringify({ a: [Symbol.iterator] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
+            'a[]=Symbol%28Symbol.iterator%29'
+        );
+        st.end();
+    });
+
+    t.test('stringifies bigints', { skip: !hasBigInt }, function (st) {
+        var three = BigInt(3);
+        var encodeWithN = function (value, defaultEncoder, charset) {
+            var result = defaultEncoder(value, defaultEncoder, charset);
+            return typeof value === 'bigint' ? result + 'n' : result;
+        };
+        st.equal(qs.stringify(three), '');
+        st.equal(qs.stringify([three]), '0=3');
+        st.equal(qs.stringify([three], { encoder: encodeWithN }), '0=3n');
+        st.equal(qs.stringify({ a: three }), 'a=3');
+        st.equal(qs.stringify({ a: three }, { encoder: encodeWithN }), 'a=3n');
+        st.equal(
+            qs.stringify({ a: [three] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
+            'a[]=3'
+        );
+        st.equal(
+            qs.stringify({ a: [three] }, { encodeValuesOnly: true, encoder: encodeWithN, arrayFormat: 'brackets' }),
+            'a[]=3n'
+        );
+        st.end();
+    });
+
+    t.test('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function (st) {
+        st.equal(
+            qs.stringify(
+                { 'name.obj': { first: 'John', last: 'Doe' } },
+                { allowDots: false, encodeDotInKeys: false }
+            ),
+            'name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe',
+            'with allowDots false and encodeDotInKeys false'
+        );
+        st.equal(
+            qs.stringify(
+                { 'name.obj': { first: 'John', last: 'Doe' } },
+                { allowDots: true, encodeDotInKeys: false }
+            ),
+            'name.obj.first=John&name.obj.last=Doe',
+            'with allowDots true and encodeDotInKeys false'
+        );
+        st.equal(
+            qs.stringify(
+                { 'name.obj': { first: 'John', last: 'Doe' } },
+                { allowDots: false, encodeDotInKeys: true }
+            ),
+            'name%252Eobj%5Bfirst%5D=John&name%252Eobj%5Blast%5D=Doe',
+            'with allowDots false and encodeDotInKeys true'
+        );
+        st.equal(
+            qs.stringify(
+                { 'name.obj': { first: 'John', last: 'Doe' } },
+                { allowDots: true, encodeDotInKeys: true }
+            ),
+            'name%252Eobj.first=John&name%252Eobj.last=Doe',
+            'with allowDots true and encodeDotInKeys true'
+        );
+
+        st.equal(
+            qs.stringify(
+                { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
+                { allowDots: false, encodeDotInKeys: false }
+            ),
+            'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe',
+            'with allowDots false and encodeDotInKeys false'
+        );
+        st.equal(
+            qs.stringify(
+                { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
+                { allowDots: true, encodeDotInKeys: false }
+            ),
+            'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe',
+            'with allowDots false and encodeDotInKeys false'
+        );
+        st.equal(
+            qs.stringify(
+                { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
+                { allowDots: false, encodeDotInKeys: true }
+            ),
+            'name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe',
+            'with allowDots false and encodeDotInKeys true'
+        );
+        st.equal(
+            qs.stringify(
+                { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
+                { allowDots: true, encodeDotInKeys: true }
+            ),
+            'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
+            'with allowDots true and encodeDotInKeys true'
+        );
+
+        st.end();
+    });
+
+    t.test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function (st) {
+        st.equal(
+            qs.stringify(
+                { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
+                { encodeDotInKeys: true }
+            ),
+            'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
+            'with allowDots undefined and encodeDotInKeys true'
+        );
+        st.end();
+    });
+
+    t.test('should encode dot in key of object when encodeDotInKeys and allowDots is provided, and nothing else when encodeValuesOnly is provided', function (st) {
+        st.equal(
+            qs.stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, {
+                encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true
+            }),
+            'name%2Eobj.first=John&name%2Eobj.last=Doe'
+        );
+
+        st.equal(
+            qs.stringify({ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }),
+            'name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe'
+        );
+
+        st.end();
+    });
+
+    t.test('throws when `commaRoundTrip` is not a boolean', function (st) {
+        st['throws'](
+            function () { qs.stringify({}, { commaRoundTrip: 'not a boolean' }); },
+            TypeError,
+            'throws when `commaRoundTrip` is not a boolean'
+        );
+
+        st.end();
+    });
+
+    t.test('throws when `encodeDotInKeys` is not a boolean', function (st) {
+        st['throws'](
+            function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); },
+            TypeError
+        );
+
+        st.end();
+    });
+
+    t.test('adds query prefix', function (st) {
+        st.equal(qs.stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b');
+        st.end();
+    });
+
+    t.test('with query prefix, outputs blank string given an empty object', function (st) {
+        st.equal(qs.stringify({}, { addQueryPrefix: true }), '');
+        st.end();
+    });
+
+    t.test('stringifies nested falsy values', function (st) {
+        st.equal(qs.stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D=');
+        st.equal(qs.stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), 'a%5Bb%5D%5Bc%5D');
+        st.equal(qs.stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false');
+        st.end();
+    });
+
+    t.test('stringifies a nested object', function (st) {
+        st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
+        st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e');
+        st.end();
+    });
+
+    t.test('`allowDots` option: stringifies a nested object with dots notation', function (st) {
+        st.equal(qs.stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c');
+        st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e');
+        st.end();
+    });
+
+    t.test('stringifies an array value', function (st) {
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }),
+            'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }),
+            'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' }),
+            'a=b%2Cc%2Cd',
+            'comma => comma'
+        );
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true }),
+            'a=b%2Cc%2Cd',
+            'comma round trip => comma'
+        );
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }),
+            'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
+            'default => indices'
+        );
+        st.end();
+    });
+
+    t.test('`skipNulls` option', function (st) {
+        st.equal(
+            qs.stringify({ a: 'b', c: null }, { skipNulls: true }),
+            'a=b',
+            'omits nulls when asked'
+        );
+
+        st.equal(
+            qs.stringify({ a: { b: 'c', d: null } }, { skipNulls: true }),
+            'a%5Bb%5D=c',
+            'omits nested nulls when asked'
+        );
+
+        st.end();
+    });
+
+    t.test('omits array indices when asked', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d');
+
+        st.end();
+    });
+
+    t.test('omits object key/value pair when value is empty array', function (st) {
+        st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz');
+
+        st.end();
+    });
+
+    t.test('should not omit object key/value pair when value is empty array and when asked', function (st) {
+        st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz');
+        st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz');
+        st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz');
+
+        st.end();
+    });
+
+    t.test('should throw when allowEmptyArrays is not of type boolean', function (st) {
+        st['throws'](
+            function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); },
+            TypeError
+        );
+
+        st['throws'](
+            function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); },
+            TypeError
+        );
+
+        st.end();
+    });
+
+    t.test('allowEmptyArrays + strictNullHandling', function (st) {
+        st.equal(
+            qs.stringify(
+                { testEmptyArray: [] },
+                { strictNullHandling: true, allowEmptyArrays: true }
+            ),
+            'testEmptyArray[]'
+        );
+
+        st.end();
+    });
+
+    t.test('stringifies an array value with one item vs multiple items', function (st) {
+        st.test('non-array item', function (s2t) {
+            s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=c');
+            s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a=c');
+            s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c');
+            s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true }), 'a=c');
+
+            s2t.end();
+        });
+
+        st.test('array with a single item', function (s2t) {
+            s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=c');
+            s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=c');
+            s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c');
+            s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a[]=c'); // so it parses back as an array
+            s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true }), 'a[0]=c');
+
+            s2t.end();
+        });
+
+        st.test('array with multiple items', function (s2t) {
+            s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=c&a[1]=d');
+            s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=c&a[]=d');
+            s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c,d');
+            s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a=c,d');
+            s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true }), 'a[0]=c&a[1]=d');
+
+            s2t.end();
+        });
+
+        st.test('array with multiple items with a comma inside', function (s2t) {
+            s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c%2Cd,e');
+            s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' }), 'a=c%2Cd%2Ce');
+
+            s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a=c%2Cd,e');
+            s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true }), 'a=c%2Cd%2Ce');
+
+            s2t.end();
+        });
+
+        st.end();
+    });
+
+    t.test('stringifies a nested array value', function (st) {
+        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[b][0]=c&a[b][1]=d');
+        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[b][]=c&a[b][]=d');
+        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a[b]=c,d');
+        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true }), 'a[b][0]=c&a[b][1]=d');
+        st.end();
+    });
+
+    t.test('stringifies comma and empty array values', function (st) {
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' }), 'a[0]=,&a[1]=&a[2]=c,d%');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' }), 'a[]=,&a[]=&a[]=c,d%');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' }), 'a=,,,c,d%');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' }), 'a=,&a=&a=c,d%');
+
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=%2C&a[1]=&a[2]=c%2Cd%25');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=%2C&a[]=&a[]=c%2Cd%25');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=%2C,,c%2Cd%25');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=%2C&a=&a=c%2Cd%25');
+
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), 'a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }), 'a%5B%5D=%2C&a%5B%5D=&a%5B%5D=c%2Cd%25');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), 'a=%2C%2C%2Cc%2Cd%25');
+        st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), 'a=%2C&a=&a=c%2Cd%25');
+
+        st.end();
+    });
+
+    t.test('stringifies comma and empty non-array values', function (st) {
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), 'a=,&b=&c=c,d%');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), 'a=,&b=&c=c,d%');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'comma' }), 'a=,&b=&c=c,d%');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'repeat' }), 'a=,&b=&c=c,d%');
+
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=%2C&b=&c=c%2Cd%25');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a=%2C&b=&c=c%2Cd%25');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=%2C&b=&c=c%2Cd%25');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=%2C&b=&c=c%2Cd%25');
+
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), 'a=%2C&b=&c=c%2Cd%25');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }), 'a=%2C&b=&c=c%2Cd%25');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), 'a=%2C&b=&c=c%2Cd%25');
+        st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), 'a=%2C&b=&c=c%2Cd%25');
+
+        st.end();
+    });
+
+    t.test('stringifies a nested array value with dots notation', function (st) {
+        st.equal(
+            qs.stringify(
+                { a: { b: ['c', 'd'] } },
+                { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }
+            ),
+            'a.b[0]=c&a.b[1]=d',
+            'indices: stringifies with dots + indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: ['c', 'd'] } },
+                { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }
+            ),
+            'a.b[]=c&a.b[]=d',
+            'brackets: stringifies with dots + brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: ['c', 'd'] } },
+                { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }
+            ),
+            'a.b=c,d',
+            'comma: stringifies with dots + comma'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: ['c', 'd'] } },
+                { allowDots: true, encodeValuesOnly: true }
+            ),
+            'a.b[0]=c&a.b[1]=d',
+            'default: stringifies with dots + indices'
+        );
+        st.end();
+    });
+
+    t.test('stringifies an object inside an array', function (st) {
+        st.equal(
+            qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true }),
+            'a[0][b]=c',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }),
+            'a[b]=c',
+            'repeat => repeat'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }),
+            'a[][b]=c',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true }),
+            'a[0][b]=c',
+            'default => indices'
+        );
+
+        st.equal(
+            qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true }),
+            'a[0][b][c][0]=1',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }),
+            'a[b][c]=1',
+            'repeat => repeat'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }),
+            'a[][b][c][]=1',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true }),
+            'a[0][b][c][0]=1',
+            'default => indices'
+        );
+
+        st.end();
+    });
+
+    t.test('stringifies an array with mixed objects and primitives', function (st) {
+        st.equal(
+            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' }),
+            'a[0][b]=1&a[1]=2&a[2]=3',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
+            'a[][b]=1&a[]=2&a[]=3',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }),
+            '???',
+            'brackets => brackets',
+            { skip: 'TODO: figure out what this should do' }
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true }),
+            'a[0][b]=1&a[1]=2&a[2]=3',
+            'default => indices'
+        );
+
+        st.end();
+    });
+
+    t.test('stringifies an object inside an array with dots notation', function (st) {
+        st.equal(
+            qs.stringify(
+                { a: [{ b: 'c' }] },
+                { allowDots: true, encode: false, arrayFormat: 'indices' }
+            ),
+            'a[0].b=c',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [{ b: 'c' }] },
+                { allowDots: true, encode: false, arrayFormat: 'brackets' }
+            ),
+            'a[].b=c',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [{ b: 'c' }] },
+                { allowDots: true, encode: false }
+            ),
+            'a[0].b=c',
+            'default => indices'
+        );
+
+        st.equal(
+            qs.stringify(
+                { a: [{ b: { c: [1] } }] },
+                { allowDots: true, encode: false, arrayFormat: 'indices' }
+            ),
+            'a[0].b.c[0]=1',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [{ b: { c: [1] } }] },
+                { allowDots: true, encode: false, arrayFormat: 'brackets' }
+            ),
+            'a[].b.c[]=1',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [{ b: { c: [1] } }] },
+                { allowDots: true, encode: false }
+            ),
+            'a[0].b.c[0]=1',
+            'default => indices'
+        );
+
+        st.end();
+    });
+
+    t.test('does not omit object keys when indices = false', function (st) {
+        st.equal(qs.stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c');
+        st.end();
+    });
+
+    t.test('uses indices notation for arrays when indices=true', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c');
+        st.end();
+    });
+
+    t.test('uses indices notation for arrays when no arrayFormat is specified', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c');
+        st.end();
+    });
+
+    t.test('uses indices notation for arrays when arrayFormat=indices', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c');
+        st.end();
+    });
+
+    t.test('uses repeat notation for arrays when arrayFormat=repeat', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c');
+        st.end();
+    });
+
+    t.test('uses brackets notation for arrays when arrayFormat=brackets', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c');
+        st.end();
+    });
+
+    t.test('stringifies a complicated object', function (st) {
+        st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e');
+        st.end();
+    });
+
+    t.test('stringifies an empty value', function (st) {
+        st.equal(qs.stringify({ a: '' }), 'a=');
+        st.equal(qs.stringify({ a: null }, { strictNullHandling: true }), 'a');
+
+        st.equal(qs.stringify({ a: '', b: '' }), 'a=&b=');
+        st.equal(qs.stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b=');
+
+        st.equal(qs.stringify({ a: { b: '' } }), 'a%5Bb%5D=');
+        st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D');
+        st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D=');
+
+        st.end();
+    });
+
+    t.test('stringifies an empty array in different arrayFormat', function (st) {
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false }), 'b[0]=&c=c');
+        // arrayFormat default
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' }), 'b[0]=&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' }), 'b[]=&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' }), 'b=&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' }), 'b=&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', commaRoundTrip: true }), 'b[]=&c=c');
+        // with strictNullHandling
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', strictNullHandling: true }), 'b[0]&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', strictNullHandling: true }), 'b[]&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', strictNullHandling: true }), 'b&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', strictNullHandling: true }), 'b&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }), 'b[]&c=c');
+        // with skipNulls
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', skipNulls: true }), 'c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', skipNulls: true }), 'c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', skipNulls: true }), 'c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', skipNulls: true }), 'c=c');
+
+        st.end();
+    });
+
+    t.test('does not crash on null/undefined entries in arrayFormat=comma with encodeValuesOnly', function (st) {
+        st.doesNotThrow(
+            function () { qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
+            'does not pass a raw null array entry to the encoder'
+        );
+        st.doesNotThrow(
+            function () { qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
+            'does not pass a raw undefined array entry to the encoder'
+        );
+        st.doesNotThrow(
+            function () { qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
+            'does not crash on a single-null array'
+        );
+
+        st.equal(
+            qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
+            'a=,b',
+            'null entry joins as empty, comma stays unencoded under encodeValuesOnly'
+        );
+        st.equal(
+            qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
+            'a=,b',
+            'undefined entry joins as empty, comma stays unencoded under encodeValuesOnly'
+        );
+        st.equal(
+            qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
+            'a=',
+            'single-null array stringifies as empty value'
+        );
+        st.equal(
+            qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, strictNullHandling: true }),
+            'a',
+            'strictNullHandling drops the equals sign for a single-null array'
+        );
+        st.equal(
+            qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, skipNulls: true }),
+            '',
+            'skipNulls drops a single-null array entirely'
+        );
+
+        st.end();
+    });
+
+    t.test('stringifies a null object', { skip: !hasProto }, function (st) {
+        st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
+        st.end();
+    });
+
+    t.test('returns an empty string for invalid input', function (st) {
+        st.equal(qs.stringify(undefined), '');
+        st.equal(qs.stringify(false), '');
+        st.equal(qs.stringify(null), '');
+        st.equal(qs.stringify(''), '');
+        st.end();
+    });
+
+    t.test('stringifies an object with a null object as a child', { skip: !hasProto }, function (st) {
+        st.equal(qs.stringify({ a: { __proto__: null, b: 'c' } }), 'a%5Bb%5D=c');
+        st.end();
+    });
+
+    t.test('drops keys with a value of undefined', function (st) {
+        st.equal(qs.stringify({ a: undefined }), '');
+
+        st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), 'a%5Bc%5D');
+        st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), 'a%5Bc%5D=');
+        st.equal(qs.stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D=');
+        st.end();
+    });
+
+    t.test('url encodes values', function (st) {
+        st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
+        st.end();
+    });
+
+    t.test('stringifies a date', function (st) {
+        var now = new Date();
+        var str = 'a=' + encodeURIComponent(now.toISOString());
+        st.equal(qs.stringify({ a: now }), str);
+        st.end();
+    });
+
+    t.test('stringifies the weird object from qs', function (st) {
+        st.equal(qs.stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F');
+        st.end();
+    });
+
+    t.test('skips properties that are part of the object prototype', function (st) {
+        st.intercept(Object.prototype, 'crash', { value: 'test' });
+
+        st.equal(qs.stringify({ a: 'b' }), 'a=b');
+        st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
+
+        st.end();
+    });
+
+    t.test('stringifies boolean values', function (st) {
+        st.equal(qs.stringify({ a: true }), 'a=true');
+        st.equal(qs.stringify({ a: { b: true } }), 'a%5Bb%5D=true');
+        st.equal(qs.stringify({ b: false }), 'b=false');
+        st.equal(qs.stringify({ b: { c: false } }), 'b%5Bc%5D=false');
+        st.end();
+    });
+
+    t.test('stringifies buffer values', function (st) {
+        st.equal(qs.stringify({ a: SaferBuffer.from('test') }), 'a=test');
+        st.equal(qs.stringify({ a: { b: SaferBuffer.from('test') } }), 'a%5Bb%5D=test');
+        st.end();
+    });
+
+    t.test('stringifies an object using an alternative delimiter', function (st) {
+        st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
+        st.end();
+    });
+
+    t.test('does not blow up when Buffer global is missing', function (st) {
+        var restore = mockProperty(global, 'Buffer', { 'delete': true });
+
+        var result = qs.stringify({ a: 'b', c: 'd' });
+
+        restore();
+
+        st.equal(result, 'a=b&c=d');
+        st.end();
+    });
+
+    t.test('does not crash when parsing circular references', function (st) {
+        var a = {};
+        a.b = a;
+
+        st['throws'](
+            function () { qs.stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); },
+            /RangeError: Cyclic object value/,
+            'cyclic values throw'
+        );
+
+        var circular = {
+            a: 'value'
+        };
+        circular.a = circular;
+        st['throws'](
+            function () { qs.stringify(circular); },
+            /RangeError: Cyclic object value/,
+            'cyclic values throw'
+        );
+
+        var arr = ['a'];
+        st.doesNotThrow(
+            function () { qs.stringify({ x: arr, y: arr }); },
+            'non-cyclic values do not throw'
+        );
+
+        st.end();
+    });
+
+    t.test('non-circular duplicated references can still work', function (st) {
+        var hourOfDay = {
+            'function': 'hour_of_day'
+        };
+
+        var p1 = {
+            'function': 'gte',
+            arguments: [hourOfDay, 0]
+        };
+        var p2 = {
+            'function': 'lte',
+            arguments: [hourOfDay, 23]
+        };
+
+        st.equal(
+            qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }),
+            'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23'
+        );
+        st.equal(
+            qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
+            'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23'
+        );
+        st.equal(
+            qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'repeat' }),
+            'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23'
+        );
+
+        st.end();
+    });
+
+    t.test('selects properties when filter=array', function (st) {
+        st.equal(qs.stringify({ a: 'b' }, { filter: ['a'] }), 'a=b');
+        st.equal(qs.stringify({ a: 1 }, { filter: [] }), '');
+
+        st.equal(
+            qs.stringify(
+                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
+                { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }
+            ),
+            'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
+                { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }
+            ),
+            'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
+                { filter: ['a', 'b', 0, 2] }
+            ),
+            'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
+            'default => indices'
+        );
+
+        st.end();
+    });
+
+    t.test('skips null/undefined entries in filter=array', function (st) {
+        st.doesNotThrow(
+            function () { qs.stringify({ a: 'b', undefined: 'x' }, { filter: ['a', undefined] }); },
+            'does not pass a raw undefined filter entry to the encoder'
+        );
+        st.doesNotThrow(
+            function () { qs.stringify({ a: 'b', 'null': 'x' }, { filter: ['a', null] }); },
+            'does not pass a raw null filter entry to the encoder'
+        );
+
+        st.equal(
+            qs.stringify({ a: 'b', undefined: 'x', c: 'd' }, { filter: ['a', undefined, 'c'] }),
+            'a=b&c=d',
+            'undefined filter entry is skipped, remaining keys are kept'
+        );
+        st.equal(
+            qs.stringify({ a: 'b', 'null': 'x', c: 'd' }, { filter: ['a', null, 'c'] }),
+            'a=b&c=d',
+            'null filter entry is skipped, remaining keys are kept'
+        );
+        st.equal(
+            qs.stringify({ a: 'b', 'null': 'x' }, { filter: [null] }),
+            '',
+            'filter array containing only null yields empty string'
+        );
+
+        st.end();
+    });
+
+    t.test('supports custom representations when filter=function', function (st) {
+        var calls = 0;
+        var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
+        var filterFunc = function (prefix, value) {
+            calls += 1;
+            if (calls === 1) {
+                st.equal(prefix, '', 'prefix is empty');
+                st.equal(value, obj);
+            } else if (prefix === 'c') {
+                return void 0;
+            } else if (value instanceof Date) {
+                st.equal(prefix, 'e[f]');
+                return value.getTime();
+            }
+            return value;
+        };
+
+        st.equal(qs.stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000');
+        st.equal(calls, 5);
+        st.end();
+    });
+
+    t.test('can disable uri encoding', function (st) {
+        st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b');
+        st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c');
+        st.equal(qs.stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), 'a=b&c');
+        st.end();
+    });
+
+    t.test('can sort the keys', function (st) {
+        var sort = function (a, b) {
+            return a.localeCompare(b);
+        };
+        st.equal(qs.stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y');
+        st.equal(qs.stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a');
+        st.end();
+    });
+
+    t.test('can sort the keys at depth 3 or more too', function (st) {
+        var sort = function (a, b) {
+            return a.localeCompare(b);
+        };
+        st.equal(
+            qs.stringify(
+                { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
+                { sort: sort, encode: false }
+            ),
+            'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'
+        );
+        st.equal(
+            qs.stringify(
+                { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
+                { sort: null, encode: false }
+            ),
+            'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'
+        );
+        st.end();
+    });
+
+    t.test('can stringify with custom encoding', function (st) {
+        st.equal(qs.stringify({ 県: '大阪府', '': '' }, {
+            encoder: function (str) {
+                if (str.length === 0) {
+                    return '';
+                }
+                var buf = iconv.encode(str, 'shiftjis');
+                var result = [];
+                for (var i = 0; i < buf.length; ++i) {
+                    result.push(buf.readUInt8(i).toString(16));
+                }
+                return '%' + result.join('%');
+            }
+        }), '%8c%a7=%91%e5%8d%e3%95%7b&=');
+        st.end();
+    });
+
+    t.test('receives the default encoder as a second argument', function (st) {
+        st.plan(8);
+
+        qs.stringify({ a: 1, b: new Date(), c: true, d: [1] }, {
+            encoder: function (str) {
+                st.match(typeof str, /^(?:string|number|boolean)$/);
+                return '';
+            }
+        });
+
+        st.end();
+    });
+
+    t.test('receives the default encoder as a second argument', function (st) {
+        st.plan(2);
+
+        qs.stringify({ a: 1 }, {
+            encoder: function (str, defaultEncoder) {
+                st.equal(defaultEncoder, utils.encode);
+            }
+        });
+
+        st.end();
+    });
+
+    t.test('throws error with wrong encoder', function (st) {
+        st['throws'](function () {
+            qs.stringify({}, { encoder: 'string' });
+        }, new TypeError('Encoder has to be a function.'));
+        st.end();
+    });
+
+    t.test('can use custom encoder for a buffer object', { skip: typeof Buffer === 'undefined' }, function (st) {
+        st.equal(qs.stringify({ a: SaferBuffer.from([1]) }, {
+            encoder: function (buffer) {
+                if (typeof buffer === 'string') {
+                    return buffer;
+                }
+                return String.fromCharCode(buffer.readUInt8(0) + 97);
+            }
+        }), 'a=b');
+
+        st.equal(qs.stringify({ a: SaferBuffer.from('a b') }, {
+            encoder: function (buffer) {
+                return buffer;
+            }
+        }), 'a=a b');
+        st.end();
+    });
+
+    t.test('serializeDate option', function (st) {
+        var date = new Date();
+        st.equal(
+            qs.stringify({ a: date }),
+            'a=' + date.toISOString().replace(/:/g, '%3A'),
+            'default is toISOString'
+        );
+
+        var mutatedDate = new Date();
+        mutatedDate.toISOString = function () {
+            throw new SyntaxError();
+        };
+        st['throws'](function () {
+            mutatedDate.toISOString();
+        }, SyntaxError);
+        st.equal(
+            qs.stringify({ a: mutatedDate }),
+            'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'),
+            'toISOString works even when method is not locally present'
+        );
+
+        var specificDate = new Date(6);
+        st.equal(
+            qs.stringify(
+                { a: specificDate },
+                { serializeDate: function (d) { return d.getTime() * 7; } }
+            ),
+            'a=42',
+            'custom serializeDate function called'
+        );
+
+        st.equal(
+            qs.stringify(
+                { a: [date] },
+                {
+                    serializeDate: function (d) { return d.getTime(); },
+                    arrayFormat: 'comma'
+                }
+            ),
+            'a=' + date.getTime(),
+            'works with arrayFormat comma'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [date] },
+                {
+                    serializeDate: function (d) { return d.getTime(); },
+                    arrayFormat: 'comma',
+                    commaRoundTrip: true
+                }
+            ),
+            'a%5B%5D=' + date.getTime(),
+            'works with arrayFormat comma'
+        );
+
+        st.end();
+    });
+
+    t.test('RFC 1738 serialization', function (st) {
+        st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC1738 }), 'a=b+c');
+        st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC1738 }), 'a+b=c+d');
+        st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }, { format: qs.formats.RFC1738 }), 'a+b=a+b');
+
+        st.equal(qs.stringify({ 'foo(ref)': 'bar' }, { format: qs.formats.RFC1738 }), 'foo(ref)=bar');
+
+        st.end();
+    });
+
+    t.test('RFC 3986 spaces serialization', function (st) {
+        st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC3986 }), 'a=b%20c');
+        st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC3986 }), 'a%20b=c%20d');
+        st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }, { format: qs.formats.RFC3986 }), 'a%20b=a%20b');
+
+        st.end();
+    });
+
+    t.test('Backward compatibility to RFC 3986', function (st) {
+        st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
+        st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }), 'a%20b=a%20b');
+
+        st.end();
+    });
+
+    t.test('Edge cases and unknown formats', function (st) {
+        ['UFO1234', false, 1234, null, {}, []].forEach(function (format) {
+            st['throws'](
+                function () {
+                    qs.stringify({ a: 'b c' }, { format: format });
+                },
+                new TypeError('Unknown format option provided.')
+            );
+        });
+        st.end();
+    });
+
+    t.test('encodeValuesOnly', function (st) {
+        st.equal(
+            qs.stringify(
+                { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
+                { encodeValuesOnly: true, arrayFormat: 'indices' }
+            ),
+            'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h',
+            'encodeValuesOnly + indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
+                { encodeValuesOnly: true, arrayFormat: 'brackets' }
+            ),
+            'a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h',
+            'encodeValuesOnly + brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
+                { encodeValuesOnly: true, arrayFormat: 'repeat' }
+            ),
+            'a=b&c=d&c=e%3Df&f=g&f=h',
+            'encodeValuesOnly + repeat'
+        );
+
+        st.equal(
+            qs.stringify(
+                { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
+                { arrayFormat: 'indices' }
+            ),
+            'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h',
+            'no encodeValuesOnly + indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
+                { arrayFormat: 'brackets' }
+            ),
+            'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h',
+            'no encodeValuesOnly + brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
+                { arrayFormat: 'repeat' }
+            ),
+            'a=b&c=d&c=e&f=g&f=h',
+            'no encodeValuesOnly + repeat'
+        );
+
+        st.end();
+    });
+
+    t.test('encodeValuesOnly - strictNullHandling', function (st) {
+        st.equal(
+            qs.stringify(
+                { a: { b: null } },
+                { encodeValuesOnly: true, strictNullHandling: true }
+            ),
+            'a[b]'
+        );
+        st.end();
+    });
+
+    t.test('strictNullHandling: applies the formatter to the encoded key (RFC1738)', function (st) {
+        st.equal(
+            qs.stringify(
+                { 'a b': null, 'c d': 'e f' },
+                { strictNullHandling: false, format: 'RFC1738' }
+            ),
+            'a+b=&c+d=e+f',
+            'without: as expected'
+        );
+
+        st.equal(
+            qs.stringify(
+                { 'a b': null, 'c d': 'e f' },
+                { strictNullHandling: true, format: 'RFC1738' }
+            ),
+            'a+b&c+d=e+f',
+            'with: as expected'
+        );
+
+        st.end();
+    });
+
+    t.test('throws if an invalid charset is specified', function (st) {
+        st['throws'](function () {
+            qs.stringify({ a: 'b' }, { charset: 'foobar' });
+        }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'));
+        st.end();
+    });
+
+    t.test('respects a charset of iso-8859-1', function (st) {
+        st.equal(qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' }), '%E6=%E6');
+        st.end();
+    });
+
+    t.test('encodes unrepresentable chars as numeric entities in iso-8859-1 mode', function (st) {
+        st.equal(qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' }), 'a=%26%239786%3B');
+        st.end();
+    });
+
+    t.test('respects an explicit charset of utf-8 (the default)', function (st) {
+        st.equal(qs.stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6');
+        st.end();
+    });
+
+    t.test('`charsetSentinel` option', function (st) {
+        st.equal(
+            qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }),
+            'utf8=%E2%9C%93&a=%C3%A6',
+            'adds the right sentinel when instructed to and the charset is utf-8'
+        );
+
+        st.equal(
+            qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }),
+            'utf8=%26%2310003%3B&a=%E6',
+            'adds the right sentinel when instructed to and the charset is iso-8859-1'
+        );
+
+        st.equal(
+            qs.stringify({ a: 1, b: 2 }, { charsetSentinel: true, delimiter: ';' }),
+            'utf8=%E2%9C%93;a=1;b=2',
+            'uses the configured delimiter after the sentinel'
+        );
+
+        st.end();
+    });
+
+    t.test('does not mutate the options argument', function (st) {
+        var options = {};
+        qs.stringify({}, options);
+        st.deepEqual(options, {});
+        st.end();
+    });
+
+    t.test('strictNullHandling works with custom filter', function (st) {
+        var filter = function (prefix, value) {
+            return value;
+        };
+
+        var options = { strictNullHandling: true, filter: filter };
+        st.equal(qs.stringify({ key: null }, options), 'key');
+        st.end();
+    });
+
+    t.test('strictNullHandling works with null serializeDate', function (st) {
+        var serializeDate = function () {
+            return null;
+        };
+        var options = { strictNullHandling: true, serializeDate: serializeDate };
+        var date = new Date();
+        st.equal(qs.stringify({ key: date }, options), 'key');
+        st.end();
+    });
+
+    t.test('allows for encoding keys and values differently', function (st) {
+        var encoder = function (str, defaultEncoder, charset, type) {
+            if (type === 'key') {
+                return defaultEncoder(str, defaultEncoder, charset, type).toLowerCase();
+            }
+            if (type === 'value') {
+                return defaultEncoder(str, defaultEncoder, charset, type).toUpperCase();
+            }
+            throw 'this should never happen! type: ' + type;
+        };
+
+        st.deepEqual(qs.stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE');
+
+        var noopEncoder = function () { return 'x'; };
+        noopEncoder();
+        st['throws'](
+            function () { encoder('x', noopEncoder, 'utf-8', 'unknown'); },
+            'this should never happen! type: unknown',
+            'encoder throws for unexpected type'
+        );
+
+        st.end();
+    });
+
+    t.test('objects inside arrays', function (st) {
+        var obj = { a: { b: { c: 'd', e: 'f' } } };
+        var withArray = { a: { b: [{ c: 'd', e: 'f' }] } };
+
+        st.equal(qs.stringify(obj, { encode: false }), 'a[b][c]=d&a[b][e]=f', 'no array, no arrayFormat');
+        st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'brackets' }), 'a[b][c]=d&a[b][e]=f', 'no array, bracket');
+        st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'indices' }), 'a[b][c]=d&a[b][e]=f', 'no array, indices');
+        st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'repeat' }), 'a[b][c]=d&a[b][e]=f', 'no array, repeat');
+        st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'comma' }), 'a[b][c]=d&a[b][e]=f', 'no array, comma');
+
+        st.equal(qs.stringify(withArray, { encode: false }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, no arrayFormat');
+        st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'brackets' }), 'a[b][][c]=d&a[b][][e]=f', 'array, bracket');
+        st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'indices' }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, indices');
+        st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'repeat' }), 'a[b][c]=d&a[b][e]=f', 'array, repeat');
+        st.equal(
+            qs.stringify(withArray, { encode: false, arrayFormat: 'comma' }),
+            '???',
+            'array, comma',
+            { skip: 'TODO: figure out what this should do' }
+        );
+
+        st.end();
+    });
+
+    t.test('stringifies sparse arrays', function (st) {
+        /* eslint no-sparse-arrays: 0 */
+        st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1]=2&a[4]=1');
+        st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=2&a[]=1');
+        st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=2&a=1');
+
+        st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][b][2][c]=1');
+        st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][b][][c]=1');
+        st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[b][c]=1');
+
+        st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][2][3][c]=1');
+        st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][][][c]=1');
+        st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[c]=1');
+
+        st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][2][3][c][1]=1');
+        st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][][][c][]=1');
+        st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[c]=1');
+
+        st.end();
+    });
+
+    t.test('encodes a very long string', function (st) {
+        var chars = [];
+        var expected = [];
+        for (var i = 0; i < 5e3; i++) {
+            chars.push(' ' + i);
+
+            expected.push('%20' + i);
+        }
+
+        var obj = {
+            foo: chars.join('')
+        };
+
+        st.equal(
+            qs.stringify(obj, { arrayFormat: 'brackets', charset: 'utf-8' }),
+            'foo=' + expected.join('')
+        );
+
+        st.end();
+    });
+
+    t.end();
+});
+
+test('stringifies empty keys', function (t) {
+    emptyTestCases.forEach(function (testCase) {
+        t.test('stringifies an object with empty string key with ' + testCase.input, function (st) {
+            st.deepEqual(
+                qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'indices' }),
+                testCase.stringifyOutput.indices,
+                'test case: ' + testCase.input + ', indices'
+            );
+            st.deepEqual(
+                qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'brackets' }),
+                testCase.stringifyOutput.brackets,
+                'test case: ' + testCase.input + ', brackets'
+            );
+            st.deepEqual(
+                qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'repeat' }),
+                testCase.stringifyOutput.repeat,
+                'test case: ' + testCase.input + ', repeat'
+            );
+
+            st.end();
+        });
+    });
+
+    t.test('edge case with object/arrays', function (st) {
+        st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3');
+        st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), '[][0]=2&[][1]=3&[a]=2');
+        st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' }), '[][0]=2&[][1]=3');
+        st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' }), '[][0]=2&[][1]=3&[a]=2');
+
+        st.end();
+    });
+
+    t.test('stringifies non-string keys', function (st) {
+        var S = Object('abc');
+        S.toString = function () {
+            return 'd';
+        };
+        var actual = qs.stringify({ a: 'b', 'false': {}, 1e+22: 'c', d: 'e' }, {
+            filter: ['a', false, null, 10000000000000000000000, S],
+            allowDots: true,
+            encodeDotInKeys: true
+        });
+
+        st.equal(actual, 'a=b&1e%2B22=c&d=e', 'stringifies correctly');
+
+        st.end();
+    });
+
+    t.test('round-trips keys containing percent-encoded bracket text', function (st) {
+        var cases = [
+            { 'a%5Bb': 'c' },
+            { 'a%5Db': 'c' },
+            { 'a%255Bb': 'c' },
+            { 'a%255Db': 'c' },
+            { a: { 'b%5Bc': 'd' } },
+            { a: { 'b%255Bc': 'd' } },
+            { 'a%5B%255Bb': 'c' }
+        ];
+        for (var i = 0; i < cases.length; i++) {
+            st.deepEqual(
+                qs.parse(qs.stringify(cases[i])),
+                cases[i],
+                'round-trips ' + JSON.stringify(cases[i])
+            );
+        }
+
+        st.end();
+    });
+
+    t.test('parses input containing percent-encoded bracket text without mangling', function (st) {
+        st.deepEqual(qs.parse('a%25255Bb=c'), { 'a%255Bb': 'c' }, 'a%25255Bb decodes to a%255Bb, not a%5Bb');
+        st.deepEqual(qs.parse('a%25255Db=c'), { 'a%255Db': 'c' }, 'a%25255Db decodes to a%255Db, not a%5Db');
+        st.deepEqual(qs.parse('a%5Bb%25255Bc%5D=d'), { a: { 'b%255Bc': 'd' } }, 'nested %25255B decodes to %255B inside segment, not %5B');
+
+        st.end();
+    });
+});
Index: frontend/node_modules/qs/test/utils.js
===================================================================
--- frontend/node_modules/qs/test/utils.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/qs/test/utils.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,432 @@
+'use strict';
+
+var test = require('tape');
+var inspect = require('object-inspect');
+var SaferBuffer = require('safer-buffer').Buffer;
+var forEach = require('for-each');
+var v = require('es-value-fixtures');
+
+var utils = require('../lib/utils');
+
+test('merge()', function (t) {
+    t.deepEqual(utils.merge(null, true), [null, true], 'merges true into null');
+
+    t.deepEqual(utils.merge(null, [42]), [null, 42], 'merges null into an array');
+
+    t.deepEqual(utils.merge({ a: 'b' }, { a: 'c' }), { a: ['b', 'c'] }, 'merges two objects with the same key');
+
+    var oneMerged = utils.merge({ foo: 'bar' }, { foo: { first: '123' } });
+    t.deepEqual(oneMerged, { foo: ['bar', { first: '123' }] }, 'merges a standalone and an object into an array');
+
+    var twoMerged = utils.merge({ foo: ['bar', { first: '123' }] }, { foo: { second: '456' } });
+    t.deepEqual(twoMerged, { foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }, 'merges a standalone and two objects into an array');
+
+    var sandwiched = utils.merge({ foo: ['bar', { first: '123', second: '456' }] }, { foo: 'baz' });
+    t.deepEqual(sandwiched, { foo: ['bar', { first: '123', second: '456' }, 'baz'] }, 'merges an object sandwiched by two standalones into an array');
+
+    var nestedArrays = utils.merge({ foo: ['baz'] }, { foo: ['bar', 'xyzzy'] });
+    t.deepEqual(nestedArrays, { foo: ['baz', 'bar', 'xyzzy'] });
+
+    var noOptionsNonObjectSource = utils.merge({ foo: 'baz' }, 'bar');
+    t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true });
+
+    var func = function f() {};
+    func();
+    t.deepEqual(
+        utils.merge(func, { foo: 'bar' }),
+        [func, { foo: 'bar' }],
+        'functions can not be merged into'
+    );
+
+    func.bar = 'baz';
+    t.deepEqual(
+        utils.merge({ foo: 'bar' }, func),
+        { foo: 'bar', bar: 'baz' },
+        'functions can be merge sources'
+    );
+
+    t.test(
+        'avoids invoking array setters unnecessarily',
+        { skip: typeof Object.defineProperty !== 'function' },
+        function (st) {
+            var setCount = 0;
+            var getCount = 0;
+            var observed = [];
+            Object.defineProperty(observed, 0, {
+                get: function () {
+                    getCount += 1;
+                    return { bar: 'baz' };
+                },
+                set: function () { setCount += 1; }
+            });
+            utils.merge(observed, [null]);
+            st.equal(setCount, 0);
+            st.equal(getCount, 1);
+            observed[0] = observed[0]; // eslint-disable-line no-self-assign
+            st.equal(setCount, 1);
+            st.equal(getCount, 2);
+            st.end();
+        }
+    );
+
+    t.test('with overflow objects (from arrayLimit)', function (st) {
+        // arrayLimit is max index, so with limit 0, max index 0 is allowed (1 element)
+        // To create overflow, need 2+ elements with limit 0, or 3+ with limit 1, etc.
+        st.test('merges primitive into overflow object at next index', function (s2t) {
+            // Create an overflow object via combine: 3 elements (indices 0-2) with limit 0
+            var overflow = utils.combine(['a', 'b'], 'c', 0, false);
+            s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
+            var merged = utils.merge(overflow, 'd');
+            s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'adds primitive at next numeric index');
+            s2t.end();
+        });
+
+        st.test('merges primitive into regular object with numeric keys normally', function (s2t) {
+            var obj = { 0: 'a', 1: 'b' };
+            s2t.notOk(utils.isOverflow(obj), 'plain object is not marked as overflow');
+            var merged = utils.merge(obj, 'c');
+            s2t.deepEqual(merged, { 0: 'a', 1: 'b', c: true }, 'adds primitive as key (not at next index)');
+            s2t.end();
+        });
+
+        st.test('merges primitive into object with non-numeric keys normally', function (s2t) {
+            var obj = { foo: 'bar' };
+            var merged = utils.merge(obj, 'baz');
+            s2t.deepEqual(merged, { foo: 'bar', baz: true }, 'adds primitive as key with value true');
+            s2t.end();
+        });
+
+        st.test('with strictMerge, wraps object and primitive in array', function (s2t) {
+            var obj = { foo: 'bar' };
+            var merged = utils.merge(obj, 'baz', { strictMerge: true });
+            s2t.deepEqual(merged, [{ foo: 'bar' }, 'baz'], 'wraps in array with strictMerge');
+            s2t.end();
+        });
+
+        st.test('merges overflow object into primitive', function (s2t) {
+            // Create an overflow object via combine: 2 elements (indices 0-1) with limit 0
+            var overflow = utils.combine(['a'], 'b', 0, false);
+            s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
+            var merged = utils.merge('c', overflow);
+            s2t.ok(utils.isOverflow(merged), 'result is also marked as overflow');
+            s2t.deepEqual(merged, { 0: 'c', 1: 'a', 2: 'b' }, 'creates object with primitive at 0, source values shifted');
+            s2t.end();
+        });
+
+        st.test('merges overflow object into primitive with plainObjects', function (s2t) {
+            var overflow = utils.combine(['a'], 'b', 0, false);
+            s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
+            var merged = utils.merge('c', overflow, { plainObjects: true });
+            s2t.ok(utils.isOverflow(merged), 'result is also marked as overflow');
+            s2t.deepEqual(merged, { __proto__: null, 0: 'c', 1: 'a', 2: 'b' }, 'creates null-proto object with primitive at 0');
+            s2t.end();
+        });
+
+        st.test('merges overflow object with multiple values into primitive', function (s2t) {
+            // Create an overflow object via combine: 3 elements (indices 0-2) with limit 0
+            var overflow = utils.combine(['b', 'c'], 'd', 0, false);
+            s2t.ok(utils.isOverflow(overflow), 'overflow object is marked');
+            var merged = utils.merge('a', overflow);
+            s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'shifts all source indices by 1');
+            s2t.end();
+        });
+
+        st.test('merges regular object into primitive as array', function (s2t) {
+            var obj = { foo: 'bar' };
+            var merged = utils.merge('a', obj);
+            s2t.deepEqual(merged, ['a', { foo: 'bar' }], 'creates array with primitive and object');
+            s2t.end();
+        });
+
+        st.test('merges primitive into array that exceeds arrayLimit', function (s2t) {
+            var arr = ['a', 'b', 'c'];
+            var merged = utils.merge(arr, 'd', { arrayLimit: 1 });
+            s2t.ok(utils.isOverflow(merged), 'result is marked as overflow');
+            s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'converts to overflow object with primitive appended');
+            s2t.end();
+        });
+
+        st.test('merges array into primitive that exceeds arrayLimit', function (s2t) {
+            var merged = utils.merge('a', ['b', 'c'], { arrayLimit: 1 });
+            s2t.ok(utils.isOverflow(merged), 'result is marked as overflow');
+            s2t.deepEqual(merged, { 0: 'a', 1: 'b', 2: 'c' }, 'converts to overflow object');
+            s2t.end();
+        });
+
+        st.end();
+    });
+
+    t.end();
+});
+
+test('assign()', function (t) {
+    var target = { a: 1, b: 2 };
+    var source = { b: 3, c: 4 };
+    var result = utils.assign(target, source);
+
+    t.equal(result, target, 'returns the target');
+    t.deepEqual(target, { a: 1, b: 3, c: 4 }, 'target and source are merged');
+    t.deepEqual(source, { b: 3, c: 4 }, 'source is untouched');
+
+    t.end();
+});
+
+test('combine()', function (t) {
+    t.test('both arrays', function (st) {
+        var a = [1];
+        var b = [2];
+        var combined = utils.combine(a, b);
+
+        st.deepEqual(a, [1], 'a is not mutated');
+        st.deepEqual(b, [2], 'b is not mutated');
+        st.notEqual(a, combined, 'a !== combined');
+        st.notEqual(b, combined, 'b !== combined');
+        st.deepEqual(combined, [1, 2], 'combined is a + b');
+
+        st.end();
+    });
+
+    t.test('one array, one non-array', function (st) {
+        var aN = 1;
+        var a = [aN];
+        var bN = 2;
+        var b = [bN];
+
+        var combinedAnB = utils.combine(aN, b);
+        st.deepEqual(b, [bN], 'b is not mutated');
+        st.notEqual(aN, combinedAnB, 'aN + b !== aN');
+        st.notEqual(a, combinedAnB, 'aN + b !== a');
+        st.notEqual(bN, combinedAnB, 'aN + b !== bN');
+        st.notEqual(b, combinedAnB, 'aN + b !== b');
+        st.deepEqual([1, 2], combinedAnB, 'first argument is array-wrapped when not an array');
+
+        var combinedABn = utils.combine(a, bN);
+        st.deepEqual(a, [aN], 'a is not mutated');
+        st.notEqual(aN, combinedABn, 'a + bN !== aN');
+        st.notEqual(a, combinedABn, 'a + bN !== a');
+        st.notEqual(bN, combinedABn, 'a + bN !== bN');
+        st.notEqual(b, combinedABn, 'a + bN !== b');
+        st.deepEqual([1, 2], combinedABn, 'second argument is array-wrapped when not an array');
+
+        st.end();
+    });
+
+    t.test('neither is an array', function (st) {
+        var combined = utils.combine(1, 2);
+        st.notEqual(1, combined, '1 + 2 !== 1');
+        st.notEqual(2, combined, '1 + 2 !== 2');
+        st.deepEqual([1, 2], combined, 'both arguments are array-wrapped when not an array');
+
+        st.end();
+    });
+
+    t.test('with arrayLimit', function (st) {
+        st.test('under the limit', function (s2t) {
+            var combined = utils.combine(['a', 'b'], 'c', 10, false);
+            s2t.deepEqual(combined, ['a', 'b', 'c'], 'returns array when under limit');
+            s2t.ok(Array.isArray(combined), 'result is an array');
+            s2t.end();
+        });
+
+        st.test('exactly at the limit stays as array', function (s2t) {
+            var combined = utils.combine(['a', 'b'], 'c', 3, false);
+            s2t.deepEqual(combined, ['a', 'b', 'c'], 'stays as array when count equals limit');
+            s2t.ok(Array.isArray(combined), 'result is an array');
+            s2t.end();
+        });
+
+        st.test('over the limit', function (s2t) {
+            var combined = utils.combine(['a', 'b', 'c'], 'd', 3, false);
+            s2t.deepEqual(combined, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'converts to object when over limit');
+            s2t.notOk(Array.isArray(combined), 'result is not an array');
+            s2t.end();
+        });
+
+        st.test('with arrayLimit 1', function (s2t) {
+            var combined = utils.combine([], 'a', 1, false);
+            s2t.deepEqual(combined, ['a'], 'stays as array when count equals limit');
+            s2t.ok(Array.isArray(combined), 'result is an array');
+            s2t.end();
+        });
+
+        st.test('with arrayLimit 0 converts single element to object', function (s2t) {
+            var combined = utils.combine([], 'a', 0, false);
+            s2t.deepEqual(combined, { 0: 'a' }, 'converts to object when count exceeds limit');
+            s2t.notOk(Array.isArray(combined), 'result is not an array');
+            s2t.end();
+        });
+
+        st.test('with arrayLimit 0 and two elements converts to object', function (s2t) {
+            var combined = utils.combine(['a'], 'b', 0, false);
+            s2t.deepEqual(combined, { 0: 'a', 1: 'b' }, 'converts to object when count exceeds limit');
+            s2t.notOk(Array.isArray(combined), 'result is not an array');
+            s2t.end();
+        });
+
+        st.test('with plainObjects option', function (s2t) {
+            var combined = utils.combine(['a', 'b'], 'c', 1, true);
+            var expected = { __proto__: null, 0: 'a', 1: 'b', 2: 'c' };
+            s2t.deepEqual(combined, expected, 'converts to object with null prototype');
+            s2t.equal(Object.getPrototypeOf(combined), null, 'result has null prototype when plainObjects is true');
+            s2t.end();
+        });
+
+        st.end();
+    });
+
+    t.test('with existing overflow object', function (st) {
+        st.test('adds to existing overflow object at next index', function (s2t) {
+            // Create overflow object first via combine: 3 elements (indices 0-2) with limit 0
+            var overflow = utils.combine(['a', 'b'], 'c', 0, false);
+            s2t.ok(utils.isOverflow(overflow), 'initial object is marked as overflow');
+
+            var combined = utils.combine(overflow, 'd', 10, false);
+            s2t.equal(combined, overflow, 'returns the same object (mutated)');
+            s2t.deepEqual(combined, { 0: 'a', 1: 'b', 2: 'c', 3: 'd' }, 'adds value at next numeric index');
+            s2t.end();
+        });
+
+        st.test('does not treat plain object with numeric keys as overflow', function (s2t) {
+            var plainObj = { 0: 'a', 1: 'b' };
+            s2t.notOk(utils.isOverflow(plainObj), 'plain object is not marked as overflow');
+
+            // combine treats this as a regular value, not an overflow object to append to
+            var combined = utils.combine(plainObj, 'c', 10, false);
+            s2t.deepEqual(combined, [{ 0: 'a', 1: 'b' }, 'c'], 'concatenates as regular values');
+            s2t.end();
+        });
+
+        st.end();
+    });
+
+    t.end();
+});
+
+test('decode', function (t) {
+    t.equal(
+        utils.decode('a+b'),
+        'a b',
+        'decodes + to space'
+    );
+
+    t.equal(
+        utils.decode('name%2Eobj'),
+        'name.obj',
+        'decodes a string'
+    );
+    t.equal(
+        utils.decode('name%2Eobj%2Efoo', null, 'iso-8859-1'),
+        'name.obj.foo',
+        'decodes a string in iso-8859-1'
+    );
+
+    t.end();
+});
+
+test('encode', function (t) {
+    forEach(v.nullPrimitives, function (nullish) {
+        t['throws'](
+            function () { utils.encode(nullish); },
+            TypeError,
+            inspect(nullish) + ' is not a string'
+        );
+    });
+
+    t.equal(utils.encode(''), '', 'empty string returns itself');
+    t.deepEqual(utils.encode([]), [], 'empty array returns itself');
+    t.deepEqual(utils.encode({ length: 0 }), { length: 0 }, 'empty arraylike returns itself');
+
+    t.test('symbols', { skip: !v.hasSymbols }, function (st) {
+        st.equal(utils.encode(Symbol('x')), 'Symbol%28x%29', 'symbol is encoded');
+
+        st.end();
+    });
+
+    t.equal(
+        utils.encode('(abc)'),
+        '%28abc%29',
+        'encodes parentheses'
+    );
+    t.equal(
+        utils.encode({ toString: function () { return '(abc)'; } }),
+        '%28abc%29',
+        'toStrings and encodes parentheses'
+    );
+
+    t.equal(
+        utils.encode('abc 123 💩', null, 'iso-8859-1'),
+        'abc%20123%20%26%2355357%3B%26%2356489%3B',
+        'encodes in iso-8859-1'
+    );
+
+    var longString = '';
+    var expectedString = '';
+    for (var i = 0; i < 1500; i++) {
+        longString += ' ';
+        expectedString += '%20';
+    }
+
+    t.equal(
+        utils.encode(longString),
+        expectedString,
+        'encodes a long string'
+    );
+
+    t.equal(
+        utils.encode('\x28\x29'),
+        '%28%29',
+        'encodes parens normally'
+    );
+    t.equal(
+        utils.encode('\x28\x29', null, null, null, 'RFC1738'),
+        '()',
+        'does not encode parens in RFC1738'
+    );
+
+    // todo RFC1738 format
+
+    t.equal(
+        utils.encode('Āက豈'),
+        '%C4%80%E1%80%80%EF%A4%80',
+        'encodes multibyte chars'
+    );
+
+    t.equal(
+        utils.encode('\uD83D \uDCA9'),
+        '%F0%9F%90%A0%F0%BA%90%80',
+        'encodes lone surrogates'
+    );
+
+    t.end();
+});
+
+test('isBuffer()', function (t) {
+    var fn = function () {};
+    fn();
+    forEach([null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], fn, /a/g], function (x) {
+        t.equal(utils.isBuffer(x), false, inspect(x) + ' is not a buffer');
+    });
+
+    var fakeBuffer = { constructor: Buffer };
+    t.equal(utils.isBuffer(fakeBuffer), false, 'fake buffer is not a buffer');
+
+    var saferBuffer = SaferBuffer.from('abc');
+    t.equal(utils.isBuffer(saferBuffer), true, 'SaferBuffer instance is a buffer');
+
+    var buffer = SaferBuffer.from('abc');
+    t.notEqual(saferBuffer, buffer, 'different buffer instances');
+    t.equal(utils.isBuffer(buffer), true, 'another Buffer instance is a buffer');
+    t.end();
+});
+
+test('isRegExp()', function (t) {
+    t.equal(utils.isRegExp(/a/g), true, 'RegExp is a RegExp');
+    t.equal(utils.isRegExp(new RegExp('a', 'g')), true, 'new RegExp is a RegExp');
+    t.equal(utils.isRegExp(new Date()), false, 'Date is not a RegExp');
+
+    forEach(v.primitives, function (primitive) {
+        t.equal(utils.isRegExp(primitive), false, inspect(primitive) + ' is not a RegExp');
+    });
+
+    t.end();
+});
