feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

22
frontend/node_modules/@exodus/schemasafe/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Mathias Buus
Copyright (c) 2020 Exodus Movement
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

269
frontend/node_modules/@exodus/schemasafe/README.md generated vendored Normal file
View File

@@ -0,0 +1,269 @@
# `@exodus/schemasafe`
A code-generating [JSON Schema](https://json-schema.org/) validator that attempts to be reasonably secure.
Supports [draft-04/06/07/2019-09/2020-12](doc/Specification-support.md) and the
[`discriminator` OpenAPI keyword](./doc/Discriminator-support.md).
[![Node CI Status](https://github.com/ExodusMovement/schemasafe/workflows/Node%20CI/badge.svg)](https://github.com/ExodusMovement/schemasafe/actions)
[![npm](https://img.shields.io/npm/v/@exodus/schemasafe.svg)](https://www.npmjs.com/package/@exodus/schemasafe)
[![codecov](https://codecov.io/gh/ExodusMovement/schemasafe/branch/master/graph/badge.svg)](https://codecov.io/gh/ExodusMovement/schemasafe)
## Features
* [Converts schemas to self-contained JavaScript files](#generate-modules), can be used in the build process.\
_Integrates nicely with bundlers, so one won't need to generate code in runtime, and that works with CSP._
* Optional `requireValidation: true` mode enforces full validation of the input object.\
**Using [`mode: "strong"`](./doc/Strong-mode.md) is recommended, — it combines that option with additional schema safety checks.**
* Does not fail open on unknown or unprocessed keywords — instead throws at build time if schema was not fully understood.
_That is implemented by tracking processed keywords and ensuring that none remain uncovered._
* Does not fail open on schema problems — instead throws at build time.\
_E.g. it will detect mistakes like `{type: "array", "maxLength": 2}`._
* [About 2000 lines of code](./doc/Auditable.md), non-minified.
* Uses [secure code generation](./doc/Secure-code-generation.md) approach to prevent data from schema from leaking into
the generated code without being JSON-wrapped.
* [0 dependencies](./doc/Auditable.md)
* [Very fast](./doc/Performance.md)
* Supports JSON Schema [draft-04/06/07/2019-09/2020-12](./doc/Specification-support.md) and a strict subset of the
[`discriminator` OpenAPI keyword](./doc/Discriminator-support.md).
* Can assign defaults and/or remove additional properties when schema allows to do that safely.
Throws at build time if those options are used with schemas that don't allow to do that safely.
* Can be used as a [schema linter](./doc/Linter.md).
## Installation
```sh
npm install --save @exodus/schemasafe
```
## Usage
Simply pass a schema to compile it:
```js
const { validator } = require('@exodus/schemasafe')
const validate = validator({
type: 'object',
required: ['hello'],
properties: {
hello: {
type: 'string'
}
}
})
console.log('should be valid', validate({ hello: 'world' }))
console.log('should not be valid', validate({}))
```
Or use the [parser API](./doc/Parser-not-validator.md) (running in
[strong mode](./doc/Strong-mode.md) by default):
```js
const { parser } = require('@exodus/schemasafe')
const parse = parser({
$schema: 'https://json-schema.org/draft/2019-09/schema',
type: 'object',
required: ['hello'],
properties: {
hello: {
pattern: '^[a-z]+$',
type: 'string'
}
},
additionalProperties: false
})
console.log(parse('{"hello": "world" }')) // { valid: true, value: { hello: 'world' } }
console.log(parse('{}')) // { valid: false }
```
Parser API is recommended, because this way you can avoid handling unvalidated JSON objects in
non-string form at all in your code.
## Options
See [options documentation](./doc/Options.md) for the full list of supported options.
## Custom formats
`@exodus/schemasafe` supports the formats specified in JSON schema v4 (such as date-time).
If you want to add your own custom formats pass them as the formats options to the validator:
```js
const validate = validator({
type: 'string',
format: 'no-foo'
}, {
formats: {
'no-foo': (str) => !str.includes('foo'),
}
})
console.log(validate('test')) // true
console.log(validate('foo')) // false
const parse = parser({
$schema: 'https://json-schema.org/draft/2019-09/schema',
type: 'string',
format: 'only-a'
}, {
formats: {
'only-a': /^a+$/,
}
})
console.log(parse('"aa"')) // { valid: true, value: 'aa' }
console.log(parse('"ab"')) // { valid: false }
```
## External schemas
You can pass in external schemas that you reference using the `$ref` attribute as the `schemas` option
```js
const ext = {
type: 'string'
}
const schema = {
$ref: 'ext#' // references another schema called ext
}
// pass the external schemas as an option
const validate = validator(schema, { schemas: { ext: ext }})
console.log(validate('hello')) // true
console.log(validate(42)) // false
```
`schemas` can be either an object as shown above, a `Map`, or plain array of schemas (given that
those have corresponding `$id` set at top level inside schemas themselves).
## Enabling errors shows information about the source of the error
When the `includeErrors` option is set to `true`, `@exodus/schemasafe` also outputs:
- `keywordLocation`: a JSON pointer string as an URI fragment indicating which sub-schema failed, e.g.
`#/properties/item/type`
- `instanceLocation`: a JSON pointer string as an URI fragment indicating which property of the object
failed validation, e.g. `#/item`
```js
const schema = {
type: 'object',
required: ['hello'],
properties: {
hello: {
type: 'string'
}
}
}
const validate = validator(schema, { includeErrors: true })
validate({ hello: 100 });
console.log(validate.errors)
// [ { keywordLocation: '#/properties/hello/type', instanceLocation: '#/hello' } ]
```
Or, similarly, with parser API:
```js
const schema = {
$schema: 'https://json-schema.org/draft/2019-09/schema',
type: 'object',
required: ['hello'],
properties: {
hello: {
type: 'string',
pattern: '^[a-z]+$',
}
},
additionalProperties: false,
}
const parse = parser(schema, { includeErrors: true })
console.log(parse('{ "hello": 100 }'));
// { valid: false,
// error: 'JSON validation failed for type at #/hello',
// errors: [ { keywordLocation: '#/properties/hello/type', instanceLocation: '#/hello' } ]
// }
```
Only the first error is reported by default unless `allErrors` option is also set to `true` in
addition to `includeErrors`.
See [Error handling](./doc/Error-handling.md) for more information.
## Generate Modules
See the [doc/samples](./doc/samples/) directory to see how `@exodus/schemasafe` compiles
supported test suites.
To compile a validator function to an IIFE, call `validate.toModule()`:
```js
const { validator } = require('@exodus/schemasafe')
const schema = {
type: 'string',
format: 'hex'
}
// This works with custom formats as well.
const formats = {
hex: (value) => /^0x[0-9A-Fa-f]*$/.test(value),
}
const validate = validator(schema, { formats })
console.log(validate.toModule())
/** Prints:
* (function() {
* 'use strict'
* const format0 = (value) => /^0x[0-9A-Fa-f]*$/.test(value);
* return (function validate(data) {
* if (data === undefined) data = null
* if (!(typeof data === "string")) return false
* if (!format0(data)) return false
* return true
* })})();
*/
```
## Performance
`@exodus/schemasafe` uses code generation to turn a JSON schema into javascript code that is easily
optimizeable by v8 and [extremely fast](https://github.com/ebdrup/json-schema-benchmark).
See [Performance](./doc/Performance.md) for information on options that might affect performance
both ways.
## Contributing
Get a fully set up development environment with:
```sh
git clone https://github.com/ExodusMovement/schemasafe
cd schemasafe
git submodule update --init --recursive
yarn
yarn lint
yarn test
```
## Previous work
This is based on a heavily rewritten version of the amazing (but outdated)
[is-my-json-valid](https://github.com/mafintosh/is-my-json-valid) by
[@mafintosh](https://github.com/mafintosh/is-my-json-valid).
Compared to `is-my-json-valid`, `@exodus/schemasafe` adds security-first design, many new features,
newer spec versions support, slimmer and more maintainable code, 0 dependencies, self-contained JS
module generation, fixes bugs and adds better test coverage, and drops support for outdated Node.js
versions.
## License
[MIT](./LICENSE)

166
frontend/node_modules/@exodus/schemasafe/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,166 @@
// These typings are experimental and known to be incomplete.
// Help wanted at https://github.com/ExodusMovement/schemasafe/issues/130
type Json = string | number | boolean | null | Array<Json> | { [id: string]: Json }
type Schema =
| true
| false
| {
// version
$schema?: string
$vocabulary?: string
// pointers
id?: string
$id?: string
$anchor?: string
$ref?: string
definitions?: { [id: string]: Schema }
$defs?: { [id: string]: Schema }
$recursiveRef?: string
$recursiveAnchor?: boolean
// generic
type?: string | Array<string>
required?: Array<string>
default?: Json
// constant values
enum?: Array<Json>
const?: Json
// logical checks
not?: Schema
allOf?: Array<Schema>
anyOf?: Array<Schema>
oneOf?: Array<Schema>
if?: Schema
then?: Schema
else?: Schema
// numbers
maximum?: number
minimum?: number
exclusiveMaximum?: number | boolean
exclusiveMinimum?: number | boolean
multipleOf?: number
divisibleBy?: number
// arrays, basic
items?: Schema | Array<Schema>
maxItems?: number
minItems?: number
additionalItems?: Schema
// arrays, complex
contains?: Schema
minContains?: number
maxContains?: number
uniqueItems?: boolean
// strings
maxLength?: number
minLength?: number
format?: string
pattern?: string
// strings content
contentEncoding?: string
contentMediaType?: string
contentSchema?: Schema
// objects
properties?: { [id: string]: Schema }
maxProperties?: number
minProperties?: number
additionalProperties?: Schema
patternProperties?: { [pattern: string]: Schema }
propertyNames?: Schema
dependencies?: { [id: string]: Array<string> | Schema }
dependentRequired?: { [id: string]: Array<string> }
dependentSchemas?: { [id: string]: Schema }
// see-through
unevaluatedProperties?: Schema
unevaluatedItems?: Schema
// Unused meta keywords not affecting validation (annotations and comments)
// https://json-schema.org/understanding-json-schema/reference/generic.html
// https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9
title?: string
description?: string
deprecated?: boolean
readOnly?: boolean
writeOnly?: boolean
examples?: Array<Json>
$comment?: string
// optimization hint and error filtering only, does not affect validation result
discriminator?: { propertyName: string; mapping?: { [value: string]: string } }
}
interface ValidationError {
keywordLocation: string
instanceLocation: string
}
interface Validate {
(value: Json): boolean
errors?: ValidationError[]
toModule(): string
toJSON(): Schema
}
interface ValidatorOptions {
mode?: string
useDefaults?: boolean
removeAdditional?: boolean | string
includeErrors?: boolean
allErrors?: boolean
contentValidation?: boolean
dryRun?: boolean
lint?: boolean
allowUnusedKeywords?: boolean
allowUnreachable?: boolean
requireSchema?: boolean
requireValidation?: boolean
requireStringValidation?: boolean
forbidNoopValues?: boolean
complexityChecks?: boolean
unmodifiedPrototypes?: boolean
isJSON?: boolean
jsonCheck?: boolean
$schemaDefault?: string | null
formatAssertion?: boolean
formats?: { [key: string]: RegExp | ((input: string) => boolean) }
weakFormats?: boolean
extraFormats?: boolean
schemas?: Map<string, Schema> | Array<Schema> | { [id: string]: Schema }
}
interface ParseResult {
valid: boolean
value?: Json
error?: string
errors?: ValidationError[]
}
interface Parse {
(value: string): ParseResult
toModule(): string
toJSON(): Schema
}
interface LintError {
message: string
keywordLocation: string
schema: Schema
}
declare const validator: (schema: Schema, options?: ValidatorOptions) => Validate
declare const parser: (schema: Schema, options?: ValidatorOptions) => Parse
declare const lint: (schema: Schema, options?: ValidatorOptions) => LintError[]
export {
validator,
parser,
lint,
Validate,
ValidationError,
ValidatorOptions,
ParseResult,
Parse,
LintError,
Json,
Schema,
}

74
frontend/node_modules/@exodus/schemasafe/package.json generated vendored Normal file
View File

@@ -0,0 +1,74 @@
{
"name": "@exodus/schemasafe",
"version": "1.3.0",
"description": "JSON Safe Parser & Schema Validator",
"license": "MIT",
"main": "src/index.js",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/ExodusMovement/schemasafe.git"
},
"bugs": {
"url": "https://github.com/ExodusMovement/schemasafe/issues"
},
"homepage": "https://github.com/ExodusMovement/schemasafe",
"files": [
"index.d.ts",
"src/compile.js",
"src/formats.js",
"src/generate-function.js",
"src/index.js",
"src/javascript.js",
"src/known-keywords.js",
"src/pointer.js",
"src/safe-format.js",
"src/scope-functions.js",
"src/scope-utils.js",
"src/tracing.js"
],
"scripts": {
"lint": "prettier --list-different '**/*.js' && eslint .",
"format": "prettier --write '**/*.js'",
"coverage": "c8 --reporter=lcov --reporter=text npm run test",
"coverage:lcov": "c8 --reporter=lcovonly npm run test",
"test:only:json-schema-testsuite": "tape test/json-schema.js | tap-spec",
"test": "npm run test:raw | tap-spec",
"test:raw": "npm run test:normal && npm run test:module",
"test:module": "tape -r ./test/tools/test-module.js test/*.js test/regressions/*.js",
"test:normal": "tape test/*.js test/regressions/*.js"
},
"dependencies": {},
"devDependencies": {
"c8": "^7.9.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^3.1.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"prettier": "~1.14.3",
"safe-regex": "^1.1.0",
"tap-spec": "^5.0.0",
"tape": "^5.3.1"
},
"resolutions": {
"tap-spec/tap-out/trim": "^1.0.1"
},
"keywords": [
"JSON",
"schema",
"validator",
"validation",
"JSON Schema",
"draft-04",
"draft-06",
"draft-07",
"draft 2019-09",
"draft 2020-12",
"jsonschema",
"json-schema",
"json-schema-validator",
"json-schema-validation"
]
}

1392
frontend/node_modules/@exodus/schemasafe/src/compile.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

208
frontend/node_modules/@exodus/schemasafe/src/formats.js generated vendored Normal file
View File

@@ -0,0 +1,208 @@
'use strict'
const core = {
// matches ajv + length checks + does not start with a dot
// note that quoted emails are deliberately unsupported (as in ajv), who would want \x01 in email
// first check is an additional fast path with lengths: 20+(1+21)*2 = 64, (1+61+1)+((1+60+1)+1)*3 = 252 < 253, that should cover most valid emails
// max length is 64 (name) + 1 (@) + 253 (host), we want to ensure that prior to feeding to the fast regex
// the second regex checks for quoted, starting-leading dot in name, and two dots anywhere
email: (input) => {
if (input.length > 318) return false
const fast = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]{1,20}(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]{1,21}){0,2}@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,60}[a-z0-9])?){0,3}$/i
if (fast.test(input)) return true
if (!input.includes('@') || /(^\.|^"|\.@|\.\.)/.test(input)) return false
const [name, host, ...rest] = input.split('@')
if (!name || !host || rest.length !== 0 || name.length > 64 || host.length > 253) return false
if (!/^[a-z0-9.-]+$/i.test(host) || !/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$/i.test(name)) return false
return host.split('.').every((part) => /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i.test(part))
},
// matches ajv + length checks
hostname: (input) => {
if (input.length > (input.endsWith('.') ? 254 : 253)) return false
const hostname = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\.?$/i
return hostname.test(input)
},
// 'time' matches ajv + length checks, 'date' matches ajv full
// date: https://tools.ietf.org/html/rfc3339#section-5.6
// date-time: https://tools.ietf.org/html/rfc3339#section-5.6
// leap year: https://tools.ietf.org/html/rfc3339#appendix-C
// 11: 1990-01-01, 1: T, 9: 00:00:00., 12: maxiumum fraction length (non-standard), 6: +00:00
date: (input) => {
if (input.length !== 10) return false
if (input[5] === '0' && input[6] === '2') {
if (/^\d\d\d\d-02-(?:[012][1-8]|[12]0|[01]9)$/.test(input)) return true
const matches = input.match(/^(\d\d\d\d)-02-29$/)
if (!matches) return false
const year = matches[1] | 0
return year % 16 === 0 || (year % 4 === 0 && year % 25 !== 0)
}
if (input.endsWith('31')) return /^\d\d\d\d-(?:0[13578]|1[02])-31$/.test(input)
return /^\d\d\d\d-(?:0[13-9]|1[012])-(?:[012][1-9]|[123]0)$/.test(input)
},
// leap second handling is special, we check it's 23:59:60.*
time: (input) => {
if (input.length > 9 + 12 + 6) return false
const time = /^(?:2[0-3]|[0-1]\d):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:z|[+-](?:2[0-3]|[0-1]\d)(?::?[0-5]\d)?)?$/i
if (!time.test(input)) return false
if (!/:60/.test(input)) return true
const p = input.match(/([0-9.]+|[^0-9.])/g)
let hm = Number(p[0]) * 60 + Number(p[2])
if (p[5] === '+') hm += 24 * 60 - Number(p[6] || 0) * 60 - Number(p[8] || 0)
else if (p[5] === '-') hm += Number(p[6] || 0) * 60 + Number(p[8] || 0)
return hm % (24 * 60) === 23 * 60 + 59
},
// first two lines specific to date-time, then tests for unanchored (at end) date, code identical to 'date' above
// input[17] === '6' is a check for :60
'date-time': (input) => {
if (input.length > 10 + 1 + 9 + 12 + 6) return false
const full = /^\d\d\d\d-(?:0[1-9]|1[0-2])-(?:[0-2]\d|3[01])[t\s](?:2[0-3]|[0-1]\d):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:z|[+-](?:2[0-3]|[0-1]\d)(?::?[0-5]\d)?)$/i
const feb = input[5] === '0' && input[6] === '2'
if ((feb && input[8] === '3') || !full.test(input)) return false
if (input[17] === '6') {
const p = input.slice(11).match(/([0-9.]+|[^0-9.])/g)
let hm = Number(p[0]) * 60 + Number(p[2])
if (p[5] === '+') hm += 24 * 60 - Number(p[6] || 0) * 60 - Number(p[8] || 0)
else if (p[5] === '-') hm += Number(p[6] || 0) * 60 + Number(p[8] || 0)
if (hm % (24 * 60) !== 23 * 60 + 59) return false
}
if (feb) {
if (/^\d\d\d\d-02-(?:[012][1-8]|[12]0|[01]9)/.test(input)) return true
const matches = input.match(/^(\d\d\d\d)-02-29/)
if (!matches) return false
const year = matches[1] | 0
return year % 16 === 0 || (year % 4 === 0 && year % 25 !== 0)
}
if (input[8] === '3' && input[9] === '1') return /^\d\d\d\d-(?:0[13578]|1[02])-31/.test(input)
return /^\d\d\d\d-(?:0[13-9]|1[012])-(?:[012][1-9]|[123]0)/.test(input)
},
/* ipv4 and ipv6 are from ajv with length restriction */
// optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html
ipv4: (ip) =>
ip.length <= 15 &&
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?)$/.test(ip),
// optimized http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses
// max length: 1000:1000:1000:1000:1000:1000:255.255.255.255
// we parse ip6 format with a simple scan, leaving embedded ipv4 validation to a regex
// s0=count(:), s1=count(.), hex=count(a-zA-Z0-9), short=count(::)>0
// 48-57: '0'-'9', 97-102, 65-70: 'a'-'f', 'A'-'F', 58: ':', 46: '.'
/* eslint-disable one-var */
// prettier-ignore
ipv6: (input) => {
if (input.length > 45 || input.length < 2) return false
let s0 = 0, s1 = 0, hex = 0, short = false, letters = false, last = 0, start = true
for (let i = 0; i < input.length; i++) {
const c = input.charCodeAt(i)
if (i === 1 && last === 58 && c !== 58) return false
if (c >= 48 && c <= 57) {
if (++hex > 4) return false
} else if (c === 46) {
if (s0 > 6 || s1 >= 3 || hex === 0 || letters) return false
s1++
hex = 0
} else if (c === 58) {
if (s1 > 0 || s0 >= 7) return false
if (last === 58) {
if (short) return false
short = true
} else if (i === 0) start = false
s0++
hex = 0
letters = false
} else if ((c >= 97 && c <= 102) || (c >= 65 && c <= 70)) {
if (s1 > 0) return false
if (++hex > 4) return false
letters = true
} else return false
last = c
}
if (s0 < 2 || (s1 > 0 && (s1 !== 3 || hex === 0))) return false
if (short && input.length === 2) return true
if (s1 > 0 && !/(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/.test(input)) return false
const spaces = s1 > 0 ? 6 : 7
if (!short) return s0 === spaces && start && hex > 0
return (start || hex > 0) && s0 < spaces
},
/* eslint-enable one-var */
// matches ajv with optimization
uri: /^[a-z][a-z0-9+\-.]*:(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|v[0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/?(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i,
// matches ajv with optimization
'uri-reference': /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|v[0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/?(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?)?(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i,
// ajv has /^(([^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?([a-z0-9_]|%[0-9a-f]{2})+(:[1-9][0-9]{0,3}|\*)?(,([a-z0-9_]|%[0-9a-f]{2})+(:[1-9][0-9]{0,3}|\*)?)*\})*$/i
// this is equivalent
// uri-template: https://tools.ietf.org/html/rfc6570
// eslint-disable-next-line no-control-regex
'uri-template': /^(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2}|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i,
// ajv has /^(\/([^~/]|~0|~1)*)*$/, this is equivalent
// JSON-pointer: https://tools.ietf.org/html/rfc6901
'json-pointer': /^(?:|\/(?:[^~]|~0|~1)*)$/,
// ajv has /^(0|[1-9][0-9]*)(#|(\/([^~/]|~0|~1)*)*)$/, this is equivalent
// relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00
'relative-json-pointer': /^(?:0|[1-9][0-9]*)(?:|#|\/(?:[^~]|~0|~1)*)$/,
// uuid: http://tools.ietf.org/html/rfc4122
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
// length restriction is an arbitrary safeguard
// first regex checks if this a week duration (can't be combined with others)
// second regex verifies symbols, no more than one fraction, at least 1 block is present, and T is not last
// third regex verifies structure
duration: (input) =>
input.length > 1 &&
input.length < 80 &&
(/^P\d+([.,]\d+)?W$/.test(input) ||
(/^P[\dYMDTHS]*(\d[.,]\d+)?[YMDHS]$/.test(input) &&
/^P([.,\d]+Y)?([.,\d]+M)?([.,\d]+D)?(T([.,\d]+H)?([.,\d]+M)?([.,\d]+S)?)?$/.test(input))),
// TODO: iri, iri-reference, idn-email, idn-hostname
}
const extra = {
// basic
alpha: /^[a-zA-Z]+$/,
alphanumeric: /^[a-zA-Z0-9]+$/,
// hex
'hex-digits': /^[0-9a-f]+$/i,
'hex-digits-prefixed': /^0x[0-9a-f]+$/i,
'hex-bytes': /^([0-9a-f][0-9a-f])+$/i,
'hex-bytes-prefixed': /^0x([0-9a-f][0-9a-f])+$/i,
base64: (input) => input.length % 4 === 0 && /^[a-z0-9+/]*={0,3}$/i.test(input),
// ajv has /^#(\/([a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i, this is equivalent
// uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A
'json-pointer-uri-fragment': /^#(|\/(\/|[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)$/i,
// draft3 backwards compat
'host-name': core.hostname,
'ip-address': core.ipv4,
// manually cleaned up from is-my-json-valid, CSS 2.1 colors only per draft03 spec
color: /^(#[0-9A-Fa-f]{3,6}|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|rgb\(\s*([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*,\s*([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*,\s*([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*\)|rgb\(\s*(\d?\d%|100%)\s*,\s*(\d?\d%|100%)\s*,\s*(\d?\d%|100%)\s*\))$/,
// style is deliberately unsupported, don't accept untrusted styles
}
const weak = {
// In weak because don't accept regexes from untrusted sources, using them can cause DoS
// matches ajv + length checks
// eslint comment outside because we don't want comments in functions, those affect output
/* eslint-disable no-new */
regex: (str) => {
if (str.length > 1e5) return false
const Z_ANCHOR = /[^\\]\\Z/
if (Z_ANCHOR.test(str)) return false
try {
new RegExp(str, 'u')
return true
} catch (e) {
return false
}
},
/* eslint-enable no-new */
}
module.exports = { core, extra, weak }

View File

@@ -0,0 +1,104 @@
'use strict'
const { format, safe, safenot } = require('./safe-format')
const { jaystring } = require('./javascript')
/*
* Construct a function from lines/blocks/if conditions.
*
* Returns a Function instance (makeFunction) or code in text format (makeModule).
*/
const INDENT_START = /[{[]/
const INDENT_END = /[}\]]/
module.exports = () => {
const lines = []
let indent = 0
const pushLine = (line) => {
if (INDENT_END.test(line.trim()[0])) indent--
lines.push({ indent, code: line })
if (INDENT_START.test(line[line.length - 1])) indent++
}
const build = () => {
if (indent !== 0) throw new Error('Unexpected indent at build()')
const joined = lines.map((line) => format('%w%s', line.indent * 2, line.code)).join('\n')
return /^[a-z][a-z0-9]*$/i.test(joined) ? `return ${joined}` : `return (${joined})`
}
const processScope = (scope) => {
const entries = Object.entries(scope)
for (const [key, value] of entries) {
if (!/^[a-z][a-z0-9]*$/i.test(key)) throw new Error('Unexpected scope key!')
if (!(typeof value === 'function' || value instanceof RegExp))
throw new Error('Unexpected scope value!')
}
return entries
}
return {
optimizedOut: false, // some branch of code has been optimized out
size: () => lines.length,
write(fmt, ...args) {
if (typeof fmt !== 'string') throw new Error('Format must be a string!')
if (fmt.includes('\n')) throw new Error('Only single lines are supported')
pushLine(format(fmt, ...args))
return true // code was written
},
block(prefix, writeBody, noInline = false) {
const oldIndent = indent
this.write('%s {', prefix)
const length = lines.length
writeBody()
if (length === lines.length) {
// no lines inside block, unwind the block
lines.pop()
indent = oldIndent
return false // nothing written
} else if (length === lines.length - 1 && !noInline) {
// a single line has been written, inline it if opt-in allows
const { code } = lines[lines.length - 1]
// check below is just for generating more readable code, it's safe to inline all !noInline
if (!/^(if|for) /.test(code)) {
lines.length -= 2
indent = oldIndent
return this.write('%s %s', prefix, code)
}
}
return this.write('}')
},
if(condition, writeBody, writeElse) {
if (`${condition}` === 'false') {
if (writeElse) writeElse()
if (writeBody) this.optimizedOut = true
} else if (`${condition}` === 'true') {
if (writeBody) writeBody()
if (writeElse) this.optimizedOut = true
} else if (writeBody && this.block(format('if (%s)', condition), writeBody, !!writeElse)) {
if (writeElse) this.block(format('else'), writeElse) // !!writeElse above ensures {} wrapping before `else`
} else if (writeElse) {
this.if(safenot(condition), writeElse)
}
},
makeModule(scope = {}) {
const scopeDefs = processScope(scope).map(
([key, val]) => `const ${safe(key)} = ${jaystring(val)};`
)
return `(function() {\n'use strict'\n${scopeDefs.join('\n')}\n${build()}})()`
},
makeFunction(scope = {}) {
const scopeEntries = processScope(scope)
const keys = scopeEntries.map((entry) => entry[0])
const vals = scopeEntries.map((entry) => entry[1])
// eslint-disable-next-line no-new-func
return Function(...keys, `'use strict'\n${build()}`)(...vals)
},
}
}

98
frontend/node_modules/@exodus/schemasafe/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,98 @@
'use strict'
const genfun = require('./generate-function')
const { buildSchemas } = require('./pointer')
const { compile } = require('./compile')
const { deepEqual } = require('./scope-functions')
const jsonCheckWithErrors = (validate) =>
function validateIsJSON(data) {
if (!deepEqual(data, JSON.parse(JSON.stringify(data)))) {
validateIsJSON.errors = [{ instanceLocation: '#', error: 'not JSON compatible' }]
return false
}
const res = validate(data)
validateIsJSON.errors = validate.errors
return res
}
const jsonCheckWithoutErrors = (validate) => (data) =>
deepEqual(data, JSON.parse(JSON.stringify(data))) && validate(data)
const validator = (
schema,
{ parse = false, multi = false, jsonCheck = false, isJSON = false, schemas = [], ...opts } = {}
) => {
if (jsonCheck && isJSON) throw new Error('Can not specify both isJSON and jsonCheck options')
if (parse && (jsonCheck || isJSON))
throw new Error('jsonCheck and isJSON options are not applicable in parser mode')
const mode = parse ? 'strong' : 'default' // strong mode is default in parser, can be overriden
const willJSON = isJSON || jsonCheck || parse
const arg = multi ? schema : [schema]
const options = { mode, ...opts, schemas: buildSchemas(schemas, arg), isJSON: willJSON }
const { scope, refs } = compile(arg, options) // only a single ref
if (opts.dryRun) return
if (opts.lint) return scope.lintErrors
const fun = genfun()
if (parse) {
scope.parseWrap = opts.includeErrors ? parseWithErrors : parseWithoutErrors
} else if (jsonCheck) {
scope.deepEqual = deepEqual
scope.jsonCheckWrap = opts.includeErrors ? jsonCheckWithErrors : jsonCheckWithoutErrors
}
if (multi) {
fun.write('[')
for (const ref of refs.slice(0, -1)) fun.write('%s,', ref)
if (refs.length > 0) fun.write('%s', refs[refs.length - 1])
fun.write(']')
if (parse) fun.write('.map(parseWrap)')
else if (jsonCheck) fun.write('.map(jsonCheckWrap)')
} else {
if (parse) fun.write('parseWrap(%s)', refs[0])
else if (jsonCheck) fun.write('jsonCheckWrap(%s)', refs[0])
else fun.write('%s', refs[0])
}
const validate = fun.makeFunction(scope)
validate.toModule = ({ semi = true } = {}) => fun.makeModule(scope) + (semi ? ';' : '')
validate.toJSON = () => schema
return validate
}
const parseWithErrors = (validate) => (src) => {
if (typeof src !== 'string') return { valid: false, error: 'Input is not a string' }
try {
const value = JSON.parse(src)
if (!validate(value)) {
const { keywordLocation, instanceLocation } = validate.errors[0]
const keyword = keywordLocation.slice(keywordLocation.lastIndexOf('/') + 1)
const error = `JSON validation failed for ${keyword} at ${instanceLocation}`
return { valid: false, error, errors: validate.errors }
}
return { valid: true, value }
} catch ({ message }) {
return { valid: false, error: message }
}
}
const parseWithoutErrors = (validate) => (src) => {
if (typeof src !== 'string') return { valid: false }
try {
const value = JSON.parse(src)
if (!validate(value)) return { valid: false }
return { valid: true, value }
} catch (e) {
return { valid: false }
}
}
const parser = function(schema, { parse = true, ...opts } = {}) {
if (!parse) throw new Error('can not disable parse in parser')
return validator(schema, { parse, ...opts })
}
const lint = function(schema, { lint: lintOption = true, ...opts } = {}) {
if (!lintOption) throw new Error('can not disable lint option in lint()')
return validator(schema, { lint: lintOption, ...opts })
}
module.exports = { validator, parser, lint }

View File

@@ -0,0 +1,170 @@
'use strict'
const { format, safe } = require('./safe-format')
const { scopeMethods } = require('./scope-utils')
const functions = require('./scope-functions')
// for building into the validation function
const types = new Map(
Object.entries({
null: (name) => format('%s === null', name),
boolean: (name) => format('typeof %s === "boolean"', name),
array: (name) => format('Array.isArray(%s)', name),
object: (n) => format('typeof %s === "object" && %s && !Array.isArray(%s)', n, n, n),
number: (name) => format('typeof %s === "number"', name),
integer: (name) => format('Number.isInteger(%s)', name),
string: (name) => format('typeof %s === "string"', name),
})
)
const buildName = ({ name, parent, keyval, keyname }) => {
if (name) {
if (parent || keyval || keyname) throw new Error('name can be used only stand-alone')
return name // top-level
}
if (!parent) throw new Error('Can not use property of undefined parent!')
const parentName = buildName(parent)
if (keyval !== undefined) {
if (keyname) throw new Error('Can not use key value and name together')
if (!['string', 'number'].includes(typeof keyval)) throw new Error('Invalid property path')
if (/^[a-z][a-z0-9_]*$/i.test(keyval)) return format('%s.%s', parentName, safe(keyval))
return format('%s[%j]', parentName, keyval)
} else if (keyname) {
return format('%s[%s]', parentName, keyname)
}
/* c8 ignore next */
throw new Error('Unreachable')
}
const jsonProtoKeys = new Set(
[].concat(
...[Object, Array, String, Number, Boolean].map((c) => Object.getOwnPropertyNames(c.prototype))
)
)
const jsHelpers = (fun, scope, propvar, { unmodifiedPrototypes, isJSON }, noopRegExps) => {
const { gensym, genpattern, genloop } = scopeMethods(scope, propvar)
const present = (obj) => {
const name = buildName(obj) // also checks for coherence, do not remove
const { parent, keyval, keyname, inKeys, checked } = obj
/* c8 ignore next */
if (checked || (inKeys && isJSON)) throw new Error('Unreachable: useless check for undefined')
if (inKeys) return format('%s !== undefined', name)
if (parent && keyname) {
scope.hasOwn = functions.hasOwn
const pname = buildName(parent)
if (isJSON) return format('%s !== undefined && hasOwn(%s, %s)', name, pname, keyname)
return format('%s in %s && hasOwn(%s, %s)', keyname, pname, pname, keyname)
} else if (parent && keyval !== undefined) {
// numbers must be converted to strings for this check, hence `${keyval}` in check below
if (unmodifiedPrototypes && isJSON && !jsonProtoKeys.has(`${keyval}`))
return format('%s !== undefined', name)
scope.hasOwn = functions.hasOwn
const pname = buildName(parent)
if (isJSON) return format('%s !== undefined && hasOwn(%s, %j)', name, pname, keyval)
return format('%j in %s && hasOwn(%s, %j)', keyval, pname, pname, keyval)
}
/* c8 ignore next */
throw new Error('Unreachable: present() check without parent')
}
const forObjectKeys = (obj, writeBody) => {
const key = gensym('key')
fun.block(format('for (const %s of Object.keys(%s))', key, buildName(obj)), () => {
writeBody(propvar(obj, key, true), key) // always own property here
})
}
const forArray = (obj, start, writeBody) => {
const i = genloop()
const name = buildName(obj)
fun.block(format('for (let %s = %s; %s < %s.length; %s++)', i, start, i, name, i), () => {
writeBody(propvar(obj, i, unmodifiedPrototypes, true), i) // own property in Array if proto not mangled
})
}
const patternTest = (pat, key) => {
// Convert common patterns to string checks, makes generated code easier to read (and a tiny perf bump)
const r = pat.replace(/[.^$|*+?(){}[\]\\]/gu, '') // Special symbols: .^$|*+?(){}[]\
if (pat === `^${r}$`) return format('(%s === %j)', key, pat.slice(1, -1)) // ^abc$ -> === abc
if (noopRegExps.has(pat)) return format('true') // known noop
// All of the below will cause warnings in enforced string validation mode, but let's make what they actually do more visible
// note that /^.*$/u.test('\n') is false, so don't combine .* with anchors here!
if ([r, `${r}+`, `${r}.*`, `.*${r}.*`].includes(pat)) return format('%s.includes(%j)', key, r)
if ([`^${r}`, `^${r}+`, `^${r}.*`].includes(pat)) return format('%s.startsWith(%j)', key, r)
if ([`${r}$`, `.*${r}$`].includes(pat)) return format('%s.endsWith(%j)', key, r)
const subr = [...r].slice(0, -1).join('') // without the last symbol, astral plane aware
if ([`${r}*`, `${r}?`].includes(pat))
return subr.length === 0 ? format('true') : format('%s.includes(%j)', key, subr) // abc*, abc? -> includes(ab)
if ([`^${r}*`, `^${r}?`].includes(pat))
return subr.length === 0 ? format('true') : format('%s.startsWith(%j)', key, subr) // ^abc*, ^abc? -> startsWith(ab)
// A normal reg-exp test
return format('%s.test(%s)', genpattern(pat), key)
}
const compare = (name, val) => {
if (!val || typeof val !== 'object') return format('%s === %j', name, val)
let type // type is needed for speedup only, deepEqual rechecks that
// small plain object/arrays are fast cases and we inline those instead of calling deepEqual
const shouldInline = (arr) => arr.length <= 3 && arr.every((x) => !x || typeof x !== 'object')
if (Array.isArray(val)) {
type = types.get('array')(name)
if (shouldInline(val)) {
let k = format('%s.length === %d', name, val.length)
for (let i = 0; i < val.length; i++) k = format('%s && %s[%d] === %j', k, name, i, val[i])
return format('%s && %s', type, k)
}
} else {
type = types.get('object')(name)
const [keys, values] = [Object.keys(val), Object.values(val)]
if (shouldInline(values)) {
let k = format('Object.keys(%s).length === %d', name, keys.length)
if (keys.length > 0) scope.hasOwn = functions.hasOwn
for (const key of keys) k = format('%s && hasOwn(%s, %j)', k, name, key)
for (const key of keys) k = format('%s && %s[%j] === %j', k, name, key, val[key])
return format('%s && %s', type, k)
}
}
scope.deepEqual = functions.deepEqual
return format('%s && deepEqual(%s, %j)', type, name, val)
}
return { present, forObjectKeys, forArray, patternTest, compare, propvar }
}
// Stringifcation of functions and regexps, for scope
const isArrowFnWithParensRegex = /^\([^)]*\) *=>/
const isArrowFnWithoutParensRegex = /^[^=]*=>/
const toJayString = Symbol.for('toJayString')
function jaystring(item) {
if (typeof item === 'function') {
if (item[toJayString]) return item[toJayString] // this is supported only for functions
if (Object.getPrototypeOf(item) !== Function.prototype)
throw new Error('Can not stringify: a function with unexpected prototype')
const stringified = `${item}`
if (item.prototype) {
if (!/^function[ (]/.test(stringified)) throw new Error('Unexpected function')
return stringified // normal function
}
if (isArrowFnWithParensRegex.test(stringified) || isArrowFnWithoutParensRegex.test(stringified))
return stringified // Arrow function
// Shortened ES6 object method declaration
throw new Error('Can not stringify: only either normal or arrow functions are supported')
} else if (typeof item === 'object') {
const proto = Object.getPrototypeOf(item)
if (item instanceof RegExp && proto === RegExp.prototype) return format('%r', item)
throw new Error('Can not stringify: an object with unexpected prototype')
}
throw new Error(`Can not stringify: unknown type ${typeof item}`)
}
module.exports = { types, buildName, jsHelpers, jaystring }

View File

@@ -0,0 +1,46 @@
'use strict'
const knownKeywords = [
...['$schema', '$vocabulary'], // version
...['id', '$id', '$anchor', '$ref', 'definitions', '$defs'], // pointers
...['$recursiveRef', '$recursiveAnchor', '$dynamicAnchor', '$dynamicRef'],
...['type', 'required', 'default'], // generic
...['enum', 'const'], // constant values
...['not', 'allOf', 'anyOf', 'oneOf', 'if', 'then', 'else'], // logical checks
...['maximum', 'minimum', 'exclusiveMaximum', 'exclusiveMinimum', 'multipleOf', 'divisibleBy'], // numbers
...['items', 'maxItems', 'minItems', 'additionalItems', 'prefixItems'], // arrays, basic
...['contains', 'minContains', 'maxContains', 'uniqueItems'], // arrays, complex
...['maxLength', 'minLength', 'format', 'pattern'], // strings
...['contentEncoding', 'contentMediaType', 'contentSchema'], // strings content
...['properties', 'maxProperties', 'minProperties', 'additionalProperties', 'patternProperties'], // objects
...['propertyNames'], // objects
...['dependencies', 'dependentRequired', 'dependentSchemas', 'propertyDependencies'], // objects (dependencies)
...['unevaluatedProperties', 'unevaluatedItems'], // see-through
// Unused meta keywords not affecting validation (annotations and comments)
// https://json-schema.org/understanding-json-schema/reference/generic.html
// https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9
...['title', 'description', 'deprecated', 'readOnly', 'writeOnly', 'examples', '$comment'], // unused meta
...['example'], // unused meta, OpenAPI
'discriminator', // optimization hint and error filtering only, does not affect validation result
'removeAdditional', // optional keyword for { removeAdditional: 'keyword' } config, to target specific objects
]
// Order is important, newer first!
const schemaDrafts = [
...['draft/next'], // not recommended to use, might change / break in an unexpected way
...['draft/2020-12', 'draft/2019-09'], // new
...['draft-07', 'draft-06', 'draft-04', 'draft-03'], // historic
]
const schemaVersions = schemaDrafts.map((draft) => `https://json-schema.org/${draft}/schema`)
const vocab2019 = ['core', 'applicator', 'validation', 'meta-data', 'format', 'content']
const vocab2020 = [
...['core', 'applicator', 'unevaluated', 'validation'],
...['meta-data', 'format-annotation', 'format-assertion', 'content'],
]
const knownVocabularies = [
...vocab2019.map((v) => `https://json-schema.org/draft/2019-09/vocab/${v}`),
...vocab2020.map((v) => `https://json-schema.org/draft/2020-12/vocab/${v}`),
]
module.exports = { knownKeywords, schemaVersions, knownVocabularies }

194
frontend/node_modules/@exodus/schemasafe/src/pointer.js generated vendored Normal file
View File

@@ -0,0 +1,194 @@
'use strict'
const { knownKeywords } = require('./known-keywords')
/*
* JSON pointer collection/resolution logic
*/
function safeSet(map, key, value, comment = 'keys') {
if (!map.has(key)) return map.set(key, value)
if (map.get(key) !== value) throw new Error(`Conflicting duplicate ${comment}: ${key}`)
}
function untilde(string) {
if (!string.includes('~')) return string
return string.replace(/~[01]/g, (match) => {
switch (match) {
case '~1':
return '/'
case '~0':
return '~'
}
/* c8 ignore next */
throw new Error('Unreachable')
})
}
function get(obj, pointer, objpath) {
if (typeof obj !== 'object') throw new Error('Invalid input object')
if (typeof pointer !== 'string') throw new Error('Invalid JSON pointer')
const parts = pointer.split('/')
if (!['', '#'].includes(parts.shift())) throw new Error('Invalid JSON pointer')
if (parts.length === 0) return obj
let curr = obj
for (const part of parts) {
if (typeof part !== 'string') throw new Error('Invalid JSON pointer')
if (objpath) objpath.push(curr) // does not include target itself, but includes head
const prop = untilde(part)
if (typeof curr !== 'object') return undefined
if (!Object.prototype.hasOwnProperty.call(curr, prop)) return undefined
curr = curr[prop]
}
return curr
}
const protocolRegex = /^https?:\/\//
function joinPath(baseFull, sub) {
if (typeof baseFull !== 'string' || typeof sub !== 'string') throw new Error('Unexpected path!')
if (sub.length === 0) return baseFull
const base = baseFull.replace(/#.*/, '')
if (sub.startsWith('#')) return `${base}${sub}`
if (!base.includes('/') || protocolRegex.test(sub)) return sub
if (protocolRegex.test(base)) return `${new URL(sub, base)}`
if (sub.startsWith('/')) return sub
return [...base.split('/').slice(0, -1), sub].join('/')
}
function objpath2path(objpath) {
const ids = objpath.map((obj) => (obj && (obj.$id || obj.id)) || '')
return ids.filter((id) => id && typeof id === 'string').reduce(joinPath, '')
}
const withSpecialChilds = ['properties', 'patternProperties', '$defs', 'definitions']
const skipChilds = ['const', 'enum', 'examples', 'example', 'comment']
const sSkip = Symbol('skip')
function traverse(schema, work) {
const visit = (sub, specialChilds = false) => {
if (!sub || typeof sub !== 'object') return
const res = work(sub)
if (res !== undefined) return res === sSkip ? undefined : res
for (const k of Object.keys(sub)) {
if (!specialChilds && !Array.isArray(sub) && !knownKeywords.includes(k)) continue
if (!specialChilds && skipChilds.includes(k)) continue
const kres = visit(sub[k], !specialChilds && withSpecialChilds.includes(k))
if (kres !== undefined) return kres
}
}
return visit(schema)
}
// Returns a list of resolved entries, in a form: [schema, root, basePath]
// basePath doesn't contain the target object $id itself
function resolveReference(root, schemas, ref, base = '') {
const ptr = joinPath(base, ref)
const results = []
const [main, hash = ''] = ptr.split('#')
const local = decodeURI(hash)
// Find in self by id path
const visit = (sub, oldPath, specialChilds = false, dynamic = false) => {
if (!sub || typeof sub !== 'object') return
const id = sub.$id || sub.id
let path = oldPath
if (id && typeof id === 'string') {
path = joinPath(path, id)
if (path === ptr || (path === main && local === '')) {
results.push([sub, root, oldPath])
} else if (path === main && local[0] === '/') {
const objpath = []
const res = get(sub, local, objpath)
if (res !== undefined) results.push([res, root, joinPath(oldPath, objpath2path(objpath))])
}
}
const anchor = dynamic ? sub.$dynamicAnchor : sub.$anchor
if (anchor && typeof anchor === 'string') {
if (anchor.includes('#')) throw new Error("$anchor can't include '#'")
if (anchor.startsWith('/')) throw new Error("$anchor can't start with '/'")
path = joinPath(path, `#${anchor}`)
if (path === ptr) results.push([sub, root, oldPath])
}
for (const k of Object.keys(sub)) {
if (!specialChilds && !Array.isArray(sub) && !knownKeywords.includes(k)) continue
if (!specialChilds && skipChilds.includes(k)) continue
visit(sub[k], path, !specialChilds && withSpecialChilds.includes(k))
}
if (!dynamic && sub.$dynamicAnchor) visit(sub, oldPath, specialChilds, true)
}
visit(root, main)
// Find in self by pointer
if (main === base.replace(/#$/, '') && (local[0] === '/' || local === '')) {
const objpath = []
const res = get(root, local, objpath)
if (res !== undefined) results.push([res, root, objpath2path(objpath)])
}
// Find in additional schemas
if (schemas.has(main) && schemas.get(main) !== root) {
const additional = resolveReference(schemas.get(main), schemas, `#${hash}`, main)
results.push(...additional.map(([res, rRoot, rPath]) => [res, rRoot, joinPath(main, rPath)]))
}
// Full refs to additional schemas
if (schemas.has(ptr)) results.push([schemas.get(ptr), schemas.get(ptr), ptr])
return results
}
function getDynamicAnchors(schema) {
const results = new Map()
traverse(schema, (sub) => {
if (sub !== schema && (sub.$id || sub.id)) return sSkip // base changed, no longer in the same resource
const anchor = sub.$dynamicAnchor
if (anchor && typeof anchor === 'string') {
if (anchor.includes('#')) throw new Error("$dynamicAnchor can't include '#'")
if (!/^[a-zA-Z0-9_-]+$/.test(anchor)) throw new Error(`Unsupported $dynamicAnchor: ${anchor}`)
safeSet(results, anchor, sub, '$dynamicAnchor')
}
})
return results
}
const hasKeywords = (schema, keywords) =>
traverse(schema, (s) => Object.keys(s).some((k) => keywords.includes(k)) || undefined) || false
const addSchemasArrayToMap = (schemas, input, optional = false) => {
if (!Array.isArray(input)) throw new Error('Expected an array of schemas')
// schema ids are extracted from the schemas themselves
for (const schema of input) {
traverse(schema, (sub) => {
const idRaw = sub.$id || sub.id
const id = idRaw && typeof idRaw === 'string' ? idRaw.replace(/#$/, '') : null // # is allowed only as the last symbol here
if (id && id.includes('://') && !id.includes('#')) {
safeSet(schemas, id, sub, "schema $id in 'schemas'")
} else if (sub === schema && !optional) {
throw new Error("Schema with missing or invalid $id in 'schemas'")
}
})
}
return schemas
}
const buildSchemas = (input, extra) => {
if (extra) return addSchemasArrayToMap(buildSchemas(input), extra, true)
if (input) {
switch (Object.getPrototypeOf(input)) {
case Object.prototype:
return new Map(Object.entries(input))
case Map.prototype:
return new Map(input)
case Array.prototype:
return addSchemasArrayToMap(new Map(), input)
}
}
throw new Error("Unexpected value for 'schemas' option")
}
module.exports = { get, joinPath, resolveReference, getDynamicAnchors, hasKeywords, buildSchemas }

View File

@@ -0,0 +1,98 @@
'use strict'
class SafeString extends String {} // used for instanceof checks
const compares = new Set(['<', '>', '<=', '>='])
const escapeCode = (code) => `\\u${code.toString(16).padStart(4, '0')}`
// Supports simple js variables only, i.e. constants and JSON-stringifiable
// Converts a variable to be safe for inclusion in JS context
// This works on top of JSON.stringify with minor fixes to negate the JS/JSON parsing differences
const jsval = (val) => {
if ([Infinity, -Infinity, NaN, undefined, null].includes(val)) return `${val}`
const primitive = ['string', 'boolean', 'number'].includes(typeof val)
if (!primitive) {
if (typeof val !== 'object') throw new Error('Unexpected value type')
const proto = Object.getPrototypeOf(val)
const ok = (proto === Array.prototype && Array.isArray(val)) || proto === Object.prototype
if (!ok) throw new Error('Unexpected object given as value')
}
return (
JSON.stringify(val)
// JSON context and JS eval context have different handling of __proto__ property name
// Refs: https://www.ecma-international.org/ecma-262/#sec-json.parse
// Refs: https://www.ecma-international.org/ecma-262/#sec-__proto__-property-names-in-object-initializers
// Replacement is safe because it's the only way that encodes __proto__ property in JSON and
// it can't occur inside strings or other properties, due to the leading `"` and traling `":`
.replace(/([{,])"__proto__":/g, '$1["__proto__"]:')
// The above line should cover all `"__proto__":` occurances except for `"...\"__proto__":`
.replace(/[^\\]"__proto__":/g, () => {
/* c8 ignore next */
throw new Error('Unreachable')
})
// https://v8.dev/features/subsume-json#security, e.g. {'\u2028':0} on Node.js 8
.replace(/[\u2028\u2029]/g, (char) => escapeCode(char.charCodeAt(0)))
)
}
const format = (fmt, ...args) => {
const res = fmt.replace(/%[%drscjw]/g, (match) => {
if (match === '%%') return '%'
if (args.length === 0) throw new Error('Unexpected arguments count')
const val = args.shift()
switch (match) {
case '%d':
if (typeof val === 'number') return val
throw new Error('Expected a number')
case '%r':
// String(regex) is not ok on Node.js 10 and below: console.log(String(new RegExp('\n')))
if (val instanceof RegExp) return format('new RegExp(%j, %j)', val.source, val.flags)
throw new Error('Expected a RegExp instance')
case '%s':
if (val instanceof SafeString) return val
throw new Error('Expected a safe string')
case '%c':
if (compares.has(val)) return val
throw new Error('Expected a compare op')
case '%j':
return jsval(val)
case '%w':
if (Number.isInteger(val) && val >= 0) return ' '.repeat(val)
throw new Error('Expected a non-negative integer for indentation')
}
/* c8 ignore next */
throw new Error('Unreachable')
})
if (args.length !== 0) throw new Error('Unexpected arguments count')
return new SafeString(res)
}
const safe = (string) => {
if (!/^[a-z][a-z0-9_]*$/i.test(string)) throw new Error('Does not look like a safe id')
return new SafeString(string)
}
// too dangereous to export, use with care
const safewrap = (fun) => (...args) => {
if (!args.every((arg) => arg instanceof SafeString)) throw new Error('Unsafe arguments')
return new SafeString(fun(...args))
}
const safepriority = (arg) =>
// simple expression and single brackets can not break priority
/^[a-z][a-z0-9_().]*$/i.test(arg) || /^\([^()]+\)$/i.test(arg) ? arg : format('(%s)', arg)
const safeor = safewrap(
(...args) => (args.some((arg) => `${arg}` === 'true') ? 'true' : args.join(' || ') || 'false')
)
const safeand = safewrap(
(...args) => (args.some((arg) => `${arg}` === 'false') ? 'false' : args.join(' && ') || 'true')
)
const safenot = (arg) => {
if (`${arg}` === 'true') return safe('false')
if (`${arg}` === 'false') return safe('true')
return format('!%s', safepriority(arg))
}
// this function is priority-safe, unlike safeor, hence it's exported and safeor is not atm
const safenotor = (...args) => safenot(safeor(...args))
module.exports = { format, safe, safeand, safenot, safenotor }

View File

@@ -0,0 +1,95 @@
'use strict'
// for correct Unicode code points processing
// https://mathiasbynens.be/notes/javascript-unicode#accounting-for-astral-symbols
const stringLength = (string) =>
/[\uD800-\uDFFF]/.test(string) ? [...string].length : string.length
// A isMultipleOf B: shortest decimal denoted as A % shortest decimal denoted as B === 0
// Optimized, coherence checks and precomputation are outside of this method
// If we get an Infinity when we multiply by the factor (which is always a power of 10), we just undo that instead of always returning false
const isMultipleOf = (value, divisor, factor, factorMultiple) => {
if (value % divisor === 0) return true
let multiple = value * factor
if (multiple === Infinity || multiple === -Infinity) multiple = value
if (multiple % factorMultiple === 0) return true
const normal = Math.floor(multiple + 0.5)
return normal / factor === value && normal % factorMultiple === 0
}
// supports only JSON-stringifyable objects, defaults to false for unsupported
// also uses ===, not Object.is, i.e. 0 === -0, NaN !== NaN
// symbols and non-enumerable properties are ignored!
const deepEqual = (obj, obj2) => {
if (obj === obj2) return true
if (!obj || !obj2 || typeof obj !== typeof obj2) return false
if (obj !== obj2 && typeof obj !== 'object') return false
const proto = Object.getPrototypeOf(obj)
if (proto !== Object.getPrototypeOf(obj2)) return false
if (proto === Array.prototype) {
if (!Array.isArray(obj) || !Array.isArray(obj2)) return false
if (obj.length !== obj2.length) return false
return obj.every((x, i) => deepEqual(x, obj2[i]))
} else if (proto === Object.prototype) {
const [keys, keys2] = [Object.keys(obj), Object.keys(obj2)]
if (keys.length !== keys2.length) return false
const keyset2 = new Set([...keys, ...keys2])
return keyset2.size === keys.length && keys.every((key) => deepEqual(obj[key], obj2[key]))
}
return false
}
const unique = (array) => {
if (array.length < 2) return true
if (array.length === 2) return !deepEqual(array[0], array[1])
const objects = []
const primitives = array.length > 20 ? new Set() : null
let primitivesCount = 0
let pos = 0
for (const item of array) {
if (typeof item === 'object') {
objects.push(item)
} else if (primitives) {
primitives.add(item)
if (primitives.size !== ++primitivesCount) return false
} else {
if (array.indexOf(item, pos + 1) !== -1) return false
}
pos++
}
for (let i = 1; i < objects.length; i++)
for (let j = 0; j < i; j++) if (deepEqual(objects[i], objects[j])) return false
return true
}
const deBase64 = (string) => {
if (typeof Buffer !== 'undefined') return Buffer.from(string, 'base64').toString('utf-8')
const b = atob(string)
return new TextDecoder('utf-8').decode(new Uint8Array(b.length).map((_, i) => b.charCodeAt(i)))
}
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty)
// special handling for stringification
hasOwn[Symbol.for('toJayString')] = 'Function.prototype.call.bind(Object.prototype.hasOwnProperty)'
// Used for error generation. Affects error performance, optimized
const pointerPart = (s) => (/~\//.test(s) ? `${s}`.replace(/~/g, '~0').replace(/\//g, '~1') : s)
const toPointer = (path) => (path.length === 0 ? '#' : `#/${path.map(pointerPart).join('/')}`)
const errorMerge = ({ keywordLocation, instanceLocation }, schemaBase, dataBase) => ({
keywordLocation: `${schemaBase}${keywordLocation.slice(1)}`,
instanceLocation: `${dataBase}${instanceLocation.slice(1)}`,
})
const propertyIn = (key, [properties, patterns]) =>
properties.includes(true) ||
properties.some((prop) => prop === key) ||
patterns.some((pattern) => new RegExp(pattern, 'u').test(key))
// id is verified to start with '#' at compile time, hence using plain objects is safe
const dynamicResolve = (anchors, id) => (anchors.filter((x) => x[id])[0] || {})[id]
const extraUtils = { toPointer, pointerPart, errorMerge, propertyIn, dynamicResolve }
module.exports = { stringLength, isMultipleOf, deepEqual, unique, deBase64, hasOwn, ...extraUtils }

View File

@@ -0,0 +1,63 @@
'use strict'
const { safe } = require('./safe-format')
const caches = new WeakMap()
// Given a scope object, generates new symbol/loop/pattern/format/ref variable names,
// also stores in-scope format/ref mapping to variable names
const scopeMethods = (scope) => {
// cache meta info for known scope variables, per meta type
if (!caches.has(scope))
caches.set(scope, { sym: new Map(), ref: new Map(), format: new Map(), pattern: new Map() })
const cache = caches.get(scope)
// Generic variable names, requires a base name aka prefix
const gensym = (name) => {
if (!cache.sym.get(name)) cache.sym.set(name, 0)
const index = cache.sym.get(name)
cache.sym.set(name, index + 1)
return safe(`${name}${index}`)
}
// Regexp pattern names
const genpattern = (p) => {
if (cache.pattern.has(p)) return cache.pattern.get(p)
const n = gensym('pattern')
scope[n] = new RegExp(p, 'u')
cache.pattern.set(p, n)
return n
}
// Loop variable names
if (!cache.loop) cache.loop = 'ijklmnopqrstuvxyz'.split('')
const genloop = () => {
const v = cache.loop.shift()
cache.loop.push(`${v}${v[0]}`)
return safe(v)
}
// Reference (validator function) names
const getref = (sub) => cache.ref.get(sub)
const genref = (sub) => {
const n = gensym('ref')
cache.ref.set(sub, n)
return n
}
// Format validation function names
const genformat = (impl) => {
let n = cache.format.get(impl)
if (!n) {
n = gensym('format')
scope[n] = impl
cache.format.set(impl, n)
}
return n
}
return { gensym, genpattern, genloop, getref, genref, genformat }
}
module.exports = { scopeMethods }

128
frontend/node_modules/@exodus/schemasafe/src/tracing.js generated vendored Normal file
View File

@@ -0,0 +1,128 @@
'use strict'
/* This file implements operations for static tracing of evaluated items/properties, which is also
* used to determine whether dynamic evaluated tracing is required or the schema can be compiled
* with only statical checks.
*
* That is done by keeping track of evaluated and potentially evaluated and accounting to that
* while doing merges and intersections.
*
* isDynamic() checks that all potentially evaluated are also definitely evaluated, seperately
* for items and properties, for use with unevaluatedItems and unevaluatedProperties.
*
* WARNING: it is important that this doesn't produce invalid information. i.e.:
* * Extra properties or patterns, too high items
* * Missing dyn.properties or dyn.patterns, too low dyn.items
* * Extra fullstring flag or required entries
* * Missing types, if type is present
* * Missing unknown or dyn.item
*
* The other way around is non-optimal but safe.
*
* null means any type (i.e. any type is possible, not validated)
* true in properties means any property (i.e. all properties were evaluated)
* fullstring means that the object is not an unvalidated string (i.e. is either validated or not a string)
* unknown means that there could be evaluated items or properties unknown to both top-level or dyn
* dyn.item (bool) means there could be possible specific evaluated items, e.g. from "contains".
*
* For normalization:
* 1. If type is applicable:
* * dyn.items >= items,
* * dyn.properties includes properties
* * dyn.patterns includes patterns.
* 2. If type is not applicable, the following rules apply:
* * `fullstring = true` if `string` type is not applicable
* * `items = Infinity`, `dyn.item = false`, `dyn.items = 0` if `array` type is not applicable
* * `properties = [true]`, `dyn.properties = []` if `object` type is not applicable
* * `patterns = dyn.patterns = []` if `object` type is not applicable
* * `required = []` if `object` type is not applicable
*
* That allows to simplify the `or` operation.
*/
const merge = (a, b) => [...new Set([...a, ...b])].sort()
const intersect = (a, b) => a.filter((x) => b.includes(x))
const wrapArgs = (f) => (...args) => f(...args.map(normalize))
const wrapFull = (f) => (...args) => normalize(f(...args.map(normalize)))
const typeIsNot = (type, t) => type && !type.includes(t) // type=null means any and includes anything
const normalize = ({ type = null, dyn: d = {}, ...A }) => ({
type: type ? [...type].sort() : type,
items: typeIsNot(type, 'array') ? Infinity : A.items || 0,
properties: typeIsNot(type, 'object') ? [true] : [...(A.properties || [])].sort(),
patterns: typeIsNot(type, 'object') ? [] : [...(A.patterns || [])].sort(),
required: typeIsNot(type, 'object') ? [] : [...(A.required || [])].sort(),
fullstring: typeIsNot(type, 'string') || A.fullstring || false,
dyn: {
item: typeIsNot(type, 'array') ? false : d.item || false,
items: typeIsNot(type, 'array') ? 0 : Math.max(A.items || 0, d.items || 0),
properties: typeIsNot(type, 'object') ? [] : merge(A.properties || [], d.properties || []),
patterns: typeIsNot(type, 'object') ? [] : merge(A.patterns || [], d.patterns || []),
},
unknown: (A.unknown && !(typeIsNot(type, 'object') && typeIsNot(type, 'array'))) || false,
})
const initTracing = () => normalize({})
// Result means that both sets A and B are correct
// type is intersected, lists of known properties are merged
const andDelta = wrapFull((A, B) => ({
type: A.type && B.type ? intersect(A.type, B.type) : A.type || B.type || null,
items: Math.max(A.items, B.items),
properties: merge(A.properties, B.properties),
patterns: merge(A.patterns, B.patterns),
required: merge(A.required, B.required),
fullstring: A.fullstring || B.fullstring,
dyn: {
item: A.dyn.item || B.dyn.item,
items: Math.max(A.dyn.items, B.dyn.items),
properties: merge(A.dyn.properties, B.dyn.properties),
patterns: merge(A.dyn.patterns, B.dyn.patterns),
},
unknown: A.unknown || B.unknown,
}))
const regtest = (pattern, value) => value !== true && new RegExp(pattern, 'u').test(value)
const intersectProps = ({ properties: a, patterns: rega }, { properties: b, patterns: regb }) => {
// properties
const af = a.filter((x) => b.includes(x) || b.includes(true) || regb.some((p) => regtest(p, x)))
const bf = b.filter((x) => a.includes(x) || a.includes(true) || rega.some((p) => regtest(p, x)))
// patterns
const ar = rega.filter((x) => regb.includes(x) || b.includes(true))
const br = regb.filter((x) => rega.includes(x) || a.includes(true))
return { properties: merge(af, bf), patterns: merge(ar, br) }
}
const inProperties = ({ properties: a, patterns: rega }, { properties: b, patterns: regb }) =>
b.every((x) => a.includes(x) || a.includes(true) || rega.some((p) => regtest(p, x))) &&
regb.every((x) => rega.includes(x) || a.includes(true))
// Result means that at least one of sets A and B is correct
// type is merged, lists of known properties are intersected, lists of dynamic properties are merged
const orDelta = wrapFull((A, B) => ({
type: A.type && B.type ? merge(A.type, B.type) : null,
items: Math.min(A.items, B.items),
...intersectProps(A, B),
required:
(typeIsNot(A.type, 'object') && B.required) ||
(typeIsNot(B.type, 'object') && A.required) ||
intersect(A.required, B.required),
fullstring: A.fullstring && B.fullstring,
dyn: {
item: A.dyn.item || B.dyn.item,
items: Math.max(A.dyn.items, B.dyn.items),
properties: merge(A.dyn.properties, B.dyn.properties),
patterns: merge(A.dyn.patterns, B.dyn.patterns),
},
unknown: A.unknown || B.unknown,
}))
const applyDelta = (stat, delta) => Object.assign(stat, andDelta(stat, delta))
const isDynamic = wrapArgs(({ unknown, items, dyn, ...stat }) => ({
items: items !== Infinity && (unknown || dyn.items > items || dyn.item),
properties: !stat.properties.includes(true) && (unknown || !inProperties(stat, dyn)),
}))
module.exports = { initTracing, andDelta, orDelta, applyDelta, isDynamic, inProperties }