diff --git a/.changeset/common-hoops-drive.md b/.changeset/common-hoops-drive.md new file mode 100644 index 00000000..e1c41f65 --- /dev/null +++ b/.changeset/common-hoops-drive.md @@ -0,0 +1,5 @@ +--- +'@lightmill/log-api': major +--- + +Removes typescript-openapi type export. The prefered way to rely on our contract's type is now to use the zod schemas directly. openapi.yaml is still being generated so typescript-openapi types can be generated from it if needed. diff --git a/.changeset/cyan-teeth-live.md b/.changeset/cyan-teeth-live.md new file mode 100644 index 00000000..35a93b59 --- /dev/null +++ b/.changeset/cyan-teeth-live.md @@ -0,0 +1,5 @@ +--- +'@lightmill/log-api': major +--- + +Switch openapi.json export to openapi.yaml. This aligns with openapi most widespread use. Author should update their code to use openapi.yamd instead of .json, and switch to corresponding parser. diff --git a/.changeset/free-pianos-play.md b/.changeset/free-pianos-play.md new file mode 100644 index 00000000..930a2c4c --- /dev/null +++ b/.changeset/free-pianos-play.md @@ -0,0 +1,5 @@ +--- +'@lightmill/log-api': minor +--- + +Export zod schemas that may be used to validate request and reponses. diff --git a/.changeset/honest-geckos-add.md b/.changeset/honest-geckos-add.md new file mode 100644 index 00000000..84d245d8 --- /dev/null +++ b/.changeset/honest-geckos-add.md @@ -0,0 +1,5 @@ +--- +'@lightmill/log-server': major +--- + +Change some validation error codes to match new log-api. Please refer to @lightmill/log-api openapi.yaml for the new validation error codes. diff --git a/.changeset/shaky-candles-fall.md b/.changeset/shaky-candles-fall.md new file mode 100644 index 00000000..3d2a71ca --- /dev/null +++ b/.changeset/shaky-candles-fall.md @@ -0,0 +1,5 @@ +--- +'@lightmill/log-client': minor +--- + +Slightly improve some client's errors. diff --git a/.changeset/thirty-needles-camp.md b/.changeset/thirty-needles-camp.md new file mode 100644 index 00000000..d12bc937 --- /dev/null +++ b/.changeset/thirty-needles-camp.md @@ -0,0 +1,5 @@ +--- +'@lightmill/log-api': major +--- + +Update validation error codes to be more explicit. Refer to openapi.yaml or the exported schemas to update your code if needed. diff --git a/.github/workflows/node.js.yaml b/.github/workflows/node.js.yaml index 225d86ac..5d7802d5 100644 --- a/.github/workflows/node.js.yaml +++ b/.github/workflows/node.js.yaml @@ -33,7 +33,9 @@ jobs: - uses: actions/upload-artifact@v4 with: name: 'build' - path: 'packages/*/dist/' + path: | + packages/*/dist/ + packages/*/src/generated/ test: needs: ['build'] diff --git a/package.json b/package.json index 4e57e4b9..2694e45b 100644 --- a/package.json +++ b/package.json @@ -17,20 +17,20 @@ "publint": "node ./scripts/publint.js" }, "devDependencies": { - "@changesets/cli": "^2.27.1", - "@eslint/js": "^9.20.0", + "@changesets/cli": "^2.29.5", + "@eslint/js": "^9.30.1", "@types/eslint-config-prettier": "^6.11.3", - "eslint": "^9.20.1", + "eslint": "^9.30.1", "eslint-config-prettier": "10.1.2", - "eslint-plugin-jsdoc": "^50.6.3", - "eslint-plugin-react": "^7.37.4", - "globals": "^16.0.0", + "eslint-plugin-jsdoc": "^50.8.0", + "eslint-plugin-react": "^7.37.5", + "globals": "^16.3.0", "jiti": "^2.4.2", "prettier": "3.5.3", "prettier-plugin-organize-imports": "^4.1.0", "publint": "^0.3.12", "typescript": "catalog:", - "typescript-eslint": "^8.27.0", + "typescript-eslint": "^8.35.1", "vitest": "catalog:" }, "packageManager": "pnpm@10.13.1", @@ -39,6 +39,6 @@ }, "dependencies": { "@tsconfig/node-ts": "^23.6.1", - "@tsconfig/node22": "^22.0.1" + "@tsconfig/node22": "^22.0.2" } } diff --git a/packages/log-api/package.json b/packages/log-api/package.json index ef9d47ca..bf847ebe 100644 --- a/packages/log-api/package.json +++ b/packages/log-api/package.json @@ -12,26 +12,27 @@ "default": "./dist/index.js" } }, - "./openapi.json": "./dist/openapi.json" + "./openapi.yaml": "./dist/openapi.yaml" }, "files": [ - "dist" + "dist", + "src" ], "scripts": { - "generate-api-types": "openapi-typescript temp/openapi.json --empty-objects-unknown --output temp/openapi.ts", - "build-api": "tsp compile ./type-spec", - "build-js": "cp -rf src/** temp/ && tsc -b tsconfig.build.json", - "build": "rm -rf temp && pnpm run build-api && pnpm run generate-api-types && pnpm run build-js && rm -rf temp", - "watch:build": "onchange -i 'src/index.ts' 'type-spec/**/*' -- pnpm build", + "build-api": "mkdir -p dist && node --experimental-strip-types scripts/build-api.ts > dist/openapi.yaml", + "build-js": "tsc -b tsconfig.build.json", + "build": "pnpm run build-api && pnpm run build-js", + "watch:build": "onchange -i 'src/*' -i 'scripts/*.ts' -- pnpm build", "prepublish": "pnpm run build" }, "devDependencies": { - "@typespec/compiler": "1.0.0", - "@typespec/http": "1.0.1", - "@typespec/openapi": "1.0.0", - "@typespec/openapi3": "1.0.0", - "@typespec/rest": "^0.70.0", + "@std/yaml": "jsr:^1.0.8", "onchange": "^7.1.0", - "openapi-typescript": "^7.6.1" + "openapi-typescript": "^7.8.0" + }, + "dependencies": { + "@asteasolutions/zod-to-openapi": "8.0.0-beta.4", + "type-fest": "^4.41.0", + "zod": "^3.25.73" } } diff --git a/packages/log-api/scripts/build-api.ts b/packages/log-api/scripts/build-api.ts new file mode 100644 index 00000000..7d26973f --- /dev/null +++ b/packages/log-api/scripts/build-api.ts @@ -0,0 +1,31 @@ +import { OpenApiGeneratorV31 } from '@asteasolutions/zod-to-openapi'; +import { stringify } from '@std/yaml'; +import type { Entries } from 'type-fest'; +import manifest from '../package.json' with { type: 'json' }; +import { routes } from '../src/routes.ts'; +import { sessionAuth } from '../src/security.ts'; +import { ServerErrorResponse } from '../src/server-errors.ts'; +import { sessionRoutes } from '../src/session-schemas.ts'; +import { registry } from '../src/zod-openapi.ts'; + +for (const [path, pathMethods] of Object.entries(routes) as Entries< + typeof sessionRoutes +>) { + for (const [method, config] of Object.entries(pathMethods) as Entries< + typeof pathMethods + >) { + registry.registerPath({ path, method: method, ...config }); + } +} + +registry.register('ServerErrorResponse', ServerErrorResponse); + +const openApiDocument = new OpenApiGeneratorV31( + registry.definitions, +).generateDocument({ + openapi: '3.1.0', + info: { title: 'Log API', version: manifest.version }, + security: [{ [sessionAuth.name]: [] }], +}); + +console.log(stringify(openApiDocument)); diff --git a/packages/log-api/src/experiment-schemas.ts b/packages/log-api/src/experiment-schemas.ts new file mode 100644 index 00000000..5ec32529 --- /dev/null +++ b/packages/log-api/src/experiment-schemas.ts @@ -0,0 +1,124 @@ +import { + getDataDocumentSchema, + getErrorDocumentSchema, + getErrorSchema, + getResourceIdentifierSchema, + mediaType, +} from './jsonapi.ts'; +import { ForbiddenErrorResponse, StringOrArrayOfStrings } from './utils.ts'; +import { z, type RouteConfig } from './zod-openapi.ts'; + +// Resource schema +// ----------------------------------------------------------------------------- +export const ExperimentResourceIdentifier = getResourceIdentifierSchema( + 'experiments', +).openapi('ExperimentResourceIdentifier'); +const ExperimentAttributes = z + .strictObject({ name: z.string().min(1).describe('Name of the experiment') }) + .openapi('ExperimentAttributes'); +// Zod recommend using the spread operator on the shape of a zod object +// instead of using the `extend` method, but we then lose zod-to-openapi's +// ability to generate inherited schemas. +export const ExperimentResource = z + .strictObject({ + ...ExperimentResourceIdentifier.shape, + attributes: ExperimentAttributes, + }) + .openapi('ExperimentResource'); +const ExperimentResourceCreate = ExperimentResource.omit({ id: true }).openapi( + 'ExperimentResourceCreate', +); + +// Query parameters schemas +// ----------------------------------------------------------------------------- +const ExperimentQueryFilter = z.strictObject({ + 'filter[name]': StringOrArrayOfStrings.optional(), +}); + +// Requests schemas +// ----------------------------------------------------------------------------- +const ExperimentPostRequest = getDataDocumentSchema({ + data: ExperimentResourceCreate, +}).openapi('ExperimentPostRequest'); + +// OK Response schemas +// ----------------------------------------------------------------------------- +const ExperimentGetResponse = getDataDocumentSchema({ + data: ExperimentResource, +}).openapi('ExperimentGetResponse'); +const ExperimentGetCollectionResponse = getDataDocumentSchema({ + data: z.array(ExperimentResource), +}).openapi('ExperimentGetCollectionResponse'); +const ExperimentPostResponse = getDataDocumentSchema({ + data: ExperimentResourceIdentifier, +}).openapi('ExperimentPostResponse'); + +// Error Response schemas +// ----------------------------------------------------------------------------- +const ExperimentNotFoundErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'EXPERIMENT_NOT_FOUND', statusText: 'Not Found' }), +).openapi('ExperimentNotFoundErrorResponse'); +const ExperimentExistsErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'EXPERIMENT_EXISTS', statusText: 'Conflict' }), +).openapi('ExperimentExistsErrorResponse'); + +// Route configuration +// ----------------------------------------------------------------------------- +export const experimentRoutes = { + '/': { + get: { + description: 'List all experiments', + request: { query: ExperimentQueryFilter }, + responses: { + 200: { + description: 'List of experiments', + content: { [mediaType]: { schema: ExperimentGetCollectionResponse } }, + }, + }, + }, + post: { + description: 'Create a new experiment.', + request: { + body: { + required: true, + content: { + [mediaType]: { schema: ExperimentPostRequest.required() }, + }, + }, + }, + responses: { + 201: { + description: 'Experiment created successfully', + content: { [mediaType]: { schema: ExperimentPostResponse } }, + headers: z.strictObject({ location: z.string() }), + }, + 403: { + description: 'Forbidden', + content: { [mediaType]: { schema: ForbiddenErrorResponse } }, + }, + 409: { + description: 'Experiment already exists', + content: { [mediaType]: { schema: ExperimentExistsErrorResponse } }, + }, + }, + }, + }, + '/{id}': { + get: { + description: 'Get a specific experiment by ID', + request: { + params: z.object({ id: z.string().describe('ID of the experiment') }), + }, + responses: { + 200: { + description: 'Experiment retrieved successfully', + content: { [mediaType]: { schema: ExperimentGetResponse } }, + }, + 404: { + description: 'Experiment not found', + content: { [mediaType]: { schema: ExperimentNotFoundErrorResponse } }, + }, + }, + }, + }, +} satisfies RouteConfig; diff --git a/packages/log-api/src/index.ts b/packages/log-api/src/index.ts index 8f77a361..04708538 100644 --- a/packages/log-api/src/index.ts +++ b/packages/log-api/src/index.ts @@ -1,4 +1,3 @@ -export type * from './openapi.js'; -import openAPI from './openapi.json' with { type: 'json' }; - -export { openAPI }; +export { openApiDocument as openAPI } from './openapi-document.ts'; +export { routes } from './routes.ts'; +export * from './server-errors.ts'; diff --git a/packages/log-api/src/jsonapi.ts b/packages/log-api/src/jsonapi.ts new file mode 100644 index 00000000..e0af163e --- /dev/null +++ b/packages/log-api/src/jsonapi.ts @@ -0,0 +1,114 @@ +import { z } from './zod-openapi.ts'; + +export function getResourceIdentifierSchema(type: T) { + return z.strictObject({ id: z.string(), type: z.literal(type) }); +} + +export function getDataDocumentSchema< + DataSchema extends z.ZodType, + IncludesSchema extends z.ZodType, +>(schemas: { + data: DataSchema; + includes: IncludesSchema; +}): z.ZodObject<{ + data: DataSchema; + included: z.ZodOptional>; +}>; +export function getDataDocumentSchema(schemas: { + data: DataSchema; +}): z.ZodObject<{ data: DataSchema }>; +export function getDataDocumentSchema(schemas: { + data: z.ZodType; + includes?: z.ZodType; +}) { + let base = z.strictObject({ data: schemas.data }); + if (schemas.includes == null) { + return base; + } + return z.strictObject({ + ...base.shape, + included: z.array(schemas.includes).optional(), + }); +} + +export const httpStatuses = { + 200: 'OK', + 201: 'Created', + 204: 'No Content', + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 409: 'Conflict', + 415: 'Unsupported Media Type', + 500: 'Internal Server Error', +} as const; +export type HttpStatusMap = typeof httpStatuses; +export type HttpStatusCode = keyof HttpStatusMap; +export type HttpStatusText = HttpStatusMap[HttpStatusCode]; + +type ValueOrArrayValue = T extends Array ? U : T; + +export function getErrorSchema< + const Options extends { code: string | string[]; statusCode: HttpStatusCode }, +>( + options: Options, +): z.ZodObject<{ + code: z.ZodType>; + status: z.ZodType; + detail: z.ZodOptional; +}>; +export function getErrorSchema< + const Options extends { code: string | string[]; statusText: HttpStatusText }, +>( + options: Options, +): z.ZodObject<{ + code: z.ZodType>; + status: z.ZodType; + detail: z.ZodOptional; +}>; +export function getErrorSchema( + options: + | { code: string | string[]; statusCode: HttpStatusCode } + | { code: string | string[]; statusText: HttpStatusText }, +) { + if (!('statusText' in options)) { + return getErrorSchema({ + ...options, + statusText: httpStatuses[options.statusCode], + }); + } + return z.strictObject({ + code: (Array.isArray(options.code) + ? z.enum(options.code) + : z.literal(options.code) + ).describe('Error code'), + status: z.literal(options.statusText).describe('HTTP status text'), + detail: z.string().optional().describe('Detailed description of the error'), + }); +} + +export function getErrorDocumentSchema< + ErrorSchema extends + | z.ZodObject<{ + code: z.ZodType; + status: z.ZodType; + detail?: z.ZodOptional; + }> + | z.ZodUnion< + z.ZodObject<{ + code: z.ZodType; + status: z.ZodType; + detail?: z.ZodOptional; + }>[] + >, +>(errorSchema: ErrorSchema) { + return z.strictObject({ errors: z.array(errorSchema).nonempty() }); +} + +export const mediaType = 'application/vnd.api+json' as const; + +export const EmptyDataDocument = getDataDocumentSchema({ + data: z.null(), +}).openapi('EmptyDataDocument'); diff --git a/packages/log-api/src/log-schemas.ts b/packages/log-api/src/log-schemas.ts new file mode 100644 index 00000000..19f859d6 --- /dev/null +++ b/packages/log-api/src/log-schemas.ts @@ -0,0 +1,212 @@ +import { ExperimentResource } from './experiment-schemas.ts'; +import { + getDataDocumentSchema, + getErrorDocumentSchema, + getErrorSchema, + getResourceIdentifierSchema, + mediaType, +} from './jsonapi.ts'; +import * as Run from './run-schemas.ts'; +import { StringOrArrayOfStrings } from './utils.ts'; +import { z, type RouteConfig } from './zod-openapi.ts'; + +// Fix circular dependencies by using lazy evaluation, but since we are using +// zod-to-openapi, we need to use the `openapi` method to ensure +// the schemas are correctly referenced in the OpenAPI document. +const RunResourceIdentifier = z + .lazy(() => Run.RunResourceIdentifier) + .openapi({ + type: 'object', + allOf: [{ $ref: '#/components/schemas/RunResourceIdentifier' }], + }); +const RunResource = z + .lazy(() => Run.RunResource) + .openapi({ + type: 'object', + allOf: [{ $ref: '#/components/schemas/RunResource' }], + }); + +// Resource schema +// ----------------------------------------------------------------------------- +export const LogResourceIdentifier = getResourceIdentifierSchema( + 'logs', +).openapi('LogResourceIdentifier'); +const LogAttributes = z.strictObject({ + number: z + .number() + .int() + .min(1) + .describe( + 'The number of the log. This is a number that is unique for the run the log belongs to. Log numbers must be sequential and start at 1. Logs must not necessarily be created in order, but any missing log must be created before the run is completed, and any log following a missing log is considered pending.', + ), + logType: z + .string() + .describe( + 'The type of the log. This is not the same as the resource type (which is always "logs" for logs). This is a type that describes the kind of log. For example, it could be "trial", "event", etc.', + ), + values: z + .record(z.string(), z.unknown()) + .describe( + 'The values of the log. They may be any JSON object. However, it is recommended to use flat objects as nested objects are difficult to serialize to CSV. It is also recommended to use a consistent schema for all logs of the same type.', + ), +}); +const LogRelationships = z + .strictObject({ run: z.strictObject({ data: RunResourceIdentifier }) }) + .openapi('LogRelationships'); +export const LogResource = z + .strictObject({ + ...LogResourceIdentifier.shape, + attributes: LogAttributes, + relationships: LogRelationships, + }) + .openapi('LogResource'); + +// Query parameters schemas +// ----------------------------------------------------------------------------- +const LogIncludeName = z.enum(['run', 'run.experiment', 'run.lastLogs']); +const LogQueryInclude = z.strictObject({ + include: z.union([LogIncludeName, z.array(LogIncludeName)]).optional(), +}); +const LogQueryFilter = z.strictObject({ + 'filter[logType]': StringOrArrayOfStrings.optional(), + 'filter[experiment.id]': StringOrArrayOfStrings.optional(), + 'filter[experiment.name]': StringOrArrayOfStrings.optional(), + 'filter[run.name]': StringOrArrayOfStrings.optional(), + 'filter[run.id]': StringOrArrayOfStrings.optional(), +}); + +// Request schemas +// ----------------------------------------------------------------------------- +const LogPostRequest = z + .strictObject({ data: LogResource.omit({ id: true }) }) + .openapi('LogPostRequest'); + +// OK Response schemas +const LogInclude = z + .union([RunResource, ExperimentResource, LogResource]) + .openapi('LogInclude'); +const LogGetResponse = getDataDocumentSchema({ + data: LogResource, + includes: LogInclude, +}).openapi('LogGetResponse'); +const LogGetCollectionResponse = getDataDocumentSchema({ + data: z.array(LogResource), + includes: LogInclude, +}).openapi('LogGetCollectionResponse'); +const LogPostResponse = getDataDocumentSchema({ + data: LogResourceIdentifier, +}).openapi('LogPostResponse'); + +// Error Response schemas +const LogNotFoundErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'LOG_NOT_FOUND', statusCode: 404 }), +).openapi('LogNotFoundErrorResponse'); + +export const logRoutes = { + '/': { + get: { + request: { + query: z.strictObject({ + ...LogQueryFilter.shape, + ...LogQueryInclude.shape, + }), + headers: z.looseObject({ + accept: z.string().optional() as z.ZodType< + // Little trick for TypeScript auto-completion to suggest the media + // type or csv. + (string & {}) | 'text/csv' | typeof mediaType + >, + }), + }, + responses: { + 200: { + description: `List of logs. Format is CSV by default, or JSON if the Accept header is set to ${mediaType}.`, + content: { + [mediaType]: { schema: LogGetCollectionResponse }, + 'text/csv': { schema: z.string() }, + }, + }, + 400: { + description: 'Not supported query parameters', + content: { + [mediaType]: { + schema: getErrorDocumentSchema( + z.strictObject({ + ...getErrorSchema({ + code: 'NOT_SUPPORTED_QUERY_PARAMETER', + statusCode: 400, + }).shape, + source: z.strictObject({ + parameter: z + .string() + .describe( + 'Parameter in the request query that is invalid', + ), + }), + }), + ), + }, + }, + }, + }, + }, + post: { + description: 'Create a new log', + request: { + body: { + required: true, + content: { [mediaType]: { schema: LogPostRequest } }, + }, + }, + responses: { + 201: { + description: 'Log created successfully', + headers: z.strictObject({ location: z.string() }), + content: { [mediaType]: { schema: LogPostResponse } }, + }, + 403: { + description: 'Forbidden', + content: { + [mediaType]: { + schema: getErrorDocumentSchema( + getErrorSchema({ + code: ['RUN_NOT_FOUND', 'INVALID_RUN_STATUS'], + statusCode: 403, + }), + ), + }, + }, + }, + 409: { + description: 'Log number already exists', + content: { + [mediaType]: { + schema: getErrorDocumentSchema( + getErrorSchema({ code: 'LOG_NUMBER_EXISTS', statusCode: 409 }), + ), + }, + }, + }, + }, + }, + }, + '/{id}': { + get: { + description: 'Get a specific log by ID', + request: { + params: z.object({ id: z.string().describe('ID of the log') }), + query: LogQueryInclude, + }, + responses: { + 200: { + description: 'Log retrieved successfully', + content: { [mediaType]: { schema: LogGetResponse } }, + }, + 404: { + description: 'Log not found', + content: { [mediaType]: { schema: LogNotFoundErrorResponse } }, + }, + }, + }, + }, +} satisfies RouteConfig; diff --git a/packages/log-api/src/openapi-document.ts b/packages/log-api/src/openapi-document.ts new file mode 100644 index 00000000..cfb8eb09 --- /dev/null +++ b/packages/log-api/src/openapi-document.ts @@ -0,0 +1,28 @@ +import { OpenApiGeneratorV31 } from '@asteasolutions/zod-to-openapi'; +import type { Entries } from 'type-fest'; +import manifest from '../package.json' with { type: 'json' }; +import { routes } from './routes.ts'; +import { sessionAuth } from './security.ts'; +import { ServerErrorResponse } from './server-errors.ts'; +import { sessionRoutes } from './session-schemas.ts'; +import { registry } from './zod-openapi.ts'; + +for (const [path, pathMethods] of Object.entries(routes) as Entries< + typeof sessionRoutes +>) { + for (const [method, config] of Object.entries(pathMethods) as Entries< + typeof pathMethods + >) { + registry.registerPath({ path, method: method, ...config }); + } +} + +registry.register('ServerErrorResponse', ServerErrorResponse); + +const generator = new OpenApiGeneratorV31(registry.definitions); +export const openApiDocument: ReturnType = + generator.generateDocument({ + openapi: '3.1.0', + info: { title: 'Log API', version: manifest.version }, + security: [{ [sessionAuth.name]: [] }], + }); diff --git a/packages/log-api/src/routes.ts b/packages/log-api/src/routes.ts new file mode 100644 index 00000000..daefdd33 --- /dev/null +++ b/packages/log-api/src/routes.ts @@ -0,0 +1,30 @@ +import { experimentRoutes } from './experiment-schemas.ts'; +import { logRoutes } from './log-schemas.ts'; +import { runRoutes } from './run-schemas.ts'; +import { sessionRoutes } from './session-schemas.ts'; +import { mapKeys } from './utils.ts'; + +type MountedRoute< + R extends Record<`/${string}`, unknown>, + B extends `/${string}`, +> = { + [K in keyof R as K extends '/' + ? B + : K extends string + ? `${B}${K}` + : never]: R[K]; +}; +function mountRoute< + const R extends Record<`/${string}`, unknown>, + const B extends `/${string}`, +>(route: R, base: B): MountedRoute { + // @ts-expect-error: Trust me. + return mapKeys(route, (k) => (k === '/' ? base : `${base}${k}`)); +} + +export const routes = { + ...mountRoute(sessionRoutes, '/sessions'), + ...mountRoute(experimentRoutes, '/experiments'), + ...mountRoute(runRoutes, '/runs'), + ...mountRoute(logRoutes, '/logs'), +}; diff --git a/packages/log-api/src/run-schemas.ts b/packages/log-api/src/run-schemas.ts new file mode 100644 index 00000000..babdd0bb --- /dev/null +++ b/packages/log-api/src/run-schemas.ts @@ -0,0 +1,250 @@ +import { + ExperimentResource, + ExperimentResourceIdentifier, +} from './experiment-schemas.ts'; +import { + getDataDocumentSchema, + getErrorDocumentSchema, + getErrorSchema, + getResourceIdentifierSchema, + mediaType, +} from './jsonapi.ts'; +import * as Log from './log-schemas.ts'; +import { StringOrArrayOfStrings } from './utils.ts'; +import { z, type RouteConfig } from './zod-openapi.ts'; + +// Fix circular dependencies by using lazy evaluation, but since we are using +// zod-to-openapi, we need to use the `openapi` method to ensure +// the schemas are correctly referenced in the OpenAPI document. +// C.f. https://github.com/asteasolutions/zod-to-openapi/issues/247 +const LogResourceIdentifier = z + .lazy(() => Log.LogResourceIdentifier) + .openapi({ + type: 'object', + allOf: [{ $ref: '#/components/schemas/LogResourceIdentifier' }], + }); + +const LogResource = z + .lazy(() => Log.LogResource) + .openapi({ + type: 'object', + allOf: [{ $ref: '#/components/schemas/LogResource' }], + }); + +// Resource schema +// ----------------------------------------------------------------------------- +export const RunResourceIdentifier = getResourceIdentifierSchema( + 'runs', +).openapi('RunResourceIdentifier'); +const RunResourceIdentifierCreate = RunResourceIdentifier.omit({ id: true }); +const RunStatus = z + .enum(['idle', 'running', 'completed', 'interrupted', 'canceled']) + .openapi('RunStatus'); +const RunAttributes = z + .strictObject({ + status: RunStatus.describe('Status of the run'), + name: z.union([z.string().min(1), z.null()]).describe('Name of the run'), + lastLogNumber: z.number().int().nonnegative(), + missingLogNumbers: z.array(z.number().int().nonnegative()), + }) + .openapi('RunAttributes'); +const RunAttributesUpdate = RunAttributes.omit({ + missingLogNumbers: true, +}).partial(); +const RunAttributesCreate = RunAttributes.omit({ + lastLogNumber: true, + missingLogNumbers: true, +}); +const RunRelationships = z.strictObject({ + experiment: z.strictObject({ data: ExperimentResourceIdentifier }), + lastLogs: z.strictObject({ data: z.array(LogResourceIdentifier) }), +}); +const RunResourceCreate = z.strictObject({ + ...RunResourceIdentifierCreate.shape, + attributes: RunAttributesCreate, + relationships: RunRelationships.pick({ experiment: true }), +}); +const RunResourceUpdate = z.strictObject({ + ...RunResourceIdentifier.shape, + attributes: RunAttributesUpdate.optional(), + relationships: RunRelationships.pick({ experiment: true }) + .partial() + .optional(), +}); +export const RunResource = z + .strictObject({ + ...RunResourceIdentifier.shape, + attributes: RunAttributes, + relationships: RunRelationships, + }) + .openapi('RunResource'); + +// Request schemas +// ----------------------------------------------------------------------------- +const RunPostRequest = getDataDocumentSchema({ + data: RunResourceCreate, +}).openapi('RunPostRequest'); +const RunPatchRequest = getDataDocumentSchema({ + data: RunResourceUpdate, +}).openapi('RunPatchRequest'); + +// Query parameters schemas +// ----------------------------------------------------------------------------- +const RunIncludeName = z.enum(['experiment', 'lastLogs']); +const RunIncludeQuery = z.strictObject({ + include: z.union([RunIncludeName, z.array(RunIncludeName)]).optional(), +}); +const RunFilterQuery = z.strictObject({ + 'filter[experiment.id]': StringOrArrayOfStrings.optional(), + 'filter[experiment.name]': StringOrArrayOfStrings.optional(), + 'filter[id]': StringOrArrayOfStrings.optional(), + 'filter[name]': StringOrArrayOfStrings.optional(), + 'filter[status]': z.union([RunStatus, z.array(RunStatus)]).optional(), +}); + +// OK Response schemas +// ----------------------------------------------------------------------------- +const RunPostResponse = getDataDocumentSchema({ + data: RunResourceIdentifier, +}).openapi('RunPostResponse'); +const RunInclude = z + .union([ExperimentResource, LogResource]) + .openapi('RunInclude'); +const RunGetResponse = getDataDocumentSchema({ + data: RunResource, + includes: RunInclude, +}).openapi('RunGetResponse'); +const RunGetCollectionResponse = getDataDocumentSchema({ + data: z.array(RunResource), + includes: RunInclude, +}).openapi('RunGetCollectionResponse'); + +// Error Response schemas +// ----------------------------------------------------------------------------- +const runOnGoingErrorHttpCode = 403 as const; +const CannotCreateRunErrorResponse = getErrorDocumentSchema( + getErrorSchema({ + code: ['ONGOING_RUNS', 'EXPERIMENT_NOT_FOUND'], + statusCode: runOnGoingErrorHttpCode, + }), +).openapi('CannotCreateRunErrorResponse'); +const runExistsErrorHttpCode = 409 as const; +const RunExistsErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'RUN_EXISTS', statusCode: runExistsErrorHttpCode }), +).openapi('RunExistsErrorReponse'); +const runNotFoundErrorHttpCode = 404 as const; +const RunNotFoundErrorResponse = getErrorDocumentSchema( + getErrorSchema({ + code: 'RUN_NOT_FOUND', + statusCode: runNotFoundErrorHttpCode, + }), +).openapi('RunNotFoundErrorResponse'); +const RunInvalidUpdateErrorResponse = getErrorDocumentSchema( + getErrorSchema({ + code: [ + 'INVALID_STATUS_TRANSITION', + 'INVALID_LAST_LOG_NUMBER', + 'PENDING_LOGS', + 'INVALID_ROLE', + 'INVALID_RUN_ID', + ], + statusCode: 403, + }), +).openapi('RunInvalidUpdateErrorResponse'); + +// Route configuration +// ----------------------------------------------------------------------------- +export const runRoutes = { + '/': { + post: { + description: 'Create a new run', + request: { + body: { + required: true, + content: { [mediaType]: { schema: RunPostRequest } }, + }, + }, + responses: { + 201: { + description: 'Run created successfully', + headers: z.strictObject({ location: z.string() }), + content: { [mediaType]: { schema: RunPostResponse } }, + }, + 403: { + description: + 'Forbidden: run creation is invalid or user is not logged in', + content: { [mediaType]: { schema: CannotCreateRunErrorResponse } }, + }, + 409: { + description: 'Run already exists', + content: { [mediaType]: { schema: RunExistsErrorResponse } }, + }, + }, + }, + get: { + description: 'List all runs', + request: { + query: z.strictObject({ + ...RunFilterQuery.shape, + ...RunIncludeQuery.shape, + }), + }, + responses: { + 200: { + description: 'List of runs', + content: { [mediaType]: { schema: RunGetCollectionResponse } }, + }, + }, + }, + }, + '/{id}': { + get: { + description: 'Get a run by its ID', + request: { + params: z.object({ id: z.string().describe('ID of the run') }), + query: RunIncludeQuery, + }, + responses: { + 200: { + description: 'Run retrieved successfully', + content: { [mediaType]: { schema: RunGetResponse } }, + }, + 404: { + description: 'Run not found', + content: { [mediaType]: { schema: RunNotFoundErrorResponse } }, + }, + }, + }, + patch: { + description: 'Update a run by its ID', + request: { + params: z.object({ id: z.string().describe('ID of the run') }), + body: { + required: true, + content: { [mediaType]: { schema: RunPatchRequest } }, + }, + }, + responses: { + 200: { + description: 'Run updated successfully', + content: { [mediaType]: { schema: RunGetResponse } }, + }, + 404: { + description: 'Run not found', + content: { [mediaType]: { schema: RunNotFoundErrorResponse } }, + }, + 403: { + description: 'Run update is invalid or user is not logged in', + content: { + [mediaType]: { + schema: z.union([ + CannotCreateRunErrorResponse, + RunInvalidUpdateErrorResponse, + ]), + }, + }, + }, + }, + }, + }, +} satisfies RouteConfig; diff --git a/packages/log-api/src/security.ts b/packages/log-api/src/security.ts new file mode 100644 index 00000000..2b785812 --- /dev/null +++ b/packages/log-api/src/security.ts @@ -0,0 +1,9 @@ +import { registry } from './zod-openapi.ts'; + +export const authCookieName = 'lightmill-session-id' as const; + +export const sessionAuth = registry.registerComponent( + 'securitySchemes', + 'SessionAuth', + { type: 'apiKey', in: 'cookie', name: authCookieName }, +); diff --git a/packages/log-api/src/server-errors.ts b/packages/log-api/src/server-errors.ts new file mode 100644 index 00000000..d948ef23 --- /dev/null +++ b/packages/log-api/src/server-errors.ts @@ -0,0 +1,63 @@ +import { getErrorDocumentSchema, getErrorSchema } from './jsonapi.ts'; +import { z } from './zod-openapi.ts'; + +export const RequestValidationErrorResponse = getErrorDocumentSchema( + z.union([ + getErrorSchema({ code: 'INVALID_REQUEST_BODY', statusCode: 400 }) + .extend({ + source: z.strictObject({ + pointer: z + .string() + .describe('Pointer to the invalid part of the request body'), + }), + }) + .openapi('InvalidRequestBodyError'), + getErrorSchema({ code: 'INVALID_REQUEST_QUERY', statusCode: 400 }) + .extend({ + source: z.strictObject({ + parameter: z + .string() + .describe('Parameter in the request query that is invalid'), + }), + }) + .openapi('InvalidRequestQueryError'), + getErrorSchema({ code: 'INVALID_REQUEST_HEADERS', statusCode: 400 }) + .extend({ + source: z.strictObject({ + header: z.string().describe('Header in the request that is invalid'), + }), + }) + .openapi('InvalidRequestHeadersError'), + ]), +).openapi('ValidationErrorResponse'); + +export const NotFoundErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'NOT_FOUND', statusCode: 404 }), +).openapi('NotFoundErrorResponse'); + +export const InternalServerErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'INTERNAL_SERVER_ERROR', statusCode: 500 }), +).openapi('InternalServerErrorResponse'); + +export const MethodNotAllowedErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'METHOD_NOT_ALLOWED', statusCode: 405 }), +).openapi('MethodNotAllowedErrorResponse'); + +export const UnsupportedMediaTypeErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'UNSUPPORTED_MEDIA_TYPE', statusCode: 415 }), +).openapi('UnsupportedMediaTypeErrorResponse'); + +export const SessionRequiredErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'SESSION_REQUIRED', statusCode: 403 }), +).openapi('SessionRequiredErrorResponse'); + +export const ServerErrorResponse = z + .union([ + NotFoundErrorResponse, + RequestValidationErrorResponse, + InternalServerErrorResponse, + MethodNotAllowedErrorResponse, + UnsupportedMediaTypeErrorResponse, + SessionRequiredErrorResponse, + ]) + .openapi('ServerErrorResponse'); diff --git a/packages/log-api/src/session-schemas.ts b/packages/log-api/src/session-schemas.ts new file mode 100644 index 00000000..e8e22300 --- /dev/null +++ b/packages/log-api/src/session-schemas.ts @@ -0,0 +1,172 @@ +import { ExperimentResource } from './experiment-schemas.ts'; +import { + EmptyDataDocument, + getDataDocumentSchema, + getErrorDocumentSchema, + getErrorSchema, + getResourceIdentifierSchema, + mediaType, +} from './jsonapi.ts'; +import { LogResource } from './log-schemas.ts'; +import { RunResource, RunResourceIdentifier } from './run-schemas.ts'; +import { sessionAuth } from './security.ts'; +import { registry, type RouteConfig, z } from './zod-openapi.ts'; + +export const UserRole = z.enum(['host', 'participant']); + +// Resource schema +// ----------------------------------------------------------------------------- +export const SessionResourceIdentifier = getResourceIdentifierSchema( + 'sessions', +).openapi('SessionResourceIdentifier'); +const SessionAttributes = z.strictObject({ role: UserRole.optional() }); +const SessionRelationships = z.strictObject({ + runs: z.strictObject({ data: z.array(RunResourceIdentifier) }), +}); +export const SessionResource = z + .strictObject({ + ...SessionResourceIdentifier.shape, + attributes: SessionAttributes, + relationships: SessionRelationships, + }) + .openapi('SessionResource'); +const SessionResourceCreate = SessionResource.omit({ + id: true, + relationships: true, +}); + +// Request schemas +// ----------------------------------------------------------------------------- +const SessionPostRequest = getDataDocumentSchema({ + data: SessionResourceCreate, +}).openapi('SessionPostRequest'); + +// Response schemas +// ----------------------------------------------------------------------------- +const SessionPostResponse = getDataDocumentSchema({ + data: SessionResource, +}).openapi('SessionPostResponse'); +const SessionGetResponse = getDataDocumentSchema({ + data: SessionResource, + includes: z.union([RunResource, LogResource, ExperimentResource]), +}).openapi('SessionGetResponse'); + +// ----------------------------------------------------------------------------- +const IncludesQuery = z + .array(z.enum(['runs', 'runs.experiment', 'runs.lastLogs'])) + .optional() + .describe('Related resources to include in the response'); + +// Error schemas +// ----------------------------------------------------------------------------- +const SessionNotFoundErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'SESSION_NOT_FOUND', statusCode: 404 }), +).openapi('SessionNotFoundErrorResponse'); +const SessionExistsErrorResponse = getErrorDocumentSchema( + getErrorSchema({ code: 'SESSION_EXISTS', statusCode: 409 }), +).openapi('SessionExistsErrorResponse'); +const InvalidCredentialErrorResponse = getErrorDocumentSchema( + getErrorSchema({ + code: ['INVALID_CREDENTIALS', 'MISSING_CREDENTIALS'], + statusCode: 403, + }), +).openapi('InvalidCredentialErrorResponse'); + +// Common answers +// ----------------------------------------------------------------------------- +const sessionNotFoundResponse = { + 404: { + description: 'Session not found or missing credentials', + content: { [mediaType]: { schema: SessionNotFoundErrorResponse } }, + }, +}; + +// Authentication +// ----------------------------------------------------------------------------- +const basicAuth = registry.registerComponent('securitySchemes', 'BasicAuth', { + type: 'http', + scheme: 'Basic', +}); + +// Route configuration +// ----------------------------------------------------------------------------- +export const sessionRoutes = { + '/': { + post: { + description: 'Create a new session', + security: [{}, { [basicAuth.name]: [] }], + request: { + body: { + required: true, + content: { [mediaType]: { schema: SessionPostRequest } }, + }, + headers: z.looseObject({ + authorization: z + .string() + .optional() + .describe( + 'Basic authentication header for host role. Format: "Basic base64(username:password)"', + ), + }), + }, + responses: { + 201: { + description: + 'Session created successfully. If necessary, login and password are provided using the basic authentication scheme. The session ID is returned in a cookie to be used with cookie authentication.', + content: { [mediaType]: { schema: SessionPostResponse } }, + headers: z.strictObject({ + location: z + .string() + .describe('The URL of the created session resource'), + 'set-cookie': z + .string() + .describe('Session cookie for the created session'), + }), + }, + 403: { + description: 'Invalid credentials provided', + content: { [mediaType]: { schema: InvalidCredentialErrorResponse } }, + }, + 409: { + description: 'Client already has an active session', + content: { [mediaType]: { schema: SessionExistsErrorResponse } }, + }, + }, + }, + }, + '/{id}': { + get: { + security: [{}, { [sessionAuth.name]: [] }], + description: 'Get a session by ID', + request: { + params: z.strictObject({ + id: z.string().describe('ID of the session to retrieve'), + }), + query: z.strictObject({ include: IncludesQuery }), + }, + responses: { + 200: { + description: 'Session retrieved successfully', + content: { [mediaType]: { schema: SessionGetResponse } }, + }, + ...sessionNotFoundResponse, + }, + }, + delete: { + security: [{}, { [basicAuth.name]: [] }], + description: 'Delete a session by ID', + request: { + params: z.strictObject({ + id: z.string().describe('ID of the session to remove'), + }), + }, + responses: { + 200: { + description: 'Session deleted successfully', + content: { [mediaType]: { schema: EmptyDataDocument } }, + }, + ...sessionNotFoundResponse, + }, + }, + }, +} satisfies RouteConfig; diff --git a/packages/log-api/src/utils.ts b/packages/log-api/src/utils.ts new file mode 100644 index 00000000..55e59dd5 --- /dev/null +++ b/packages/log-api/src/utils.ts @@ -0,0 +1,28 @@ +import { getErrorDocumentSchema, getErrorSchema } from './jsonapi.ts'; +import { z } from './zod-openapi.ts'; + +export function mapKeys< + T extends object, + M extends (k: Extract) => PropertyKey, +>(obj: T, fn: M) { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => { + // @ts-expect-error: we assume T has no extra properties so key is + // a valid key of T. + return [fn(key), value]; + }), + ) as { [K in keyof T as ReturnType]: T[K] }; +} + +// Common types +export const StringOrArrayOfStrings = z + .union([z.string(), z.array(z.string())]) + .openapi('StringOrArrayOfStrings'); + +export const ForbiddenErrorResponse = getErrorDocumentSchema( + getErrorSchema({ + code: 'FORBIDDEN', + statusText: 'Forbidden', + statusCode: 403, + }), +).openapi('ForbiddenErrorResponse'); diff --git a/packages/log-api/src/zod-openapi.ts b/packages/log-api/src/zod-openapi.ts new file mode 100644 index 00000000..908bb19f --- /dev/null +++ b/packages/log-api/src/zod-openapi.ts @@ -0,0 +1,18 @@ +import { + extendZodWithOpenApi, + OpenAPIRegistry, + type RouteConfig as OpenAPIRouteConfig, +} from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod/v4'; + +extendZodWithOpenApi(z); + +export { z }; + +export const registry = new OpenAPIRegistry(); + +export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; +export type RouteConfig = Record< + `/${string}`, + Partial>> +>; diff --git a/packages/log-api/tsconfig.build.json b/packages/log-api/tsconfig.build.json index 0d3d0270..9b9c51d5 100644 --- a/packages/log-api/tsconfig.build.json +++ b/packages/log-api/tsconfig.build.json @@ -1,10 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../tsconfig.json", - "include": ["./temp/**/*.ts"], - "compilerOptions": { - "rootDir": "./temp", - "outDir": "./dist", - "sourceMap": false - } + "include": ["./src/**/*.ts"], + "compilerOptions": { "rootDir": "./src", "outDir": "./dist" } } diff --git a/packages/log-api/tsconfig.json b/packages/log-api/tsconfig.json index ddd84917..9b9c51d5 100644 --- a/packages/log-api/tsconfig.json +++ b/packages/log-api/tsconfig.json @@ -2,5 +2,5 @@ "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../tsconfig.json", "include": ["./src/**/*.ts"], - "compilerOptions": { "rootDir": "./src", "outDir": "./temp" } + "compilerOptions": { "rootDir": "./src", "outDir": "./dist" } } diff --git a/packages/log-api/type-spec/json-api.tsp b/packages/log-api/type-spec/json-api.tsp deleted file mode 100644 index b84ce945..00000000 --- a/packages/log-api/type-spec/json-api.tsp +++ /dev/null @@ -1,75 +0,0 @@ -import "@typespec/http"; - -namespace JsonApi; - -model ResourceIdentifier< - Type extends string = string, - Id extends string = string -> { - type: Type; - - @removeVisibility(Lifecycle.Create) - id: Id; -} - -model Resource< - Type extends string, - Attributes extends {}, - Id extends string = string -> { - @key - type: Type; - - @removeVisibility(Lifecycle.Create) - id: Id; - - attributes: Attributes; -} - -@Utils.withPartial(#["attributes"]) -model ResourcePatch - is Resource>; - -// JSON API document with data. -model DataDocument { - data: Data; -} - -// JSON API document with no data. Used for empty responses. -model EmptyDataDocument is DataDocument; - -// A JSON API error document that contains an array of errors. -model ErrorDocument { - errors: E[]; -} - -model BaseError { - status: HttpStatus; - code: string; - detail?: string; -} - -// JSON API error object with a code and status. -model Error< - Code extends string = string, - Status extends HttpStatus = HttpStatus -> extends BaseError { - status: Status; - code: Code; -} - -enum HttpStatus { - `200`: "OK", - `201`: "Created", - `204`: "No Content", - `400`: "Bad Request", - `401`: "Unauthorized", - `403`: "Forbidden", - `404`: "Not Found", - `405`: "Method Not Allowed", - `409`: "Conflict", - `415`: "Unsupported Media Type", - `500`: "Internal Server Error", -} - -alias APIContentType = "application/vnd.api+json"; diff --git a/packages/log-api/type-spec/main.tsp b/packages/log-api/type-spec/main.tsp deleted file mode 100644 index 66e3f6a5..00000000 --- a/packages/log-api/type-spec/main.tsp +++ /dev/null @@ -1,477 +0,0 @@ -import "@typespec/http"; -import "@typespec/openapi"; -import "./json-api.tsp"; -import "./utils.tsp"; - -using TypeSpec.Http; -using Utils; - -@OpenAPI.info(#{ version: "3.0" }) -@service(#{ title: "Experiment Logging Service" }) -namespace LogService; - -@route("/sessions") -namespace Session { - model ResourceIdentifier is JsonApi.ResourceIdentifier<"sessions", string>; - - model Resource extends ResourceIdentifier { - attributes: { - role: UserRole; - }; - - @visibility(Lifecycle.Read) - relationships: { - runs: { - data: Run.ResourceIdentifier[]; - }; - }; - } - - model NotFoundErrorResponse extends NotFoundResponse { - @body doc: NotFoundErrorDocTemplate<"SESSION_NOT_FOUND">; - @header contentType: JsonApi.APIContentType; - } - - enum Include { - runs, - `runs.experiment`, - `runs.lastLogs`, - } - model Includes { - @visibility(Lifecycle.Read) - included?: Array; - } - - /** - * The role of a user. - * - `host` users have access to all running experiments, runs and logs. - * - `participant` users may create runs, and log events. - */ - enum UserRole { - host, - participant, - } - - @post - @useAuth(BasicAuth | NoAuth) - op post( - @body doc: JsonApi.DataDocument, - @header Authorization?: string, - @header contentType: JsonApi.APIContentType, - ): { - @statusCode statusCode: 201; - @body doc: JsonApi.DataDocument; - @header contentType: JsonApi.APIContentType; - @header("Set-Cookie") sessionCookie: string; - @header("Location") location: string; - } | InvalidCredentialsErrorResponse | { - @statusCode statusCode: 409; - @body doc: JsonApi.ErrorDocument>; - @header contentType: JsonApi.APIContentType; - }; - - @get - @useAuth(CookieSessionAuth | NoAuth) - op get(@path id: string, @query include?: Array | Include): { - @body doc: JsonApi.DataDocument & Includes; - @header contentType: JsonApi.APIContentType; - } | NotFoundErrorResponse; - - @delete - @useAuth(CookieSessionAuth | NoAuth) - op delete(@path id: string): { - @body doc: JsonApi.EmptyDataDocument; - @header contentType: JsonApi.APIContentType; - } | NotFoundErrorResponse; -} - -@route("/experiments") -namespace Experiment { - model Resource is JsonApi.Resource<"experiments", Attributes>; - model Attributes { - @minLength(1) - @visibility(Lifecycle.Read, Lifecycle.Create) - name: string; - } - model ResourceIdentifier is JsonApi.ResourceIdentifier<"experiments">; - - model NotFoundErrorResponse extends NotFoundResponse { - @body doc: NotFoundErrorDocTemplate<"EXPERIMENT_NOT_FOUND">; - @header contentType: JsonApi.APIContentType; - } - - @get - @useAuth(CookieSessionAuth | NoAuth) - op getCollection(@query `filter[name]`?: string | string[]): { - @body doc: JsonApi.DataDocument; - @header contentType: JsonApi.APIContentType; - } | ForbiddenErrorResponse | SessionRequiredErrorResponse; - - @get - @useAuth(CookieSessionAuth | NoAuth) - op getSingle(@path id: string): - | { - @body doc: JsonApi.DataDocument; - @header contentType: JsonApi.APIContentType; - } - | ForbiddenErrorResponse - | NotFoundErrorResponse - | SessionRequiredErrorResponse; - - @post - @useAuth(CookieSessionAuth | NoAuth) - op post( - @body experiment: JsonApi.DataDocument, - @header contentType: JsonApi.APIContentType, - ): - | { - @statusCode statusCode: 201; - @header("Location") location: string; - @body doc: JsonApi.DataDocument; - @header contentType: JsonApi.APIContentType; - } - | ForbiddenErrorResponse - | SessionRequiredErrorResponse - | { - @statusCode statusCode: 409; - @body doc: JsonApi.ErrorDocument>; - @header contentType: JsonApi.APIContentType; - }; -} - -@route("/runs") -namespace Run { - model Attributes { - @minLength(1) - @visibility(Lifecycle.Read, Lifecycle.Create) - name: string | null; - - status: Status; - - @removeVisibility(Lifecycle.Create) - lastLogNumber: int32; - - @visibility(Lifecycle.Read) - missingLogNumbers: int32[]; - } - - model ResourceIdentifier is JsonApi.ResourceIdentifier<"runs">; - - model Resource is JsonApi.Resource<"runs", Attributes> { - @visibility(Lifecycle.Read, Lifecycle.Create) - relationships: { - @visibility(Lifecycle.Read, Lifecycle.Create) - experiment: { - data: { - id: string; - type: "experiments"; - }; - }; - - @removeVisibility(Lifecycle.Create, Lifecycle.Update) - lastLogs: { - data: { - id: string; - type: "logs"; - }[]; - }; - }; - } - - model ResourcePatch is JsonApi.ResourcePatch<"runs", Attributes>; - - model Includes { - @visibility(Lifecycle.Read) - included?: Array; - } - - model NotFoundErrorResponse extends NotFoundResponse { - @body doc: NotFoundErrorDocTemplate<"RUN_NOT_FOUND">; - @header contentType: JsonApi.APIContentType; - } - - model OngoingRunsErrorResponse extends ForbiddenResponse { - @statusCode statusCode: 403; - @body doc: ForbiddenErrorDocTemplate<"ONGOING_RUNS">; - @header contentType: JsonApi.APIContentType; - } - - /** - * The status of a run. - * - `idle` means that the run has not started yet. It may be started, or - * canceled. - * - `running` means that the run is currently running. It may be interrupted, - * canceled, or completed later. - * - `completed` means that the run has been completed. It may not be resumed, - * interrupted, or canceled anymore. - * - `interrupted` means that the run has been interrupted, is not currently - * running, but hasn't been completed yet. It may be resumed later. - * - `canceled` means that the run has been canceled. It may not be resumed, - * interrupted, or completed anymore. This status make it possible to - * recreate the run (i.e. start a new run with the same name for the same - * experiment). - */ - enum Status { - idle, - running, - completed, - interrupted, - canceled, - } - - enum Include { - experiment, - lastLogs, - } - - @useAuth(CookieSessionAuth) - @post - op post( - @body run: JsonApi.DataDocument, - @header contentType: JsonApi.APIContentType, - ): - | { - @statusCode statusCode: 201; - @body doc: JsonApi.DataDocument; - @header("Location") location: string; - @header contentType: JsonApi.APIContentType; - } - | SessionRequiredErrorResponse - | ForbiddenErrorResponse - | OngoingRunsErrorResponse - | { - @statusCode statusCode: 409; - @body doc: JsonApi.ErrorDocument>; - @header contentType: JsonApi.APIContentType; - }; - - @get - @useAuth(CookieSessionAuth) - op getSingle(@path id: string, @query include?: Array | Include): - | { - @body doc: JsonApi.DataDocument & Includes; - @header contentType: JsonApi.APIContentType; - } - | SessionRequiredErrorResponse - | ForbiddenErrorResponse - | NotFoundErrorResponse; - - @get - @useAuth(CookieSessionAuth) - op getCollection( - @query `filter[experiment.id]`?: string | string[], - @query `filter[experiment.name]`?: string | string[], - @query `filter[id]`?: string | string[], - @query `filter[name]`?: string | string[], - @query `filter[status]`?: Status | Status[], - @query include?: Array | Include, - ): { - @body doc: JsonApi.DataDocument & Includes; - @header contentType: JsonApi.APIContentType; - } | ForbiddenErrorResponse | SessionRequiredErrorResponse; - - @patch(#{ implicitOptionality: false }) - @useAuth(CookieSessionAuth) - op patch( - @path id: string, - @body run: JsonApi.DataDocument, - @header contentType: JsonApi.APIContentType, - ): - | { - @statusCode statusCode: 200; - @body doc: JsonApi.DataDocument; - @header contentType: JsonApi.APIContentType; - } - | { - @statusCode statusCode: 403; - - @body - doc: ForbiddenErrorDocTemplate< - | "INVALID_STATUS_TRANSITION" - | "INVALID_LAST_LOG_NUMBER" - | "PENDING_LOGS" - | "INVALID_ROLE" - | "INVALID_RUN_ID">; - - @header contentType: JsonApi.APIContentType; - } - | OngoingRunsErrorResponse - | NotFoundErrorResponse - | SessionRequiredErrorResponse; -} - -@route("/logs") -namespace Log { - model ResourceIdentifier is JsonApi.ResourceIdentifier<"logs">; - - model Resource is JsonApi.Resource<"logs", Attributes> { - @visibility(Lifecycle.Read, Lifecycle.Create) - relationships: { - run: { - data: { - type: "runs"; - id: string; - }; - }; - }; - } - - model Attributes { - /** - * The type of the log. This is not the same as the resource type (which is always "logs" for - * logs). This is a type that describes the kind of log. For example, it could - * be "trial", "event", etc. - */ - logType: string; - - /** - * The number of the log. This is a number that is unique for the run - * the log belongs to. Log numbers must be sequential and start at 1. - * Logs must not necessarily be created in order, but any missing log - * must be created before the run is completed, and any log - * following a missing log is considered pending. - */ - @minValue(1) - number: int32; - - /** - * The log values. They may be any JSON object. However, it is recommended - * to use flat objects as nested objects are difficult to serialize to CSV. - * It is also recommended to use a consistent schema for all logs of the - * same type. - */ - values: Record; - } - - enum Include { - run, - `run.experiment`, - `run.lastLogs`, - } - model Includes { - @visibility(Lifecycle.Read) - included?: Array; - } - - model NotFoundErrorResponse extends NotFoundResponse { - @body doc: NotFoundErrorDocTemplate<"LOG_NOT_FOUND">; - @header contentType: JsonApi.APIContentType; - } - - /** - * Get all logs, optionally filtered using query parameters. - * Defaults to CSV format so `Accept` header is required to get JSON. - */ - @get - @useAuth(CookieSessionAuth) - // Note: I experimented with '@overload' to distinguish between JSON and CSV - // requests, but it didn't seem to work with openapi. It's even more unclear - // how it would play out with @lightmill/log-server. - op getCollection( - @query `filter[logType]`?: string | string[], - @query `filter[experiment.id]`?: string | string[], - @query `filter[experiment.name]`?: string | string[], - @query `filter[run.name]`?: string | string[], - @query `filter[run.id]`?: string | string[], - @query include?: Include | Include[], - @header accept?: string, - ): - | { - @statusCode statusCode: 200; - @body doc: JsonApi.DataDocument & Includes; - @header contentType: JsonApi.APIContentType; - } - | { - @statusCode statusCode: 200; - @header contentType: "text/csv"; - @body csv: string; - } - | { - @statusCode statusCode: 400; - @body doc: InvalidQueryParameterDoc; - @header contentType: JsonApi.APIContentType; - } - | ForbiddenErrorResponse - | UnsupportedMediaTypeErrorResponse - | SessionRequiredErrorResponse; - - @get - @useAuth(CookieSessionAuth) - op getSingle(@path id: string, @query include?: Include | Include[]): - | { - @body - body: JsonApi.DataDocument; - - @header contentType: JsonApi.APIContentType; - } - | ForbiddenErrorResponse - | NotFoundErrorResponse - | SessionRequiredErrorResponse; - - @post - @useAuth(CookieSessionAuth) - op post( - @body log: JsonApi.DataDocument, - @header contentType: JsonApi.APIContentType, - ): - | { - @statusCode statusCode: 201; - @body doc: JsonApi.DataDocument; - @header contentType: JsonApi.APIContentType; - @header("Location") location: string; - } - | SessionRequiredErrorResponse - | ForbiddenErrorResponse - | { - @statusCode statusCode: 403; - - @body - doc: ForbiddenErrorDocTemplate<"INVALID_RUN_STATUS" | "RUN_NOT_FOUND">; - - @header contentType: JsonApi.APIContentType; - } - | { - @statusCode statusCode: 409; - - @body - doc: JsonApi.ErrorDocument>; - - @header contentType: JsonApi.APIContentType; - }; -} - -alias NonRouterError = - | NotFoundError - | InvalidCredentialsError - | UnsupportedMediaTypeError - | JsonApi.Error<"METHOD_NOT_ALLOWED", JsonApi.HttpStatus.`405`> - | JsonApi.Error<"INTERNAL_SERVER", JsonApi.HttpStatus.`500`> - | (JsonApi.Error<"BODY_VALIDATION", JsonApi.HttpStatus.`400`> & { - source: { - pointer: string; - }; - }) - | (JsonApi.Error<"QUERY_VALIDATION", JsonApi.HttpStatus.`400`> & { - source: { - parameter: string; - }; - }) - | (JsonApi.Error<"HEADERS_VALIDATION", JsonApi.HttpStatus.`400`> & { - source: { - header: string; - }; - }); - -model NonRouterErrorDocument is JsonApi.ErrorDocument; diff --git a/packages/log-api/type-spec/utils.js b/packages/log-api/type-spec/utils.js deleted file mode 100644 index cfa3f34f..00000000 --- a/packages/log-api/type-spec/utils.js +++ /dev/null @@ -1,24 +0,0 @@ -function updateModelPropertiesInPlace(model, callback) { - for (const [key, prop] of model.properties) { - const updatedProp = callback(key, prop); - model.properties.set(key, updatedProp); - } -} - -export function withPartial(_context, target, props) { - let notFoundProps = new Set(props ?? []); - updateModelPropertiesInPlace(target, (_key, prop) => { - if (props == null || props.includes(prop.name)) { - notFoundProps.delete(prop.name); - return { ...prop, optional: true }; - } - return prop; - }); - if (notFoundProps.size > 0) { - throw new Error( - `Partial decorator: properties ${Array.from(notFoundProps).join(', ')} not found`, - ); - } -} - -export const $decorators = { Utils: { withPartial } }; diff --git a/packages/log-api/type-spec/utils.tsp b/packages/log-api/type-spec/utils.tsp deleted file mode 100644 index 649c00e1..00000000 --- a/packages/log-api/type-spec/utils.tsp +++ /dev/null @@ -1,90 +0,0 @@ -import "@typespec/http"; -import "./json-api.tsp"; -import "./utils.js"; - -using TypeSpec.Http; -using JsonApi; - -namespace Utils; - -extern dec withPartial(target: Reflection.Model, props?: valueof Array); - -model InvalidQueryParameterDoc - is ErrorDocument & { - source: { - parameter: string; - }; - }>; - -/** - * Error document sent when an operation is rejected due to insufficient - * permissions. - */ -model ForbiddenErrorDocTemplate - is ErrorDocument>; -model ForbiddenErrorDoc is ForbiddenErrorDocTemplate<"FORBIDDEN">; -model ForbiddenErrorResponse extends ForbiddenResponse { - @body doc: ForbiddenErrorDoc; - @header contentType: JsonApi.APIContentType; -} - -model InvalidCredentialsError is Error<"INVALID_CREDENTIALS", HttpStatus.`403`>; -model InvalidCredentialsErrorResponse extends ForbiddenResponse { - @body doc: ErrorDocument; - @header contentType: JsonApi.APIContentType; -} - -model NotFoundError - is Error; -/** - * Error document sent when a resource is not found. - */ -model NotFoundErrorDocTemplate - is ErrorDocument>; -model NotFoundErrorDoc is NotFoundErrorDocTemplate<"NOT_FOUND">; -model NotFoundErrorResponse extends NotFoundResponse { - @body doc: NotFoundErrorDoc; - @header contentType: JsonApi.APIContentType; -} - -/** - * Error document sent when a request is made with an invalid session. - */ -model SessionRequiredError is Error<"SESSION_REQUIRED", HttpStatus.`403`>; -model SessionRequiredErrorResponse extends ForbiddenResponse { - @body doc: ErrorDocument; - @header contentType: JsonApi.APIContentType; -} - -model UnsupportedMediaTypeError - is Error<"UNSUPPORTED_MEDIA_TYPE", HttpStatus.`415`>; -/** - * Error document sent when an unsupported media type is requested. - */ -model UnsupportedMediaTypeErrorDoc - is ErrorDocument>; -model UnsupportedMediaTypeErrorResponse extends Response<415> { - @body doc: ErrorDocument; - @header contentType: JsonApi.APIContentType; -} - -alias CookieSessionName = "lightmill-session-id"; - -// I don't want this to be in main because I don't want this model to be -// included in the generated OpenAPI schemas. -@doc("Cookie-based session") -model CookieSessionAuth { - @doc("Http authentication") - type: AuthType.apiKey; - - @doc("location of the API key") - in: ApiKeyLocation.cookie; - - @doc("name of the API key") - name: CookieSessionName; -} - -@withPartial -model Partial { - ...T; -} diff --git a/packages/log-client/__mocks__/mock-server.ts b/packages/log-client/__mocks__/mock-server.ts index 6d369e2d..248dc9aa 100644 --- a/packages/log-client/__mocks__/mock-server.ts +++ b/packages/log-client/__mocks__/mock-server.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { type paths } from '@lightmill/log-api'; import { http, HttpResponse, @@ -10,6 +9,7 @@ import { import { setupServer, SetupServerApi } from 'msw/node'; import type { IsNever, RequiredKeysOf } from 'type-fest'; import { test, vi, type Mock } from 'vitest'; +import { type paths } from '../src/generated/openapi.js'; import { apiMediaType } from '../src/utils.js'; export type ApiMediaType = typeof apiMediaType; @@ -32,14 +32,18 @@ type PathMethod = Extract< type ApiRequestBody< Path extends keyof paths, Method extends PathMethod, -> = paths extends { - [P in Path]: { - [M in Method]: { - requestBody: { content: { [K in ApiMediaType]: infer R } }; - }; - }; -} - ? R +> = paths extends { [P in Path]: { [M in Method]: infer Route } } + ? Route extends { + requestBody: { content: { [K in ApiMediaType]: infer Content } }; + } + ? Content + : Route extends { requestBody?: never } + ? undefined + : Route extends { + requestBody?: { content: { [K in ApiMediaType]: infer Content } }; + } + ? Content | undefined + : never : never; type ApiRequestPathParams< diff --git a/packages/log-client/package.json b/packages/log-client/package.json index 207e7bce..d311f4c8 100644 --- a/packages/log-client/package.json +++ b/packages/log-client/package.json @@ -25,6 +25,7 @@ "cross-env": "^7.0.3", "msw": "^2.7.3", "onchange": "^7.1.0", + "openapi-typescript": "^7.8.0", "type-fest": "^4.39.1", "typescript": "catalog:", "vitest": "catalog:" @@ -34,7 +35,9 @@ }, "scripts": { "test": "cross-env NODE_ENV=test vitest", - "build": "tsc -b tsconfig.build.json", + "generate-openapi-types": "openapi-typescript node_modules/@lightmill/log-api/dist/openapi.yaml --output src/generated/openapi.ts", + "build-ts": "tsc -b tsconfig.build.json", + "build": "pnpm run generate-openapi-types && pnpm run build-ts", "prepublish": "pnpm run build" }, "keywords": [ diff --git a/packages/log-client/src/client.ts b/packages/log-client/src/client.ts index 4ebeb75b..16064477 100644 --- a/packages/log-client/src/client.ts +++ b/packages/log-client/src/client.ts @@ -1,5 +1,5 @@ -import type { components, paths } from '@lightmill/log-api'; import createClient from 'openapi-fetch'; +import type { components, paths } from './generated/openapi.js'; import { anyLogSerializer } from './log-serializer.js'; import { LightmillLogger } from './logger.js'; import type { @@ -207,13 +207,12 @@ export class LightmillClient { let response = await this.#fetchClient.GET('/experiments', { params: { query: { 'filter[name]': experimentName } }, }); - if (response.data != null) return response.data.data[0]; - if (response.response.status === 404) return null; - let error = response.error.errors[0]; - throw new Error( - error.detail ?? - `Could not fetch experiment: server returned ${error.code}`, - ); + // Checking the length isn't strictly necessary, but it makes the + // intention clearer, and let typescript know that it may return null. + if (response.data != null) { + return response.data.data.length > 0 ? response.data.data[0] : null; + } + throw new RequestError(response); } async #getRunFromName( @@ -231,12 +230,12 @@ export class LightmillClient { }, }, }); - if (response.data != null) return response.data.data[0]; - if (response.response.status === 404) return null; - let error = response.error.errors[0]; - throw new Error( - error.detail ?? `Could not fetch run: server returned ${error.code}`, - ); + // Checking the length isn't strictly necessary, but it makes the + // intention clearer, and let typescript know that it may return null. + if (response.data != null) { + return response.data.data.length > 0 ? response.data.data[0] : null; + } + throw new RequestError(response); } async #getRunIdFromName( @@ -355,5 +354,5 @@ export class LightmillClient { } } -type ExperimentResource = components['schemas']['Experiment.Resource']; -type LogResource = components['schemas']['Log.Resource']; +type ExperimentResource = components['schemas']['ExperimentResource']; +type LogResource = components['schemas']['LogResource']; diff --git a/packages/log-client/src/logger.ts b/packages/log-client/src/logger.ts index 3937ddee..e67abc91 100644 --- a/packages/log-client/src/logger.ts +++ b/packages/log-client/src/logger.ts @@ -1,6 +1,6 @@ -import type { paths } from '@lightmill/log-api'; import type { Client as FetchClient } from 'openapi-fetch'; import type { JsonValue } from 'type-fest'; +import type { paths } from './generated/openapi.js'; import { Subject } from './subject.ts'; import type { LogValuesSerializer, RunStatus } from './types.js'; import { apiMediaType, RequestError } from './utils.js'; diff --git a/packages/log-server/__tests__/__snapshots__/app-runs.test.ts.snap b/packages/log-server/__tests__/__snapshots__/app-runs.test.ts.snap index 363ea1ad..fd7b8bd7 100644 --- a/packages/log-server/__tests__/__snapshots__/app-runs.test.ts.snap +++ b/packages/log-server/__tests__/__snapshots__/app-runs.test.ts.snap @@ -614,3 +614,315 @@ exports[`LogServer: get /runs ('participant' / 'sqlite') > returns a 200 with pa ], } `; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'canceled' to 'completed' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status. Run status canceled is terminal.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'canceled' to 'idle' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status. Run status canceled is terminal.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'canceled' to 'interrupted' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status. Run status canceled is terminal.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'canceled' to 'running' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status. Run status canceled is terminal.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'completed' to 'idle' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from completed to idle. Allowed transitions are: completed -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'completed' to 'interrupted' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from completed to interrupted. Allowed transitions are: completed -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'completed' to 'running' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from completed to running. Allowed transitions are: completed -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'idle' to 'completed' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from idle to completed. Allowed transitions are: idle -> running or idle -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'idle' to 'interrupted' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from idle to interrupted. Allowed transitions are: idle -> running or idle -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'interrupted' to 'completed' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from interrupted to completed. Allowed transitions are: interrupted -> running or interrupted -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'interrupted' to 'idle' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from interrupted to idle. Allowed transitions are: interrupted -> running or interrupted -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to change the status of a run from 'running' to 'idle' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from running to idle. Allowed transitions are: running -> interrupted, running -> canceled, or running -> completed.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('host' / 'sqlite') > refuses to complete a run if there are pending logs 1`] = ` +{ + "errors": [ + { + "code": "PENDING_LOGS", + "detail": "Cannot complete run with pending logs. Ensure all logs are added to the run before completing it.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'canceled' to 'completed' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status. Run status canceled is terminal.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'canceled' to 'idle' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status. Run status canceled is terminal.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'canceled' to 'interrupted' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status. Run status canceled is terminal.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'canceled' to 'running' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status. Run status canceled is terminal.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'completed' to 'idle' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from completed to idle. Allowed transitions are: completed -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'completed' to 'interrupted' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from completed to interrupted. Allowed transitions are: completed -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'completed' to 'running' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from completed to running. Allowed transitions are: completed -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'idle' to 'completed' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from idle to completed. Allowed transitions are: idle -> running or idle -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'idle' to 'interrupted' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from idle to interrupted. Allowed transitions are: idle -> running or idle -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'interrupted' to 'completed' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from interrupted to completed. Allowed transitions are: interrupted -> running or interrupted -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'interrupted' to 'idle' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from interrupted to idle. Allowed transitions are: interrupted -> running or interrupted -> canceled.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to change the status of a run from 'running' to 'idle' 1`] = ` +{ + "errors": [ + { + "code": "INVALID_STATUS_TRANSITION", + "detail": "Cannot change run status from running to idle. Allowed transitions are: running -> interrupted, running -> canceled, or running -> completed.", + "status": "Forbidden", + }, + ], +} +`; + +exports[`LogServer: patch /runs/:run ('participant' / 'sqlite') > refuses to complete a run if there are pending logs 1`] = ` +{ + "errors": [ + { + "code": "PENDING_LOGS", + "detail": "Cannot complete run with pending logs. Ensure all logs are added to the run before completing it.", + "status": "Forbidden", + }, + ], +} +`; diff --git a/packages/log-server/__tests__/api-utils.test-d.ts b/packages/log-server/__tests__/api-utils.test-d.ts deleted file mode 100644 index 9b60dca1..00000000 --- a/packages/log-server/__tests__/api-utils.test-d.ts +++ /dev/null @@ -1,227 +0,0 @@ -import type { paths } from '@lightmill/log-api'; -import type { Merge, Simplify } from 'type-fest'; -import { expectTypeOf, test } from 'vitest'; -import type { - AllApiPathFromMethod, - Api, - ApiMethodFromPath, - ApiOperation, - ApiPathFromMethod, - ApiRequestContent, - ApiRequestParameters, - ApiResponseContent, -} from '../src/api-utils.ts'; - -type TestApi = { - '/{id}': { - parameters: { query: { x: 'just something to mess things up' } }; - get: { - parameters: { - query: { q: 'a'[] }; - header?: never; - path: { id: 'x' | 'y' }; - }; - requestBody?: never; - responses: { - 404: { - headers: { [key: string]: unknown }; - content: { 'root-get-404-content-type': 'root-get-404-body' }; - }; - 200: { - headers: { [key: string]: unknown }; - content: { 'root-get-200-content-type': 'root-get-200-body' }; - }; - }; - }; - put?: never; - post?: never; - delete: { - parameters: { - query: { q: 'a'[] }; - header?: never; - path: { id: 'x' | 'y' }; - }; - requestBody?: never; - responses: { - 200: { - headers: { [key: string]: unknown }; - content: { 'root-delete-200-content-type': 'root-delete-200-body' }; - }; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/resources': { - parameters: { query: 'something else to mess things up' }; - put?: never; - patch?: never; - delete?: never; - trace?: never; - post: { - parameters: { query?: never; header?: never; path?: never }; - requestBody: { content: { 'res-post-content-type': 're-post-body' } }; - responses: { - 404: { - headers: { [key: string]: unknown }; - content: { 'res-post-404-content-type': 'res-post-404-body' }; - }; - 201: { - headers: Merge<{ [key: string]: unknown }, { Location: string }>; - content: { 'res-post-201-content-type': 'res-post-200-body' }; - }; - }; - }; - get: { - parameters: { query: { q1: 'q1' }; header?: never; path?: never }; - requestBody?: never; - responses: { - 404: { - headers: { [key: string]: unknown }; - content: { 'res-get-404-content-type': 'res-get-404-body' }; - }; - 200: { - headers: { [key: string]: unknown }; - content: { 'res-get-200-content-type': 'res-get-200-body' }; - }; - }; - }; - }; -}; - -test('Api', () => { - expectTypeOf().toMatchTypeOf(); - expectTypeOf>().toMatchTypeOf(); - expectTypeOf>().toMatchTypeOf(); -}); - -test('ApiOperation', () => { - expectTypeOf>().toEqualTypeOf< - TestApi['/{id}']['delete'] - >(); - expectTypeOf< - ApiOperation - >().toEqualTypeOf(); - expectTypeOf< - ApiOperation - >().toEqualTypeOf< - | TestApi['/{id}']['delete'] - | TestApi['/{id}']['get'] - | TestApi['/resources']['get'] - >(); - - expectTypeOf().toMatchTypeOf(); -}); - -test('ApiPathFromMethod', () => { - expectTypeOf>().toEqualTypeOf< - '/{id}' | '/resources' - >(); - expectTypeOf< - ApiPathFromMethod - >().toEqualTypeOf<'/resources'>(); - expectTypeOf< - ApiPathFromMethod - >().toEqualTypeOf<'/resources'>(); - // No paths supports both post and delete - expectTypeOf< - ApiPathFromMethod - >().toEqualTypeOf(); - // No paths supports patch - expectTypeOf>().toEqualTypeOf(); -}); - -test('ApiMethodFromPath', () => { - expectTypeOf>().toEqualTypeOf< - 'get' | 'post' - >(); - expectTypeOf>().toEqualTypeOf< - 'delete' | 'get' - >(); - expectTypeOf< - ApiMethodFromPath - >().toEqualTypeOf<'get'>(); - expectTypeOf< - ApiMethodFromPath - >().toEqualTypeOf(); -}); - -test('ApiRequestContent', () => { - { - type Actual = ApiRequestContent; - type Expected = { - contentType: 'res-post-content-type'; - body: 're-post-body'; - }; - expectTypeOf().toEqualTypeOf(); - } - { - type Actual = ApiRequestContent; - type Expected = never; - expectTypeOf().toEqualTypeOf(); - } -}); - -test('ApiResponseContent (get /{id})', () => { - type Actual = ApiResponseContent; - type Expected = - | { - contentType: 'root-get-404-content-type'; - body: 'root-get-404-body'; - status: 404; - headers?: { [key: string]: unknown } | undefined; - } - | { - contentType: 'root-get-200-content-type'; - body: 'root-get-200-body'; - status: 200; - headers?: { [key: string]: unknown } | undefined; - }; - expectTypeOf().toEqualTypeOf(); -}); - -test('ApiResponseContent (post /resources)', () => { - type Actual = ApiResponseContent; - type Expected = - | { - contentType: 'res-post-404-content-type'; - body: 'res-post-404-body'; - status: 404; - headers?: { [key: string]: unknown } | undefined; - } - | { - contentType: 'res-post-201-content-type'; - body: 'res-post-200-body'; - status: 201; - headers: Merge<{ [key: string]: unknown }, { Location: string }>; - }; - expectTypeOf().toEqualTypeOf(); -}); - -test('ApiRequestParameters', () => { - expectTypeOf< - ApiRequestParameters - >().toEqualTypeOf<{ query: { q1: 'q1' }; header?: never; path?: never }>(); - - expectTypeOf< - ApiRequestParameters - >().toEqualTypeOf<{ - query: { q: 'a'[] }; - header?: never; - path: { id: 'x' | 'y' }; - }>(); -}); - -test('AllApiPathFromMethod', () => { - expectTypeOf< - AllApiPathFromMethod - >().toEqualTypeOf<'/resources' | '/{id}'>(); - expectTypeOf< - AllApiPathFromMethod - >().toEqualTypeOf<'/resources'>(); - expectTypeOf>().toEqualTypeOf< - '/resources' | '/{id}' - >(); -}); diff --git a/packages/log-server/__tests__/app-errors.test.ts b/packages/log-server/__tests__/app-errors.test.ts index 5792247f..6e7644d2 100644 --- a/packages/log-server/__tests__/app-errors.test.ts +++ b/packages/log-server/__tests__/app-errors.test.ts @@ -3,7 +3,7 @@ import express from 'express'; import request from 'supertest'; import { afterEach, describe, test, vi } from 'vitest'; -import { apiMediaType, type ServerRequestContent } from '../src/app-utils.ts'; +import { apiMediaType } from '../src/api.ts'; import { apiContentTypeRegExp, createAllRoute, @@ -19,235 +19,252 @@ afterEach(() => { type Fixture = { api: request.Agent }; -describe.for(storeTypes)( - 'LogServer Errors ($storesType server)', - (storeType) => { - const it = test.extend({ - api: async ({}, use) => { - let { server } = await createServerContext({ type: storeType }); - let app = express().use(server.middleware); - let api = request.agent(app); - await use(api); - }, - }); +describe.for(storeTypes)('LogServer Errors (%s server)', (storeType) => { + const it = test.extend({ + api: async ({}, use) => { + let { server } = await createServerContext({ type: storeType }); + let app = express().use(server.middleware); + let api = request.agent(app); + await use(api); + }, + }); - it('returns a 404 error if the requested route does not exist', async ({ - api, - }) => { - await api - .get('/not-a-route') - .expect('Content-Type', apiContentTypeRegExp) - .expect(404, { - errors: [ - { - status: 'Not Found', - code: 'NOT_FOUND', - detail: 'resource /not-a-route does not exist', - }, - ], - }); + it('returns a 404 error if the requested route does not exist', async ({ + api, + }) => { + await api + .get('/not-a-route') + .expect('Content-Type', apiContentTypeRegExp) + .expect(404, { + errors: [ + { + status: 'Not Found', + code: 'NOT_FOUND', + detail: 'Resource /not-a-route does not exist.', + }, + ], + }); - await api - .get('/resources/that-do-not-exist') - .expect('Content-Type', apiContentTypeRegExp) - .expect(404, { - errors: [ - { - status: 'Not Found', - code: 'NOT_FOUND', - detail: 'resource /resources/that-do-not-exist does not exist', - }, - ], - }); - }); + await api + .get('/resources/that-do-not-exist') + .expect('Content-Type', apiContentTypeRegExp) + .expect(404, { + errors: [ + { + status: 'Not Found', + code: 'NOT_FOUND', + detail: 'Resource /resources/that-do-not-exist does not exist.', + }, + ], + }); + }); - it('returns a 415 error if the requested route does not accept the media type', async ({ - api, - }) => { - await api - .post('/sessions') - .set('content-type', 'application/json') - .send({ - data: { type: 'sessions', attributes: { role: 'participant' } }, - } satisfies ServerRequestContent<'/sessions', 'post'>['body']) - .expect(415, { - errors: [ - { - status: 'Unsupported Media Type', - code: 'UNSUPPORTED_MEDIA_TYPE', - detail: 'unsupported media type application/json', - }, - ], - }); - }); + it('returns a 415 error if the requested route does not accept the media type', async ({ + api, + expect, + }) => { + const response = await api + .post('/sessions') + .set('content-type', 'application/json') + .send({ data: { type: 'sessions', attributes: { role: 'participant' } } }) + .expect(415); + expect(response.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "code": "UNSUPPORTED_MEDIA_TYPE", + "detail": "Content type must be 'application/vnd.api+json'. Set 'Content-Type' header to 'application/vnd.api+json'.", + "status": "Unsupported Media Type", + }, + ], + } + `); + }); - it('returns a 405 error if an unsupported method is used with an existing resource', async ({ - api, - }) => { - await api - .put('/sessions/current') - .set('Content-Type', apiMediaType) - .send({}) - .expect('Content-Type', apiContentTypeRegExp) - .expect(405, { - errors: [ - { - status: 'Method Not Allowed', - code: 'METHOD_NOT_ALLOWED', - detail: 'PUT method not allowed', - }, - ], - }) - .expect('Allow', 'DELETE, GET'); - await api - .delete('/logs') - .expect('Content-Type', apiContentTypeRegExp) - .expect(405, { - errors: [ - { - status: 'Method Not Allowed', - code: 'METHOD_NOT_ALLOWED', - detail: 'DELETE method not allowed', - }, - ], - }) - .expect('Allow', 'POST, GET'); - await api - .put('/experiments/exp-id') - .set('Content-Type', apiMediaType) - .send({}) - .expect('Content-Type', apiContentTypeRegExp) - .expect(405, { - errors: [ - { - status: 'Method Not Allowed', - code: 'METHOD_NOT_ALLOWED', - detail: 'PUT method not allowed', - }, - ], - }) - .expect('Allow', 'GET'); - }); + it('returns a 405 error if an unsupported method is used with an existing resource', async ({ + api, + expect, + }) => { + let response1 = await api + .put('/sessions/current') + .set('Content-Type', apiMediaType) + .send({}) + .expect('Content-Type', apiContentTypeRegExp) + .expect(405) + .expect('Allow', 'DELETE, GET'); + expect(response1.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "code": "METHOD_NOT_ALLOWED", + "detail": "PUT method is not allowed for resource /sessions/{id}. Allowed methods are DELETE and GET.", + "status": "Method Not Allowed", + }, + ], + } + `); + let response2 = await api + .delete('/logs') + .expect('Content-Type', apiContentTypeRegExp) + .expect(405) + .expect('Allow', 'GET, POST'); + expect(response2.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "code": "METHOD_NOT_ALLOWED", + "detail": "DELETE method is not allowed for resource /logs. Allowed methods are GET and POST.", + "status": "Method Not Allowed", + }, + ], + } + `); + let response3 = await api + .put('/experiments/exp-id') + .set('Content-Type', apiMediaType) + .send({}) + .expect('Content-Type', apiContentTypeRegExp) + .expect(405) + .expect('Allow', 'GET'); + expect(response3.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "code": "METHOD_NOT_ALLOWED", + "detail": "PUT method is not allowed for resource /experiments/{id}. Allowed methods are GET.", + "status": "Method Not Allowed", + }, + ], + } + `); + }); - it('returns 400 error if a request body is invalid', async ({ api }) => { - await api - .post('/sessions') - .set('Content-Type', apiMediaType) - .send({}) - .expect('Content-Type', apiContentTypeRegExp) - .expect(400, { - errors: [ - { - status: 'Bad Request', - code: 'BODY_VALIDATION', - detail: "must have required property 'data'", - source: { pointer: '/data' }, - }, - ], - }); - // Create a session. - await api - .post('/sessions') - .set('Content-Type', apiMediaType) - .send({ - data: { type: 'sessions', attributes: { role: 'participant' } }, - }) - .expect(201); - await api - .post('/logs') - .set('Content-Type', apiMediaType) - .send({ data: 'invalid' }) - .expect('Content-Type', apiContentTypeRegExp) - .expect(400, { - errors: [ - { - status: 'Bad Request', - code: 'BODY_VALIDATION', - detail: 'must be object', - source: { pointer: '/data' }, - }, - ], - }); - await api - .post('/runs') - .set('Content-Type', apiMediaType) - .send({ - data: { - type: 'runs', - attributes: { - name: 'run-name', - status: 'running', - extraProp: 'invalid', - }, - relationships: { - experiment: { data: { type: 'experiments', id: '1' } }, - }, + it('returns 400 error if a request body is invalid', async ({ api }) => { + await api + .post('/sessions') + .set('Content-Type', apiMediaType) + .send({}) + .expect('Content-Type', apiContentTypeRegExp) + .expect(400, { + errors: [ + { + status: 'Bad Request', + code: 'INVALID_REQUEST_BODY', + detail: 'Invalid input: expected object, received undefined', + source: { pointer: '/data' }, }, - }) - .expect('Content-Type', apiContentTypeRegExp) - .expect(400, { - errors: [ - { - status: 'Bad Request', - code: 'BODY_VALIDATION', - detail: 'must NOT be valid', - source: { pointer: '/data/attributes/extraProp' }, - }, - ], - }); - }); + ], + }); + // Create a session. + await api + .post('/sessions') + .set('Content-Type', apiMediaType) + .send({ data: { type: 'sessions', attributes: { role: 'participant' } } }) + .expect(201); + await api + .post('/logs') + .set('Content-Type', apiMediaType) + .send({ data: 'invalid' }) + .expect('Content-Type', apiContentTypeRegExp) + .expect(400, { + errors: [ + { + status: 'Bad Request', + code: 'INVALID_REQUEST_BODY', + detail: 'Invalid input: expected object, received string', + source: { pointer: '/data' }, + }, + ], + }); + await api + .post('/runs') + .set('Content-Type', apiMediaType) + .send({ + data: { + type: 'runs', + attributes: { + name: 'run-name', + status: 'running', + extraProp: 'invalid', + }, + relationships: { + experiment: { data: { type: 'experiments', id: '1' } }, + }, + }, + }) + .expect('Content-Type', apiContentTypeRegExp) + .expect(400, { + errors: [ + { + status: 'Bad Request', + code: 'INVALID_REQUEST_BODY', + detail: 'Unrecognized key: "extraProp"', + source: { pointer: '/data/attributes' }, + }, + ], + }); + }); - it('returns a 415 error if the request content is not of the expected type', async ({ - api, - }) => { - await api - .post('/sessions') - .set('Content-Type', 'text/plain') - .send("I'm obviously not a JSON object") - .expect('Content-Type', apiContentTypeRegExp) - .expect(415, { - errors: [ + it('returns a 415 error if the request content is not of the expected type', async ({ + api, + expect, + }) => { + let response = await api + .post('/sessions') + .set('Content-Type', 'text/plain') + .send("I'm obviously not a JSON object") + .expect('Content-Type', apiContentTypeRegExp) + .expect(415); + expect(response.body).toMatchInlineSnapshot(` + { + "errors": [ + { + "code": "UNSUPPORTED_MEDIA_TYPE", + "detail": "Content type must be 'application/vnd.api+json'. Set 'Content-Type' header to 'application/vnd.api+json'.", + "status": "Unsupported Media Type", + }, + ], + } + `); + }); + + const testRoutes = it.for(allRoutes.filter((r) => r.requireAuth)); + testRoutes( + 'returns a 403 error when trying to $method $path without a session', + async (route, { api, expect }) => { + let response = await api[route.method](route.path).expect(403); + expect(response.body).toMatchInlineSnapshot(` + { + "errors": [ { - status: 'Unsupported Media Type', - code: 'UNSUPPORTED_MEDIA_TYPE', - detail: 'unsupported media type text/plain', + "code": "SESSION_REQUIRED", + "detail": "A session is required. Post to /sessions to create one.", + "status": "Forbidden", }, ], - }); - }); + } + `); + }, + ); - const testRoutes = it.for(allRoutes.filter((r) => r.requireAuth)); - testRoutes( - 'returns a 403 error when trying to $method $path without a session', - async (route, { api }) => { - await api[route.method](route.path).expect(403, { - errors: [ + testRoutes( + 'returns a 403 error when trying to $method $path with an invalid session key', + async (route, { expect, api }) => { + let response = await api[route.method](route.path) + .set('Cookie', ['lightmill-session-id=invalid']) + .expect('Content-Type', apiContentTypeRegExp) + .expect(403); + expect(response.body).toMatchInlineSnapshot(` + { + "errors": [ { - status: 'Forbidden', - code: 'SESSION_REQUIRED', - detail: 'session required, post to /sessions', + "code": "SESSION_REQUIRED", + "detail": "A session is required. Post to /sessions to create one.", + "status": "Forbidden", }, ], - }); - }, - ); - - testRoutes( - 'returns a 403 error when trying to $method $path with an invalid session key', - async (route, { api }) => { - await api[route.method](route.path) - .set('Cookie', ['lightmill-session-id=invalid']) - .expect('Content-Type', apiContentTypeRegExp) - .expect(403, { - errors: [ - { - status: 'Forbidden', - code: 'SESSION_REQUIRED', - detail: 'session required, post to /sessions', - }, - ], - }); - }, - ); - }, -); + } + `); + }, + ); +}); diff --git a/packages/log-server/__tests__/app-experiments.test.ts b/packages/log-server/__tests__/app-experiments.test.ts index f38532b8..a06bf552 100644 --- a/packages/log-server/__tests__/app-experiments.test.ts +++ b/packages/log-server/__tests__/app-experiments.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, vi } from 'vitest'; -import { apiMediaType } from '../src/app-utils.ts'; +import { apiMediaType } from '../src/api.ts'; import { apiContentTypeRegExp, createSessionTest, @@ -41,25 +41,29 @@ describe.for(hostTests)( it('refuses to create an experiment if there are name conflicts', async ({ session: { api, dataStore }, + expect, }) => { await dataStore.addExperiment({ experimentName: 'exp-name' }); - await api + const answer = await api .post('/experiments') .set('Content-Type', apiMediaType) .send({ data: { type: 'experiments', attributes: { name: 'exp-name' } }, }) - .expect(409, { - errors: [ + .expect(409) + .expect('Content-Type', apiContentTypeRegExp); + expect(answer.body).toMatchInlineSnapshot(` + { + "errors": [ { - status: 'Conflict', - code: 'EXPERIMENT_EXISTS', - detail: 'An experiment named "exp-name" already exists', + "code": "EXPERIMENT_EXISTS", + "detail": "An experiment named "exp-name" already exists. Choose a different name.", + "status": "Conflict", }, ], - }) - .expect('Content-Type', apiContentTypeRegExp); + } + `); }); }, ); @@ -67,23 +71,29 @@ describe.for(hostTests)( describe.for(participantTests)( 'LogServer: post /experiments ($sessionType session, $storeType store)', ({ test: it }) => { - it('refuses to create an experiment', async ({ session: { api } }) => { - await api + it('refuses to create an experiment', async ({ + expect, + session: { api }, + }) => { + const answer = await api .post('/experiments') .set('Content-Type', apiMediaType) .send({ data: { type: 'experiments', attributes: { name: 'exp-name' } }, }) - .expect(403, { - errors: [ + .expect(403) + .expect('Content-Type', apiContentTypeRegExp); + expect(answer.body).toMatchInlineSnapshot(` + { + "errors": [ { - status: 'Forbidden', - code: 'FORBIDDEN', - detail: 'Only hosts can create experiments', + "code": "FORBIDDEN", + "detail": "Only hosts can create experiments. Log in as a host to create an experiment.", + "status": "Forbidden", }, ], - }) - .expect('Content-Type', apiContentTypeRegExp); + } + `); }); }, ); @@ -194,7 +204,7 @@ describe.for(allTests)( { status: 'Not Found', code: 'EXPERIMENT_NOT_FOUND', - detail: 'Experiment non-existent not found', + detail: 'Experiment "non-existent" not found.', }, ], }) diff --git a/packages/log-server/__tests__/app-logs.test.ts b/packages/log-server/__tests__/app-logs.test.ts index fbddbad3..792e8b06 100644 --- a/packages/log-server/__tests__/app-logs.test.ts +++ b/packages/log-server/__tests__/app-logs.test.ts @@ -1,7 +1,7 @@ import type { Store as SessionStore } from 'express-session'; import request from 'supertest'; import { beforeEach, describe } from 'vitest'; -import { apiMediaType } from '../src/app-utils.ts'; +import { apiMediaType } from '../src/api.ts'; import { DataStoreError } from '../src/data-store-errors.ts'; import type { DataStore, ExperimentId, RunId } from '../src/data-store.ts'; import { @@ -248,7 +248,9 @@ describe.each(storeTypes)('LogServer: post /logs (%s)', (storeType) => { { status: 'Conflict', code: 'LOG_NUMBER_EXISTS', - detail: `Cannot add log to run '${runId}', log number 2 already exists`, + detail: + `Cannot add log to run '1', log number 2 already exists.` + + ` Ensure the log number is unique within the run.`, }, ], }) diff --git a/packages/log-server/__tests__/app-mount.test.ts b/packages/log-server/__tests__/app-mount.test.ts index f01537ad..5c33c257 100644 --- a/packages/log-server/__tests__/app-mount.test.ts +++ b/packages/log-server/__tests__/app-mount.test.ts @@ -1,7 +1,7 @@ import express from 'express'; import request from 'supertest'; import { describe, it } from 'vitest'; -import { apiMediaType } from '../src/app-utils.ts'; +import { apiMediaType } from '../src/api.ts'; import { LogServer } from '../src/app.ts'; import { createServerContext, storeTypes } from './test-utils.ts'; diff --git a/packages/log-server/__tests__/app-runs.test.ts b/packages/log-server/__tests__/app-runs.test.ts index ecaae664..feb82620 100644 --- a/packages/log-server/__tests__/app-runs.test.ts +++ b/packages/log-server/__tests__/app-runs.test.ts @@ -4,7 +4,7 @@ import type { Store as SessionStore } from 'express-session'; import { prop, sortBy } from 'remeda'; import request from 'supertest'; import { test as baseTest, beforeEach, describe, vi } from 'vitest'; -import { apiMediaType } from '../src/app-utils.ts'; +import { apiMediaType } from '../src/api.ts'; import type { DataStore, ExperimentId, RunStatus } from '../src/data-store.ts'; import { fromAsync } from '../src/utils.ts'; import { @@ -466,7 +466,7 @@ describeForAll( runStatus: originalStatus, }); await addRunToSession({ api, runId, sessionStore }); - await api + let result = await api .patch(`/runs/${runId}`) .set('content-type', apiMediaType) .send({ @@ -476,16 +476,9 @@ describeForAll( attributes: { status: targetStatus }, }, }) - .expect(403, { - errors: [ - { - status: 'Forbidden', - code: 'INVALID_STATUS_TRANSITION', - detail: `Cannot transition run status from ${originalStatus} to ${targetStatus}`, - }, - ], - }) + .expect(403) .expect('Content-Type', apiContentTypeRegExp); + expect(result.body).toMatchSnapshot(); const [runRecord] = await dataStore.getRuns({ runId }); expect(runRecord!.runStatus).toBe(originalStatus); }, @@ -556,7 +549,7 @@ describeForAll( { type: 'log-type', number: 3, values: {} }, ]); await addRunToSession({ api, runId: runRecord.runId, sessionStore }); - await api + let answer = await api .patch(`/runs/${runRecord.runId}`) .set('content-type', apiMediaType) .send({ @@ -566,16 +559,9 @@ describeForAll( attributes: { status: 'completed' }, }, }) - .expect(403, { - errors: [ - { - status: 'Forbidden', - code: 'PENDING_LOGS', - detail: 'Cannot complete run with pending logs', - }, - ], - }) + .expect(403) .expect('Content-Type', apiContentTypeRegExp); + expect(answer.body).toMatchSnapshot(); const [r1] = await dataStore.getRuns({ runId: runRecord.runId }); expect(r1).toEqual(runRecord); }); @@ -644,7 +630,7 @@ describeForAll( status: 'Forbidden', code: 'INVALID_LAST_LOG_NUMBER', detail: - 'Updating last log number is only allowed when resuming a run', + 'Updating last log number is only allowed when resuming a run.', }, ], }) diff --git a/packages/log-server/__tests__/app-sessions.test.ts b/packages/log-server/__tests__/app-sessions.test.ts index b355c332..7221be05 100644 --- a/packages/log-server/__tests__/app-sessions.test.ts +++ b/packages/log-server/__tests__/app-sessions.test.ts @@ -4,7 +4,7 @@ import express, { type Application } from 'express'; import { Store as SessionStore } from 'express-session'; import request from 'supertest'; import { describe, test as vitestTest } from 'vitest'; -import { apiMediaType, type ServerRequestContent } from '../src/app-utils.ts'; +import { apiMediaType } from '../src/api.ts'; import { LogServer } from '../src/app.ts'; import type { DataStore } from '../src/data-store.ts'; import { @@ -43,7 +43,7 @@ const suite = storeTypes.map((storeType) => ({ secureCookies: false, }); let app = express().use(server.middleware); - await use(app satisfies Application); + await use(app); }, api: async ({ app }, use) => { let api = request.agent(app); @@ -61,7 +61,7 @@ describe.for(suite)( .set('content-type', apiMediaType) .send({ data: { type: 'sessions', attributes: { role: 'participant' } }, - } satisfies ServerRequestContent<'/sessions', 'post'>['body']) + }) .expect(201, { data: { id: 'current', @@ -82,7 +82,7 @@ describe.for(suite)( type: 'sessions', attributes: { role: 'something-else' as 'participant' }, }, - } satisfies ServerRequestContent<'/sessions', 'post'>['body']) + }) .expect(400); }); @@ -145,7 +145,7 @@ describe.for(suite)( { status: 'Forbidden', code: 'INVALID_CREDENTIALS', - detail: 'Invalid credentials for role: host', + detail: 'Invalid credentials for role: host. Check the password.', }, ], }) @@ -164,8 +164,9 @@ describe.for(suite)( errors: [ { status: 'Forbidden', - code: 'INVALID_CREDENTIALS', - detail: 'Invalid credentials for role: host', + code: 'MISSING_CREDENTIALS', + detail: + 'Authentication is required for role: host. Provide credentials in the "authorization" header.', }, ], }) @@ -194,7 +195,7 @@ describe.for(suite)( { status: 'Conflict', code: 'SESSION_EXISTS', - detail: 'Session already exists, delete it first', + detail: 'A session already exists. Delete it first.', }, ], }) @@ -216,7 +217,7 @@ describe.for(suite)( { status: 'Not Found', code: 'SESSION_NOT_FOUND', - detail: 'Session "current" not found', + detail: 'Session "current" not found.', }, ], }) @@ -240,7 +241,7 @@ describe.for(suite)( { status: 'Not Found', code: 'SESSION_NOT_FOUND', - detail: 'Session "something-else" not found', + detail: 'Session "something-else" not found.', }, ], }) diff --git a/packages/log-server/__tests__/test-utils.ts b/packages/log-server/__tests__/test-utils.ts index 0873c42a..eedafc9a 100644 --- a/packages/log-server/__tests__/test-utils.ts +++ b/packages/log-server/__tests__/test-utils.ts @@ -8,9 +8,8 @@ import { last } from 'remeda'; import request from 'supertest'; import type { RequiredKeysOf, Simplify, ValueOf } from 'type-fest'; import { test, vi, type Mock, type TestAPI } from 'vitest'; -import type { HttpMethod } from '../src/api-utils.ts'; -import { apiMediaType } from '../src/app-utils.ts'; -import { LogServer, SESSION_COOKIE_NAME } from '../src/app.ts'; +import { apiMediaType, type HttpMethod } from '../src/api.ts'; +import { LogServer } from '../src/app.ts'; import type { DataStore, RunId, RunStatus } from '../src/data-store.ts'; import { SQLiteDataStore } from '../src/sqlite-data-store.ts'; diff --git a/packages/log-server/__tests__/typed-server.test-d.ts b/packages/log-server/__tests__/typed-server.test-d.ts deleted file mode 100644 index 5d7972e8..00000000 --- a/packages/log-server/__tests__/typed-server.test-d.ts +++ /dev/null @@ -1,251 +0,0 @@ -import type { Request, Response } from 'express'; -import { Readable } from 'node:stream'; -import type { Merge, Simplify } from 'type-fest'; -import { describe, expectTypeOf, it } from 'vitest'; -import { createTypedExpressServer } from '../src/typed-server.ts'; - -type TestApi = { - '/{id}': { - parameters: { query?: never }; - delete: { - parameters: { - query: { q: 'a'[] }; - header?: never; - path: { id: 'x' | 'y' }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { [N: string]: unknown }; - content: { - 'root-delete-200-content-type': { data: 'root-delete-200-body' }; - }; - }; - }; - }; - get: { - parameters: { - query: { q: 'q' }; - header?: never; - path: { id: 'id' }; - cookie?: never; - }; - requestBody?: never; - responses: { - 404: { - headers: { [N: string]: unknown }; - content: { - 'root-get-404-content-type': { data: 'root-get-404-body' }; - }; - }; - 200: { - headers: { [N: string]: unknown }; - content: { - 'root-get-200-content-type': { data: 'root-get-200-body' }; - }; - }; - }; - }; - put?: never; - post?: never; - patch?: never; - trace?: never; - }; - '/resources': { - parameters: { query?: never }; - put?: never; - patch?: never; - delete?: never; - trace?: never; - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { 'res-post-content-type': { data: 'req-post-body' } }; - }; - responses: { - 201: { - headers: Merge< - { 'res-post-header-type': 'res-post-header' }, - { [N: string]: unknown } - >; - content: { 'res-post-content-type': { data: 'res-post-body' } }; - }; - }; - }; - }; -}; - -describe('createTypedExpressServer', () => { - it('supports handlers that never returns', () => { - type Expected = Parameters>[1]; - expectTypeOf<{ - '/{id}': { get: () => never; delete: () => never }; - '/resources': { get: () => never; post: () => never }; - }>().toExtend(); - }); - - it('supports correct handlers', () => { - type Input = Parameters>[1]; - type Expected = { - '/{id}': { - get: (arg: { - body: Record; - parameters: { - path: { id: 'id' }; - query: { q: 'q' }; - headers: Record; - cookies: Record; - }; - request: Request; - response: Response; - }) => Promise< - | { - status: 200; - contentType: 'root-get-200-content-type'; - body: { data: 'root-get-200-body' } | Readable; - } - | { - status: 404; - contentType: 'root-get-404-content-type'; - body: { data: 'root-get-404-body' } | Readable; - } - >; - delete: (arg: { - body: Record; - parameters: { - path: { id: 'x' | 'y' }; - query: { q: 'a'[] }; - headers: Record; - cookies: Record; - }; - request: Request; - response: Response; - }) => Promise<{ - status: 200; - contentType: 'root-delete-200-content-type'; - body: { data: 'root-delete-200-body' } | Readable; - }>; - }; - '/resources': { - post: (arg: { - body: { data: 'req-post-body' }; - parameters: { - path: Record; - query: Record; - headers: Record; - cookies: Record; - }; - }) => Promise<{ - headers: Simplify< - { 'res-post-header-type': 'res-post-header' } & Record< - string, - unknown - > - >; - status: 201; - contentType: 'res-post-content-type'; - body: { data: 'res-post-body' } | Readable; - }>; - }; - }; - expectTypeOf().toExtend(); - }); - - it('does not allow for missing method', () => { - expectTypeOf<{ - '/{id}': { delete: () => never }; - '/resources': { post: () => never }; - }>().not.toMatchTypeOf< - Parameters>[0] - >(); - }); - - it('does not allow for incompatible body', () => { - expectTypeOf<{ - post: (arg: { - body: 'some other body'; - parameters: { path: null; query: null; headers: null; cookies: null }; - }) => Promise<{ - status: 201; - contentType: 'res-post-content-type'; - body: { data: 'res-post-body' } | ReadableStream; - }>; - }>().not.toExtend< - Parameters>[1]['/resources'] - >(); - }); - - it('does not allow for incompatible parameters', () => { - expectTypeOf<{ - post: (arg: { - body: 're-post-body'; - parameters: { - path: { x: 'invalid path' }; - query: null; - headers: null; - cookies: null; - }; - }) => Promise<{ - status: 201; - contentType: 'res-post-content-type'; - body: { data: 'res-post-body' } | ReadableStream; - }>; - }>().not.toExtend< - Parameters>[1]['/resources'] - >(); - }); - it('does not allow for incompatible return type', () => { - expectTypeOf<{ - post: (arg: { - body: 're-post-body'; - parameters: { path: null; query: null; headers: null; cookies: null }; - }) => Promise<{ - status: 400; // Another status code. - contentType: 'res-post-content-type'; - body: { data: 'res-post-body' } | ReadableStream; - }>; - }>().not.toExtend< - Parameters>[1]['/resources'] - >(); - - expectTypeOf<{ - post: (arg: { - body: 're-post-body'; - parameters: { path: null; query: null; headers: null; cookies: null }; - }) => Promise<{ - status: 201; - contentType: 'another-content-type'; - body: { data: 'res-post-body' } | ReadableStream; - }>; - }>().not.toExtend< - Parameters>[1]['/resources'] - >(); - }); - - it('allow narrower return type', () => { - expectTypeOf<{ - post: (arg: { - body: { data: 'req-post-body' }; - parameters: { - path: Record; - query: Record; - headers: Record; - cookies: Record; - }; - }) => Promise<{ - status: 201; - contentType: 'res-post-content-type'; - body: Readable; - headers: { 'res-post-header-type': 'res-post-header' }; - }>; - }>().toExtend< - Parameters>[1]['/resources'] - >(); - }); -}); diff --git a/packages/log-server/__tests__/typed-server.test.ts b/packages/log-server/__tests__/typed-server.test.ts deleted file mode 100644 index a0ae2621..00000000 --- a/packages/log-server/__tests__/typed-server.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import express from 'express'; -import request from 'supertest'; -import { test } from 'vitest'; -import type { DataStore } from '../src/data-store.ts'; -import { createTypedExpressServer } from '../src/typed-server.ts'; - -type TestApi = { - '/resources/{id}': { - parameters: { query?: never }; - get: { - parameters: { - query: { q: 'q' }; - path: { id: 'id-1' | 'id-2' | 'id-3' }; - header?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { [N: string]: unknown }; - content: { 'application/json': { dataget: unknown } }; - }; - }; - }; - delete: { - parameters: { - query: { q: Array<'a' | 'b'> }; - path: { id: 'id-x' | 'id-y' }; - header?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { [N: string]: unknown }; - content: { 'application/json': { datadel: unknown } }; - }; - }; - }; - }; - '/resources': { - parameters: { query?: never; header?: never; cookie?: never; path?: never }; - post: { - parameters: Record; - requestBody: { - content: { 'application/json': { data: 're-post-body' } }; - }; - responses: { - 201: { - headers: { [N: string]: unknown }; - content: { 'application/json': { datapost: unknown } }; - }; - }; - }; - }; -}; - -test('createTypedServer', async () => { - const mid = createTypedExpressServer({} as DataStore, { - '/resources/{id}': { - async get({ parameters, body }) { - return { - status: 200, - body: { - dataget: { - path: parameters.path, - query: parameters.query, - body, - handler: 'get', - }, - }, - contentType: 'application/json', - }; - }, - async delete({ parameters, body }) { - return { - status: 200, - body: { - datadel: { - path: parameters.path, - query: parameters.query, - body, - handler: 'delete', - }, - }, - contentType: 'application/json', - }; - }, - }, - '/resources': { - async post({ parameters, body }) { - return { - status: 201, - body: { - datapost: { - path: parameters.path, - query: parameters.query, - body, - handler: 'post', - }, - }, - contentType: 'application/json', - }; - }, - }, - }); - const server = express(); - server.use(express.json()); - server.use(mid); - - await request(server) - .get('/resources/id-x?q=q') - .expect('Content-Type', /json/) - .expect(200, { - dataget: { - path: { id: 'id-x' }, - query: { q: 'q' }, - body: {}, - handler: 'get', - }, - }); - - await request(server) - .delete('/resources/id-y?q=a&q=b&q=a') - .expect('Content-Type', /json/) - .expect(200, { - datadel: { - path: { id: 'id-y' }, - query: { q: ['a', 'b', 'a'] }, - body: {}, - handler: 'delete', - }, - }); - - await request(server) - .post('/resources/') - .send({ data: 're-post-body' }) - .expect('Content-Type', /json/) - .expect(201, { - datapost: { - path: {}, - query: {}, - body: { data: 're-post-body' }, - handler: 'post', - }, - }); -}); diff --git a/packages/log-server/package.json b/packages/log-server/package.json index 64d7706b..f222909f 100644 --- a/packages/log-server/package.json +++ b/packages/log-server/package.json @@ -51,14 +51,12 @@ "express": "^5.1.0" }, "dependencies": { - "@gabriel/ts-pattern": "npm:@jsr/gabriel__ts-pattern@^5.7.1", + "@standard-schema/spec": "^1.0.0", "better-sqlite3": "^11.10.0", - "cookie-parser": "^1.4.7", "cors": "^2.8.5", "csv": "^6.3.11", "dotenv": "^16.5.0", "express": "^5.1.0", - "express-openapi-validator": "5.4.9", "express-session": "^1.18.1", "kysely": "^0.28.2", "loglevel": "^1.9.2", diff --git a/packages/log-server/src/api-utils.ts b/packages/log-server/src/api-utils.ts deleted file mode 100644 index 7c09e0be..00000000 --- a/packages/log-server/src/api-utils.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { - ConditionalKeys, - Merge, - RequireAtLeastOne, - RequiredKeysOf, - UnionToIntersection, -} from 'type-fest'; -import type { EntryAsObject, RemoveReadonlyPropsDeep } from './utils.js'; - -export type HttpCode = number; - -export const httpMethods = [ - 'get', - 'put', - 'post', - 'delete', - 'options', - 'head', - 'patch', - 'trace', -] as const; -export type HttpMethod = (typeof httpMethods)[number]; - -export const httpStatuses = { - 200: 'OK', - 201: 'Created', - 202: 'Accepted', - 204: 'No Content', - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 406: 'Not Acceptable', - 409: 'Conflict', - 415: 'Unsupported Media Type', - 500: 'Internal Server Error', -} as const; -export type HttpStatusMap = typeof httpStatuses; -export type HttpStatusCode = keyof HttpStatusMap; -export type HttpStatus = HttpStatusMap[HttpStatusCode]; - -export type ApiPath = keyof Api; - -export type ApiPathFromMethod< - Api, - M extends AllApiMethodForPaths | (HttpMethod & {}), -> = Extract, string> & - // Not really useful, but let TS know that ApiPathFromMethod extends - // AllApiPathFromMethod - AllApiPathFromMethod; - -export type AllApiPathFromMethod< - Api, - M extends AllApiMethodForPaths | (HttpMethod & {}) = HttpMethod, -> = Extract< - ConditionalKeys>, - string ->; - -// This must return methods supported by all paths in Path. -// There must be an easier more efficient way to do this but it works. -export type ApiMethodFromPath< - Api, - Path extends keyof Api | (string & {}), -> = UnionToIntersection< - { - [K in Path]: Api extends { [K1 in K]: object } - ? { x: RequiredKeysOf & HttpMethod } - : never; - }[Path] ->['x']; - -export type AllApiMethodForPaths< - Api, - Path extends keyof Api = keyof Api, -> = Api[Path] extends object ? RequiredKeysOf & HttpMethod : never; - -export type ApiRequestContent< - A extends Api, - P extends ApiPath, - M extends AllApiMethodForPaths, -> = ApiOperationRequestContent>; - -export type ApiResponseContent< - A extends Api, - P extends ApiPath, - M extends AllApiMethodForPaths, -> = ApiOperationResponseContent>; - -export type ApiRequestParameters< - A extends Api, - P extends ApiPath, - M extends AllApiMethodForPaths, -> = ApiOperationParameters>; - -export type ApiOperation< - A extends Api = Api, - P extends ApiPath = ApiPath, - M extends HttpMethod = HttpMethod, -> = NonNullable; - -interface QueryParameter { - [key: string]: string | string[] | QueryParameter; -} - -export interface RequestParameters { - path?: Record | undefined; - query?: QueryParameter | undefined; - header?: Record | undefined; - cookie?: Record | undefined; -} - -interface BaseApiOperation { - parameters: RequestParameters; - responses: Record< - HttpCode, - { - headers?: Record | undefined; - content?: Record | undefined; - } - >; - requestBody?: { content: Record } | undefined; -} - -export type Api

= Record< - P, - { [K2 in HttpMethod]?: BaseApiOperation | undefined } ->; - -export type ApiOperationParameters = O['parameters']; - -export type ApiOperationResponseContent = { - [Status in keyof O['responses']]: O['responses'][Status] extends { - content: infer C; - headers: infer H; - } - ? Merge< - EntryAsObject, - Record extends H - ? { status: Status; headers?: H | undefined } - : { status: Status; headers: H } - > - : never; -}[keyof O['responses']]; - -export type ApiOperationRequestContent = - RemoveReadonlyPropsDeep< - EntryAsObject< - NonNullable['content'], - { key: 'contentType'; value: 'body' } - > - >; diff --git a/packages/log-server/src/app-utils.ts b/packages/log-server/src/api.ts similarity index 53% rename from packages/log-server/src/app-utils.ts rename to packages/log-server/src/api.ts index ad1d3fc1..a63f3d64 100644 --- a/packages/log-server/src/app-utils.ts +++ b/packages/log-server/src/api.ts @@ -1,91 +1,28 @@ -import type { paths } from '@lightmill/log-api'; import type { SessionData } from 'express-session'; import { groupBy, intersection, map, pipe, uniqueBy } from 'remeda'; -import type { Simplify, WritableDeep } from 'type-fest'; -import { - type ApiPath, - type HttpMethod, - httpStatuses, - type HttpStatusMap, -} from './api-utils.js'; -import type { DataStore, RunId } from './data-store.ts'; -import type { - Handler, - HandlerParameters, - HandlerResult, - RequestContent, - ServerDescription, -} from './typed-server.js'; -import { arrayify } from './utils.js'; - -declare module 'express-session' { - interface SessionData { - data: { role: 'participant' | 'host'; runs: RunId[] }; - } -} - -// This needs to be a type (not an interface) so we can use it with -// typed-server... I don't know why, but it's not worth investigating. -export type ServerApi = Simplify; - -export type ServerHandler< - Path extends ApiPath, - Method extends HttpMethod, -> = Handler; - -export type ServerHandlerResult< - Path extends ApiPath, - Method extends HttpMethod, -> = HandlerResult; - -export type ServerHandlerBody< - Path extends ApiPath, - Method extends HttpMethod, -> = ServerHandlerResult['body']; - -export type ServerHandlerIncluded< - Path extends ApiPath, - Method extends HttpMethod, -> = - ServerHandlerBody extends infer B - ? B extends { readonly included?: infer I } - ? I - : never - : never; - -export type ServerHandlerParameter< - Path extends ApiPath, - Method extends HttpMethod, -> = HandlerParameters; - -export type ServerRequestContent< - Path extends ApiPath, - Method extends HttpMethod, -> = RequestContent; +import type { ConditionalKeys } from 'type-fest'; +import type { DataStore } from './data-store.ts'; +import { arrayify } from './utils.ts'; export function getErrorResponse< - const T extends { - status: keyof HttpStatusMap; - code: string; - detail?: string; - source?: object; - }, ->(option: T) { - let error: WritableDeep> & { - status: HttpStatusMap[T['status']]; - } = { ...structuredClone(option), status: httpStatuses[option.status] }; + const Error extends { code: string; status: HttpStatusText }, +>( + errors: Array | Error, + statusCode?: HttpStatusCodeFromText, +) { + errors = Array.isArray(errors) ? errors : [errors]; + let firstError = errors[0]; + if (firstError == null) { + throw new Error('No errors provided'); + } return { - status: option.status as T['status'], - body: { errors: [error] }, contentType: apiMediaType, + status: + statusCode ?? httpStatusCodeFromText(firstError.status), + body: { errors }, }; } -export type SubServerDescription = Pick< - ServerDescription, - Extract, `${K}${string}`> ->; - type GetRunResourcesOptions = | { filter: Parameters[0] } | { @@ -175,3 +112,67 @@ export function getAllowedAndFilteredRunIds( export const apiMediaType = 'application/vnd.api+json' as const; export type ApiMediaType = typeof apiMediaType; + +export function parseCookies(cookieHeader: string | undefined) { + if (cookieHeader == null) return {}; + return Object.fromEntries( + cookieHeader.split(';').map((cookie) => { + const [key, value] = cookie + .split('=') + .map((part) => decodeURIComponent(part.trim())); + if (key == null || value == null) { + throw new Error( + `Invalid cookie format: "${cookie}". Expected "key=value" format.`, + ); + } + return [key, value]; + }), + ); +} + +export const httpStatuses = { + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 204: 'No Content', + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 409: 'Conflict', + 415: 'Unsupported Media Type', + 500: 'Internal Server Error', +} as const; +export const reverseHttpStatuses = Object.fromEntries( + Object.entries(httpStatuses).map(([code, text]) => [text, Number(code)]), +) as ReverseHttpStatusMap; + +export function httpStatusCodeFromText( + status: Text, +): HttpStatusCodeFromText { + return reverseHttpStatuses[status]; +} + +export function httpStatusTextFromCode( + status: Code, +): HttpStatusTextFromCode { + return httpStatuses[status]; +} + +export type HttpStatusMap = typeof httpStatuses; +export type ReverseHttpStatusMap = { + [Text in HttpStatusText]: ConditionalKeys; +}; +export type HttpStatusCodeFromText = + ReverseHttpStatusMap[Text]; +export type HttpStatusTextFromCode = + HttpStatusMap[Code]; +export type HttpStatusCode = keyof HttpStatusMap; +export type HttpStatusText = HttpStatusMap[HttpStatusCode]; + +export type UserRole = 'host' | 'participant'; + +export const httpMethods = ['get', 'post', 'put', 'patch', 'delete'] as const; +export type HttpMethod = (typeof httpMethods)[number]; diff --git a/packages/log-server/src/app-experiments-handlers.ts b/packages/log-server/src/app-experiments-handlers.ts index 14565cca..6e4221ae 100644 --- a/packages/log-server/src/app-experiments-handlers.ts +++ b/packages/log-server/src/app-experiments-handlers.ts @@ -1,22 +1,15 @@ -import { - getErrorResponse, - type ServerHandlerBody, - type ServerHandlerResult, - type SubServerDescription, -} from './app-utils.js'; +import { getErrorResponse } from './api.ts'; import { DataStoreError } from './data-store-errors.ts'; +import type { PathHandlers } from './router.ts'; -export const experimentHandlers = (): SubServerDescription<'/experiments'> => ({ +export const experimentHandlers = (): PathHandlers<'/experiments'> => ({ '/experiments': { - async post({ - body, - store, - request, - }): Promise> { - if (request.session.data?.role !== 'host') { + async post({ body, dataStore: store, sessionData, protocol, host }) { + if (sessionData.role !== 'host') { return getErrorResponse({ - status: 403, - detail: 'Only hosts can create experiments', + status: 'Forbidden', + detail: + 'Only hosts can create experiments. Log in as a host to create an experiment.', code: 'FORBIDDEN', }); } @@ -27,7 +20,7 @@ export const experimentHandlers = (): SubServerDescription<'/experiments'> => ({ status: 201, body: { data: { id: experimentId.toString(), type: 'experiments' } }, headers: { - location: `${request.protocol + '://' + request.get('host')}/experiments/${experimentId}`, + location: `${protocol + '://' + host}/experiments/${experimentId}`, }, }; } catch (error) { @@ -36,8 +29,8 @@ export const experimentHandlers = (): SubServerDescription<'/experiments'> => ({ error.code === DataStoreError.EXPERIMENT_EXISTS ) { return getErrorResponse({ - status: 409, - detail: `An experiment named "${experimentName}" already exists`, + status: 'Conflict', + detail: `An experiment named "${experimentName}" already exists. Choose a different name.`, code: 'EXPERIMENT_EXISTS', }); } @@ -45,42 +38,39 @@ export const experimentHandlers = (): SubServerDescription<'/experiments'> => ({ } }, - async get({ store, parameters: { query } }) { + async get({ dataStore: store, parameters: { query } }) { + // Note: There is currently no restrictions on who can access this + // endpoint. const experiments = await store.getExperiments({ experimentName: query['filter[name]'], }); - const data: Extract< - ServerHandlerBody<'/experiments', 'get'>, - { data: unknown } - >['data'] = []; - for (const experiment of experiments) { - data.push({ - id: experiment.experimentId, - type: 'experiments', - attributes: { name: experiment.experimentName }, - }); - } - return { status: 200, body: { data } }; + return { + status: 200, + body: { + data: experiments.map((experiment) => ({ + id: experiment.experimentId, + type: 'experiments' as const, + attributes: { name: experiment.experimentName }, + })), + }, + }; }, }, '/experiments/{id}': { - async get({ request, parameters: { path }, store }) { - if (request.session.data == null) { - throw new Error('Session data is not initialized'); - } + async get({ parameters: { path }, dataStore: store }) { const experiments = await store.getExperiments({ experimentId: path.id }); if (experiments.length > 1) { + // This should not happen, but we handle it gracefully. throw new Error('Multiple experiments found for the given ID'); } const experiment = experiments[0]; - const notFoundErrorResponse = getErrorResponse({ - status: 404, - detail: `Experiment ${path.id} not found`, - code: 'EXPERIMENT_NOT_FOUND', - }); if (experiment == null) { - return notFoundErrorResponse; + return getErrorResponse({ + status: 'Not Found', + detail: `Experiment "${path.id}" not found.`, + code: 'EXPERIMENT_NOT_FOUND', + }); } return { status: 200, diff --git a/packages/log-server/src/app-logs-handlers.ts b/packages/log-server/src/app-logs-handlers.ts index 055eccdb..9f100282 100644 --- a/packages/log-server/src/app-logs-handlers.ts +++ b/packages/log-server/src/app-logs-handlers.ts @@ -1,4 +1,5 @@ -import type { components, operations } from '@lightmill/log-api'; +import type { routes } from '@lightmill/log-api'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; import { Readable } from 'node:stream'; import type { JsonObject } from 'type-fest'; import { parseAcceptHeader } from './accept-headers.ts'; @@ -8,29 +9,28 @@ import { getErrorResponse, getRunResources, type ApiMediaType, - type ServerHandlerResult, - type SubServerDescription, -} from './app-utils.js'; +} from './api.ts'; import { csvExportStream } from './csv-export.js'; import type { AllFilter } from './data-filters.ts'; import { DataStoreError } from './data-store-errors.ts'; import type { DataStore } from './data-store.ts'; +import type { HandlerResponseFromRoute, PathHandlers } from './router.ts'; import { arrayify, firstStrict } from './utils.js'; -export const logHandlers = (): SubServerDescription<'/logs'> => ({ +export const logHandlers = (): PathHandlers<'/logs'> => ({ '/logs': { async get({ - request, - store, + sessionData, + dataStore: store, parameters: { query, headers }, - }): Promise> { + }): Promise> { let responseMimeType = getResponseMimeType(headers.accept, { defaultMimeType: 'csv', }); let filter: AllFilter = { logType: query['filter[logType]'], runId: getAllowedAndFilteredRunIds( - request.session.data, + sessionData, query['filter[run.id]'], ), experimentId: query['filter[experiment.id]'], @@ -41,9 +41,11 @@ export const logHandlers = (): SubServerDescription<'/logs'> => ({ if (responseMimeType === 'csv') { if (includeQuery.length > 0) { return getErrorResponse({ - status: 400, - code: 'INVALID_QUERY_PARAMETER', - detail: `CSV content does not support include query parameter`, + status: 'Bad Request', + code: 'NOT_SUPPORTED_QUERY_PARAMETER', + detail: + `Include query parameter is not supported with CSV log format.` + + ` Remove the 'include' query parameter, or set 'accept' header to '${apiMediaType}' to get logs in JSON format.`, source: { parameter: 'include' }, }); } @@ -65,18 +67,14 @@ export const logHandlers = (): SubServerDescription<'/logs'> => ({ }; }, - async post({ store, body, request }) { + async post({ dataStore: store, body, sessionData, protocol, host }) { let runId = body.data.relationships.run.data.id; let runNotFoundError = getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'RUN_NOT_FOUND', detail: `Run "${runId}" not found`, }); - let sessionRuns = request.session.data?.runs ?? []; - if ( - request.session.data?.role !== 'host' && - !sessionRuns.includes(runId) - ) { + if (sessionData.role !== 'host' && !sessionData.runs.includes(runId)) { return runNotFoundError; } let matchingRuns = await store.getRuns({ runId }); @@ -87,9 +85,9 @@ export const logHandlers = (): SubServerDescription<'/logs'> => ({ if (run == null) return runNotFoundError; if (run.runStatus != 'running') { return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'INVALID_RUN_STATUS', - detail: `Cannot add logs to run '${runId}', run is not running`, + detail: `Cannot add logs to run '${runId}', run is not running. Ensure the run is running before adding logs.`, }); } try { @@ -98,7 +96,8 @@ export const logHandlers = (): SubServerDescription<'/logs'> => ({ { number: body.data.attributes.number, type: body.data.attributes.logType, - // values is necessarily a JsonObject since it's coming from the request body. + // values is necessarily a JsonObject since it's coming from the + // request body. values: body.data.attributes.values as JsonObject, }, ]), @@ -106,7 +105,7 @@ export const logHandlers = (): SubServerDescription<'/logs'> => ({ return { status: 201, headers: { - location: `${request.protocol + '://' + request.get('host')}/logs/${insertedLogId}`, + location: `${protocol + '://' + host}/logs/${insertedLogId}`, }, body: { data: { id: insertedLogId, type: 'logs' } }, }; @@ -116,9 +115,9 @@ export const logHandlers = (): SubServerDescription<'/logs'> => ({ e.code === 'LOG_NUMBER_EXISTS_IN_SEQUENCE' ) { return getErrorResponse({ - status: 409, + status: 'Conflict', code: 'LOG_NUMBER_EXISTS', - detail: `Cannot add log to run '${runId}', log number ${body.data.attributes.number} already exists`, + detail: `Cannot add log to run '${runId}', log number ${body.data.attributes.number} already exists. Ensure the log number is unique within the run.`, }); } throw e; @@ -127,9 +126,9 @@ export const logHandlers = (): SubServerDescription<'/logs'> => ({ }, '/logs/{id}': { - async get({ request, store, parameters: { path, query } }) { - let isHost = request.session.data?.role === 'host'; - let runFilter = isHost ? undefined : (request.session.data?.runs ?? []); + async get({ sessionData, dataStore: store, parameters: { path, query } }) { + let isHost = sessionData.role === 'host'; + let runFilter = isHost ? undefined : (sessionData.runs ?? []); let filter: AllFilter = { runId: runFilter, logId: path.id }; let includeQuery = arrayify(query['include'], true); let dataString = ''; @@ -140,15 +139,13 @@ export const logHandlers = (): SubServerDescription<'/logs'> => ({ })) { dataString += chunk; } - let data = JSON.parse( - dataString, - ) as operations['Log_getCollection']['responses']['200']['content'][ApiMediaType]; + let data = JSON.parse(dataString); if (data.data.length > 1) { throw new Error(`More than one log found for id '${path.id}'`); } if (data.data.length === 0) { return getErrorResponse({ - status: 404, + status: 'Not Found', code: 'LOG_NOT_FOUND', detail: `Log "${path.id}" not found`, }); @@ -191,17 +188,24 @@ function jsonResponseStream( return Readable.from(jsonResponseChunkGenerator(store, filter, includes)); } +type RunResource = StandardSchemaV1.InferOutput< + (typeof routes)['/runs/{id}']['get']['responses'][200]['content'][ApiMediaType]['schema'] +>['data']; +type ExperimentResource = StandardSchemaV1.InferOutput< + (typeof routes)['/experiments/{id}']['get']['responses'][200]['content'][ApiMediaType]['schema'] +>['data']; +type LogResource = StandardSchemaV1.InferOutput< + (typeof routes)['/logs/{id}']['get']['responses'][200]['content'][ApiMediaType]['schema'] +>['data']; + async function* jsonResponseChunkGenerator( store: DataStore, filter: Omit, includes: { run?: boolean; experiment?: boolean; lastLogs?: boolean }, ) { - let runs = new Map(); - let experiments = new Map< - string, - components['schemas']['Experiment.Resource'] - >(); - let includedLogs = new Array(); + let runs = new Map(); + let experiments = new Map(); + let includedLogs = new Array(); let logs = await store.getLogs({ ...filter, runStatus: '-canceled' }); yield '{"data":['; let started = false; @@ -218,7 +222,7 @@ async function* jsonResponseChunkGenerator( values: log.values, }, relationships: { run: { data: { type: 'runs', id: log.runId } } }, - } satisfies components['schemas']['Log.Resource'], + } satisfies LogResource, stringifyDateSerializer, ); if ( diff --git a/packages/log-server/src/app-runs-handlers.ts b/packages/log-server/src/app-runs-handlers.ts index eec7fcdb..87fd5cfe 100644 --- a/packages/log-server/src/app-runs-handlers.ts +++ b/packages/log-server/src/app-runs-handlers.ts @@ -2,11 +2,10 @@ import { getAllowedAndFilteredRunIds, getErrorResponse, getRunResources, - type ServerHandlerResult, - type SubServerDescription, -} from './app-utils.js'; +} from './api.ts'; import { DataStoreError } from './data-store-errors.ts'; import { type RunStatus } from './data-store.ts'; +import type { HandlerResponseFromRoute, PathHandlers } from './router.ts'; import { arrayify, firstStrict } from './utils.js'; const allowedStatusTransitions = [ @@ -20,12 +19,12 @@ const allowedStatusTransitions = [ { from: 'completed', to: 'canceled' }, ] as const satisfies Array<{ from: RunStatus; to: RunStatus }>; -export const runHandlers = (): SubServerDescription<'/runs'> => ({ +export const runHandlers = (): PathHandlers<'/runs'> => ({ '/runs': { - async get({ request, parameters, store }) { + async get({ sessionData, parameters, dataStore: store }) { const filter = { runId: getAllowedAndFilteredRunIds( - request.session.data, + sessionData, parameters.query['filter[id]'], ), runStatus: parameters.query['filter[status]'], @@ -46,25 +45,17 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ body: included == null ? { data: runs } : { data: runs, included }, }; }, - - async post({ - store, - request: req, - body, - }): Promise> { + async post({ dataStore: store, sessionData, body, protocol, host }) { const { status, name } = body.data.attributes; const { id: experimentId } = body.data.relationships.experiment.data; - if (req.session.data == null) { - throw new Error('No session data'); - } try { const onGoingRuns = await store.getRuns({ - runId: req.session.data.runs, + runId: sessionData.runs, runStatus: ['running', 'interrupted'], }); if (onGoingRuns.length > 0) { return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'ONGOING_RUNS', detail: 'Client already has ongoing runs, end them first', }); @@ -74,12 +65,15 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ experimentId: experimentId, runName: name, }); - req.session.data.runs = [...req.session.data.runs, run.runId]; return { + sessionData: { + ...sessionData, + runs: [...sessionData.runs, run.runId], + }, status: 201, body: { data: { id: run.runId, type: 'runs' } }, headers: { - location: `${req.protocol + '://' + req.get('host')}/runs/${run.runId}`, + location: `${protocol + '://' + host}/runs/${run.runId}` as const, }, }; } catch (e) { @@ -89,7 +83,7 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ ) { return getErrorResponse({ code: 'RUN_EXISTS', - status: 409, + status: 'Conflict', detail: `A run named ${name} already exists for experiment ${experimentId}`, }); } @@ -99,16 +93,13 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ }, '/runs/{id}': { - async get({ request, parameters, store }) { - if (request.session.data == null) { - throw new Error('No session data'); - } + async get({ sessionData, parameters, dataStore: store }) { if ( - request.session.data.role !== 'host' && - !request.session.data.runs.includes(parameters.path.id) + sessionData.role !== 'host' && + !sessionData.runs.includes(parameters.path.id) ) { return getErrorResponse({ - status: 404, + status: 'Not Found', code: 'RUN_NOT_FOUND', detail: `Run "${parameters.path.id}" not found`, }); @@ -119,7 +110,7 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ const run = runs[0]; if (run === undefined) { return getErrorResponse({ - status: 404, + status: 'Not Found', code: 'RUN_NOT_FOUND', detail: `Run "${parameters.path.id}" not found`, }); @@ -137,22 +128,19 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ }, async patch({ - request, - store, + sessionData, + dataStore: store, body, parameters: { path: { id: runId }, }, - }): Promise> { + }): Promise> { const unknownRunAnswer = getErrorResponse({ - status: 404, + status: 'Not Found', code: 'RUN_NOT_FOUND', detail: `Run "${runId}" not found`, }); - if ( - request.session.data?.role !== 'host' && - !request.session.data?.runs.includes(runId) - ) { + if (sessionData.role !== 'host' && !sessionData.runs.includes(runId)) { return unknownRunAnswer; } let matchingRuns = await store.getRuns({ runId }); @@ -163,9 +151,9 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ // Run not found errors must be handled before this. if (body.data.id !== runId) { return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'INVALID_RUN_ID', - detail: `A run's id cannot be changed`, + detail: `A run's id cannot be changed. Remove the 'id' attribute from the request body.`, }); } @@ -173,32 +161,42 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ const oldRunStatus = targetRun.runStatus; const newRunStatus = body.data.attributes?.status; + const allowedNextStatus: RunStatus[] = allowedStatusTransitions + .filter((t) => t.from === oldRunStatus) + .map((t) => t.to); if ( newRunStatus !== undefined && newRunStatus !== oldRunStatus && - !allowedStatusTransitions.some( - (t) => t.from === oldRunStatus && t.to === newRunStatus, - ) + !allowedNextStatus.includes(newRunStatus) ) { + let listFormat = new Intl.ListFormat('en', { + style: 'long', + type: 'disjunction', + }); + let message = + allowedNextStatus.length > 0 + ? `Cannot change run status from ${oldRunStatus} to ${newRunStatus}.` + + ` Allowed transitions are: ${listFormat.format( + allowedNextStatus.map((s) => `${oldRunStatus} -> ${s}`), + )}.` + : `Cannot change run status. Run status ${oldRunStatus} is terminal.`; return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'INVALID_STATUS_TRANSITION', - detail: `Cannot transition run status from ${oldRunStatus} to ${newRunStatus}`, + detail: message, }); } if (newRunStatus !== 'canceled') { let otherOngoingRuns = await store.getRuns({ runStatus: ['running', 'interrupted'], - runId: (request.session.data?.runs ?? []).filter( - (r) => r !== targetRun.runId, - ), + runId: (sessionData.runs ?? []).filter((r) => r !== targetRun.runId), }); if (otherOngoingRuns.length > 0) { return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'ONGOING_RUNS', - detail: `Client already has ongoing runs, end them first`, + detail: `Client already has ongoing runs. End them first before updating this run.`, }); } } @@ -211,9 +209,9 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ let pendingLogs = await store.getMissingLogs({ runId }); if (pendingLogs.length > 0) { return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'PENDING_LOGS', - detail: `Cannot complete run with pending logs`, + detail: `Cannot complete run with pending logs. Ensure all logs are added to the run before completing it.`, }); } } @@ -226,16 +224,18 @@ export const runHandlers = (): SubServerDescription<'/runs'> => ({ requestedLastLogNumber !== lastLogNumber ) { return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'INVALID_LAST_LOG_NUMBER', - detail: `Updating last log number is only allowed when resuming a run`, + detail: `Updating last log number is only allowed when resuming a run.`, }); } if (lastLogNumber < requestedLastLogNumber) { return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'INVALID_LAST_LOG_NUMBER', - detail: `Cannot set last log number to ${requestedLastLogNumber}, run has only ${lastLogNumber} logs`, + detail: + `Cannot set last log number to ${requestedLastLogNumber}, run has only ${lastLogNumber} logs.` + + ` Ensure the last log number is less than or equal to the last log number of the run.`, }); } await store.resumeRun(targetRun.runId, { diff --git a/packages/log-server/src/app-sessions-handlers.ts b/packages/log-server/src/app-sessions-handlers.ts index a5eb8c89..00f0f51f 100644 --- a/packages/log-server/src/app-sessions-handlers.ts +++ b/packages/log-server/src/app-sessions-handlers.ts @@ -1,14 +1,6 @@ -import type express from 'express'; -import { promisify } from 'node:util'; -import type { Writable } from 'type-fest'; -import { - getErrorResponse, - getRunResources, - type ServerHandlerBody, - type ServerHandlerResult, - type SubServerDescription, -} from './app-utils.js'; -import { type DataStore } from './data-store.ts'; +import { getErrorResponse, getRunResources, type UserRole } from './api.ts'; +import { type DataStore, type RunId } from './data-store.ts'; +import type { PathHandlers } from './router.ts'; import { arrayify, checkBasicAuth } from './utils.js'; type SessionHandlerOptions = { @@ -18,84 +10,94 @@ type SessionHandlerOptions = { export const sessionHandlers = ({ hostPassword, hostUser, -}: SessionHandlerOptions): SubServerDescription<'/sessions'> => ({ +}: SessionHandlerOptions): PathHandlers<'/sessions'> => ({ '/sessions': { async post({ - request, + sessionData, body, parameters: { headers }, - store, - }): Promise> { + dataStore: store, + protocol, + host, + }) { const { role: requestedRole = 'participant' } = body.data?.attributes ?? {}; - // TODO: create a middleware to deal with Basic Auth. - let isAuthorized = - requestedRole === 'participant' || - (requestedRole === 'host' && - (hostPassword == null || - checkBasicAuth(headers.authorization, hostUser, hostPassword))); + if ( + requestedRole === 'host' && + hostPassword != null && + headers.authorization == null + ) { + return getErrorResponse({ + status: 'Forbidden', + code: 'MISSING_CREDENTIALS', + detail: + `Authentication is required for role: ${requestedRole}.` + + ` Provide credentials in the "authorization" header.`, + }); + } - if (!isAuthorized) { + if ( + requestedRole === 'host' && + hostPassword != null && + !checkBasicAuth(headers.authorization, hostUser, hostPassword) + ) { return getErrorResponse({ - status: 403, + status: 'Forbidden', code: 'INVALID_CREDENTIALS', - detail: `Invalid credentials for role: ${requestedRole}`, + detail: `Invalid credentials for role: ${requestedRole}. Check the password.`, }); } - if (request.session.data != null) { + if (sessionData != null) { return getErrorResponse({ - status: 409, + status: 'Conflict', code: 'SESSION_EXISTS', - detail: `Session already exists, delete it first`, + detail: `A session already exists. Delete it first.`, }); } - - request.session.data = { role: requestedRole, runs: [] }; + sessionData = { role: requestedRole, runs: [] }; return { - headers: { - location: `${request.protocol + '://' + request.get('host')}/sessions/current`, - }, + sessionData, + headers: { location: `${protocol + '://' + host}/sessions/current` }, status: 201, - body: await getSessionResource(request, store), + body: await getSessionResource(sessionData, store), }; }, }, '/sessions/{id}': { - async get({ request, parameters: { path, query }, store }) { - if (path.id !== 'current' || request.session.data == null) { + async get({ sessionData, parameters: { path, query }, dataStore: store }) { + if (path.id !== 'current' || sessionData == null) { return getErrorResponse({ - status: 404, + status: 'Not Found', code: 'SESSION_NOT_FOUND', - detail: `Session "${path.id}" not found`, + detail: `Session "${path.id}" not found.`, }); } return { status: 200, - body: await getSessionResource(request, store, { + body: await getSessionResource(sessionData, store, { includeRuns: arrayify(query.include, true).includes('runs'), }), }; }, - async delete({ request, parameters: { path } }) { - if (path.id !== 'current' || request.session.data == null) { + async delete({ sessionData, parameters: { path } }) { + if (path.id !== 'current' || sessionData == null) { return getErrorResponse({ - status: 404, + status: 'Not Found', code: 'SESSION_NOT_FOUND', - detail: `Session "${path.id}" not found`, + detail: `Session "${path.id}" not found.`, }); } - await promisify(request.session.destroy.bind(request.session))(); - return { status: 200, body: { data: null } }; + return { status: 200, sessionData: null, body: { data: null } }; }, }, }); async function getSessionResource( - req: express.Request, + sessionData: { runs: RunId[]; role: UserRole }, store: DataStore, { includeRuns = false, @@ -107,7 +109,6 @@ async function getSessionResource( includeRunLastLogs?: boolean; } = {}, ) { - let sessionData = req.session.data; if (sessionData == null) { throw new Error('Session not populated'); } @@ -119,25 +120,27 @@ async function getSessionResource( }), }, }; - let result: Writable< - Extract, { data: unknown }> - > = { - data: { - type: 'sessions' as const, - id: 'current' as const, - attributes, - relationships, - }, - }; + let included; if (includeRuns || includeExperiment || includeRunLastLogs) { let { runs, experiments, lastLogs } = await getRunResources(store, { filter: { runId: sessionData.runs }, }); - result.included = [ + included = [ ...(includeRuns ? runs : []), ...(includeExperiment ? experiments : []), ...(includeRunLastLogs ? lastLogs : []), ]; + } else { + included = undefined; } - return result; + + return { + data: { + type: 'sessions' as const, + id: 'current' as const, + attributes, + relationships, + }, + included, + }; } diff --git a/packages/log-server/src/app.ts b/packages/log-server/src/app.ts index c989e583..f70425ea 100644 --- a/packages/log-server/src/app.ts +++ b/packages/log-server/src/app.ts @@ -1,29 +1,22 @@ -import { match, P } from '@gabriel/ts-pattern'; -import { openAPI as lightmillAPI, type components } from '@lightmill/log-api'; -import cookieParser from 'cookie-parser'; +import type { InternalServerErrorResponse } from '@lightmill/log-api'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; import express, { type NextFunction } from 'express'; -import * as OpenApiValidator from 'express-openapi-validator'; -import type { - OpenAPIV3, - ValidationErrorItem, -} from 'express-openapi-validator/dist/framework/types.js'; import session from 'express-session'; import log from 'loglevel'; import MemorySessionStoreModule from 'memorystore'; +import { apiMediaType } from './api.ts'; import { experimentHandlers } from './app-experiments-handlers.js'; import { logHandlers } from './app-logs-handlers.js'; import { runHandlers } from './app-runs-handlers.js'; import { sessionHandlers } from './app-sessions-handlers.js'; -import { apiMediaType, type ServerApi } from './app-utils.js'; import type { DataStore } from './data-store.ts'; -import { createTypedExpressServer } from './typed-server.js'; -import { firstStrict } from './utils.js'; +import { createRouter, validateHandlers } from './router.ts'; export const SESSION_COOKIE_NAME = 'lightmill-session-id'; const MemorySessionStore = MemorySessionStoreModule(session); -type CreateLogServerOptions = { +interface CreateLogServerOptions { dataStore: DataStore; hostUser?: string | undefined; hostPassword?: string | undefined; @@ -33,7 +26,8 @@ type CreateLogServerOptions = { secureCookies?: boolean | undefined; sessionStore?: session.Store; baseUrl?: string; -}; + trustProxy?: boolean | undefined; +} export function LogServer({ dataStore, @@ -44,10 +38,12 @@ export function LogServer({ secureCookies = allowCrossOrigin, mode = process.env.NODE_ENV ?? 'production', sessionStore = new MemorySessionStore({ checkPeriod: 1000 * 60 * 60 * 24 }), - baseUrl = '/', + trustProxy = true, }: CreateLogServerOptions): { middleware: express.RequestHandler } { const app = express(); + app.set('trust proxy', trustProxy); + app.set('query parser', (str: string | null) => { if (str == null) return {}; let params = new URLSearchParams(decodeURIComponent(str)); @@ -67,10 +63,6 @@ export function LogServer({ app.use(express.json({ type: [apiMediaType, 'application/json'] })); - // Required for open api validator, but be careful to use - // the same keys as the session middleware. - app.use(cookieParser(sessionKeys)); - app.use( session({ store: sessionStore, @@ -86,81 +78,25 @@ export function LogServer({ }), ); - app.use( - OpenApiValidator.middleware({ - apiSpec: { - ...(lightmillAPI as OpenAPIV3.DocumentV3), - servers: [{ url: baseUrl }], - }, - validateApiSpec: mode !== 'production', - validateRequests: { allErrors: mode !== 'production' }, - validateResponses: mode !== 'production', - validateSecurity: { - handlers: { - CookieSessionAuth: (req, _token, _schema) => { - if (req.session.data != null) return true; - throw new Error('Login required'); - }, - }, - }, - }), - ); - - createTypedExpressServer( - dataStore, - { + const handlers = validateHandlers({ + validateResponse: mode !== 'test', + handlers: { ...sessionHandlers({ hostPassword, hostUser }), ...experimentHandlers(), ...runHandlers(), ...logHandlers(), }, - app, - ); + }); - app.use( - ( - err: Error, - req: express.Request, - res: express.Response, - next: NextFunction, - ) => { - if ( - err instanceof OpenApiValidator.error.BadRequest || - err instanceof OpenApiValidator.error.NotFound || - err instanceof OpenApiValidator.error.MethodNotAllowed || - err instanceof OpenApiValidator.error.UnsupportedMediaType || - err instanceof OpenApiValidator.error.Unauthorized || - err instanceof OpenApiValidator.error.Forbidden - ) { - for (let [key, value] of Object.entries(err.headers ?? {})) { - res.header(key, value); - } - const errorEntries = err.errors.map((errItem) => - getErrorEntry({ - errorItem: errItem, - errorStatus: err.status, - errorName: err.name, - request: req, - baseUrl: baseUrl, - }), - ); - res - .status(firstStrict(errorEntries).statusCode) - .header('content-type', apiMediaType) - .json({ errors: errorEntries.map((error) => error.content) }); - return; - } - next(err); - }, - ); + app.use(createRouter({ handlers, dataStore })); app.use( ( err: Error, _req: express.Request, res: express.Response, - // We don't use _next, but we need to declare all four parameters - // so its an error handler middleware. + // We don't use _next, but we do need to declare all four parameters + // so express recognizes it as an error handler middleware. _next: NextFunction, ) => { log.error(err); @@ -171,117 +107,15 @@ export function LogServer({ errors: [ { status: 'Internal Server Error', - code: 'INTERNAL_SERVER', + code: 'INTERNAL_SERVER_ERROR', detail: err.message, }, ], - } satisfies components['schemas']['NonRouterErrorDocument']); + } satisfies StandardSchemaV1.InferOutput< + typeof InternalServerErrorResponse + >); }, ); return { middleware: app }; } - -type NonRouterError = - components['schemas']['NonRouterErrorDocument']['errors'][number]; -type SessionRequiredError = components['schemas']['Utils.SessionRequiredError']; - -function getErrorEntry(options: { - errorItem: ValidationErrorItem; - errorStatus: number; - errorName: string; - request: express.Request; - baseUrl: string; -}) { - return match(options) - .returnType<{ - statusCode: number; - content: NonRouterError | SessionRequiredError; - }>() - .with( - P.union( - { errorStatus: 404 }, - { - errorStatus: 400, - errorItem: { path: P.when((p) => p.startsWith('/params/')) }, - }, - ), - ({ request }) => ({ - statusCode: 404, - content: { - status: 'Not Found', - code: 'NOT_FOUND', - detail: `resource ${request.originalUrl} does not exist`, - }, - }), - ) - .with({ errorStatus: 405 }, ({ errorItem }) => ({ - statusCode: 405, - content: { - status: 'Method Not Allowed', - code: 'METHOD_NOT_ALLOWED', - detail: errorItem.message, - }, - })) - .with({ errorStatus: 415 }, ({ errorItem }) => ({ - statusCode: 415, - content: { - status: 'Unsupported Media Type', - code: 'UNSUPPORTED_MEDIA_TYPE', - detail: errorItem.message, - }, - })) - .with( - { - errorStatus: 400, - errorItem: { path: P.string.startsWith('/headers') }, - }, - ({ errorItem }) => { - return { - statusCode: 400, - content: { - status: 'Bad Request', - code: 'HEADERS_VALIDATION', - detail: errorItem.message, - source: { header: errorItem.path.substring('/headers/'.length) }, - }, - }; - }, - ) - .with( - { errorStatus: 400, errorItem: { path: P.string.startsWith('/body') } }, - ({ errorItem }) => ({ - statusCode: 400, - content: { - status: 'Bad Request', - code: 'BODY_VALIDATION', - detail: errorItem.message, - source: { pointer: errorItem.path.substring('/body'.length) }, - }, - }), - ) - .with( - { errorStatus: 400, errorItem: { path: P.string.startsWith('/query') } }, - ({ errorItem }) => ({ - statusCode: 400, - content: { - status: 'Bad Request', - code: 'QUERY_VALIDATION', - detail: errorItem.message, - source: { parameter: errorItem.path.substring('/query/'.length) }, - }, - }), - ) - .with({ errorStatus: 401 }, () => { - let postPath = `${options.baseUrl}${options.baseUrl.endsWith('/') ? '' : '/'}sessions`; - return { - statusCode: 403, - content: { - status: 'Forbidden', - code: 'SESSION_REQUIRED', - detail: `session required, post to ${postPath}`, - }, - }; - }) - .run(); -} diff --git a/packages/log-server/src/index.ts b/packages/log-server/src/index.ts index 5a133a49..70bb008b 100644 --- a/packages/log-server/src/index.ts +++ b/packages/log-server/src/index.ts @@ -1,3 +1,3 @@ -export { LogServer } from './app.js'; +export { LogServer } from './app.ts'; export type { DataStore } from './data-store.ts'; export { SQLiteDataStore as SQLiteDataStore } from './sqlite-data-store.ts'; diff --git a/packages/log-server/src/router.ts b/packages/log-server/src/router.ts new file mode 100644 index 00000000..04dda811 --- /dev/null +++ b/packages/log-server/src/router.ts @@ -0,0 +1,598 @@ +import * as LogApi from '@lightmill/log-api'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; +import * as Express from 'express'; +import type { SessionData } from 'express-session'; +import Stream from 'node:stream'; +import { promisify } from 'node:util'; +import type { Simplify } from 'type-fest'; +import { z } from 'zod/v4'; +import { + apiMediaType, + httpStatusCodeFromText, + parseCookies, + type HttpStatusCodeFromText, + type HttpStatusText, + type UserRole, +} from './api.ts'; +import type { DataStore, RunId } from './data-store.ts'; +import { unsafeEntries, type ConditionalOptionalProps } from './utils.ts'; + +declare module 'express-session' { + interface SessionData { + data: { role: UserRole; runs: RunId[] }; + } +} + +export function validateHandlers({ + handlers, + validateResponse = false, +}: { + handlers: Handlers; + validateResponse?: boolean; +}): HandlersWithValidation { + const result = {} as HandlersWithValidation; + for (const [path, methods] of unsafeEntries(LogApi.routes)) { + // @ts-expect-error: We will fill this in later, and we know path is + // a valid key of Handlers. + result[path] = {}; + for (const [method, route] of unsafeEntries(methods)) { + const handler: Handler = + handlers[path][method as keyof Handlers[typeof path]]; + const routeRequest = route.request; + const cookiesSchema = + 'cookies' in routeRequest ? routeRequest.cookies : z.looseObject({}); + const responseSchemas = unsafeEntries(route.responses).flatMap( + ([status, response]) => { + return unsafeEntries(response.content).map( + ([contentType, content]) => ({ + contentType, + status, + body: content.schema, + }), + ); + }, + ); + let bodySchema; + if ('body' in routeRequest) { + bodySchema = routeRequest.body.content[apiMediaType].schema; + let isRequired = + 'required' in routeRequest.body && + routeRequest.body.required === true; + if (!isRequired) { + bodySchema = z.union([ + // Little hack: wrap into a new object schema to get rid of the + // openapi property from @lightmill/log-api schemas causing + // typescript issues. + z.strictObject(bodySchema.shape), + z.null(), + ]); + } + } else { + bodySchema = z.null(); + } + const schemas: HandlerSchemaEntry = { + body: bodySchema, + parameters: { + query: + 'query' in routeRequest ? routeRequest.query : z.strictObject({}), + headers: + 'headers' in routeRequest + ? routeRequest.headers + : z.looseObject({}), + cookies: cookiesSchema as unknown extends typeof cookiesSchema + ? StandardSchemaV1> + : typeof cookiesSchema, + path: + 'params' in routeRequest ? routeRequest.params : z.strictObject({}), + }, + responses: responseSchemas, + }; + const isSessionRequired = + !('security' in route) || + route.security.every((entry) => 'SessionAuth' in entry); + + const newHandler = validateHandler({ + handler, + schemas, + isSessionRequired, + validateResponse, + }); + // @ts-expect-error: Type should be correct. + result[path][method] = newHandler; + } + } + return result; +} + +export function createRouter({ + handlers, + router = Express.Router(), + dataStore, +}: { + handlers: HandlersWithValidation; + router?: Express.Router; + dataStore: DataStore; +}): Express.Router { + for (const [path, methods] of unsafeEntries(handlers)) { + const expressPath = path.replace(/{(\w+)}/g, ':$1'); + const route = router.route(expressPath); + for (const [method, handler] of unsafeEntries(methods)) { + route[method](async (request, response) => { + const { headers, params, query, body, session } = request; + if ( + ('content-type' in headers && + headers['content-type'] != apiMediaType) || + (request.body != null && !('content-type' in headers)) + ) { + await processResponse({ + result: getErrorResponse({ + status: 'Unsupported Media Type', + code: 'UNSUPPORTED_MEDIA_TYPE', + detail: + `Content type must be '${apiMediaType}'.` + + ` Set 'Content-Type' header to '${apiMediaType}'.`, + }), + request, + response, + }); + return; + } + const cookies = parseCookies(headers['cookie']); + + const result = await handler({ + body, + parameters: { headers, path: params, query, cookies }, + sessionData: session?.data ?? null, + dataStore, + protocol: request.protocol, + host: request.host, + }); + await processResponse({ result, request, response }); + }); + } + route.all(async (request, response) => { + const allowedMethods = Object.keys(methods) + .map((m) => m.toUpperCase()) + .sort(); + const conjunctionListFormat = new Intl.ListFormat('en', { + type: 'conjunction', + }); + await processResponse({ + result: { + ...getErrorResponse({ + status: 'Method Not Allowed', + code: 'METHOD_NOT_ALLOWED', + detail: + `${request.method} method is not allowed for resource ${path}.` + + ` Allowed methods are ${conjunctionListFormat.format(allowedMethods)}.`, + }), + headers: { allow: allowedMethods.join(', ') }, + }, + request, + response, + }); + }); + } + + router.use(async (request, response) => { + await processResponse({ + result: getErrorResponse({ + status: 'Not Found', + code: 'NOT_FOUND', + detail: `Resource ${request.originalUrl} does not exist.`, + }), + request, + response, + }); + }); + + return router; +} + +function validateHandler({ + schemas, + validateResponse, + isSessionRequired, + handler, +}: { + schemas: HandlerSchemaEntry; + validateResponse?: boolean; + isSessionRequired: boolean; + handler: Handler; +}): Handler { + return async ({ sessionData, body, parameters, ...otherHandlerOptions }) => { + if (isSessionRequired && sessionData == null) { + return getErrorResponse({ + status: 'Forbidden', + code: 'SESSION_REQUIRED', + detail: 'A session is required. Post to /sessions to create one.', + }); + } + + const validatedPath = await schemas['parameters']['path'][ + '~standard' + ].validate(parameters.path); + + if (validatedPath.issues != null) { + return getErrorResponse({ status: 'Not Found', code: 'NOT_FOUND' }); + } + + const validatedBody = await schemas['body']['~standard'].validate( + body ?? null, + ); + const validatedQuery = await schemas['parameters']['query'][ + '~standard' + ].validate(parameters.query); + const validatedHeader = await schemas['parameters']['headers'][ + '~standard' + ].validate(parameters.headers); + const validatedCookie = await schemas['parameters']['cookies'][ + '~standard' + ].validate(parameters.cookies); + + if ( + validatedBody.issues != null || + validatedQuery.issues != null || + validatedHeader.issues != null || + validatedCookie.issues != null + ) { + const errors: Array = [ + ...(validatedBody.issues ?? []).map( + (issue): ValidationError => ({ + code: 'INVALID_REQUEST_BODY', + status: 'Bad Request', + detail: issue.message, + source: { pointer: '/' + (issue.path?.join('/') ?? '') }, + }), + ), + ...(validatedQuery.issues ?? []).map( + (issue): ValidationError => ({ + code: 'INVALID_REQUEST_QUERY', + status: 'Bad Request', + detail: issue.message, + source: { parameter: issue.path?.join('.') ?? '' }, + }), + ), + ...(validatedHeader.issues ?? []).map( + (issue): ValidationError => ({ + code: 'INVALID_REQUEST_HEADERS', + status: 'Bad Request', + detail: issue.message, + source: { header: issue.path?.join('.') ?? '' }, + }), + ), + ...(validatedCookie.issues ?? []).map( + (issue): ValidationError => ({ + code: 'INVALID_REQUEST_HEADERS', + status: 'Bad Request', + detail: issue.message, + source: { header: 'cookie' }, + }), + ), + ]; + return getErrorResponse(errors); + } + + const response = await handler({ + body: validatedBody.value, + parameters: { + path: validatedPath.value, + query: validatedQuery.value, + headers: validatedHeader.value, + cookies: validatedCookie.value, + }, + sessionData: sessionData, + ...otherHandlerOptions, + }); + // We do not validate the response if it is a stream. It may be possible + // but would imply starting to stream the answer and only failing before + // sending the last chunk. + if (!validateResponse || response.body instanceof Stream) return response; + const responseSchema = schemas.responses.find( + (r) => + r.status === response.status && r.contentType === response.contentType, + ); + if (responseSchema == null) { + throw new Error( + `Unexpected response with status ${response.status} and content type ${response.contentType}`, + ); + } + const result = await responseSchema.body['~standard'].validate( + response.body, + ); + if (result.issues != null) { + throw new Error( + `Response validation failed: ${result.issues + .map((i) => i.message) + .join(', ')}`, + ); + } + return response; + }; +} + +async function processResponse({ + result, + request, + response, +}: { + result: HandlerResponse; + request: Express.Request; + response: Express.Response; +}) { + if ('sessionData' in result) { + if (result.sessionData == null) { + await promisify(request.session.destroy.bind(request.session))(); + } else { + request.session.data = result.sessionData; + } + } + response + .status(result.status ?? 200) + .contentType(result.contentType ?? apiMediaType); + for (const [key, value] of Object.entries(result.headers ?? {})) { + response.setHeader(key, String(value)); + } + if (result.body instanceof Stream) { + result.body.pipe(response); + return; + } + response.send(result.body); +} + +function getErrorResponse< + Error extends { code: string; status: HttpStatusText }, +>( + errors: Array | Error, + statusCode?: HttpStatusCodeFromText, +) { + errors = Array.isArray(errors) ? errors : [errors]; + let firstError = errors[0]; + if (firstError == null) { + throw new Error('No errors provided'); + } + return { + contentType: apiMediaType, + status: + statusCode ?? httpStatusCodeFromText(firstError.status), + body: { errors }, + }; +} + +export type Handlers = { + [Path in keyof Routes]: { + [Method in keyof Routes[Path]]: Handler< + HandlerOptionsMap[Path][Method], + HandlerResponseMap[Path][Method] + >; + }; +}; + +export type PathHandlers = { + [Path in Extract]: Handlers[Path]; +}; + +export type HandlerResponseFromRoute< + P extends keyof Routes, + M extends keyof Routes[P], +> = Routes extends { + [K in P]: { + [L in M]: { + responses: infer Responses extends Record< + PropertyKey, + { content: unknown } + >; + }; + }; +} + ? { + [Status in keyof Responses]: { + [ContentType in keyof Responses[Status]['content']]: Responses[Status]['content'][ContentType] extends { + schema: infer Schema extends StandardSchemaV1; + } + ? HandlerResponse< + StandardSchemaV1.InferOutput, + Extract, + Extract, + Responses[Status] extends { + headers: infer Headers extends StandardSchemaV1; + } + ? StandardSchemaV1.InferOutput + : Record + > + : never; + }[keyof Responses[Status]['content']]; + }[keyof Responses] + : never; + +export type HandlerOptionsFromRoute< + P extends Path, + M extends keyof Routes[P], +> = HandlerOptions< + RequestBodyFromRoute, + RequestParameterFromRoute, + RequestParameterFromRoute, + RequestParameterFromRoute, + RequestParameterFromRoute, + IsSessionRequiredFromRoute +>; + +interface Handler< + Options = HandlerOptions, + Response extends HandlerResponse = HandlerResponse, +> { + (options: Options): Promise; +} + +interface ResponseSchemaEntry< + BodySchema extends StandardSchemaV1 = StandardSchemaV1, +> { + contentType: string; + status: number; + body: BodySchema; +} + +interface HandlerSchemaEntry< + BodySchema extends StandardSchemaV1 = StandardSchemaV1, + PathSchema extends StandardSchemaV1 = StandardSchemaV1, + QuerySchema extends StandardSchemaV1 = StandardSchemaV1, + HeadersSchema extends StandardSchemaV1 = StandardSchemaV1, + CookiesSchema extends StandardSchemaV1 = StandardSchemaV1, + ResponseSchemaEntries extends + Array = Array, +> { + body: BodySchema; + parameters: { + path: PathSchema; + query: QuerySchema; + headers: HeadersSchema; + cookies: CookiesSchema; + }; + responses: ResponseSchemaEntries; +} + +interface HandlerOptions< + Body = unknown, + Path = unknown, + Query = unknown, + Headers = unknown, + Cookies = unknown, + IsSessionRequired extends boolean = false, +> { + body: Body; + parameters: { query: Query; headers: Headers; cookies: Cookies; path: Path }; + sessionData: IsSessionRequired extends true + ? SessionData['data'] + : SessionData['data'] | null; + dataStore: DataStore; + protocol: string; + host: string; +} + +type HandlerResponse< + Body = unknown, + ContentType extends string = string, + Status extends number = number, + Headers = Record, +> = Simplify< + { sessionData?: SessionData['data'] | null } & (Body extends null + ? { body?: null } + : { body: Body | Stream }) & + (string extends ContentType + ? { contentType?: string | undefined } + : ConditionalOptionalProps< + { contentType: ContentType }, + typeof apiMediaType + >) & + (number extends Status + ? { status?: number | undefined } + : ConditionalOptionalProps<{ status: Status }, 200>) & + (Record extends Headers + ? { headers?: Record | undefined } + : Omit extends Record + ? { headers?: Record | undefined } + : { headers: Omit }) +>; + +type Routes = typeof LogApi.routes; +type Path = keyof Routes; +type RequestSchemas< + P extends keyof Routes, + M extends keyof Routes[P], +> = Routes extends { [K in P]: { [L in M]: { request: infer R } } } ? R : never; + +type RequestBodySchemaFromRoute

= + RequestSchemas extends { + body: { + required?: infer Required; + content: { + [K in typeof apiMediaType]: { + schema: infer B extends StandardSchemaV1; + }; + }; + }; + } + ? Required extends true + ? StandardSchemaV1< + StandardSchemaV1.InferInput, + StandardSchemaV1.InferOutput + > + : StandardSchemaV1< + StandardSchemaV1.InferInput | null, + StandardSchemaV1.InferOutput | null + > + : StandardSchemaV1; + +type RequestParameterSchemaFromRoute< + P extends Path, + Method extends keyof Routes[P], + Param extends 'headers' | 'query' | 'cookies' | 'path', +> = + RequestSchemas extends { + [K in Param as Param extends 'path' ? 'params' : Param]: infer B extends + StandardSchemaV1; + } + ? StandardSchemaV1< + StandardSchemaV1.InferInput, + StandardSchemaV1.InferOutput + > + : StandardSchemaV1>; + +type RequestBodyFromRoute< + P extends Path, + M extends keyof Routes[P], +> = StandardSchemaV1.InferOutput>; + +type RequestParameterFromRoute< + P extends Path, + Method extends keyof Routes[P], + Param extends 'headers' | 'query' | 'cookies' | 'path', +> = StandardSchemaV1.InferOutput< + RequestParameterSchemaFromRoute +>; + +// By default, the session is required, unless the security argument specifies +// another security scheme. +type IsSessionRequiredFromRoute< + P extends Path, + M extends keyof Routes[P], +> = Routes[P][M] extends { security: Array } + ? S extends { SessionAuth: unknown } + ? true + : false + : true; + +type HandlerOptionsMap = { + [P in Path]: { + [Method in keyof Routes[P]]: HandlerOptionsFromRoute; + }; +}; +type HandlerResponseMap = { + [P in Path]: { + [Method in keyof Routes[P]]: HandlerResponseFromRoute; + }; +}; + +type HandlersWithValidation = { + [P in Path]: { + [Method in keyof Routes[P]]: Handler< + HandlerOptions, + HandlerResponseMap[P][Method] | ValidationResponse + >; + }; +}; + +type ValidationError = StandardSchemaV1.InferOutput< + typeof LogApi.RequestValidationErrorResponse +>['errors'][number]; +interface ServerErrorResponse { + contentType: typeof apiMediaType; + status: Status; + body: Body; + sessionData?: SessionData['data'] | null; + headers?: Record; +} +type ValidationResponse = ServerErrorResponse< + HttpStatusCodeFromText<'Bad Request'>, + { errors: ValidationError[] } +>; diff --git a/packages/log-server/src/typed-server.ts b/packages/log-server/src/typed-server.ts deleted file mode 100644 index 0fcf9fb1..00000000 --- a/packages/log-server/src/typed-server.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { - Router, - type NextFunction, - type Request, - type Response, -} from 'express'; -import { Stream } from 'node:stream'; -import type { Merge, SetOptional, Simplify } from 'type-fest'; -import type { - Api, - ApiMethodFromPath, - ApiOperation, - ApiOperationParameters, - ApiOperationRequestContent, - ApiOperationResponseContent, - ApiPath, - HttpMethod, -} from './api-utils.js'; -import { apiMediaType, type ApiMediaType } from './app-utils.ts'; -import type { DataStore } from './data-store.ts'; -import type { LowercaseProps } from './utils.js'; - -const pathParamRegex = /{([^}]+)}/g; -export function createTypedExpressServer( - store: DataStore, - server: ServerDescription, - router: Router = Router(), -): Middleware { - for (let path in server) { - let convertedPath = path.replace(pathParamRegex, ':$1'); - for (let method in server[path]) { - let handler = server[path][method]; - router[method as HttpMethod]( - convertedPath, - async (request: Request, response: Response, next: NextFunction) => { - try { - const parameters = { - path: request.params ?? {}, - query: request.query ?? {}, - headers: request.headers ?? {}, - cookies: request.cookies ?? {}, - }; - let result = await handler({ - store, - body: request.body ?? {}, - // @ts-expect-error We assume this has been validated. - parameters, - // @ts-expect-error We assume this has been validated. - request, - response, - }); - if (result.headers != null) { - response.header(result.headers); - } - if (result.body instanceof Stream) { - result.body.pipe( - response - .contentType(result.contentType ?? apiMediaType) - .status(result.status), - ); - return; - } - response - .contentType(result.contentType ?? apiMediaType) - .status(result.status) - .send(result.body); - } catch (error) { - next(error); - return; - } - next(); - }, - ); - } - } - return router; -} - -export type ServerDescription = { - [P in ApiPath]: { - [M in ApiMethodFromPath]: OperationTypedHandler< - ApiOperation - >; - }; -}; - -export type Handler< - A extends Api, - Path extends ApiPath, - Method extends HttpMethod, -> = OperationTypedHandler>; - -export type HandlerParameters< - A extends Api, - Path extends ApiPath, - Method extends HttpMethod, -> = OperationHandlerParameters>; - -export interface HandlerResultBase { - status: number; - body: unknown; - contentType?: string | undefined; - headers?: Record | undefined; -} - -export type HandlerResult< - A extends Api, - Path extends ApiPath, - Method extends HttpMethod, -> = OperationHandlerResult>; - -export type RequestContent< - A extends Api, - Path extends ApiPath, - Method extends HttpMethod, -> = ApiOperationRequestContent>; - -export interface Middleware { - (req: Request, res: Response, next: NextFunction): void; -} - -interface OperationTypedHandler { - (context: { - store: DataStore; - body: ReplaceNever< - ApiOperationRequestContent['body'], - Record - >; - parameters: Simplify>; - request: Request< - OperationHandlerParameters['path'], - ApiOperationResponseContent['body'], - ReplaceNever['body'], null>, - OperationHandlerParameters['query'] - >; - response: Response['body']>; - }): Promise>; -} - -type ReplaceNever = [T] extends [never] ? R : T; - -interface OperationHandlerParameters { - query: ReplaceNever< - NonNullable['query']>, - Record - >; - path: ReplaceNever< - NonNullable['path']>, - Record - >; - headers: LowercaseProps< - ReplaceNever< - NonNullable['header']>, - Record - > - >; - cookies: ReplaceNever< - NonNullable['cookie']>, - Record - >; -} - -type OperationHandlerResult = Simplify< - RemoveSetCookiesFromHeaders< - LowerCaseHeaders< - AddReadableStreamToBody< - SetOptionalJsonContentType> - > - > - > & - HandlerResultBase ->; - -type SetOptionalJsonContentType = T extends { contentType: ApiMediaType } - ? SetOptional - : T; - -type LowerCaseHeaders }> = - T extends { headers: Record } - ? Omit & { headers: LowercaseProps } - : Omit & { - headers?: LowercaseProps> | undefined; - }; - -type RemoveSetCookiesFromHeaders = T extends { - headers: infer Headers extends { 'set-cookie': string }; -} - ? Merge, { headers: SetOptional }> - : T; - -type AddReadableStreamToBody = AddTypeToProp< - T, - 'body', - Stream ->; - -type AddTypeToProp< - T extends object, - Prop extends PropertyKey, - AdditionalType, -> = { [K in keyof T]: T[K] | (K extends Prop ? AdditionalType : never) }; diff --git a/packages/log-server/src/utils.ts b/packages/log-server/src/utils.ts index 56ec0cce..8db3a41d 100644 --- a/packages/log-server/src/utils.ts +++ b/packages/log-server/src/utils.ts @@ -1,6 +1,7 @@ import { mapKeys, toSnakeCase } from 'remeda'; import type { ArrayIndices, + Entries, IsNever, Merge, UnionToIntersection, @@ -261,3 +262,36 @@ export async function fromAsync( } return values; } + +/** + * This is a wrapper around `Object.entries` with typed key and values of the + * entries. + * WARNING: This is unsafe because it assumes the object does not contain any + * extra properties that are not in the type `T`. Only use when you are sure + * that the object does not have any extra properties that the ones specified in + * its type. + * + * For example, `let x = { a: 'foo', b: 42 }; let y: { a: string } = x;` is + * correct (and common) in TypeScript. y in this case contains an extra + * property, `b`, that TS isn't aware of, making the result type of + * `unsafeEntries` incorrect. + * + * @param obj - The object to get entries from + * @returns The entries of the object as an array of key-value pairs. + */ +export function unsafeEntries(obj: T): Entries { + return Object.entries(obj) as Entries; +} + +export type ConditionalOptionalProps< + T extends object, + V, + K extends PropertyKey = keyof T, +> = { [P in Exclude & K>]: T[P] } & { + [P in ConditionalKeys & K]?: T[P] | undefined; +}; + +export type ConditionalKeys = Extract< + keyof T, + { [K in keyof T]-?: Required[K] extends V ? K : never }[keyof T] +>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c435e92..7dcd7f75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,33 +36,33 @@ importers: specifier: ^23.6.1 version: 23.6.1 '@tsconfig/node22': - specifier: ^22.0.1 + specifier: ^22.0.2 version: 22.0.2 devDependencies: '@changesets/cli': - specifier: ^2.27.1 - version: 2.29.4 + specifier: ^2.29.5 + version: 2.29.5 '@eslint/js': - specifier: ^9.20.0 - version: 9.28.0 + specifier: ^9.30.1 + version: 9.30.1 '@types/eslint-config-prettier': specifier: ^6.11.3 version: 6.11.3 eslint: - specifier: ^9.20.1 - version: 9.28.0(jiti@2.4.2) + specifier: ^9.30.1 + version: 9.30.1(jiti@2.4.2) eslint-config-prettier: specifier: 10.1.2 - version: 10.1.2(eslint@9.28.0(jiti@2.4.2)) + version: 10.1.2(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-jsdoc: - specifier: ^50.6.3 - version: 50.7.1(eslint@9.28.0(jiti@2.4.2)) + specifier: ^50.8.0 + version: 50.8.0(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-react: - specifier: ^7.37.4 - version: 7.37.5(eslint@9.28.0(jiti@2.4.2)) + specifier: ^7.37.5 + version: 7.37.5(eslint@9.30.1(jiti@2.4.2)) globals: - specifier: ^16.0.0 - version: 16.2.0 + specifier: ^16.3.0 + version: 16.3.0 jiti: specifier: ^2.4.2 version: 2.4.2 @@ -79,11 +79,11 @@ importers: specifier: 'catalog:' version: 5.8.3 typescript-eslint: - specifier: ^8.27.0 - version: 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.35.1 + version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) vitest: specifier: 'catalog:' - version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1) + version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0) packages/convert-touchstone: dependencies: @@ -108,33 +108,31 @@ importers: version: 5.8.3 vite: specifier: 'catalog:' - version: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) + version: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) vitest: specifier: 'catalog:' - version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1) + version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0) packages/log-api: + dependencies: + '@asteasolutions/zod-to-openapi': + specifier: 8.0.0-beta.4 + version: 8.0.0-beta.4(zod@3.25.73) + type-fest: + specifier: ^4.41.0 + version: 4.41.0 + zod: + specifier: ^3.25.73 + version: 3.25.73 devDependencies: - '@typespec/compiler': - specifier: 1.0.0 - version: 1.0.0(@types/node@22.14.0) - '@typespec/http': - specifier: 1.0.1 - version: 1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)) - '@typespec/openapi': - specifier: 1.0.0 - version: 1.0.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0))) - '@typespec/openapi3': - specifier: 1.0.0 - version: 1.0.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)))(@typespec/openapi@1.0.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)))) - '@typespec/rest': - specifier: ^0.70.0 - version: 0.70.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0))) + '@std/yaml': + specifier: jsr:^1.0.8 + version: '@jsr/std__yaml@1.0.8' onchange: specifier: ^7.1.0 version: 7.1.0 openapi-typescript: - specifier: ^7.6.1 + specifier: ^7.8.0 version: 7.8.0(typescript@5.8.3) packages/log-client: @@ -161,6 +159,9 @@ importers: onchange: specifier: ^7.1.0 version: 7.1.0 + openapi-typescript: + specifier: ^7.8.0 + version: 7.8.0(typescript@5.8.3) type-fest: specifier: ^4.39.1 version: 4.41.0 @@ -169,22 +170,19 @@ importers: version: 5.8.3 vitest: specifier: 'catalog:' - version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1) + version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0) packages/log-server: dependencies: - '@gabriel/ts-pattern': - specifier: npm:@jsr/gabriel__ts-pattern@^5.7.1 - version: '@jsr/gabriel__ts-pattern@5.7.1' '@lightmill/log-api': specifier: workspace:* version: link:../log-api + '@standard-schema/spec': + specifier: ^1.0.0 + version: 1.0.0 better-sqlite3: specifier: ^11.10.0 version: 11.10.0 - cookie-parser: - specifier: ^1.4.7 - version: 1.4.7 cors: specifier: ^2.8.5 version: 2.8.5 @@ -197,9 +195,6 @@ importers: express: specifier: ^5.1.0 version: 5.1.0 - express-openapi-validator: - specifier: 5.4.9 - version: 5.4.9(express@5.1.0) express-session: specifier: ^1.18.1 version: 1.18.1 @@ -272,7 +267,7 @@ importers: version: 5.8.3 vitest: specifier: 'catalog:' - version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1) + version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0) packages/react-experiment: dependencies: @@ -297,7 +292,7 @@ importers: version: 18.3.23 '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.5.2(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1)) + version: 4.5.2(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -318,10 +313,10 @@ importers: version: 5.8.3 vite: specifier: 'catalog:' - version: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) + version: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) vitest: specifier: 'catalog:' - version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1) + version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0) packages/runner: devDependencies: @@ -333,10 +328,10 @@ importers: version: 5.8.3 vite: specifier: 'catalog:' - version: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) + version: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) vitest: specifier: 'catalog:' - version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1) + version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0) packages/static-design: devDependencies: @@ -348,10 +343,10 @@ importers: version: 5.8.3 vite: specifier: 'catalog:' - version: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) + version: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) vitest: specifier: 'catalog:' - version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1) + version: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0) packages: @@ -362,29 +357,14 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@apidevtools/json-schema-ref-parser@11.7.2': - resolution: {integrity: sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==} - engines: {node: '>= 16'} - - '@apidevtools/json-schema-ref-parser@11.9.3': - resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} - engines: {node: '>= 16'} - - '@apidevtools/openapi-schemas@2.1.0': - resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} - engines: {node: '>=10'} - - '@apidevtools/swagger-methods@3.0.2': - resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} - - '@apidevtools/swagger-parser@10.1.1': - resolution: {integrity: sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==} - peerDependencies: - openapi-types: '>=7' - '@asamuzakjp/css-color@2.8.3': resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@asteasolutions/zod-to-openapi@8.0.0-beta.4': + resolution: {integrity: sha512-om7KH4B6/Rd/jjrxUjV3yC7vUwKKQcM1m7R8jd12uZyBqYd4ihqa1mMHp9MG3Hau8M/ha6teNJLouDUvwZXpNg==} + peerDependencies: + zod: ~3.25.1 + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -469,6 +449,10 @@ packages: resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -503,14 +487,14 @@ packages: '@changesets/apply-release-plan@7.0.12': resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} - '@changesets/assemble-release-plan@6.0.8': - resolution: {integrity: sha512-y8+8LvZCkKJdbUlpXFuqcavpzJR80PN0OIfn8HZdwK7Sh6MgLXm4hKY5vu6/NDoKp8lAlM4ERZCqRMLxP4m+MQ==} + '@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.4': - resolution: {integrity: sha512-VW30x9oiFp/un/80+5jLeWgEU6Btj8IqOgI+X/zAYu4usVOWXjPIK5jSSlt5jsCU7/6Z7AxEkarxBxGUqkAmNg==} + '@changesets/cli@2.29.5': + resolution: {integrity: sha512-0j0cPq3fgxt2dPdFsg4XvO+6L66RC0pZybT9F4dG5TBrLA3jA/1pNkdTXH9IBBVHkgsKrNKenI3n1mPyPlIydg==} hasBin: true '@changesets/config@3.1.1': @@ -522,8 +506,8 @@ packages: '@changesets/get-dependents-graph@2.1.3': resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - '@changesets/get-release-plan@4.0.12': - resolution: {integrity: sha512-KukdEgaafnyGryUwpHG2kZ7xJquOmWWWk5mmoeQaSvZTWH1DC5D/Sw6ClgGFYtQnOMSQhgoEbDxAbpIIayKH1g==} + '@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==} @@ -737,12 +721,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.1': - resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -753,32 +731,36 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.1': - resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.14.0': resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.28.0': - resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} + '@eslint/js@9.30.1': + resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + '@eslint/plugin-kit@0.3.3': + resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': @@ -797,28 +779,10 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.2': - resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@inquirer/checkbox@4.1.8': - resolution: {integrity: sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/confirm@5.1.12': - resolution: {integrity: sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/confirm@5.1.9': resolution: {integrity: sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==} engines: {node: '>=18'} @@ -837,104 +801,10 @@ packages: '@types/node': optional: true - '@inquirer/core@10.1.13': - resolution: {integrity: sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/editor@4.2.13': - resolution: {integrity: sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/expand@4.0.15': - resolution: {integrity: sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/figures@1.0.11': resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} engines: {node: '>=18'} - '@inquirer/figures@1.0.12': - resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==} - engines: {node: '>=18'} - - '@inquirer/input@4.1.12': - resolution: {integrity: sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/number@3.0.15': - resolution: {integrity: sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/password@4.0.15': - resolution: {integrity: sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/prompts@7.5.3': - resolution: {integrity: sha512-8YL0WiV7J86hVAxrh3fE5mDCzcTDe1670unmJRz6ArDgN+DBK1a0+rbnNWp4DUB5rPMwqD5ZP6YHl9KK1mbZRg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/rawlist@4.1.3': - resolution: {integrity: sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/search@3.0.15': - resolution: {integrity: sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/select@4.2.3': - resolution: {integrity: sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/type@3.0.6': resolution: {integrity: sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==} engines: {node: '>=18'} @@ -944,19 +814,6 @@ packages: '@types/node': optional: true - '@inquirer/type@3.0.7': - resolution: {integrity: sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -969,17 +826,14 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jsdevtools/ono@7.1.3': - resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - - '@jsr/gabriel__ts-pattern@5.7.1': - resolution: {integrity: sha512-wUJKELFQQvIuaauhFpMaD6umx7KbIfU3T9uN9qFyq/pV3N4Q7UoSCNqYfeiB3yZ/LcCXBPcu/3SuD7ejzfZUbg==, tarball: https://npm.jsr.io/~/11/@jsr/gabriel__ts-pattern/5.7.1.tgz} + '@jsr/std__yaml@1.0.8': + resolution: {integrity: sha512-iLe84pvIEtG7EFKxP5fIQn7CFQle7gzkb6KSPqf9zkyR2NVViSJjK8xbXg82SUEFSmpdY18qCGpVEnTG3oRMVg==, tarball: https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.8.tgz} '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1139,9 +993,8 @@ packages: cpu: [x64] os: [win32] - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -1219,12 +1072,12 @@ packages: '@types/eslint-config-prettier@6.11.3': resolution: {integrity: sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.0.6': resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} @@ -1246,9 +1099,6 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/multer@1.4.12': - resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==} - '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -1297,123 +1147,65 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@typescript-eslint/eslint-plugin@8.34.0': - resolution: {integrity: sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==} + '@typescript-eslint/eslint-plugin@8.35.1': + resolution: {integrity: sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.34.0 + '@typescript-eslint/parser': ^8.35.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.34.0': - resolution: {integrity: sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==} + '@typescript-eslint/parser@8.35.1': + resolution: {integrity: sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.34.0': - resolution: {integrity: sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==} + '@typescript-eslint/project-service@8.35.1': + resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.34.0': - resolution: {integrity: sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==} + '@typescript-eslint/scope-manager@8.35.1': + resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.34.0': - resolution: {integrity: sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==} + '@typescript-eslint/tsconfig-utils@8.35.1': + resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.34.0': - resolution: {integrity: sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==} + '@typescript-eslint/type-utils@8.35.1': + resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.32.1': - resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.34.0': - resolution: {integrity: sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==} + '@typescript-eslint/types@8.35.1': + resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.34.0': - resolution: {integrity: sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==} + '@typescript-eslint/typescript-estree@8.35.1': + resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.34.0': - resolution: {integrity: sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==} + '@typescript-eslint/utils@8.35.1': + resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.34.0': - resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==} + '@typescript-eslint/visitor-keys@8.35.1': + resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typespec/asset-emitter@0.70.1': - resolution: {integrity: sha512-X8hRA7LLWkNIWqAkWaWoa84PzDMUvjj3qCLQKT29k5twS419nN1GGT7BQaDQnYfPTsSssEFxgRgugr0AAErEsA==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@typespec/compiler': ^1.0.0 - - '@typespec/compiler@1.0.0': - resolution: {integrity: sha512-QFy0otaB4xkN4kQmYyT17yu3OVhN0gti9+EKnZqs5JFylw2Xecx22BPwUE1Byj42pZYg5d9WlO+WwmY5ALtRDg==} - engines: {node: '>=20.0.0'} - hasBin: true - - '@typespec/http@1.0.1': - resolution: {integrity: sha512-J5tqBWlmkvI/W+kJn4EFuN0laGxbY8qT68jzEQEiYeAXSfNyFGRSoCwn8Ex6dJphq4IozOMdVTNtOZWIJlwmfw==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@typespec/compiler': ^1.0.0 - '@typespec/streams': ^0.70.0 - peerDependenciesMeta: - '@typespec/streams': - optional: true - - '@typespec/openapi3@1.0.0': - resolution: {integrity: sha512-cDsnNtJkQCx0R/+9AqXzqAKH6CgtwmnQGQMQHbkw0/Sxs5uk6hoiexx7vz0DUR7H4492MqPT2kE4351KZbDYMw==} - engines: {node: '>=20.0.0'} - hasBin: true - peerDependencies: - '@typespec/compiler': ^1.0.0 - '@typespec/http': ^1.0.0 - '@typespec/json-schema': ^1.0.0 - '@typespec/openapi': ^1.0.0 - '@typespec/versioning': ^0.70.0 - '@typespec/xml': '*' - peerDependenciesMeta: - '@typespec/json-schema': - optional: true - '@typespec/versioning': - optional: true - '@typespec/xml': - optional: true - - '@typespec/openapi@1.0.0': - resolution: {integrity: sha512-pONzKIdK4wHgD1vBfD9opUk66zDG55DlHbueKOldH2p1LVf5FnMiuKE4kW0pl1dokT/HBNR5OJciCzzVf44AgQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@typespec/compiler': ^1.0.0 - '@typespec/http': ^1.0.0 - - '@typespec/rest@0.70.0': - resolution: {integrity: sha512-pn3roMQV6jBNT4bVA/hnrBAAHleXSyfWQqNO+DhI3+tLU4jCrJHmUZDi82nI9xBl+jkmy2WZFZOelZA9PSABeg==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@typespec/compiler': ^1.0.0 - '@typespec/http': ^1.0.0 - '@vitejs/plugin-react@4.5.2': resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1437,8 +1229,8 @@ packages: '@vitest/pretty-format@3.1.2': resolution: {integrity: sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==} - '@vitest/pretty-format@3.1.4': - resolution: {integrity: sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==} + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} '@vitest/runner@3.1.2': resolution: {integrity: sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==} @@ -1466,8 +1258,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1475,28 +1267,9 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} - ajv-draft-04@1.0.0: - resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} - peerDependencies: - ajv: ^8.5.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - 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'} @@ -1521,9 +1294,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - append-field@1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} @@ -1548,8 +1318,8 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} array-union@2.1.0: @@ -1621,11 +1391,11 @@ packages: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -1636,16 +1406,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1662,17 +1425,10 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} - call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} - engines: {node: '>= 0.4'} - call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - call-me-maybe@1.0.2: - resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1709,10 +1465,6 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -1749,10 +1501,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-stream@1.6.2: - resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} - engines: {'0': node >= 0.8} - content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -1764,13 +1512,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-parser@1.4.7: - resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} - engines: {node: '>= 0.8.0'} - - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -1785,9 +1526,6 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -1848,15 +1586,6 @@ packages: supports-color: optional: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -1961,12 +1690,8 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - env-paths@3.0.0: - resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -2022,8 +1747,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-jsdoc@50.7.1: - resolution: {integrity: sha512-XBnVA5g2kUVokTNUiE1McEPse5n9/mNUmuJcx52psT6zBs2eVcXSmQBvjfa7NZdfLVSy3u1pEDDUxoxpwy89WA==} + eslint-plugin-jsdoc@50.8.0: + resolution: {integrity: sha512-UyGb5755LMFWPrZTEqqvTJ3urLz1iqj+bYOHFNag+sw3NvaMWP9K2z+uIn37XfNALmQLQyrBlJ5mkiVPL7ADEg==} engines: {node: '>=18'} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 @@ -2034,24 +1759,20 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.28.0: - resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + eslint@9.30.1: + resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2060,8 +1781,8 @@ packages: jiti: optional: true - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: @@ -2100,11 +1821,6 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} - express-openapi-validator@5.4.9: - resolution: {integrity: sha512-NdU3VSkujFNpESNLXOvR2qVc/kO+zps8O5sBPQpvj8r075hLTtH823kWaGIJyVThWTd/9c8/bXaCJH/m3m9SAQ==} - peerDependencies: - express: '*' - express-session@1.18.1: resolution: {integrity: sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==} engines: {node: '>= 0.8.0'} @@ -2136,14 +1852,11 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fdir@6.4.4: - resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -2237,10 +1950,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.2.7: - resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} - engines: {node: '>= 0.4'} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2275,8 +1984,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.2.0: - resolution: {integrity: sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==} + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} engines: {node: '>=18'} globalthis@1.0.4: @@ -2287,10 +1996,6 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globby@14.1.0: - resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} - engines: {node: '>=18'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2464,6 +2169,10 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} @@ -2509,10 +2218,6 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} - is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} - is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -2529,9 +2234,6 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2622,13 +2324,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - - lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2646,8 +2341,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2669,10 +2364,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -2740,26 +2431,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} - engines: {node: '>= 18'} - mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2784,15 +2458,6 @@ packages: typescript: optional: true - multer@1.4.5-lts.2: - resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==} - engines: {node: '>= 6.0.0'} - deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. - - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2869,15 +2534,9 @@ packages: resolution: {integrity: sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==} hasBin: true - ono@7.1.3: - resolution: {integrity: sha512-9jnfVriq7uJM4o5ganUY54ntUm+5EK21EGaQ5NWnkWg3zz5ywbbonlBguRcnmF1/HDiIe3zxNxXcO1YPBmPcQQ==} - openapi-fetch@0.13.8: resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} - openapi-types@12.1.3: - resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} - openapi-typescript-helpers@0.0.15: resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} @@ -2887,6 +2546,9 @@ packages: peerDependencies: typescript: ^5.x + openapi3-ts@4.5.0: + resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2933,11 +2595,11 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-manager-detector@0.2.9: - resolution: {integrity: sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==} + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - package-manager-detector@1.2.0: - resolution: {integrity: sha512-PutJepsOtsqVfUsxCzgTTpyXmiAgvKptIgY4th5eq5UXXFhj5PxfQ9hnGkypMeovpAvVshFRItoFHYO18TCOqA==} + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -2982,15 +2644,11 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - path-type@6.0.0: - resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} - engines: {node: '>=18'} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} picocolors@1.1.1: @@ -3053,9 +2711,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -3085,6 +2740,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -3130,9 +2788,6 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -3212,9 +2867,6 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3243,11 +2895,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -3321,10 +2968,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3354,13 +2997,13 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + stream-transform@3.3.3: resolution: {integrity: sha512-dALXrXe+uq4aO5oStdHKlfCM/b3NBdouigvxVPxCdrMRAU6oHh3KNss20VbTPQNQmjAHzZGKGe66vgwegFEIog==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -3391,9 +3034,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -3424,6 +3064,7 @@ packages: supertest@7.1.1: resolution: {integrity: sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==} engines: {node: '>=14.18.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net supports-color@10.0.0: resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} @@ -3447,16 +3088,6 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - - temporal-polyfill@0.3.0: - resolution: {integrity: sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g==} - - temporal-spec@0.3.0: - resolution: {integrity: sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ==} - term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -3467,12 +3098,12 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.13: - resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} - tinypool@1.0.2: - resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + 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: @@ -3548,10 +3179,6 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -3572,11 +3199,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - - typescript-eslint@8.34.0: - resolution: {integrity: sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==} + typescript-eslint@8.35.1: + resolution: {integrity: sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3598,10 +3222,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} - universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -3709,23 +3329,6 @@ packages: jsdom: optional: true - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -3758,8 +3361,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.18: - resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} which@2.0.2: @@ -3806,10 +3409,6 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3820,16 +3419,12 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yaml-ast-parser@0.0.43: resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@21.1.1: @@ -3851,6 +3446,9 @@ packages: zod@3.25.57: resolution: {integrity: sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA==} + zod@3.25.73: + resolution: {integrity: sha512-fuIKbQAWQl22Ba5d1quwEETQYjqnpKVyZIWAhbnnHgnDd3a+z4YgEfkI5SZ2xMELnLAXo/Flk2uXgysZNf0uaA==} + snapshots: '@adobe/css-tools@4.4.2': {} @@ -3860,40 +3458,18 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@apidevtools/json-schema-ref-parser@11.7.2': + '@asamuzakjp/css-color@2.8.3': dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - js-yaml: 4.1.0 + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 - '@apidevtools/json-schema-ref-parser@11.9.3': + '@asteasolutions/zod-to-openapi@8.0.0-beta.4(zod@3.25.73)': dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - js-yaml: 4.1.0 - - '@apidevtools/openapi-schemas@2.1.0': {} - - '@apidevtools/swagger-methods@3.0.2': {} - - '@apidevtools/swagger-parser@10.1.1(openapi-types@12.1.3)': - dependencies: - '@apidevtools/json-schema-ref-parser': 11.7.2 - '@apidevtools/openapi-schemas': 2.1.0 - '@apidevtools/swagger-methods': 3.0.2 - '@jsdevtools/ono': 7.1.3 - ajv: 8.17.1 - ajv-draft-04: 1.0.0(ajv@8.17.1) - call-me-maybe: 1.0.2 - openapi-types: 12.1.3 - - '@asamuzakjp/css-color@2.8.3': - dependencies: - '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 - lru-cache: 10.4.3 + openapi3-ts: 4.5.0 + zod: 3.25.73 '@babel/code-frame@7.26.2': dependencies: @@ -3998,6 +3574,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.27.6': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -4057,30 +3635,30 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.7.1 + semver: 7.7.2 - '@changesets/assemble-release-plan@6.0.8': + '@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.1 + semver: 7.7.2 '@changesets/changelog-git@0.2.1': dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.4': + '@changesets/cli@2.29.5': dependencies: '@changesets/apply-release-plan': 7.0.12 - '@changesets/assemble-release-plan': 6.0.8 + '@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.12 + '@changesets/get-release-plan': 4.0.13 '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 @@ -4096,10 +3674,10 @@ snapshots: fs-extra: 7.0.1 mri: 1.2.0 p-limit: 2.3.0 - package-manager-detector: 0.2.9 + package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.7.1 + semver: 7.7.2 spawndamnit: 3.0.1 term-size: 2.2.1 @@ -4122,11 +3700,11 @@ snapshots: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.7.1 + semver: 7.7.2 - '@changesets/get-release-plan@4.0.12': + '@changesets/get-release-plan@4.0.13': dependencies: - '@changesets/assemble-release-plan': 6.0.8 + '@changesets/assemble-release-plan': 6.0.9 '@changesets/config': 3.1.1 '@changesets/pre': 2.0.2 '@changesets/read': 0.6.5 @@ -4207,8 +3785,8 @@ snapshots: '@es-joy/jsdoccomment@0.50.2': dependencies: - '@types/estree': 1.0.7 - '@typescript-eslint/types': 8.32.1 + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.35.1 comment-parser: 1.4.1 esquery: 1.6.0 jsdoc-type-pratt-parser: 4.1.0 @@ -4288,37 +3866,36 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.28.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))': dependencies: - eslint: 9.28.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0(jiti@2.4.2))': - dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0 + debug: 4.4.1(supports-color@10.0.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.1': {} + '@eslint/config-helpers@0.3.0': {} '@eslint/core@0.14.0': dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0 - espree: 10.3.0 + debug: 4.4.1(supports-color@10.0.0) + espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 @@ -4328,13 +3905,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.28.0': {} + '@eslint/js@9.30.1': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.3.3': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.15.1 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -4348,24 +3925,7 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.2': {} - - '@inquirer/checkbox@4.1.8(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@22.14.0) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/confirm@5.1.12(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/type': 3.0.7(@types/node@22.14.0) - optionalDependencies: - '@types/node': 22.14.0 + '@humanwhocodes/retry@0.4.3': {} '@inquirer/confirm@5.1.9(@types/node@22.14.0)': dependencies: @@ -4387,146 +3947,41 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 - '@inquirer/core@10.1.13(@types/node@22.14.0)': - dependencies: - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@22.14.0) - ansi-escapes: 4.3.2 - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/editor@4.2.13(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/type': 3.0.7(@types/node@22.14.0) - external-editor: 3.1.0 - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/expand@4.0.15(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/type': 3.0.7(@types/node@22.14.0) - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.14.0 - '@inquirer/figures@1.0.11': {} - '@inquirer/figures@1.0.12': {} - - '@inquirer/input@4.1.12(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/type': 3.0.7(@types/node@22.14.0) - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/number@3.0.15(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/type': 3.0.7(@types/node@22.14.0) - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/password@4.0.15(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/type': 3.0.7(@types/node@22.14.0) - ansi-escapes: 4.3.2 - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/prompts@7.5.3(@types/node@22.14.0)': - dependencies: - '@inquirer/checkbox': 4.1.8(@types/node@22.14.0) - '@inquirer/confirm': 5.1.12(@types/node@22.14.0) - '@inquirer/editor': 4.2.13(@types/node@22.14.0) - '@inquirer/expand': 4.0.15(@types/node@22.14.0) - '@inquirer/input': 4.1.12(@types/node@22.14.0) - '@inquirer/number': 3.0.15(@types/node@22.14.0) - '@inquirer/password': 4.0.15(@types/node@22.14.0) - '@inquirer/rawlist': 4.1.3(@types/node@22.14.0) - '@inquirer/search': 3.0.15(@types/node@22.14.0) - '@inquirer/select': 4.2.3(@types/node@22.14.0) - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/rawlist@4.1.3(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/type': 3.0.7(@types/node@22.14.0) - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/search@3.0.15(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@22.14.0) - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.14.0 - - '@inquirer/select@4.2.3(@types/node@22.14.0)': - dependencies: - '@inquirer/core': 10.1.13(@types/node@22.14.0) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@22.14.0) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.14.0 - '@inquirer/type@3.0.6(@types/node@22.14.0)': optionalDependencies: '@types/node': 22.14.0 - '@inquirer/type@3.0.7(@types/node@22.14.0)': - optionalDependencies: - '@types/node': 22.14.0 - - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.4 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.4': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@jsdevtools/ono@7.1.3': {} + '@jridgewell/sourcemap-codec': 1.5.4 - '@jsr/gabriel__ts-pattern@5.7.1': {} + '@jsr/std__yaml@1.0.8': {} '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.27.6 '@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.26.9 + '@babel/runtime': 7.27.6 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -4658,7 +4113,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.41.0': optional: true - '@sindresorhus/merge-streams@2.3.0': {} + '@standard-schema/spec@1.0.0': {} '@testing-library/dom@10.4.0': dependencies: @@ -4748,10 +4203,10 @@ snapshots: '@types/eslint-config-prettier@6.11.3': {} - '@types/estree@1.0.6': {} - '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.0.6': dependencies: '@types/node': 22.14.0 @@ -4777,10 +4232,6 @@ snapshots: '@types/mime@1.3.5': {} - '@types/multer@1.4.12': - dependencies: - '@types/express': 5.0.3 - '@types/node@12.20.55': {} '@types/node@22.14.0': @@ -4837,15 +4288,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/type-utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + eslint: 9.30.1(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -4854,57 +4305,55 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 debug: 4.4.1(supports-color@10.0.0) - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.34.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 debug: 4.4.1(supports-color@10.0.0) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.34.0': + '@typescript-eslint/scope-manager@8.35.1': dependencies: - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 - '@typescript-eslint/tsconfig-utils@8.34.0(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1(supports-color@10.0.0) - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.32.1': {} - - '@typescript-eslint/types@8.34.0': {} + '@typescript-eslint/types@8.35.1': {} - '@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.34.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/visitor-keys': 8.34.0 + '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 debug: 4.4.1(supports-color@10.0.0) fast-glob: 3.3.3 is-glob: 4.0.3 @@ -4915,73 +4364,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.34.0 - '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.34.0': + '@typescript-eslint/visitor-keys@8.35.1': dependencies: - '@typescript-eslint/types': 8.34.0 + '@typescript-eslint/types': 8.35.1 eslint-visitor-keys: 4.2.1 - '@typespec/asset-emitter@0.70.1(@typespec/compiler@1.0.0(@types/node@22.14.0))': - dependencies: - '@typespec/compiler': 1.0.0(@types/node@22.14.0) - - '@typespec/compiler@1.0.0(@types/node@22.14.0)': - dependencies: - '@babel/code-frame': 7.26.2 - '@inquirer/prompts': 7.5.3(@types/node@22.14.0) - ajv: 8.17.1 - change-case: 5.4.4 - env-paths: 3.0.0 - globby: 14.1.0 - is-unicode-supported: 2.1.0 - mustache: 4.2.0 - picocolors: 1.1.1 - prettier: 3.5.3 - semver: 7.7.2 - tar: 7.4.3 - temporal-polyfill: 0.3.0 - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - yaml: 2.7.1 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - '@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0))': - dependencies: - '@typespec/compiler': 1.0.0(@types/node@22.14.0) - - '@typespec/openapi3@1.0.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)))(@typespec/openapi@1.0.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0))))': - dependencies: - '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) - '@typespec/asset-emitter': 0.70.1(@typespec/compiler@1.0.0(@types/node@22.14.0)) - '@typespec/compiler': 1.0.0(@types/node@22.14.0) - '@typespec/http': 1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)) - '@typespec/openapi': 1.0.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0))) - openapi-types: 12.1.3 - yaml: 2.7.1 - - '@typespec/openapi@1.0.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)))': - dependencies: - '@typespec/compiler': 1.0.0(@types/node@22.14.0) - '@typespec/http': 1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)) - - '@typespec/rest@0.70.0(@typespec/compiler@1.0.0(@types/node@22.14.0))(@typespec/http@1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)))': - dependencies: - '@typespec/compiler': 1.0.0(@types/node@22.14.0) - '@typespec/http': 1.0.1(@typespec/compiler@1.0.0(@types/node@22.14.0)) - - '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1))': + '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -4989,7 +4388,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -5000,20 +4399,20 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.2(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1))': + '@vitest/mocker@3.1.2(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.2 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.10.2(@types/node@22.14.0)(typescript@5.8.3) - vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) '@vitest/pretty-format@3.1.2': dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@3.1.4': + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -5039,14 +4438,14 @@ snapshots: flatted: 3.3.3 pathe: 2.0.3 sirv: 3.0.1 - tinyglobby: 0.2.13 + tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1) + vitest: 3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0) '@vitest/utils@3.1.2': dependencies: '@vitest/pretty-format': 3.1.2 - loupe: 3.1.3 + loupe: 3.1.4 tinyrainbow: 2.0.0 accepts@2.0.0: @@ -5054,22 +4453,14 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.14.0): + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.0 + acorn: 8.15.0 - acorn@8.14.0: {} + acorn@8.15.0: {} agent-base@7.1.3: {} - ajv-draft-04@1.0.0(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - - ajv-formats@3.0.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5077,13 +4468,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -5103,8 +4487,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - append-field@1.0.0: {} - are-docs-informative@0.0.2: {} arg@4.1.3: {} @@ -5123,17 +4505,19 @@ snapshots: array-buffer-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-includes@3.1.8: + array-includes@3.1.9: dependencies: call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-string: 1.1.1 + math-intrinsics: 1.1.0 array-union@2.1.0: {} @@ -5141,7 +4525,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -5150,21 +4534,21 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-shim-unscopables: 1.1.0 array.prototype.tosorted@1.1.4: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-shim-unscopables: 1.1.0 @@ -5173,9 +4557,9 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 asap@2.0.6: {} @@ -5229,12 +4613,12 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -5249,17 +4633,11 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.0) - buffer-from@1.1.2: {} - buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - bytes@3.1.2: {} cac@6.7.14: {} @@ -5273,21 +4651,14 @@ snapshots: dependencies: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 set-function-length: 1.2.2 - call-bound@1.0.3: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.2.7 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - call-me-maybe@1.0.2: {} - callsites@3.1.0: {} caniuse-lite@1.0.30001721: {} @@ -5297,8 +4668,8 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.3 - pathval: 2.0.0 + loupe: 3.1.4 + pathval: 2.0.1 chalk@3.0.0: dependencies: @@ -5330,8 +4701,6 @@ snapshots: chownr@1.1.4: {} - chownr@3.0.0: {} - ci-info@3.9.0: {} cli-width@4.1.0: {} @@ -5360,13 +4729,6 @@ snapshots: concat-map@0.0.1: {} - concat-stream@1.6.2: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 2.3.8 - typedarray: 0.0.6 - content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -5375,13 +4737,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie-parser@1.4.7: - dependencies: - cookie: 0.7.2 - cookie-signature: 1.0.6 - - cookie-signature@1.0.6: {} - cookie-signature@1.0.7: {} cookie-signature@1.2.2: {} @@ -5390,8 +4745,6 @@ snapshots: cookiejar@2.1.4: {} - core-util-is@1.0.3: {} - cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -5436,19 +4789,19 @@ snapshots: data-view-buffer@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-offset@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 @@ -5456,10 +4809,6 @@ snapshots: dependencies: ms: 2.0.0 - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.1(supports-color@10.0.0): dependencies: ms: 2.1.3 @@ -5544,15 +4893,13 @@ snapshots: entities@4.5.0: {} - env-paths@3.0.0: {} - - es-abstract@1.23.9: + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 data-view-buffer: 1.0.2 data-view-byte-length: 1.0.2 data-view-byte-offset: 1.0.1 @@ -5562,7 +4909,7 @@ snapshots: es-set-tostringtag: 2.1.0 es-to-primitive: 1.3.0 function.prototype.name: 1.1.8 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 get-symbol-description: 1.1.0 globalthis: 1.0.4 @@ -5575,7 +4922,9 @@ snapshots: is-array-buffer: 3.0.5 is-callable: 1.2.7 is-data-view: 1.0.2 + is-negative-zero: 2.0.3 is-regex: 1.2.1 + is-set: 2.0.3 is-shared-array-buffer: 1.0.4 is-string: 1.1.1 is-typed-array: 1.1.15 @@ -5590,6 +4939,7 @@ snapshots: safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 string.prototype.trim: 1.2.10 string.prototype.trimend: 1.0.9 string.prototype.trimstart: 1.0.8 @@ -5598,7 +4948,7 @@ snapshots: typed-array-byte-offset: 1.0.4 typed-array-length: 1.0.7 unbox-primitive: 1.1.0 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 es-define-property@1.0.1: {} @@ -5607,13 +4957,13 @@ snapshots: es-iterator-helpers@1.2.1: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-set-tostringtag: 2.1.0 function-bind: 1.1.2 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 globalthis: 1.0.4 gopd: 1.2.0 has-property-descriptors: 1.0.2 @@ -5680,19 +5030,19 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.2(eslint@9.28.0(jiti@2.4.2)): + eslint-config-prettier@10.1.2(eslint@9.30.1(jiti@2.4.2)): dependencies: - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) - eslint-plugin-jsdoc@50.7.1(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-jsdoc@50.8.0(eslint@9.30.1(jiti@2.4.2)): dependencies: '@es-joy/jsdoccomment': 0.50.2 are-docs-informative: 0.0.2 comment-parser: 1.4.1 debug: 4.4.1(supports-color@10.0.0) escape-string-regexp: 4.0.0 - eslint: 9.28.0(jiti@2.4.2) - espree: 10.3.0 + eslint: 9.30.1(jiti@2.4.2) + espree: 10.4.0 esquery: 1.6.0 parse-imports-exports: 0.2.4 semver: 7.7.2 @@ -5700,15 +5050,15 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.28.0(jiti@2.4.2)): + eslint-plugin-react@7.37.5(eslint@9.30.1(jiti@2.4.2)): dependencies: - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.28.0(jiti@2.4.2) + eslint: 9.30.1(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -5722,40 +5072,38 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-scope@8.3.0: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} - eslint-visitor-keys@4.2.1: {} - eslint@9.28.0(jiti@2.4.2): + eslint@9.30.1(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.28.0 - '@eslint/plugin-kit': 0.3.1 + '@eslint/js': 9.30.1 + '@eslint/plugin-kit': 0.3.3 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.6 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.1(supports-color@10.0.0) escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -5775,11 +5123,11 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.3.0: + espree@10.4.0: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) - eslint-visitor-keys: 4.2.0 + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} @@ -5795,7 +5143,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 esutils@2.0.3: {} @@ -5805,23 +5153,6 @@ snapshots: expect-type@1.2.1: {} - express-openapi-validator@5.4.9(express@5.1.0): - dependencies: - '@apidevtools/json-schema-ref-parser': 11.9.3 - '@types/multer': 1.4.12 - ajv: 8.17.1 - ajv-draft-04: 1.0.0(ajv@8.17.1) - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - express: 5.1.0 - json-schema-traverse: 1.0.0 - lodash.clonedeep: 4.5.0 - lodash.get: 4.4.2 - media-typer: 1.1.0 - multer: 1.4.5-lts.2 - ono: 7.1.3 - path-to-regexp: 8.2.0 - express-session@1.18.1: dependencies: cookie: 0.7.2 @@ -5891,13 +5222,11 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.0.6: {} - fastq@1.19.1: dependencies: reusify: 1.1.0 - fdir@6.4.4(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -5984,7 +5313,7 @@ snapshots: function.prototype.name@1.1.8: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 hasown: 2.0.2 @@ -5996,19 +5325,6 @@ snapshots: get-caller-file@2.0.5: {} - get-intrinsic@1.2.7: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6029,9 +5345,9 @@ snapshots: get-symbol-description@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-tsconfig@4.10.0: dependencies: @@ -6051,7 +5367,7 @@ snapshots: globals@14.0.0: {} - globals@16.2.0: {} + globals@16.3.0: {} globalthis@1.0.4: dependencies: @@ -6067,15 +5383,6 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globby@14.1.0: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.3 - ignore: 7.0.5 - path-type: 6.0.0 - slash: 5.1.0 - unicorn-magic: 0.3.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -6176,13 +5483,13 @@ snapshots: is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-async-function@2.1.1: dependencies: async-function: 1.0.0 - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -6197,7 +5504,7 @@ snapshots: is-boolean-object@1.2.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-callable@1.2.7: {} @@ -6208,26 +5515,26 @@ snapshots: is-data-view@1.0.2: dependencies: - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-typed-array: 1.1.15 is-date-object@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-fullwidth-code-point@3.0.0: {} is-generator-function@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -6238,11 +5545,13 @@ snapshots: is-map@2.0.3: {} + is-negative-zero@2.0.3: {} + is-node-process@1.2.0: {} is-number-object@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-number@7.0.0: {} @@ -6253,7 +5562,7 @@ snapshots: is-regex@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -6262,11 +5571,11 @@ snapshots: is-shared-array-buffer@1.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-string@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-subdir@1.2.0: @@ -6275,31 +5584,27 @@ snapshots: is-symbol@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.18 - - is-unicode-supported@2.1.0: {} + which-typed-array: 1.1.19 is-weakmap@2.0.2: {} is-weakref@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-weakset@2.0.4: dependencies: - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-windows@1.0.2: {} - isarray@1.0.0: {} - isarray@2.0.5: {} isexe@2.0.0: {} @@ -6308,7 +5613,7 @@ snapshots: dependencies: define-data-property: 1.1.4 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 has-symbols: 1.1.0 set-function-name: 2.0.2 @@ -6375,7 +5680,7 @@ snapshots: jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.flat: 1.3.3 object.assign: 4.1.7 object.values: 1.2.1 @@ -6399,10 +5704,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.clonedeep@4.5.0: {} - - lodash.get@4.4.2: {} - lodash.merge@4.6.2: {} lodash.startcase@4.4.0: {} @@ -6415,7 +5716,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.3: {} + loupe@3.1.4: {} lru-cache@10.4.3: {} @@ -6432,12 +5733,10 @@ snapshots: magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.4 math-intrinsics@1.1.0: {} - media-typer@0.3.0: {} - media-typer@1.1.0: {} memorystore@1.6.7: @@ -6478,32 +5777,20 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@5.1.6: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} - minipass@7.1.2: {} - - minizlib@3.0.2: - dependencies: - minipass: 7.1.2 - mkdirp-classic@0.5.3: {} - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - - mkdirp@3.0.1: {} - mri@1.2.0: {} mrmime@2.0.1: {} @@ -6537,18 +5824,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - multer@1.4.5-lts.2: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 1.6.2 - mkdirp: 0.5.6 - object-assign: 4.1.1 - type-is: 1.6.18 - xtend: 4.0.2 - - mustache@4.2.0: {} - mute-stream@2.0.0: {} nanoid@3.3.11: {} @@ -6578,7 +5853,7 @@ snapshots: object.assign@4.1.7: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 has-symbols: 1.1.0 @@ -6595,13 +5870,13 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 object.values@1.2.1: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -6625,16 +5900,10 @@ snapshots: ignore: 5.3.2 tree-kill: 1.2.2 - ono@7.1.3: - dependencies: - '@jsdevtools/ono': 7.1.3 - openapi-fetch@0.13.8: dependencies: openapi-typescript-helpers: 0.0.15 - openapi-types@12.1.3: {} - openapi-typescript-helpers@0.0.15: {} openapi-typescript@7.8.0(typescript@5.8.3): @@ -6647,6 +5916,10 @@ snapshots: typescript: 5.8.3 yargs-parser: 21.1.1 + openapi3-ts@4.5.0: + dependencies: + yaml: 2.8.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6664,7 +5937,7 @@ snapshots: own-keys@1.0.1: dependencies: - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 @@ -6692,9 +5965,11 @@ snapshots: p-try@2.2.0: {} - package-manager-detector@0.2.9: {} + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.10 - package-manager-detector@1.2.0: {} + package-manager-detector@1.3.0: {} parent-module@1.0.1: dependencies: @@ -6706,7 +5981,7 @@ snapshots: parse-json@8.3.0: dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 index-to-position: 1.1.0 type-fest: 4.41.0 @@ -6730,11 +6005,9 @@ snapshots: path-type@4.0.0: {} - path-type@6.0.0: {} - pathe@2.0.3: {} - pathval@2.0.0: {} + pathval@2.0.1: {} picocolors@1.1.1: {} @@ -6786,8 +6059,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - process-nextick-args@2.0.1: {} - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -6808,7 +6079,7 @@ snapshots: publint@0.3.12: dependencies: '@publint/pack': 0.1.2 - package-manager-detector: 1.2.0 + package-manager-detector: 1.3.0 picocolors: 1.1.1 sade: 1.8.1 @@ -6823,6 +6094,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.10: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -6868,16 +6141,6 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -6897,10 +6160,10 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -6988,13 +6251,11 @@ snapshots: safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 - safe-buffer@5.1.2: {} - safe-buffer@5.2.1: {} safe-push-apply@1.0.0: @@ -7004,7 +6265,7 @@ snapshots: safe-regex-test@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 @@ -7022,8 +6283,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.1: {} - semver@7.7.2: {} send@1.2.0: @@ -7056,7 +6315,7 @@ snapshots: define-data-property: 1.1.4 es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 gopd: 1.2.0 has-property-descriptors: 1.0.2 @@ -7129,8 +6388,6 @@ snapshots: slash@3.0.0: {} - slash@5.1.0: {} - source-map-js@1.2.1: {} spawndamnit@3.0.1: @@ -7155,9 +6412,12 @@ snapshots: std-env@3.9.0: {} - stream-transform@3.3.3: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 - streamsearch@1.1.0: {} + stream-transform@3.3.3: {} strict-event-emitter@0.5.1: {} @@ -7172,12 +6432,12 @@ snapshots: string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 gopd: 1.2.0 has-symbols: 1.1.0 internal-slot: 1.1.0 @@ -7188,22 +6448,22 @@ snapshots: string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -7213,10 +6473,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -7281,33 +6537,18 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tar@7.4.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 - yallist: 5.0.0 - - temporal-polyfill@0.3.0: - dependencies: - temporal-spec: 0.3.0 - - temporal-spec@0.3.0: {} - term-size@2.2.1: {} tinybench@2.9.0: {} tinyexec@0.3.2: {} - tinyglobby@0.2.13: + tinyglobby@0.2.14: dependencies: - fdir: 6.4.4(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 - tinypool@1.0.2: {} + tinypool@1.1.1: {} tinyrainbow@2.0.0: {} @@ -7371,11 +6612,6 @@ snapshots: type-fest@4.41.0: {} - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -7384,7 +6620,7 @@ snapshots: typed-array-buffer@1.0.3: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 @@ -7415,14 +6651,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typedarray@0.0.6: {} - - typescript-eslint@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.34.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.28.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7435,15 +6669,13 @@ snapshots: unbox-primitive@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-bigints: 1.1.0 has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 undici-types@6.21.0: {} - unicorn-magic@0.3.0: {} - universalify@0.1.2: {} universalify@0.2.0: {} @@ -7471,13 +6703,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.1.2(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1): + vite-node@3.1.2(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@10.0.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -7492,26 +6724,26 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1): + vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0): dependencies: esbuild: 0.25.4 - fdir: 6.4.4(picomatch@4.0.2) + fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 postcss: 8.5.3 rollup: 4.41.0 - tinyglobby: 0.2.13 + tinyglobby: 0.2.14 optionalDependencies: '@types/node': 22.14.0 fsevents: 2.3.3 jiti: 2.4.2 tsx: 4.19.3 - yaml: 2.7.1 + yaml: 2.8.0 - vitest@3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.7.1): + vitest@3.1.2(@types/node@22.14.0)(@vitest/ui@3.1.2)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(tsx@4.19.3)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.2 - '@vitest/mocker': 3.1.2(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1)) - '@vitest/pretty-format': 3.1.4 + '@vitest/mocker': 3.1.2(msw@2.10.2(@types/node@22.14.0)(typescript@5.8.3))(vite@6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.1.2 '@vitest/snapshot': 3.1.2 '@vitest/spy': 3.1.2 @@ -7524,11 +6756,11 @@ snapshots: std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.13 - tinypool: 1.0.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) - vite-node: 3.1.2(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1) + vite: 6.3.5(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) + vite-node: 3.1.2(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.14.0 @@ -7548,21 +6780,6 @@ snapshots: - tsx - yaml - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.12: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -7590,7 +6807,7 @@ snapshots: which-builtin-type@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 function.prototype.name: 1.1.8 has-tostringtag: 1.0.2 is-async-function: 2.1.1 @@ -7602,7 +6819,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 which-collection@1.0.2: dependencies: @@ -7611,12 +6828,13 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.18: + which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 for-each: 0.3.5 + get-proto: 1.0.1 gopd: 1.2.0 has-tostringtag: 1.0.2 @@ -7651,19 +6869,15 @@ snapshots: xmlchars@2.2.0: {} - xtend@4.0.2: {} - y18n@5.0.8: {} yallist@2.1.2: {} yallist@3.1.1: {} - yallist@5.0.0: {} - yaml-ast-parser@0.0.43: {} - yaml@2.7.1: {} + yaml@2.8.0: {} yargs-parser@21.1.1: {} @@ -7682,3 +6896,5 @@ snapshots: yoctocolors-cjs@2.1.2: {} zod@3.25.57: {} + + zod@3.25.73: {}