diff --git a/.changeset/dull-parrots-sip.md b/.changeset/dull-parrots-sip.md new file mode 100644 index 0000000..9871617 --- /dev/null +++ b/.changeset/dull-parrots-sip.md @@ -0,0 +1,8 @@ +--- +'@journeyapps-labs/micro-streaming': major +'@journeyapps-labs/micro-codecs': major +'@journeyapps-labs/micro-errors': major +'@journeyapps-labs/micro-schema': major +--- + +Initial publish diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6dedf36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..cca759a --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +layout node +use node + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81da146 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.idea/ +.vscode +.DS_Store +.swc + +node_modules +coverage +dist + +yarn-error.log +.pnpm-debug.log + +tsconfig.tsbuildinfo diff --git a/.ncurc.json b/.ncurc.json new file mode 100644 index 0000000..18e4722 --- /dev/null +++ b/.ncurc.json @@ -0,0 +1,3 @@ +{ + "reject": ["/@opentelemetry.*/", "chalk", "node-fetch", "@types/node-fetch"] +} diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e69de29 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..e551598 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.15 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..dd651a5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "printWidth": 120, + "trailingComma": "none" +} diff --git a/README.md b/README.md index d278435..90cd311 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# micro +# JourneyApps Labs Micro + +Micro-service framework + +## Release + +### Dev release + +1. Ensure a changeset has been created `pnpm changeset` +2. Run https://github.com/journeyapps-labs/common/actions/workflows/dev-packages.yaml the workflow manually on the branch you need + +### Production release + +1. Ensure a changeset has been created `pnpm changeset` +2. Merge PR and then merge the versions PR which gets created. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..53b5057 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "micro", + "private": "true", + "packageManager": "pnpm@10.14.0", + "scripts": { + "preinstall": "npx only-allow pnpm", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx}\"", + "release": "pnpm build && pnpm changeset publish", + "build": "./node_modules/.bin/tsc --build", + "test": "pnpm run -r test", + "ncu": "ncu -u && pnpm recursive exec -- ncu -u" + }, + "devDependencies": { + "@changesets/cli": "^2.29.5", + "npm-check-updates": "^18.0.2", + "prettier": "3.6.2", + "typescript": "^5.9.2" + } +} \ No newline at end of file diff --git a/packages/codecs/package.json b/packages/codecs/package.json new file mode 100644 index 0000000..536e86f --- /dev/null +++ b/packages/codecs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@journeyapps-labs/micro-codecs", + "main": "./dist/index", + "typings": "./dist/index", + "version": "0.0.1", + "repository": "https://github.com/journeyapps-labs/journey-micro", + "files": [ + "dist/**" + ], + "scripts": { + "test": "vitest" + }, + "dependencies": { + "@types/node": "^20.17.6", + "bson": "^6.7.0", + "ts-codec": "^1.3.0", + "vitest": "^3.2.4" + } +} diff --git a/packages/codecs/src/codecs.ts b/packages/codecs/src/codecs.ts new file mode 100644 index 0000000..144fe41 --- /dev/null +++ b/packages/codecs/src/codecs.ts @@ -0,0 +1,128 @@ +import * as t from 'ts-codec'; +import * as bson from 'bson'; + +export const buffer = t.codec( + 'Buffer', + (buffer) => { + if (!Buffer.isBuffer(buffer)) { + throw new t.TransformError([`Expected buffer but got ${typeof buffer}`]); + } + return buffer.toString('base64'); + }, + (buffer) => Buffer.from(buffer, 'base64') +); + +export const date = t.codec( + 'Date', + (date) => { + if (!(date instanceof Date)) { + throw new t.TransformError([`Expected Date but got ${typeof date}`]); + } + return date.toISOString(); + }, + (date) => { + const parsed = new Date(date); + if (isNaN(parsed.getTime())) { + throw new t.TransformError([`Invalid date`]); + } + return parsed; + } +); + +const assertObjectId = (value: any) => { + if (!bson.ObjectId.isValid(value)) { + throw new t.TransformError([`Expected an ObjectId but got ${typeof value}`]); + } +}; +export const ObjectId = t.codec( + 'ObjectId', + (id) => { + assertObjectId(id); + return id.toHexString(); + }, + (id) => { + assertObjectId(id); + return new bson.ObjectId(id); + } +); + +const assertObjectWithField = (field: string, data: any) => { + if (typeof data !== 'object') { + throw new t.TransformError([`Expected an object but got ${typeof data}`]); + } + if (!(field in data)) { + throw new t.TransformError([`Expected ${field} to be a member of object`]); + } +}; +export const ResourceId = t.codec<{ _id: bson.ObjectId }, { id: string }>( + 'ResourceId', + (data) => { + assertObjectWithField('_id', data); + return { + id: ObjectId.encode(data._id) + }; + }, + (data) => { + assertObjectWithField('id', data); + return { + _id: ObjectId.decode(data.id) + }; + } +); + +export const Timestamps = t.object({ + created_at: date, + updated_at: date +}); + +export const Resource = ResourceId.and(Timestamps); + +export const QueryFilter = t.object({ + exists: t.boolean +}); + +export const makeQueryFilter = (type: t.AnyCodec) => { + return type.or(t.array(type)).or(QueryFilter).optional(); +}; + +export const FilterProperties = ( + type: T +): t.Codec< + { + [K in keyof t.Encoded]?: t.Ix[K] | t.Ix[K][] | t.Ix; + }, + { + [K in keyof t.Encoded]?: t.Ox[K] | t.Ox[K][] | t.Ox; + }, + 'FilterProperties' +> => { + let codecs = new Map(); + + const addCodecs = (codec: t.ObjectCodec) => { + if (codec.props?.shape) { + Object.keys(codec.props.shape).forEach((k) => { + codecs.set(k, codec.props.shape[k]); + }); + } + }; + + if (type._tag === t.CodecType.Object) { + addCodecs(type); + } else if (type._tag === t.CodecType.Intersection) { + type.props.codecs.forEach((c: t.AnyCodec) => { + addCodecs(c); + }); + } + + t.object({ + test: t.string + }); + + // @ts-ignore + return t.object( + Array.from(codecs.keys()).reduce((prev: any, cur) => { + prev[cur] = makeQueryFilter(codecs.get(cur)!); + return prev; + }, {}) + ); +}; diff --git a/packages/codecs/src/index.ts b/packages/codecs/src/index.ts new file mode 100644 index 0000000..b086af5 --- /dev/null +++ b/packages/codecs/src/index.ts @@ -0,0 +1,2 @@ +export * from './parsers'; +export * from './codecs'; diff --git a/packages/codecs/src/parsers.ts b/packages/codecs/src/parsers.ts new file mode 100644 index 0000000..038636e --- /dev/null +++ b/packages/codecs/src/parsers.ts @@ -0,0 +1,60 @@ +import * as codecs from './codecs'; +import * as t from 'ts-codec'; + +export const ObjectIdParser = t.createParser(codecs.ObjectId._tag, (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: 'string' }; + } + case t.TransformTarget.Decoded: { + return { bsonType: 'ObjectId' }; + } + } +}); + +export const ResourceIdParser = t.createParser(codecs.ResourceId._tag, (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { + type: 'object', + properties: { + id: { type: 'string' } + }, + required: ['id'] + }; + } + case t.TransformTarget.Decoded: { + return { + type: 'object', + properties: { + _id: { bsonType: 'ObjectId' } + }, + required: ['_id'] + }; + } + } +}); + +export const DateParser = t.createParser(codecs.date._tag, (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: 'string' }; + } + case t.TransformTarget.Decoded: { + return { nodeType: 'date' }; + } + } +}); + +export const BufferParser = t.createParser(codecs.buffer._tag, (_, { target }) => { + switch (target) { + case t.TransformTarget.Encoded: { + return { type: 'string' }; + } + case t.TransformTarget.Decoded: { + return { nodeType: 'buffer' }; + } + } +}); + +export const parsers = [ObjectIdParser, ResourceIdParser, DateParser, BufferParser]; diff --git a/packages/codecs/tests/__snapshots__/parsers.test.ts.snap b/packages/codecs/tests/__snapshots__/parsers.test.ts.snap new file mode 100644 index 0000000..ae79196 --- /dev/null +++ b/packages/codecs/tests/__snapshots__/parsers.test.ts.snap @@ -0,0 +1,190 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`parsers > should correctly generate Filterable schema from Object 1`] = ` +{ + "decoded": { + "additionalProperties": false, + "definitions": {}, + "properties": { + "test1": { + "anyOf": [ + { + "type": "string", + }, + { + "items": { + "type": "string", + }, + "type": "array", + }, + { + "additionalProperties": false, + "properties": { + "exists": { + "type": "boolean", + }, + }, + "required": [ + "exists", + ], + "type": "object", + }, + ], + }, + "test2": { + "anyOf": [ + { + "type": "number", + }, + { + "items": { + "type": "number", + }, + "type": "array", + }, + { + "additionalProperties": false, + "properties": { + "exists": { + "type": "boolean", + }, + }, + "required": [ + "exists", + ], + "type": "object", + }, + ], + }, + }, + "required": [], + "type": "object", + }, + "encoded": { + "additionalProperties": false, + "definitions": {}, + "properties": { + "test1": { + "anyOf": [ + { + "type": "string", + }, + { + "items": { + "type": "string", + }, + "type": "array", + }, + { + "additionalProperties": false, + "properties": { + "exists": { + "type": "boolean", + }, + }, + "required": [ + "exists", + ], + "type": "object", + }, + ], + }, + "test2": { + "anyOf": [ + { + "type": "number", + }, + { + "items": { + "type": "number", + }, + "type": "array", + }, + { + "additionalProperties": false, + "properties": { + "exists": { + "type": "boolean", + }, + }, + "required": [ + "exists", + ], + "type": "object", + }, + ], + }, + }, + "required": [], + "type": "object", + }, +} +`; + +exports[`parsers > should correctly generate ObjectId schemas 1`] = ` +{ + "decoded": { + "bsonType": "ObjectId", + "definitions": {}, + }, + "encoded": { + "definitions": {}, + "type": "string", + }, +} +`; + +exports[`parsers > should correctly generate ResourceId schemas 1`] = ` +{ + "decoded": { + "definitions": {}, + "properties": { + "_id": { + "bsonType": "ObjectId", + }, + }, + "required": [ + "_id", + ], + "type": "object", + }, + "encoded": { + "definitions": {}, + "properties": { + "id": { + "type": "string", + }, + }, + "required": [ + "id", + ], + "type": "object", + }, +} +`; + +exports[`parsers > should correctly generate buffer schemas 1`] = ` +{ + "decoded": { + "definitions": {}, + "nodeType": "buffer", + }, + "encoded": { + "definitions": {}, + "type": "string", + }, +} +`; + +exports[`parsers > should correctly generate date schemas 1`] = ` +{ + "decoded": { + "definitions": {}, + "nodeType": "date", + }, + "encoded": { + "definitions": {}, + "type": "string", + }, +} +`; diff --git a/packages/codecs/tests/parsers.test.ts b/packages/codecs/tests/parsers.test.ts new file mode 100644 index 0000000..a8249d2 --- /dev/null +++ b/packages/codecs/tests/parsers.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; + +import * as codecs from '../src'; +import * as t from 'ts-codec'; + +const generate = (codec: t.AnyCodec) => { + const encoded = t.generateJSONSchema(codec, { + parsers: codecs.parsers, + target: t.TransformTarget.Encoded + }); + const decoded = t.generateJSONSchema(codec, { + parsers: codecs.parsers, + target: t.TransformTarget.Decoded + }); + + return { encoded, decoded }; +}; + +describe('parsers', () => { + it('should correctly generate date schemas', () => { + expect(generate(codecs.date)).toMatchSnapshot(); + }); + + it('should correctly generate buffer schemas', () => { + expect(generate(codecs.buffer)).toMatchSnapshot(); + }); + + it('should correctly generate ObjectId schemas', () => { + expect(generate(codecs.ObjectId)).toMatchSnapshot(); + }); + + it('should correctly generate ResourceId schemas', () => { + expect(generate(codecs.ResourceId)).toMatchSnapshot(); + }); + + it('should correctly generate Filterable schema from Object', () => { + const filterObject1 = t.object({ + test1: t.string + }); + + const filterObject2 = t.object({ + test2: t.number + }); + + const filterObject3 = t.object({ + test1: t.string + }); + + const filterObject4 = filterObject1.and(filterObject2).and(filterObject3); + + const filterableObject = codecs.FilterProperties(filterObject4); + expect(generate(filterableObject)).toMatchSnapshot(); + }); +}); diff --git a/packages/codecs/tsconfig.json b/packages/codecs/tsconfig.json new file mode 100644 index 0000000..5dcb608 --- /dev/null +++ b/packages/codecs/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/errors/package.json b/packages/errors/package.json new file mode 100644 index 0000000..f79c4e9 --- /dev/null +++ b/packages/errors/package.json @@ -0,0 +1,17 @@ +{ + "name": "@journeyapps-labs/micro-errors", + "main": "./dist/index", + "typings": "./dist/index", + "version": "0.0.1", + "repository": "https://github.com/journeyapps-labs/journey-micro", + "files": [ + "dist/**" + ], + "scripts": { + "test": "vitest" + }, + "devDependencies": { + "@types/node": "^20.17.6", + "vitest": "^3.2.4" + } +} diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts new file mode 100644 index 0000000..ba9c10c --- /dev/null +++ b/packages/errors/src/errors.ts @@ -0,0 +1,174 @@ +/** + * This error class is designed to give consumers of Journey Micro + * a consistent way of "throwing" errors. Specifically, these errors + * will give clients to Journey Micro implementations two things: + * + * 1. An consistent, static error code by which to easily classify errors + * 2. An error message intended for humans + * + * Errors will usually assume that there is some client side error and default to 400 for + * a rest-like response. This can be changed however to more accurately, in restful terms, + * indicate what went wrong. + * + */ + +export enum ErrorSeverity { + INFO = 'info', + WARNING = 'warning', + ERROR = 'error' +} + +export type ErrorData = { + name?: string; + /** + * enum such as `VALIDATION_ERROR` etc.. + */ + code: string; + /** + * Human-readable description of the error. This is typically shown to the user in various UI + */ + description: string; + severity?: ErrorSeverity; + details?: string; + /** + * Status code (400 / 500 etc..) + */ + status?: number; + /** + * Multi-line stack trace + */ + stack?: string; + + origin?: string; + + trace_id?: string; + /** + * If this error should be reported to tools such as Sentry. + * In some cases we may deliberately throw validation errors to show information to end users which are picked + * up by our routes, and then we don't want these reported (in which case set this to false) + * + * @default true + */ + report?: boolean; +}; + +export class JourneyError extends Error { + is_journey_error = true; + + errorData: ErrorData; + + constructor(data: ErrorData) { + super(`[${data.code}] ${data.description}\n ${data.details}`); + + this.errorData = data; + if (data.stack) { + this.stack = data.stack; + } + + this.name = data.name || this.constructor.name; + this.errorData.name = this.name; + } + + get shouldReport() { + return this.errorData.report ?? true; + } + + toString() { + return this.stack; + } + + toJSON(): ErrorData { + if (process.env.NODE_ENV !== 'production') { + return this.errorData; + } + return { + name: this.errorData.name, + code: this.errorData.code, + status: this.errorData.status, + description: this.errorData.description, + details: this.errorData.details, + trace_id: this.errorData.trace_id, + severity: this.errorData.severity, + origin: this.errorData.origin + }; + } + + setTraceId(id: string) { + this.errorData.trace_id = id; + } +} + +export class ValidationError extends JourneyError { + static CODE = 'VALIDATION_ERROR'; + constructor(errors: any) { + super({ + code: ValidationError.CODE, + status: 400, + description: 'Validation failed', + details: JSON.stringify(errors), + report: false + }); + } +} + +export class AuthorizationError extends JourneyError { + static CODE = 'AUTHORIZATION'; + constructor(errors: any) { + super({ + code: AuthorizationError.CODE, + status: 401, + description: 'Authorization failed', + details: errors + }); + } +} + +export class InternalServerError extends JourneyError { + static CODE = 'INTERNAL_SERVER_ERROR'; + constructor(err: Error) { + super({ + code: InternalServerError.CODE, + severity: ErrorSeverity.ERROR, + status: 500, + description: 'Something went wrong', + details: err.message, + stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined + }); + } +} + +export class ResourceNotFound extends JourneyError { + static CODE = 'RESOURCE_NOT_FOUND'; + + /** + * @deprecated Use the (resource, id) constructor instead. + * @param id + */ + constructor(id: string); + constructor(resource: string, id: string); + + constructor(resource: string, id?: string) { + const combinedId = id ? `${resource}/${id}` : resource; + super({ + code: ResourceNotFound.CODE, + status: 404, + description: 'The requested resource does not exist on this server', + details: `The resource ${combinedId} does not exist on this server`, + severity: ErrorSeverity.INFO + }); + } +} + +export class ResourceConflict extends JourneyError { + static CODE = 'RESOURCE_CONFLICT'; + + constructor(details: string) { + super({ + code: ResourceConflict.CODE, + status: 409, + description: 'The specified resource already exists on this server', + details: details, + severity: ErrorSeverity.INFO + }); + } +} diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts new file mode 100644 index 0000000..37810c0 --- /dev/null +++ b/packages/errors/src/index.ts @@ -0,0 +1,2 @@ +export * from './errors'; +export * from './utils'; diff --git a/packages/errors/src/utils.ts b/packages/errors/src/utils.ts new file mode 100644 index 0000000..cbee7fb --- /dev/null +++ b/packages/errors/src/utils.ts @@ -0,0 +1,20 @@ +import { ErrorData, JourneyError } from './errors'; + +export const isJourneyError = (err: any): err is JourneyError => { + const matches = err instanceof JourneyError || err.is_journey_error; + return !!matches; +}; + +export const getErrorData = (err: Error | any): ErrorData | undefined => { + if (!isJourneyError(err)) { + return; + } + return err.toJSON(); +}; + +export const matchesErrorCode = (err: Error | any, code: string) => { + if (isJourneyError(err)) { + return err.errorData.code === code; + } + return false; +}; diff --git a/packages/errors/tests/__snapshots__/errors.test.ts.snap b/packages/errors/tests/__snapshots__/errors.test.ts.snap new file mode 100644 index 0000000..93bda28 --- /dev/null +++ b/packages/errors/tests/__snapshots__/errors.test.ts.snap @@ -0,0 +1,10 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`errors > utilities should properly match a journey error 1`] = ` +{ + "code": "CUSTOM_JOURNEY_ERROR", + "description": "This is a custom error", + "details": "this is some more detailed information", + "name": "CustomJourneyError", +} +`; diff --git a/packages/errors/tests/errors.test.ts b/packages/errors/tests/errors.test.ts new file mode 100644 index 0000000..d3ad058 --- /dev/null +++ b/packages/errors/tests/errors.test.ts @@ -0,0 +1,49 @@ +import { describe, test, expect } from 'vitest'; + +import * as micro_errors from '../src'; + +class CustomJourneyError extends micro_errors.JourneyError { + constructor() { + super({ + code: 'CUSTOM_JOURNEY_ERROR', + description: 'This is a custom error', + details: 'this is some more detailed information' + }); + } +} + +describe('errors', () => { + test('it should respond to instanceof checks', () => { + const error = new CustomJourneyError(); + + expect(error instanceof Error).toBe(true); + expect(error instanceof micro_errors.JourneyError).toBe(true); + expect(error.name).toBe('CustomJourneyError'); + }); + + test('it should serialize properly', () => { + const error = new CustomJourneyError(); + + // The error stack will contain host specific path information. We only care about the header + // anyway and that the stack is shown - indicated by the initial `at` text + const initial = `CustomJourneyError: [CUSTOM_JOURNEY_ERROR] This is a custom error + this is some more detailed information + at`; + + expect(`${error}`.startsWith(initial)).toBe(true); + }); + + test('utilities should properly match a journey error', () => { + const standard_error = new Error('non-journey error'); + const error = new CustomJourneyError(); + + expect(micro_errors.isJourneyError(standard_error)).toBe(false); + expect(micro_errors.isJourneyError(error)).toBe(true); + + expect(micro_errors.matchesErrorCode(error, 'CUSTOM_JOURNEY_ERROR')).toBe(true); + expect(micro_errors.matchesErrorCode(standard_error, 'CUSTOM_JOURNEY_ERROR')).toBe(false); + + expect(micro_errors.getErrorData(error)).toMatchSnapshot(); + expect(micro_errors.getErrorData(standard_error)).toBe(undefined); + }); +}); diff --git a/packages/errors/tsconfig.json b/packages/errors/tsconfig.json new file mode 100644 index 0000000..c546971 --- /dev/null +++ b/packages/errors/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "dist", + "rootDir": "src" + }, + "include": ["./src/**/*.ts"] +} diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 0000000..c395e87 --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,23 @@ +{ + "name": "@journeyapps-labs/micro-schema", + "main": "./dist/index", + "typings": "./dist/index", + "version": "0.0.1", + "repository": "https://github.com/journeyapps-labs/journey-micro", + "scripts": { + "test": "vitest" + }, + "files": [ + "dist/**" + ], + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.1.0", + "@journeyapps-labs/micro-codecs": "workspace:^", + "@journeyapps-labs/micro-errors": "workspace:^", + "ajv": "^8.11.2", + "better-ajv-errors": "^1.2.0", + "ts-codec": "^1.3.0", + "vitest": "^3.2.4", + "zod": "^3.19.1" + } +} diff --git a/packages/schema/src/better-ajv-errors.d.ts b/packages/schema/src/better-ajv-errors.d.ts new file mode 100644 index 0000000..0a8a5a6 --- /dev/null +++ b/packages/schema/src/better-ajv-errors.d.ts @@ -0,0 +1,26 @@ +declare module 'better-ajv-errors' { + import type { ErrorObject } from 'ajv'; + + export interface IOutputError { + start: { line: number; column: number; offset: number }; + // Optional for required + end?: { line: number; column: number; offset: number }; + error: string; + suggestion?: string; + } + + export interface IInputOptions { + format?: 'cli' | 'js'; + indent?: number | null; + + /** Raw JSON used when highlighting error location */ + json?: string | null; + } + + export default function ( + schema: S, + data: T, + errors: Array, + options?: Options + ): Options extends { format: 'js' } ? Array : string; +} diff --git a/packages/schema/src/definitions.ts b/packages/schema/src/definitions.ts new file mode 100644 index 0000000..e293df3 --- /dev/null +++ b/packages/schema/src/definitions.ts @@ -0,0 +1,20 @@ +export type JSONSchema = { + definitions?: Record; + [key: string]: any; +}; + +export type IValidationRight = { + valid: true; +}; + +export type ValidationLeft = { + valid: false; + errors: T; +}; + +export type ValidationResponse = ValidationLeft | IValidationRight; + +export type MicroValidator = { + validate: (data: T) => ValidationResponse; + toJSONSchema?: () => JSONSchema; +}; diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts new file mode 100644 index 0000000..a6f21a1 --- /dev/null +++ b/packages/schema/src/index.ts @@ -0,0 +1,6 @@ +export * from './json-schema/keywords'; +export * from './json-schema/parser'; + +export * from './definitions'; +export * from './validators'; +export * from './utils'; diff --git a/packages/schema/src/json-schema/keywords.ts b/packages/schema/src/json-schema/keywords.ts new file mode 100644 index 0000000..06ee104 --- /dev/null +++ b/packages/schema/src/json-schema/keywords.ts @@ -0,0 +1,27 @@ +import * as ajv from 'ajv'; + +export const BufferNodeType: ajv.KeywordDefinition = { + keyword: 'nodeType', + metaSchema: { + type: 'string', + enum: ['buffer', 'date'] + }, + error: { + message: ({ schemaCode }) => { + return ajv.str`should be a ${schemaCode}`; + } + }, + code(context) { + switch (context.schema) { + case 'buffer': { + return context.fail(ajv._`!Buffer.isBuffer(${context.data})`); + } + case 'date': { + return context.fail(ajv._`!(${context.data} instanceof Date)`); + } + default: { + context.fail(ajv._`true`); + } + } + } +}; diff --git a/packages/schema/src/json-schema/parser.ts b/packages/schema/src/json-schema/parser.ts new file mode 100644 index 0000000..38f4593 --- /dev/null +++ b/packages/schema/src/json-schema/parser.ts @@ -0,0 +1,127 @@ +import * as schema_validator from '../validators/schema-validator'; +import * as defs from '../definitions'; + +/** + * Recursively walk a given schema resolving a list of refs that are actively used in some way by the + * root schema. This information can then later be used to prune unused definitions. + * + * This only works for top-level references to 'definitions' as this is intended to be used in + * conjunction with tools that generate schemas in deterministic ways. For a more general + * implementation one should make use of `$RefParser`. + * + * We don't use this here as it resolves to a Promise, which we want to avoid for this tool + */ +export const findUsedRefs = (schema: any, definitions = schema.definitions, cache: string[] = []): string[] => { + if (Array.isArray(schema)) { + return schema + .map((subschema) => { + return findUsedRefs(subschema, definitions, cache); + }) + .flat(); + } + + if (typeof schema === 'object') { + return Object.keys(schema).reduce((used: string[], key) => { + const value = schema[key]; + if (key === '$ref') { + if (cache.includes(value)) { + return used; + } + cache.push(value); + const subschema = definitions[value.replace('#/definitions/', '')]; + used.push(value, ...findUsedRefs(subschema, definitions, cache)); + return used; + } + if (key === 'definitions') { + return used; + } + return used.concat(findUsedRefs(value, definitions, cache)); + }, []); + } + + return []; +}; + +/** + * Prune a given subschema definitions map by comparing keys against a given collection of + * definition keys that are referenced in some way, either directly or indirectly, by the + * root schema + */ +export const pruneDefinitions = (definitions: Record, refs: string[]) => { + return Object.keys(definitions).reduce((pruned: Record, key) => { + if (refs.includes(`#/definitions/${key}`)) { + pruned[key] = definitions[key]; + } + return pruned; + }, {}); +}; + +export type CompilerFunction = () => defs.JSONSchema; +export type ValidatorFunction = ( + params?: schema_validator.CreateSchemaValidatorParams +) => schema_validator.SchemaValidator; + +export type Compilers = { + compile: CompilerFunction; + validator: ValidatorFunction; +}; +export type WithCompilers = Compilers & { + [P in keyof T]: T[P] extends Record ? WithCompilers : T[P]; +}; + +/** + * Given a JSON Schema containing a `definitions` entry, return a Proxy representation of the same + * schema which responds to `compile` and `validator` arbitrarily deep. + * + * Calling compile on a sub-schema will 'inject' the root schema `definitions` mapping and remove + * the Proxy wrapping. + * + * Calling `validator` on a sub-schema will `compile` and then create a SchemaValidator from the + * resulting schema + */ +export const parseJSONSchema = ( + schema: T, + definitions = schema.definitions +): WithCompilers => { + return new Proxy(schema, { + get(target: any, prop) { + const compile: CompilerFunction = () => { + const schema = { + definitions: definitions, + ...target + }; + + if (!schema.definitions) { + return schema; + } + + const used = findUsedRefs(schema); + return { + ...schema, + definitions: pruneDefinitions(schema.definitions, used) + }; + }; + const validator: ValidatorFunction = (options) => { + return schema_validator.createSchemaValidator(compile(), options); + }; + + if (prop === 'compile') { + return compile; + } + if (prop === 'validator') { + return validator; + } + + const subschema = target[prop]; + + if (Array.isArray(subschema)) { + return subschema; + } + if (typeof subschema !== 'object') { + return subschema; + } + + return parseJSONSchema(subschema, definitions); + } + }); +}; diff --git a/packages/schema/src/utils.ts b/packages/schema/src/utils.ts new file mode 100644 index 0000000..47dd2ad --- /dev/null +++ b/packages/schema/src/utils.ts @@ -0,0 +1,48 @@ +import * as micro_errors from '@journeyapps-labs/micro-errors'; +import * as defs from './definitions'; + +export type Schema = { + additionalProperties?: boolean | Schema; + [key: string]: any; +}; + +/** + * Utility function to strip out `additionalProperties` fields from a given JSON-Schema. This can be used + * to make a schema less strict which may be necessary for certain use-cases + */ +export const allowAdditionalProperties = (schema: T): T => { + return Object.keys(schema).reduce((next_schema: any, key) => { + const value = schema[key]; + + if (key === 'additionalProperties' && typeof value === 'boolean') { + return next_schema; + } + + if (Array.isArray(value)) { + next_schema[key] = value.map((value) => { + if (typeof value !== 'object') { + return value; + } + return allowAdditionalProperties(value); + }); + } else if (typeof value === 'object') { + next_schema[key] = allowAdditionalProperties(value); + } else { + next_schema[key] = value; + } + + return next_schema; + }, {}); +}; + +/** + * A small utility for validating some data using a MicroValidator. Will return the valid data (typed correctly) or throw + * a validation error + */ +export const validateData = (event: any, validator: defs.MicroValidator): T => { + const result = validator.validate(event); + if (!result.valid) { + throw new micro_errors.ValidationError(result.errors); + } + return event; +}; diff --git a/packages/schema/src/validators/index.ts b/packages/schema/src/validators/index.ts new file mode 100644 index 0000000..e9db392 --- /dev/null +++ b/packages/schema/src/validators/index.ts @@ -0,0 +1,3 @@ +export * from './ts-codec-validator'; +export * from './schema-validator'; +export * from './zod-validator'; diff --git a/packages/schema/src/validators/schema-validator.ts b/packages/schema/src/validators/schema-validator.ts new file mode 100644 index 0000000..270dc4c --- /dev/null +++ b/packages/schema/src/validators/schema-validator.ts @@ -0,0 +1,81 @@ +import * as keywords from '../json-schema/keywords'; +import AjvErrorFormatter from 'better-ajv-errors'; +import * as defs from '../definitions'; +import * as utils from '../utils'; +import AJV, * as ajv from 'ajv'; + +export class SchemaValidatorError extends Error { + constructor(message: string) { + super(message); + + this.name = this.constructor.name; + } +} + +export type SchemaValidator = defs.MicroValidator; + +export type CreateSchemaValidatorParams = { + ajv?: ajv.Options; + + /** + * Allow making the given schema loosely typed to allow accepting additional properties. This + * is useful in certain scenarios such as accepting kafka events that are going through a + * migration and having additional properties set + */ + allowAdditional?: boolean; + + fail_fast?: boolean; +}; + +/** + * Create a validator from a given JSON-Schema schema object. This makes uses of AJV internally + * to compile a validation function + */ +export const createSchemaValidator = ( + schema: defs.JSONSchema, + params: CreateSchemaValidatorParams = {} +): SchemaValidator => { + try { + const ajv = new AJV({ + allErrors: !(params.fail_fast ?? false), + keywords: [keywords.BufferNodeType], + ...(params.ajv || {}) + }); + + let processed_schema = schema; + if (params.allowAdditional) { + processed_schema = utils.allowAdditionalProperties(processed_schema); + } + + const validator = ajv.compile(processed_schema); + + return { + toJSONSchema: () => { + return schema; + }, + + validate: (data) => { + const valid = validator(data); + + if (!valid) { + const errors = AjvErrorFormatter(processed_schema, data, validator.errors || [], { + format: 'js' + })?.map((error) => error.error); + + return { + valid: false, + errors: errors || [] + }; + } + + return { + valid: true + }; + } + }; + } catch (err) { + // Here we re-throw the error because the original error thrown by AJV has a deep stack that + // obfuscates the location of the error in application code + throw new SchemaValidatorError(err.message); + } +}; diff --git a/packages/schema/src/validators/ts-codec-validator.ts b/packages/schema/src/validators/ts-codec-validator.ts new file mode 100644 index 0000000..72b42f5 --- /dev/null +++ b/packages/schema/src/validators/ts-codec-validator.ts @@ -0,0 +1,27 @@ +import * as codecs from '@journeyapps-labs/micro-codecs'; +import * as schema_validator from './schema-validator'; +import * as defs from '../definitions'; +import * as t from 'ts-codec'; + +export type TsCodecValidator< + C extends t.AnyCodec, + T extends t.TransformTarget = t.TransformTarget.Encoded +> = T extends t.TransformTarget.Encoded ? defs.MicroValidator> : defs.MicroValidator>; + +type ValidatorOptions = Partial> & { + target?: T; +}; + +/** + * Create a validator from a given ts-codec codec + */ +export const createTsCodecValidator = ( + codec: C, + options?: ValidatorOptions +): TsCodecValidator => { + const schema = t.generateJSONSchema(codec, { + ...(options || {}), + parsers: [...(options?.parsers ?? []), ...codecs.parsers] + }); + return schema_validator.createSchemaValidator(schema); +}; diff --git a/packages/schema/src/validators/zod-validator.ts b/packages/schema/src/validators/zod-validator.ts new file mode 100644 index 0000000..c83658d --- /dev/null +++ b/packages/schema/src/validators/zod-validator.ts @@ -0,0 +1,29 @@ +import * as defs from '../definitions'; +import * as t from 'zod'; + +export type ZodValidator> = defs.MicroValidator> & { + schema: T; +}; + +/** + * Create a validator from a given Zod schema + * https://github.com/colinhacks/zod + */ +export const createZodValidator = >(schema: T): ZodValidator => { + return { + schema: schema, + validate: (data) => { + const result = schema.safeParse(data); + if (!result.success) { + return { + valid: false, + errors: [JSON.stringify(result.error.format())] + }; + } + + return { + valid: true + }; + } + }; +}; diff --git a/packages/schema/tests/__snapshots__/parser.test.ts.snap b/packages/schema/tests/__snapshots__/parser.test.ts.snap new file mode 100644 index 0000000..52d9475 --- /dev/null +++ b/packages/schema/tests/__snapshots__/parser.test.ts.snap @@ -0,0 +1,73 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`schema-tools > it should correctly prune unused definitions 1`] = ` +{ + "definitions": { + "b": { + "properties": { + "prop": { + "type": "string", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + "b1": { + "properties": { + "prop": { + "$ref": "#/definitions/b", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + "c": { + "properties": { + "prop": { + "$ref": "#/definitions/c", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + }, + "properties": { + "a": { + "properties": { + "a": { + "$ref": "#/definitions/b1", + }, + "b": { + "enum": [ + "A", + ], + }, + }, + "type": "object", + }, + "b": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "a": { + "$ref": "#/definitions/c", + }, + }, + "required": [ + "a", + ], + "type": "object", + }, + ], + }, + }, + "type": "object", +} +`; diff --git a/packages/schema/tests/__snapshots__/schema-validation.test.ts.snap b/packages/schema/tests/__snapshots__/schema-validation.test.ts.snap new file mode 100644 index 0000000..5aa01b1 --- /dev/null +++ b/packages/schema/tests/__snapshots__/schema-validation.test.ts.snap @@ -0,0 +1,66 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`json-schema-validation > fails validation for json-schema 1`] = ` +{ + "errors": [ + "/name/b must be equal to one of the allowed values: A", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > fails validation for json-schema 2`] = ` +{ + "errors": [ + "/name must have required property 'a'", + "/b: oneOf must match exactly one schema in oneOf", + "/b/a: type must be number", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > fails validation for json-schema due to additional properties 1`] = ` +{ + "errors": [ + "/name Property c is not expected to be here", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > it correctly validates node types 1`] = ` +{ + "errors": [ + "/a: nodeType should be a buffer", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > it should correctly validate subschemas 1`] = ` +{ + "errors": [ + "/b: type must be string", + ], + "valid": false, +} +`; + +exports[`json-schema-validation > passes json-schema validation with additional properties when allowed 1`] = ` +{ + "valid": true, +} +`; + +exports[`json-schema-validation > passes validation for json-schema 1`] = ` +{ + "valid": true, +} +`; + +exports[`json-schema-validation > passes validation with refs 1`] = ` +{ + "valid": true, +} +`; diff --git a/packages/schema/tests/__snapshots__/ts-codec-validation.test.ts.snap b/packages/schema/tests/__snapshots__/ts-codec-validation.test.ts.snap new file mode 100644 index 0000000..9b26288 --- /dev/null +++ b/packages/schema/tests/__snapshots__/ts-codec-validation.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ts-codec validation > fails validation for runtime codec 1`] = ` +{ + "errors": [ + " must have required property 'surname'", + "/name: type must be string", + "/other/b: const must be equal to constant", + "/tuple/0: type must be string", + "/or: type must be number", + "/or: type must be string", + "/or: anyOf must match a schema in anyOf", + "/complex must have required property 'c'", + ], + "valid": false, +} +`; diff --git a/packages/schema/tests/__snapshots__/utils.test.ts.snap b/packages/schema/tests/__snapshots__/utils.test.ts.snap new file mode 100644 index 0000000..8a0c33d --- /dev/null +++ b/packages/schema/tests/__snapshots__/utils.test.ts.snap @@ -0,0 +1,91 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`utils > allow additional properties in a json schema 1`] = ` +{ + "definitions": { + "c": { + "properties": { + "prop": { + "type": "string", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + }, + "properties": { + "b": { + "oneOf": [ + { + "properties": { + "a": { + "type": "number", + }, + }, + "required": [ + "a", + ], + "type": "object", + }, + ], + }, + "d": { + "$ref": "#/definitions/c", + }, + "name": { + "properties": { + "a": { + "type": "string", + }, + "b": { + "enum": [ + "A", + ], + }, + }, + "required": [ + "a", + ], + "type": "object", + }, + }, + "required": [ + "name", + "b", + ], + "type": "object", +} +`; + +exports[`utils > it should only modify additionalProperties if it is a boolean 1`] = ` +{ + "definitions": { + "a": { + "properties": { + "prop": { + "type": "string", + }, + }, + "required": [ + "prop", + ], + "type": "object", + }, + }, + "properties": { + "name": { + "additionalProperties": { + "$ref": "#/definitions/a", + }, + "type": "object", + }, + }, + "required": [ + "name", + "b", + ], + "type": "object", +} +`; diff --git a/packages/schema/tests/__snapshots__/zod-validation.test.ts.snap b/packages/schema/tests/__snapshots__/zod-validation.test.ts.snap new file mode 100644 index 0000000..16d8c7b --- /dev/null +++ b/packages/schema/tests/__snapshots__/zod-validation.test.ts.snap @@ -0,0 +1,16 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`zod-validation > fails validation for runtime codec 1`] = ` +{ + "errors": [ + "{"_errors":[],"name":{"_errors":["Expected string, received number"]},"surname":{"_errors":["Required"]},"other":{"_errors":[],"a":{"_errors":["Required"]},"b":{"_errors":["Invalid literal value, expected \\"optional\\""]}}}", + ], + "valid": false, +} +`; + +exports[`zod-validation > passes validation for runtime codec 1`] = ` +{ + "valid": true, +} +`; diff --git a/packages/schema/tests/fixtures/schema.ts b/packages/schema/tests/fixtures/schema.ts new file mode 100644 index 0000000..3a64ec7 --- /dev/null +++ b/packages/schema/tests/fixtures/schema.ts @@ -0,0 +1,49 @@ +export default { + definitions: { + c: { + type: 'object', + properties: { + prop: { + type: 'string' + } + }, + required: ['prop'] + } + }, + + type: 'object', + properties: { + name: { + type: 'object', + properties: { + a: { + type: 'string' + }, + b: { + enum: ['A'] + } + }, + required: ['a'], + additionalProperties: false + }, + b: { + oneOf: [ + { + type: 'object', + properties: { + a: { + type: 'number' + } + }, + required: ['a'], + additionalProperties: false + } + ] + }, + d: { + $ref: '#/definitions/c' + } + }, + required: ['name', 'b'], + additionalProperties: false +}; diff --git a/packages/schema/tests/parser.test.ts b/packages/schema/tests/parser.test.ts new file mode 100644 index 0000000..73c54ef --- /dev/null +++ b/packages/schema/tests/parser.test.ts @@ -0,0 +1,84 @@ +import { describe, test, expect } from 'vitest'; + +import * as micro_schema from '../src'; + +describe('schema-tools', () => { + test('it should correctly prune unused definitions', () => { + const schema = micro_schema.parseJSONSchema({ + definitions: { + // unused, should be stripped out + a: { + type: 'object', + properties: { + prop: { + type: 'string' + } + }, + required: ['prop'] + }, + + // extended reference, should be included after walking the full schema + b: { + type: 'object', + properties: { + prop: { + type: 'string' + } + }, + required: ['prop'] + }, + b1: { + type: 'object', + properties: { + prop: { + $ref: '#/definitions/b' + } + }, + required: ['prop'] + }, + + // circular reference, should not result in the walker getting stuck + c: { + type: 'object', + properties: { + prop: { + $ref: '#/definitions/c' + } + }, + required: ['prop'] + } + }, + + type: 'object', + properties: { + a: { + type: 'object', + properties: { + a: { + $ref: '#/definitions/b1' + }, + b: { + enum: ['A'] + } + } + }, + b: { + oneOf: [ + { + type: 'object', + properties: { + a: { + $ref: '#/definitions/c' + } + }, + required: ['a'], + additionalProperties: false + } + ] + } + } + }); + + expect(schema.compile()).toMatchSnapshot(); + }); +}); diff --git a/packages/schema/tests/schema-validation.test.ts b/packages/schema/tests/schema-validation.test.ts new file mode 100644 index 0000000..0a48344 --- /dev/null +++ b/packages/schema/tests/schema-validation.test.ts @@ -0,0 +1,180 @@ +import { describe, test, it, expect } from 'vitest'; + +import base_schema from './fixtures/schema'; +import * as micro_schema from '../src'; + +const base_validator = micro_schema.createSchemaValidator(base_schema); + +describe('json-schema-validation', () => { + test('passes validation for json-schema', () => { + const result = base_validator.validate({ + name: { + a: '1', + b: 'A' + }, + b: { + a: 2 + } + }); + + expect(result).toMatchSnapshot(); + }); + + test('fails validation for json-schema', () => { + const result1 = base_validator.validate({ + name: { + a: '1', + b: 'B' + }, + b: { + a: 2 + } + }); + + expect(result1).toMatchSnapshot(); + + const result2 = base_validator.validate({ + name: {}, + b: { + a: '' + } + }); + + expect(result2).toMatchSnapshot(); + }); + + test('passes validation with refs', () => { + const result = base_validator.validate({ + name: { + a: '1', + b: 'A' + }, + b: { + a: 2 + }, + d: { + prop: 'abc' + } + }); + + expect(result).toMatchSnapshot(); + }); + + test('fails validation for json-schema due to additional properties', () => { + const result = base_validator.validate({ + name: { + a: '1', + b: 'A', + c: 'additional property' + }, + b: { + a: 2 + } + }); + + expect(result).toMatchSnapshot(); + }); + + test('passes json-schema validation with additional properties when allowed', () => { + const validator = micro_schema.createSchemaValidator(base_schema, { + allowAdditional: true + }); + + const result = validator.validate({ + name: { + a: '1', + b: 'A', + c: 'additional property' + }, + b: { + a: 2 + } + }); + + expect(result).toMatchSnapshot(); + }); + + const subschema = micro_schema.parseJSONSchema({ + definitions: { + a: { + type: 'string' + }, + b: { + type: 'object', + properties: { + a: { type: 'string' }, + b: { $ref: '#/definitions/a' } + }, + required: ['b'] + } + } + }); + + test('it should correctly validate subschemas', () => { + const validator = subschema.definitions.b.validator(); + + const res1 = validator.validate({ + a: 'a', + b: 1 + }); + expect(res1).toMatchSnapshot(); + + const res2 = validator.validate({ + a: 'a', + b: 'b' + }); + + expect(res2.valid).toBe(true); + }); + + test('it correctly validates node types', () => { + const validator = micro_schema.createSchemaValidator({ + type: 'object', + properties: { + a: { + nodeType: 'buffer' + }, + b: { + nodeType: 'date' + } + }, + required: ['a'] + }); + + const res = validator.validate({ + a: Buffer.from('123'), + b: new Date() + }); + expect(res.valid).toBe(true); + + const res2 = validator.validate({ + a: '123' + }); + expect(res2.valid).toBe(false); + expect(res2).toMatchSnapshot(); + }); + + it('should fail to compile invalid node types', () => { + try { + micro_schema.createSchemaValidator({ + type: 'object', + properties: { + a: { + nodeType: 'Buffer' + }, + b: { + nodeType: 'Date' + }, + c: { + nodeType: 'unknown' + } + }, + required: ['a', 'b', 'c'] + }); + } catch (err) { + expect(err).toBeInstanceOf(micro_schema.SchemaValidatorError); + } + + expect.assertions(1); + }); +}); diff --git a/packages/schema/tests/ts-codec-validation.test.ts b/packages/schema/tests/ts-codec-validation.test.ts new file mode 100644 index 0000000..ef6b871 --- /dev/null +++ b/packages/schema/tests/ts-codec-validation.test.ts @@ -0,0 +1,103 @@ +import { describe, test, expect } from 'vitest'; + +import * as micro_schema from '../src'; +import * as t from 'ts-codec'; + +describe('ts-codec validation', () => { + enum Values { + A = 'A', + B = 'B' + } + + const codec = t.object({ + name: t.string, + surname: t.string, + other: t.object({ + a: t.array(t.string), + b: t.literal('optional').optional() + }), + tuple: t.tuple([t.string, t.number]), + or: t.number.or(t.string), + enum: t.Enum(Values), + + complex: t + .object({ + a: t.string + }) + .and( + t.object({ + b: t.number + }) + ) + .and( + t.object({ + c: t + .object({ + a: t.string + }) + .and( + t + .object({ + b: t.boolean + }) + .or( + t.object({ + c: t.number + }) + ) + ) + }) + ) + }); + + test('passes validation for codec', () => { + const validator = micro_schema.createTsCodecValidator(codec); + + const result = validator.validate({ + name: 'a', + surname: 'b', + other: { + a: ['nice'], + b: 'optional' + }, + tuple: ['string', 1], + or: 1, + enum: Values.A, + + complex: { + a: '', + b: 1, + c: { + a: '', + b: true + } + } + }); + + expect(result.valid).toBe(true); + }); + + test('fails validation for runtime codec', () => { + const validator = micro_schema.createTsCodecValidator(codec); + + const result = validator.validate({ + // @ts-ignore + name: 1, + other: { + a: ['nice'], + // @ts-ignore + b: 'op' + }, + // @ts-ignore + tuple: [1, 1], + // @ts-ignore + enum: 'c', + // @ts-ignore + or: [], + // @ts-ignore + complex: {} + }); + + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/schema/tests/utils.test.ts b/packages/schema/tests/utils.test.ts new file mode 100644 index 0000000..4979648 --- /dev/null +++ b/packages/schema/tests/utils.test.ts @@ -0,0 +1,41 @@ +import { describe, test, expect } from 'vitest'; + +import * as utils from '../src/utils'; +import schema from './fixtures/schema'; + +describe('utils', () => { + test('allow additional properties in a json schema', () => { + const cleaned_schema = utils.allowAdditionalProperties(schema); + expect(cleaned_schema).toMatchSnapshot(); + }); + + test('it should only modify additionalProperties if it is a boolean', () => { + const cleaned_schema = utils.allowAdditionalProperties({ + definitions: { + a: { + type: 'object', + properties: { + prop: { + type: 'string' + } + }, + additionalProperties: false, + required: ['prop'] + } + }, + + type: 'object', + properties: { + name: { + type: 'object', + additionalProperties: { + $ref: '#/definitions/a' + } + } + }, + required: ['name', 'b'], + additionalProperties: false + }); + expect(cleaned_schema).toMatchSnapshot(); + }); +}); diff --git a/packages/schema/tests/zod-validation.test.ts b/packages/schema/tests/zod-validation.test.ts new file mode 100644 index 0000000..d37c2f5 --- /dev/null +++ b/packages/schema/tests/zod-validation.test.ts @@ -0,0 +1,45 @@ +import { describe, test, expect } from 'vitest'; + +import * as micro_schema from '../src'; +import * as t from 'zod'; + +describe('zod-validation', () => { + const schema = t.object({ + name: t.string(), + surname: t.string(), + other: t.object({ + a: t.array(t.string()), + b: t.literal('optional').optional() + }) + }); + + test('passes validation for runtime codec', () => { + const validator = micro_schema.createZodValidator(schema); + + const result = validator.validate({ + name: 'a', + surname: 'b', + other: { + a: ['nice'], + b: 'optional' + } + }); + + expect(result).toMatchSnapshot(); + }); + + test('fails validation for runtime codec', () => { + const validator = micro_schema.createZodValidator(schema); + + const result = validator.validate({ + // @ts-ignore + name: 1, + other: { + // @ts-ignore + b: 'op' + } + }); + + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/schema/tsconfig.json b/packages/schema/tsconfig.json new file mode 100644 index 0000000..2fbe8b2 --- /dev/null +++ b/packages/schema/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "dist", + "rootDir": "src", + "resolveJsonModule": true + }, + "include": [ + "./src/**/*.ts", + "./more/schema/**/*.json" + ], + "references": [ + { + "path": "../codecs" + }, + { + "path": "../errors" + } + ] +} diff --git a/packages/streaming/package.json b/packages/streaming/package.json new file mode 100644 index 0000000..5393c3f --- /dev/null +++ b/packages/streaming/package.json @@ -0,0 +1,29 @@ +{ + "name": "@journeyapps-labs/micro-streaming", + "main": "./dist/index", + "browser": "./dist/web", + "typings": "./dist/index", + "version": "0.0.1", + "repository": "https://github.com/journeyapps-labs/journey-micro", + "files": [ + "dist/**" + ], + "exports": { + "node": "./dist/index.js", + "default": "./dist/web.js" + }, + "scripts": { + "test": "vitest" + }, + "dependencies": { + "@journeyapps-labs/micro-errors": "workspace:^", + "@journeyapps-labs/micro-schema": "workspace:^", + "@types/express": "^4.17.14", + "bson": "^6.7.0" + }, + "devDependencies": { + "@types/lodash": "^4.14.191", + "lodash": "^4.17.21", + "vitest": "^3.2.4" + } +} diff --git a/packages/streaming/src/bson/buffer-array.ts b/packages/streaming/src/bson/buffer-array.ts new file mode 100644 index 0000000..4f7c933 --- /dev/null +++ b/packages/streaming/src/bson/buffer-array.ts @@ -0,0 +1,86 @@ +/** + * Read a given `size` number of bytes from a give array of `chunks` as a Buffer. The returned Buffer could be + * larger than the requested size. If the chunks array contains less than the requested size then null is + * returned + */ +export const readBufferFromChunks = (chunks: Buffer[], size: number) => { + let batch = []; + let current_size = 0; + for (const chunk of chunks) { + batch.push(chunk); + current_size += chunk.length; + if (current_size >= size) { + return { + buffer: Buffer.concat(batch), + chunks_read: batch.length + }; + } + } + return null; +}; + +/** + * Read exactly `size` bytes from a given array of `chunks`, modifying the passed array to remove what + * was read. + * + * If more than `size` is read from the chunks array then the remainder is unshifted back onto the array + */ +export const readBufferFromChunksAndModify = (chunks: Buffer[], size: number): Buffer | null => { + const res = readBufferFromChunks(chunks, size); + if (!res) { + return null; + } + + if (res.buffer.length > size) { + chunks.splice(0, res.chunks_read, res.buffer.slice(size)); + return res.buffer.slice(0, size); + } + chunks.splice(0, res.chunks_read); + return res.buffer; +}; + +/** + * Creates a abstraction on top of a compressed set of Buffer chunks that keeps track of the + * entire byte size of the chunks array. + * + * Offers methods to get the byte size, peak at a given amount of data and destructively read + * a given amount of data + */ +export const createReadableBufferArray = () => { + let chunks: Buffer[] = []; + let current_size = 0; + return { + push(...new_chunks: Array) { + const normalized_chunks = new_chunks.map((chunk) => (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); + chunks.push(...normalized_chunks); + current_size = new_chunks.reduce((size, chunk) => { + return size + chunk.length; + }, current_size); + }, + read(size: number) { + const buffer = readBufferFromChunksAndModify(chunks, size); + if (buffer) { + current_size -= size; + } + + return buffer; + }, + peek(size: number) { + if (current_size < size) { + return null; + } + + const res = readBufferFromChunks(chunks, 4); + if (!res) { + return null; + } + + return res.buffer; + }, + size() { + return current_size; + } + }; +}; + +export type ReadableBufferArray = ReturnType; diff --git a/packages/streaming/src/bson/constants.ts b/packages/streaming/src/bson/constants.ts new file mode 100644 index 0000000..1a74274 --- /dev/null +++ b/packages/streaming/src/bson/constants.ts @@ -0,0 +1 @@ +export const TERMINATOR = Buffer.from([0x00, 0x00, 0x00, 0x00]); diff --git a/packages/streaming/src/bson/decoder.ts b/packages/streaming/src/bson/decoder.ts new file mode 100644 index 0000000..1c16ce1 --- /dev/null +++ b/packages/streaming/src/bson/decoder.ts @@ -0,0 +1,79 @@ +import * as buffer_array from './buffer-array'; +import * as stream from '../core/cross-stream'; +import * as constants from './constants'; +import * as bson from 'bson'; + +export type BSONStreamDecoderParams = { + deserialize_options?: bson.DeserializeOptions; + require_terminator?: boolean; + + writableStrategy?: QueuingStrategy; + readableStrategy?: QueuingStrategy; +}; +export const createBSONStreamDecoder = (params?: BSONStreamDecoderParams) => { + const buffer = buffer_array.createReadableBufferArray(); + let frame_size: null | number = null; + + function* decodeFrames() { + while (true) { + if (frame_size === null) { + frame_size = buffer.peek(4)?.readInt32LE(0) || null; + } + if (frame_size === null) { + break; + } + if (buffer.size() < frame_size) { + break; + } + + const frame = buffer.read(frame_size); + if (!frame) { + break; + } + + frame_size = null; + yield bson.deserialize(frame, { + promoteBuffers: true, + validation: { + utf8: false + }, + ...(params?.deserialize_options || {}) + }); + } + } + + let writableStrategy = params?.writableStrategy; + if (!writableStrategy) { + writableStrategy = new stream.ByteLengthStrategy({ + highWaterMark: 1024 * 16 + }); + } + + return new stream.Transform( + { + transform(chunk, controller) { + buffer.push(chunk); + for (const frame of decodeFrames()) { + controller.enqueue(frame as T); + } + }, + flush(controller) { + for (const frame of decodeFrames()) { + controller.enqueue(frame as T); + } + + const tail = buffer.peek(4); + if (tail && Buffer.compare(constants.TERMINATOR, tail) === 0) { + return; + } + if (params?.require_terminator === false) { + return; + } + + throw new Error('stream did not complete successfully'); + } + }, + writableStrategy, + params?.readableStrategy + ); +}; diff --git a/packages/streaming/src/bson/encoder.ts b/packages/streaming/src/bson/encoder.ts new file mode 100644 index 0000000..b67701e --- /dev/null +++ b/packages/streaming/src/bson/encoder.ts @@ -0,0 +1,34 @@ +import * as stream from '../core/cross-stream'; +import * as constants from './constants'; +import * as bson from 'bson'; + +export type BSONStreamEncoderParams = { + serialize_options?: bson.SerializeOptions; + sendTerminatorOnEnd?: boolean; + writableStrategy?: QueuingStrategy; + readableStrategy?: QueuingStrategy; +}; + +export const createBSONStreamEncoder = (params?: BSONStreamEncoderParams) => { + let readableStrategy = params?.readableStrategy; + if (!readableStrategy) { + readableStrategy = new stream.ByteLengthStrategy({ + highWaterMark: 1024 * 16 + }); + } + + return new stream.Transform( + { + transform(chunk, controller) { + controller.enqueue(Buffer.from(bson.serialize(chunk, params?.serialize_options))); + }, + flush(controller) { + if (params?.sendTerminatorOnEnd ?? true) { + controller.enqueue(constants.TERMINATOR); + } + } + }, + params?.writableStrategy, + readableStrategy + ); +}; diff --git a/packages/streaming/src/bson/header.ts b/packages/streaming/src/bson/header.ts new file mode 100644 index 0000000..09ab136 --- /dev/null +++ b/packages/streaming/src/bson/header.ts @@ -0,0 +1,71 @@ +import * as buffer_array from './buffer-array'; +import * as bson from 'bson'; + +export type DecodedResponse = { + header: T; + stream: AsyncIterable; +}; + +export type ExtractHeaderParams = { + deserialize_options?: bson.DeserializeOptions; +}; +export const extractHeaderFromStream = async ( + input_stream: AsyncIterable, + params?: ExtractHeaderParams +): Promise> => { + const iterator = input_stream[Symbol.asyncIterator](); + + const buffer = buffer_array.createReadableBufferArray(); + let frame_size: number | null = null; + + async function* resplice(data: Buffer | null, iterator: AsyncIterator) { + if (data) { + yield data; + } + + while (true) { + const next = await iterator.next(); + if (next.done) { + return; + } + + yield next.value; + } + } + + while (true) { + const chunk = await iterator.next(); + if (chunk.done) { + throw new Error('Stream did not complete successfully'); + } + + buffer.push(chunk.value); + + if (frame_size === null) { + frame_size = buffer.peek(4)?.readInt32LE(0) || null; + } + if (frame_size === null) { + continue; + } + + const frame = buffer.read(frame_size); + if (!frame) { + continue; + } + + const header = bson.deserialize(frame, { + promoteBuffers: true, + ...(params?.deserialize_options || {}) + }); + + return { + header: header as T, + stream: resplice(buffer.read(buffer.size()), iterator) + }; + } +}; + +export async function* prependHeaderToStream(header: any, input_stream: Iterable | AsyncIterable) { + yield bson.serialize(header); + yield* input_stream; +} diff --git a/packages/streaming/src/bson/index.ts b/packages/streaming/src/bson/index.ts new file mode 100644 index 0000000..a11cf3b --- /dev/null +++ b/packages/streaming/src/bson/index.ts @@ -0,0 +1,5 @@ +export * from './buffer-array'; +export * from './constants'; +export * from './encoder'; +export * from './decoder'; +export * from './header'; diff --git a/packages/streaming/src/core/backpressure.ts b/packages/streaming/src/core/backpressure.ts new file mode 100644 index 0000000..8c3dad7 --- /dev/null +++ b/packages/streaming/src/core/backpressure.ts @@ -0,0 +1,38 @@ +import * as stream from 'stream'; + +export type SimpleReadableLike = stream.Readable | stream.Transform; + +/** + * This provides an abstraction for pushing data onto a Readable stream, returning a Promise that + * resolves once any present backpressure is drained. + * + * This is special as it can handle Transform streams in addition to normal Readables. Transform + * streams don't natively expose a method for hooking into the readable sides drain and causes + * problems when building inflating transforms. + * + * The comment at the top of the official node implementation provides more detailed information + * on the problem: https://github.com/nodejs/node/blob/master/lib/internal/streams/transform.js + */ +export const push = async (readable: SimpleReadableLike, data: any) => { + const can_continue = readable.push(data); + if (can_continue) { + return; + } + + // We can't really support transform streams + if (readable instanceof stream.Transform) { + return; + } + + return new Promise((resolve) => { + const errorHandler = () => { + resolve(); + }; + + readable.once('error', errorHandler); + readable.once('drain', () => { + readable.removeListener('error', errorHandler); + resolve(); + }); + }); +}; diff --git a/packages/streaming/src/core/cross-stream.ts b/packages/streaming/src/core/cross-stream.ts new file mode 100644 index 0000000..4eb4b77 --- /dev/null +++ b/packages/streaming/src/core/cross-stream.ts @@ -0,0 +1,18 @@ +import type * as stream from 'stream/web'; + +let Readable: typeof stream.ReadableStream; +let Transform: typeof stream.TransformStream; +let ByteLengthStrategy: typeof stream.ByteLengthQueuingStrategy; + +if (typeof window !== 'undefined') { + Readable = ReadableStream as any; + Transform = TransformStream as any; + ByteLengthStrategy = ByteLengthQueuingStrategy as typeof stream.ByteLengthQueuingStrategy; +} else { + const webstream = require('stream/web'); + Readable = webstream.ReadableStream; + Transform = webstream.TransformStream; + ByteLengthStrategy = webstream.ByteLengthQueuingStrategy; +} + +export { Readable, Transform, ByteLengthStrategy }; diff --git a/packages/streaming/src/core/index.ts b/packages/streaming/src/core/index.ts new file mode 100644 index 0000000..dc1f8f2 --- /dev/null +++ b/packages/streaming/src/core/index.ts @@ -0,0 +1,4 @@ +export * from './backpressure'; +export * from './transformers'; +export * from './node-utils'; +export * from './utils'; diff --git a/packages/streaming/src/core/node-utils.ts b/packages/streaming/src/core/node-utils.ts new file mode 100644 index 0000000..e658818 --- /dev/null +++ b/packages/streaming/src/core/node-utils.ts @@ -0,0 +1,13 @@ +import * as stream from 'stream'; + +/** + * Returns a promise that resolves once the given stream finishes. If the stream emits an error then + * then promise will reject + */ +export const wait = (stream: stream.Stream) => { + return new Promise((resolve, reject) => { + stream.on('error', reject); + stream.on('end', resolve); + stream.on('finish', resolve); + }); +}; diff --git a/packages/streaming/src/core/transformers.ts b/packages/streaming/src/core/transformers.ts new file mode 100644 index 0000000..07177e9 --- /dev/null +++ b/packages/streaming/src/core/transformers.ts @@ -0,0 +1,89 @@ +/** + * A set of standard helpers for working with iterators. + * + * The majority of the utils included here can be removed if and when the tc39 iterator-helpers proposal lands: + * https://github.com/tc39/proposal-iterator-helpers + */ + +import { StreamLike } from './utils'; + +export type ExtractStreamElementType> = + T extends Iterable ? I : T extends AsyncIterable ? I : never; + +/** + * Takes n number of streams (or any AsyncIterators) and returns an AsyncGenerator that + * yields the concatenated output of all input streams + */ +export async function* concat>(...sources: S[]): AsyncGenerator> { + for (const source of sources) { + yield* source as unknown as AsyncGenerator>; + } +} + +/** + * Implements `Array.prototype.map` for iterables + */ +export async function* map(iterable: StreamLike, transform: (element: I) => O | Promise) { + for await (const element of iterable) { + yield await transform(element); + } +} + +/** + * Implements `Array.prototype.filter` for iterables + */ +export async function* filter(iterable: StreamLike, comparator: (element: I) => boolean | Promise) { + for await (const element of iterable) { + if (await comparator(element)) { + yield element; + } + } +} + +type ReducedValue = { + __reduced: true; + value: T; +}; +export const reduced = (value: T) => { + return { + __reduced: true, + value + }; +}; + +const isReducedValue = (value: any | ReducedValue): value is ReducedValue => { + return '__reduced' in value && value.__reduced; +}; + +/** + * Implements `Array.prototype.reduce` for iterables. + * + * The reducer can return `reduced(accumulator)` to end execution early + */ +export const reduce = async ( + iterable: StreamLike, + reducer: (accumulator: A, element: I) => A | ReducedValue | Promise>, + init: A +) => { + let accumulator = init; + for await (const element of iterable) { + const result = await reducer(accumulator, element); + if (isReducedValue(result)) { + return result.value; + } + accumulator = result; + } + return accumulator; +}; + +/** + * Drain a given iterables contents into an array. This is kind of like `Array.from` except it works + * with AsyncIterables + */ +export const drain = async (iterator: StreamLike) => { + const data: T[] = []; + for await (const chunk of iterator) { + data.push(chunk); + } + return data; +}; diff --git a/packages/streaming/src/core/utils.ts b/packages/streaming/src/core/utils.ts new file mode 100644 index 0000000..e9cf282 --- /dev/null +++ b/packages/streaming/src/core/utils.ts @@ -0,0 +1,82 @@ +import * as micro_schema from '@journeyapps-labs/micro-schema'; +import * as micro_errors from '@journeyapps-labs/micro-errors'; +import * as cross_stream from './cross-stream'; +import type * as webstreams from 'stream/web'; + +export type StreamLike = Iterable | AsyncIterable; + +/** + * Construct an AsyncIterator from a given ReadableStream. + * + * This is only really intended to be used from browser runtimes or within code intended to + * be used cross-labs. This is because Node ReadableStreams already implement AsyncIterators + */ +export async function* iterableFromReadable(readable: ReadableStream | webstreams.ReadableStream) { + const reader = readable.getReader(); + + try { + while (true) { + const res = await reader.read(); + if (res.done) { + return; + } + yield res.value; + } + } finally { + reader.releaseLock(); + } +} + +/** + * Construct a ReadableStream from a given Iterable or AsyncIterable. + * + * If the given iterable is already a readable then this is a noop + */ +export const readableFrom = ( + iterable: StreamLike, + strategy?: QueuingStrategy +): webstreams.ReadableStream => { + if (iterable instanceof cross_stream.Readable) { + return iterable; + } + + let resume: (() => void) | undefined; + return new cross_stream.Readable( + { + start(controller) { + void (async function () { + for await (const chunk of iterable) { + controller.enqueue(chunk); + + if (controller.desiredSize != null && controller.desiredSize <= 0) { + await new Promise((resolve) => { + resume = resolve; + }); + } + } + + controller.close(); + })().catch((err) => { + controller.error(err); + }); + }, + async pull() { + resume?.(); + } + }, + strategy + ); +}; + +/** + * Yield a generator that validates data flowing through it + */ +export async function* validateDataStream(iterable: StreamLike, validator: micro_schema.MicroValidator) { + for await (const chunk of iterable) { + const res = validator.validate(chunk); + if (!res.valid) { + throw new micro_errors.ValidationError(res.errors); + } + yield chunk; + } +} diff --git a/packages/streaming/src/index.ts b/packages/streaming/src/index.ts new file mode 100644 index 0000000..ad35b15 --- /dev/null +++ b/packages/streaming/src/index.ts @@ -0,0 +1,4 @@ +export * as compat from './core/cross-stream'; +export * as middleware from './middleware'; +export * as bson from './bson'; +export * from './core'; diff --git a/packages/streaming/src/middleware/index.ts b/packages/streaming/src/middleware/index.ts new file mode 100644 index 0000000..fc01127 --- /dev/null +++ b/packages/streaming/src/middleware/index.ts @@ -0,0 +1,2 @@ +export * from './streamed-body-parser-v1'; +export * from './streamed-body-parser-v2'; diff --git a/packages/streaming/src/middleware/streamed-body-parser-v1.ts b/packages/streaming/src/middleware/streamed-body-parser-v1.ts new file mode 100644 index 0000000..3e009a4 --- /dev/null +++ b/packages/streaming/src/middleware/streamed-body-parser-v1.ts @@ -0,0 +1,56 @@ +import * as querystring from 'querystring'; +import * as express from 'express'; + +/** + * Parse an incoming request decoding a base64 encoded, JSON formatted `payload` query string + * onto the request body. This is useful in streaming scenarios where the request body is a + * stream that needs to be processed independently from the request 'params' or 'metadata' + * + * For example, the request + * + * POST /a/b?payload=ey... + * + * + * would be decoded into a `body` field on the request without touching the stream + * + * @deprecated + */ +export const streamedRequestBodyParser = (req: express.Request, res: express.Response, next: express.NextFunction) => { + try { + let payload = req.query.payload; + if (!payload) { + return next(); + } + if (typeof payload !== 'string') { + return next(); + } + req.body = JSON.parse(Buffer.from(payload, 'base64').toString()); + delete req.query.payload; + + next(); + } catch (err) { + next(err); + } +}; + +/** + * Given some payload of data encode it for using as a journey streamed URL payload + * + * This means JSON stringified, base64 url-encoded + * + * @deprecated + */ +export const encodeStreamingPayload = (data: object) => { + return querystring.escape(Buffer.from(JSON.stringify(data)).toString('base64')); +}; + +/** + * Encode a given payload for streaming and append it as a query string to a given + * URL + * + * @deprecated + */ +export const encodeURLPayload = (url: string, data: object) => { + const payload = encodeStreamingPayload(data); + return `${url}?payload=${payload}`; +}; diff --git a/packages/streaming/src/middleware/streamed-body-parser-v2.ts b/packages/streaming/src/middleware/streamed-body-parser-v2.ts new file mode 100644 index 0000000..9cecea0 --- /dev/null +++ b/packages/streaming/src/middleware/streamed-body-parser-v2.ts @@ -0,0 +1,33 @@ +declare module 'express' { + interface Request { + stream?: AsyncIterable; + } +} + +import * as stream_headers from '../bson/header'; +import * as express from 'express'; + +/** + * Parse an incoming request by decoding the body as a bson stream. This will read the first + * incoming document as the request payload and then passthrough the remaining body as a + * property called `stream` on the request + */ +export const streamedRequestBodyParserV2 = async ( + req: express.Request, + _: express.Response, + next: express.NextFunction +) => { + try { + if (!req.is('application/*+header')) { + return next(); + } + + const { header, stream } = await stream_headers.extractHeaderFromStream(req); + + req.stream = stream; + req.body = header; + next(); + } catch (err) { + next(err); + } +}; diff --git a/packages/streaming/src/web.ts b/packages/streaming/src/web.ts new file mode 100644 index 0000000..e511714 --- /dev/null +++ b/packages/streaming/src/web.ts @@ -0,0 +1,5 @@ +export * as compat from './core/cross-stream'; +export * as bson from './bson'; + +export * from './core/transformers'; +export * from './core/utils'; diff --git a/packages/streaming/tests/__snapshots__/bson-transform.test.ts.snap b/packages/streaming/tests/__snapshots__/bson-transform.test.ts.snap new file mode 100644 index 0000000..8a738d9 --- /dev/null +++ b/packages/streaming/tests/__snapshots__/bson-transform.test.ts.snap @@ -0,0 +1,83 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`bson-transformer > end-to-end streaming 1`] = ` +[ + { + "a": "b", + }, + { + "c": "d", + }, + { + "e": "f", + }, +] +`; + +exports[`bson-transformer > it successfully deserializes streamed bson data 1`] = ` +[ + { + "a": "b", + }, + { + "c": "d", + }, + { + "e": "f", + }, +] +`; + +exports[`bson-transformer > it successfully serializes objects to bson stream 1`] = ` +{ + "data": [ + 14, + 0, + 0, + 0, + 2, + 97, + 0, + 2, + 0, + 0, + 0, + 98, + 0, + 0, + 14, + 0, + 0, + 0, + 2, + 99, + 0, + 2, + 0, + 0, + 0, + 100, + 0, + 0, + 14, + 0, + 0, + 0, + 2, + 101, + 0, + 2, + 0, + 0, + 0, + 102, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "type": "Buffer", +} +`; diff --git a/packages/streaming/tests/__snapshots__/common.test.ts.snap b/packages/streaming/tests/__snapshots__/common.test.ts.snap new file mode 100644 index 0000000..c49b587 --- /dev/null +++ b/packages/streaming/tests/__snapshots__/common.test.ts.snap @@ -0,0 +1,66 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`common > it should concatenate two streams 1`] = ` +[ + { + "value": 0, + }, + { + "value": 1, + }, + { + "value": 2, + }, + { + "value": 3, + }, + { + "value": 4, + }, + { + "value": 5, + }, + { + "value": 6, + }, + { + "value": 7, + }, + { + "value": 8, + }, + { + "value": 9, + }, + { + "value": 10, + }, + { + "value": 11, + }, + { + "value": 12, + }, + { + "value": 13, + }, + { + "value": 14, + }, + { + "value": 15, + }, + { + "value": 16, + }, + { + "value": 17, + }, + { + "value": 18, + }, + { + "value": 19, + }, +] +`; diff --git a/packages/streaming/tests/bson-transform.test.ts b/packages/streaming/tests/bson-transform.test.ts new file mode 100644 index 0000000..25d8822 --- /dev/null +++ b/packages/streaming/tests/bson-transform.test.ts @@ -0,0 +1,66 @@ +import { describe, test, expect } from 'vitest'; + +import * as micro_streaming from '../src'; +import * as stream from 'stream/web'; +import * as bson from 'bson'; + +describe('bson-transformer', () => { + test('it successfully reads data from chunks array', () => { + const chunks = Array.from(Array(10).keys()).map((_, i) => Buffer.alloc(i)); + + expect(micro_streaming.bson.readBufferFromChunks(chunks, 50)).toBe(null); + expect(micro_streaming.bson.readBufferFromChunks(chunks, 5)?.chunks_read).toBe(4); + expect(micro_streaming.bson.readBufferFromChunks(chunks, 2)?.buffer.length).toBe(3); + }); + + test('it successfully reads data from chunks array, modifying original', () => { + const chunks = Array.from(Array(10).keys()).map((_, i) => Buffer.alloc(i)); + + const read1 = micro_streaming.bson.readBufferFromChunksAndModify(chunks, 1); + expect(read1?.length).toBe(1); + expect(chunks.length).toBe(8); + + const read2 = micro_streaming.bson.readBufferFromChunksAndModify(chunks, 1); + expect(read2?.length).toBe(1); + expect(chunks.length).toBe(8); + expect(chunks[0].length).toBe(1); + }); + + test('it successfully deserializes streamed bson data', async () => { + const source = new stream.ReadableStream({ + start(controller) { + controller.enqueue(bson.serialize({ a: 'b' })); + controller.enqueue(bson.serialize({ c: 'd' })); + controller.enqueue(bson.serialize({ e: 'f' })); + controller.enqueue(micro_streaming.bson.TERMINATOR); + controller.close(); + } + }); + + const sink = source.pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); + expect(await micro_streaming.drain(sink)).toMatchSnapshot(); + }); + + test('it successfully serializes objects to bson stream', async () => { + const source = new stream.ReadableStream({ + start(controller) { + controller.enqueue({ a: 'b' }); + controller.enqueue({ c: 'd' }); + controller.enqueue({ e: 'f' }); + controller.close(); + } + }); + + const sink = source.pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); + expect(Buffer.concat(await micro_streaming.drain(sink))).toMatchSnapshot(); + }); + + test('end-to-end streaming', async () => { + const output = micro_streaming + .readableFrom([{ a: 'b' }, { c: 'd' }, { e: 'f' }]) + .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()) + .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); + + expect(await micro_streaming.drain(output)).toMatchSnapshot(); + }); +}); diff --git a/packages/streaming/tests/common.test.ts b/packages/streaming/tests/common.test.ts new file mode 100644 index 0000000..920e582 --- /dev/null +++ b/packages/streaming/tests/common.test.ts @@ -0,0 +1,68 @@ +import { describe, test, expect } from 'vitest'; + +import * as micro_schema from '@journeyapps-labs/micro-schema'; +import * as micro_errors from '@journeyapps-labs/micro-errors'; +import * as micro_streaming from '../src'; +import * as _ from 'lodash'; + +describe('common', () => { + test('it should concatenate two streams', async () => { + async function* one() { + for (let i = 0; i < 10; i++) { + yield { + value: i + }; + } + } + + async function* two() { + for (let i = 0; i < 10; i++) { + yield { + value: 10 + i + }; + } + } + + const concat_stream = micro_streaming.concat(one(), two()); + expect(await micro_streaming.drain(concat_stream)).toMatchSnapshot(); + }); + + test('it should validate stream data', async () => { + function* generateLessThan10() { + for (const i of _.range(10)) { + yield { + item: i + }; + } + } + + function* generateGreaterThan10() { + for (const i of _.range(15)) { + yield { + item: i + }; + } + } + + const validator: micro_schema.MicroValidator<{ item: number }> = { + validate: (datum) => { + if (datum.item < 10) { + return { + valid: true + }; + } + + return { + valid: false, + errors: ['not less than 10'] + }; + } + }; + + const validated_correct = micro_streaming.validateDataStream(generateLessThan10(), validator); + await micro_streaming.drain(validated_correct); + + const validated_incorrect = micro_streaming.validateDataStream(generateGreaterThan10(), validator); + await expect(micro_streaming.drain(validated_incorrect)).rejects.toThrowError(micro_errors.ValidationError); + }); +}); diff --git a/packages/streaming/tests/header.test.ts b/packages/streaming/tests/header.test.ts new file mode 100644 index 0000000..3cec74d --- /dev/null +++ b/packages/streaming/tests/header.test.ts @@ -0,0 +1,72 @@ +import { describe, test, expect } from 'vitest'; + +import * as micro_streaming from '../src'; +import * as crypto from 'crypto'; +import * as _ from 'lodash'; + +describe('bson-stream-header', () => { + test('it should successfully extract the header from a bson stream', async () => { + const bson_stream = micro_streaming + .readableFrom([{ a: 'b' }, { c: 'd' }]) + .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); + + const { header, stream: remaining } = await micro_streaming.bson.extractHeaderFromStream(bson_stream); + expect(header).toEqual({ + a: 'b' + }); + + const decoded_stream = micro_streaming + .readableFrom(remaining) + .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); + expect(await micro_streaming.drain(decoded_stream)).toEqual([ + { + c: 'd' + } + ]); + }); + + test('it should handle a lot of data', async () => { + function* generator() { + for (const i of _.range(20)) { + yield { + data: crypto.randomBytes(1024 * 1024) + }; + } + } + + const bson_stream = micro_streaming + .readableFrom(generator()) + .pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); + + const { header, stream: remaining } = await micro_streaming.bson.extractHeaderFromStream(bson_stream); + expect(Buffer.isBuffer(header.data)).toBe(true); + + const decoded_stream = await micro_streaming.drain( + micro_streaming.readableFrom(remaining).pipeThrough(micro_streaming.bson.createBSONStreamDecoder()) + ); + expect(decoded_stream.length).toBe(19); + }); + + test('it should properly prepend a header to a bson stream', async () => { + const data = [{ a: 'b' }, { c: 'd' }]; + const bson_stream = micro_streaming.readableFrom(data).pipeThrough(micro_streaming.bson.createBSONStreamEncoder()); + + const stream_with_header = micro_streaming.bson.prependHeaderToStream( + { + key: 'value' + }, + bson_stream + ); + + const decoded = micro_streaming + .readableFrom(stream_with_header) + .pipeThrough(micro_streaming.bson.createBSONStreamDecoder()); + + expect(await micro_streaming.drain(decoded)).toEqual([ + { + key: 'value' + }, + ...data + ]); + }); +}); diff --git a/packages/streaming/tsconfig.json b/packages/streaming/tsconfig.json new file mode 100644 index 0000000..0e4b9bf --- /dev/null +++ b/packages/streaming/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "dist", + "rootDir": "src", + "lib": ["DOM", "ES2020"] + }, + "include": ["src"], + "references": [ + { + "path": "../errors" + }, + { + "path": "../schema" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..302d920 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2223 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.29.5 + version: 2.29.7(@types/node@24.7.0) + npm-check-updates: + specifier: ^18.0.2 + version: 18.3.1 + prettier: + specifier: 3.6.2 + version: 3.6.2 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + + packages/codecs: + dependencies: + '@types/node': + specifier: ^20.17.6 + version: 20.19.19 + bson: + specifier: ^6.7.0 + version: 6.10.4 + ts-codec: + specifier: ^1.3.0 + version: 1.3.0 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.19.19) + + packages/errors: + devDependencies: + '@types/node': + specifier: ^20.17.6 + version: 20.19.19 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.19.19) + + packages/schema: + dependencies: + '@apidevtools/json-schema-ref-parser': + specifier: ^9.1.0 + version: 9.1.2 + '@journeyapps-labs/micro-codecs': + specifier: workspace:^ + version: link:../codecs + '@journeyapps-labs/micro-errors': + specifier: workspace:^ + version: link:../errors + ajv: + specifier: ^8.11.2 + version: 8.17.1 + better-ajv-errors: + specifier: ^1.2.0 + version: 1.2.0(ajv@8.17.1) + ts-codec: + specifier: ^1.3.0 + version: 1.3.0 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.7.0) + zod: + specifier: ^3.19.1 + version: 3.25.76 + + packages/streaming: + dependencies: + '@journeyapps-labs/micro-errors': + specifier: workspace:^ + version: link:../errors + '@journeyapps-labs/micro-schema': + specifier: workspace:^ + version: link:../schema + '@types/express': + specifier: ^4.17.14 + version: 4.17.23 + bson: + specifier: ^6.7.0 + version: 6.10.4 + devDependencies: + '@types/lodash': + specifier: ^4.14.191 + version: 4.17.20 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.7.0) + +packages: + + '@apidevtools/json-schema-ref-parser@9.1.2': + resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@changesets/apply-release-plan@7.0.13': + resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.29.7': + resolution: {integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==} + hasBin: true + + '@changesets/config@3.1.1': + resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-release-plan@4.0.13': + resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.1': + resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.5': + resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@humanwhocodes/momoa@2.0.4': + resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} + engines: {node: '>=10.10.0'} + + '@inquirer/external-editor@1.0.2': + resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} + cpu: [x64] + os: [win32] + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.7': + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + + '@types/express@4.17.23': + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@20.19.19': + resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==} + + '@types/node@24.7.0': + resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/send@1.2.0': + resolution: {integrity: sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==} + + '@types/serve-static@1.15.9': + resolution: {integrity: sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + better-ajv-errors@1.2.0: + resolution: {integrity: sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + ajv: 4.11.8 - 8 + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chardet@2.1.0: + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + human-id@4.1.2: + resolution: {integrity: sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==} + hasBin: true + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + npm-check-updates@18.3.1: + resolution: {integrity: sha512-5HwKPq7ybOOA1xB4FZg/1ToZZ5/i93U8m3co1mb3GYZAZPDkcxEFukQTTp/Abym+ZY6ShfrHl45Y0rCcwsNnQA==} + engines: {node: ^18.18.0 || >=20.0.0, npm: '>=8.12.1'} + hasBin: true + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-codec@1.3.0: + resolution: {integrity: sha512-OOaGvS0UwjyOychFZwjqSm47K65lzTCSup47RDG30crZr2MGnQCHQ13duAI4OcnzuYITNN6JDdS8RrtB0g204Q==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.1.9: + resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@apidevtools/json-schema-ref-parser@9.1.2': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/runtime@7.28.4': {} + + '@changesets/apply-release-plan@7.0.13': + dependencies: + '@changesets/config': 3.1.1 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.2 + + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.2 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.29.7(@types/node@24.7.0)': + dependencies: + '@changesets/apply-release-plan': 7.0.13 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.1 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.13 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.2(@types/node@24.7.0) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + ci-info: 3.9.0 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + p-limit: 2.3.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.2 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.1': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.2 + + '@changesets/get-release-plan@4.0.13': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.1': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 3.14.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.5': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.1 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.2 + prettier: 2.8.8 + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@humanwhocodes/momoa@2.0.4': {} + + '@inquirer/external-editor@1.0.2(@types/node@24.7.0)': + dependencies: + chardet: 2.1.0 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 24.7.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jsdevtools/ono@7.1.3': {} + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.28.4 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.28.4 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@rollup/rollup-android-arm-eabi@4.52.4': + optional: true + + '@rollup/rollup-android-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.4': + optional: true + + '@rollup/rollup-darwin-x64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.4': + optional: true + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.19 + + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.19 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.7': + dependencies: + '@types/node': 20.19.19 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.0 + + '@types/express@4.17.23': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.9 + + '@types/http-errors@2.0.5': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash@4.17.20': {} + + '@types/mime@1.3.5': {} + + '@types/node@12.20.55': {} + + '@types/node@20.19.19': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.7.0': + dependencies: + undici-types: 7.14.0 + optional: true + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.19.19 + + '@types/send@1.2.0': + dependencies: + '@types/node': 20.19.19 + + '@types/serve-static@1.15.9': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.19 + '@types/send': 0.17.5 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@20.19.19))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.9(@types/node@20.19.19) + + '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@24.7.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.9(@types/node@24.7.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.19 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + better-ajv-errors@1.2.0(ajv@8.17.1): + dependencies: + '@babel/code-frame': 7.27.1 + '@humanwhocodes/momoa': 2.0.4 + ajv: 8.17.1 + chalk: 4.1.2 + jsonpointer: 5.0.1 + leven: 3.1.0 + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bson@6.10.4: {} + + cac@6.7.14: {} + + call-me-maybe@1.0.2: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@2.1.0: {} + + check-error@2.1.1: {} + + ci-info@3.9.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + es-module-lexer@1.7.0: {} + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + esprima@4.0.1: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.2.2: {} + + extendable-error@0.1.7: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + human-id@4.1.2: {} + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpointer@5.0.1: {} + + leven@3.1.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + lodash@4.17.21: {} + + loupe@3.2.1: {} + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + npm-check-updates@18.3.1: {} + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@4.0.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@2.8.8: {} + + prettier@3.6.2: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rollup@4.52.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + term-size@2.2.1: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-codec@1.3.0: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici-types@7.14.0: + optional: true + + universalify@0.1.2: {} + + vite-node@3.2.4(@types/node@20.19.19): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.9(@types/node@20.19.19) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-node@3.2.4(@types/node@24.7.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.9(@types/node@24.7.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.1.9(@types/node@20.19.19): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.19 + fsevents: 2.3.3 + + vite@7.1.9(@types/node@24.7.0): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.7.0 + fsevents: 2.3.3 + + vitest@3.2.4(@types/node@20.19.19): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@20.19.19)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.9(@types/node@20.19.19) + vite-node: 3.2.4(@types/node@20.19.19) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.19 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@3.2.4(@types/node@24.7.0): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.7.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.9(@types/node@24.7.0) + vite-node: 3.2.4(@types/node@24.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.7.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3b32839 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: +- packages/* diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..a8bcdfa --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "declaration": true, + "composite": true, + "lib": ["ESNext"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2020", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "useUnknownInCatchVariables": false + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..56f1166 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "files": [], + "references": [ + { + "path": "./packages/codecs" + }, + { + "path": "./packages/errors" + }, + { + "path": "./packages/streaming" + }, + { + "path": "./packages/schema" + } + ] +} \ No newline at end of file