diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b9f1898..6de4db5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,20 +8,19 @@ on: permissions: contents: read checks: write + id-token: write jobs: ci: uses: makerxstudio/shared-config/.github/workflows/node-ci.yml@main with: - node-version: 20.x audit-script: npm run audit output-test-results: true test-script: npm run test:ci publish: needs: ci - uses: makerxstudio/shared-config/.github/workflows/node-publish-public.yml@main + uses: makerxstudio/shared-config/.github/workflows/node-trusted-publish.yml@main with: - node-version: 20.x - secrets: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + access: public + tags: beta diff --git a/.tstoolkitrc.ts b/.tstoolkitrc.ts index 864bec7..df46d0a 100644 --- a/.tstoolkitrc.ts +++ b/.tstoolkitrc.ts @@ -1,4 +1,4 @@ -import type { TsToolkitConfig } from "@makerx/ts-toolkit"; +import type { TsToolkitConfig } from '@makerx/ts-toolkit' const config: TsToolkitConfig = { packageConfig: { @@ -8,9 +8,9 @@ const config: TsToolkitConfig = { main: 'index.ts', exports: { '.': 'index.ts', - './testing': 'testing.ts', - './subscriptions': 'subscriptions/index.ts' - } - } + './shield': 'shield.ts', + './subscriptions': 'subscriptions/index.ts', + }, + }, } export default config diff --git a/README.md b/README.md index 2ca168f..239d5af 100644 --- a/README.md +++ b/README.md @@ -10,41 +10,45 @@ Note: See explanation on \*Express peer dependency below. `createContextFactory` returns a function that creates your GraphQL context using a standard (extensible) representation, including: -- `logger`: a logger instance to use downstream of resolvers, usually logging some request metadata to assist correlating log entries (for example the X-Correlation-Id header value) -- `requestInfo`: useful request info, for example to define per-request behaviour (multi-tenant apps), pass through correlation headers to downstream services etc -- `user`: an object representing the user or system identity (see definition below, defaults to creating a `User` based on JWT claims) -- anything else you wish to add to the context +- `logger`: a logger instance to use downstream of resolvers, built by your `requestLogger` factory, which receives both the resolved request metadata and the resolved `user` so you can enrich log output with user-derived fields (see [Request logger](#request-logger)) +- `requestInfo`: useful request info — `source` (`http` or `subscription`), `protocol` (`http`/`https`/`ws`/`wss`), `host`, `baseUrl`, `url`, correlation/client headers, etc. Use it for per-request behaviour (multi-tenant apps), passing correlation headers downstream, etc. See [Request info](#request-info) +- `user`: an object representing the user or system identity (see [User](#user); defaults to a `User` built from JWT claims when `createUser` is omitted) +- anything else you wish to add to the context via `augmentContext` ### Step 1 - Define your context + creation context.ts ```ts -// define the base context type, setting the logger type -type BaseContext = GraphQLContextBase // define the extra stuff added to our app's context type ExtraContext = { services: Services loaders: Loaders } -// our app's context type, returned from the createContext function -export type GraphQLContext = BaseContext & ExtraContext // configure the createContext function -export const createContext = createContextFactory({ - // set the keys of the user claims (JWT payload) we want added to the request metadata passed to the requestLogger factory +// TUser is inferred from `createUser`, TAugment is inferred from `augmentContext`'s return type +export const createContext = createContextFactory({ + // keys of the user claims (JWT payload) to include in the request metadata passed to the requestLogger factory claimsToLog: ['oid', 'aud', 'tid', 'azp', 'iss', 'scp', 'roles'], - // set the keys of the request info we want added to the request metadata passed to the requestLogger factory + // keys of the request info to include in the request metadata passed to the requestLogger factory requestInfoToLog: ['origin', 'requestId', 'correlationId'], - // use a winston child logger to add metadata to log output - requestLogger: (requestMetadata) => logger.child(requestMetadata), - // build the rest of the app context + // build the per-request logger; receives the request metadata and the resolved user + // e.g. enrich log output with user-derived fields like multi-tenant `instance` + requestLogger: (requestMetadata, user) => logger.child({ ...requestMetadata, instance: user?.instance }), + // resolve the user for each request — optional; omit to use the default User-from-JWT behaviour + // (required when you supply a narrower TUser generic) + createUser: async ({ claims }) => new AppUser(claims), + // build the rest of the app context — annotate the return type to lock in inference augmentContext: (context): ExtraContext => { const services = createServices(context) const loaders = createLoaders(services) return { services, loaders } }, }) + +// derive the full context type from the factory's return type +export type GraphQLContext = Awaited> ``` ### Step 2 - Map the context creation to implementation @@ -83,6 +87,50 @@ const graphqlServer = createServer({ }) ``` +## Request info + +`context.requestInfo` is built for every request — both HTTP and websocket subscription connects — so downstream code can distinguish sources, rebuild URLs, pass through correlation headers, etc. + +| Field | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `requestId` | `x-request-id` header if present, otherwise a freshly generated UUID. | +| `source` | `'http'` for regular requests, `'subscription'` for websocket connects. | +| `protocol` | `'http'` / `'https'` for HTTP, `'ws'` / `'wss'` for subscriptions (resolved via `x-forwarded-proto` or TLS socket encryption). | +| `host` | Hostname only (no port). Prefers `x-forwarded-host`, falls back to the `Host` header, then `req.hostname` (Express only). | +| `port` | Port parsed from `x-forwarded-host` / `Host` header when present; `undefined` otherwise. | +| `baseUrl` | Fully-qualified origin (`scheme://host[:port]`) with default ports stripped. For subscriptions the scheme is normalised to `http(s)` so the value composes with relative URLs. | +| `url` | `req.originalUrl` for HTTP, `req.url` for subscription connects. | +| `origin` | `Origin` header. | +| `referer` | `Referer` header. | +| `correlationId` | `x-correlation-id` header. | +| `arrLogId` | `x-arr-log-id` header (Azure Front Door / ARR). | +| `clientIp` | First value from `x-forwarded-for`, falling back to `socket.remoteAddress`. | +| `userAgent` | `User-Agent` header. | + +You can add more via `augmentRequestInfo(input)`. Lambda deployments also get `functionName` and `awsRequestId` when a `LambdaContext` is supplied. + +Helpers are exported for custom wiring: `buildBaseRequestInfo(req)` (Express), `buildConnectRequestInfo(req)` (websocket `IncomingMessage`), and `requestBaseUrl` / `connectRequestBaseUrl`. + +## Request logger + +The `requestLogger` config accepts either a pre-built `Logger` or a factory `(requestMetadata, user) => Logger`. + +The factory form runs per request and receives: + +- `requestMetadata` — an object containing `request` (the subset of `requestInfo` selected by `requestInfoToLog`) and `user` (the subset of claims selected by `claimsToLog`) +- `user` — the resolved `user` value returned by `createUser` (typed as your `TUser`) + +This lets you enrich log output with fields derived from the resolved user, for example a multi-tenant instance id or an internal user id from your database, that aren't present on the raw JWT claims: + +```ts +requestLogger: (requestMetadata, user) => + logger.child({ + ...requestMetadata, + instance: user?.instance, + userId: user?.id, + }), +``` + ## User By default, if `claims` (decoded token `JwtPayload`) are available, the `GraphQLContext.user` property will be set by constructing a `User` instance. @@ -140,29 +188,36 @@ This library includes a `subscriptions` module to provide simple setup using the Example showing both normal context + subscription context creation: ```ts - const augmentContext = (context: GraphQLContext) => { + type ExtraContext = { services: Services; dataSource: DataSource; dataLoaders: DataLoaders } + + // the `context` arg is typed `GraphQLContext` here — + // TUser flows through from `createUser`, so just annotate the return type and let inference do the rest + const augmentContext = (context: GraphQLContext): ExtraContext => { const services = createServices(context) const dataLoaders = createDataLoaders() return { services, dataSource, dataLoaders } } - // create a context using request based input - const createContext = createContextFactory({ + // create a context using request based input — TUser / TAugment inferred from the config + const createContext = createContextFactory({ claimsToLog, requestInfoToLog, - requestLogger: (requestMetadata) => logger.child(requestMetadata), + requestLogger: (requestMetadata, user) => logger.child({ ...requestMetadata, instance: user?.instance }), createUser: ({ claims, req }) => findUpdateOrCreateUser(claims, req.headers.authorization?.substring(7)), augmentContext, }) // create a context using graphql-ws Server#context callback input - const createSubscriptionContext = createSubscriptionContextFactory({ - claimsToLog + const createSubscriptionContext = createSubscriptionContextFactory({ + claimsToLog, requestInfoToLog, - requestLogger: (requestMetadata) => logger.child(requestMetadata), + requestLogger: (requestMetadata, user) => logger.child({ ...requestMetadata, instance: user?.instance }), createUser: ({ claims, connectionParams }) => findUpdateOrCreateUser(claims, extractTokenFromConnectionParams(connectionParams)), augmentContext, }) + + // share one context type between query and subscription paths + export type GraphQLContext = Awaited> ``` 1. Create a subscriptions server, using the ws-server cleanup function in your server lifecycle. @@ -204,85 +259,6 @@ This library includes a `subscriptions` module to provide simple setup using the 1. For authorisation, clients can include a connection parameter named `authorization` or `Authorization` using the HTTP header format `Bearer `. Note: [Apollo Sandbox](https://studio.apollographql.com/sandbox/explorer) will include an `Authorization` connection parameter when you specify an HTTP `Authorization` header via the UI. -## Testing - -The testing submodule exports utility functions for easily constructing ApolloClient instances for integration testing on NodeJS. The `errorPolicy` is set to `all` so that returned errors can be checked. - -### Setup - -If you use this module, you need to install `@apollo/client`: - -``` -npm install --save-dev @apollo/client -``` - -### Usage - -- `createTestClient` accepts a url and optional accessToken. -- `createTestClientWithClientCredentials` accepts a url and client credentials config and will fetch and attach an access token to each request. - -testing.ts - -```ts -export const testClient = createTestClientWithClientCredentials(process.env.INTEGRATION_TEST_URL, clientCredentialsConfig) - -export const unauthenticatedClient = createTestClient(process.env.INTEGRATION_TEST_URL) -``` - -tweets.spec.ts - -```ts -describe('tweets query', () => { - const tweetsQuery = gql` - query Tweets($input: TweetsWhere) { - tweets(input: $input) { - data { - text - createdAt - } - } - } - ` - - it('returns tweets with sensible default limit', async () => { - const { - data: { tweets }, - errors, - } = await testClient.query({ - query: tweetsQuery, - }) - - expect(errors).toBeUndefined() - expect(tweets).toBeDefined() - expect(tweets?.data?.length).toBe(10) - }) - - it('guards against high limit', async () => { - const tooHighLimit = 101 - await expect(async () => { - await testClient.query({ - query: tweetsQuery, - variables: { - input: { - maxResults: tooHighLimit, - }, - }, - }) - }).rejects.toThrowErrorMatchingInlineSnapshot(`"Response not successful: Received status code 400"`) - }) - - it('requires authorisation', async () => { - const { data, errors } = await unauthenticatedClient.query({ - query: tweetsQuery, - }) - - expect(data.tweets).toBeNull() - expect(errors?.length).toBe(1) - expect(errors?.[0].message).toMatchInlineSnapshot(`"User is not authorized to access Query.tweets"`) - }) -}) -``` - ## Utils - `isIntrospectionQuery`: indicates whether the query is an introspection query, based on the operation name or query content. diff --git a/package-lock.json b/package-lock.json index 17621aa..9426db8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,17 @@ { "name": "@makerx/graphql-core", - "version": "2.3.0", + "version": "3.0.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makerx/graphql-core", - "version": "2.3.0", + "version": "3.0.0-beta.0", "license": "MIT", "dependencies": { "@makerx/node-common": "^1.5.0" }, "devDependencies": { - "@apollo/client": "^3.8.10", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.36.0", "@makerx/eslint-config": "4.2.0", @@ -31,39 +30,35 @@ "@vitest/coverage-v8": "3.2.4", "better-npm-audit": "^3.11.0", "copyfiles": "^2.4.1", + "es-toolkit": "^1.39.10", "eslint": "^9.36.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.5.4", "express": "^5.1.0", "graphql-shield": "^7.6.5", - "graphql-ws": "^5.14.3", - "lodash": "^4.17.21", + "graphql-ws": "^5.16.2", "npm-run-all": "^4.1.5", "prettier": "3.6.2", "rimraf": "^6.0.1", - "rollup": "4.52.0", + "rollup": "^4.60.2", "tsx": "4.20.5", "typescript": "^5.9.2", "vitest": "3.2.4", - "ws": "^8.18.3" + "ws": "^8.20.0" }, "engines": { "node": ">=20.0" }, "peerDependencies": { - "@apollo/client": "*", + "es-toolkit": ">=1", "express": "*", "graphql": "*", "graphql-shield": "*", "graphql-ws": "*", - "lodash": "*", "ws": "*" }, "peerDependenciesMeta": { - "@apollo/client": { - "optional": true - }, "graphql-ws": { "optional": true }, @@ -86,49 +81,6 @@ "node": ">=6.0.0" } }, - "node_modules/@apollo/client": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.14.0.tgz", - "integrity": "sha512-0YQKKRIxiMlIou+SekQqdCo0ZTHxOcES+K8vKB53cIDpwABNR0P0yRzPgsbgcj3zRJniD93S/ontsnZsCLZrxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "@wry/caches": "^1.0.0", - "@wry/equality": "^0.5.6", - "@wry/trie": "^0.5.0", - "graphql-tag": "^2.12.6", - "hoist-non-react-statics": "^3.3.2", - "optimism": "^0.18.0", - "prop-types": "^15.7.2", - "rehackt": "^0.1.0", - "symbol-observable": "^4.0.0", - "ts-invariant": "^0.10.3", - "tslib": "^2.3.0", - "zen-observable-ts": "^1.2.5" - }, - "peerDependencies": { - "graphql": "^15.0.0 || ^16.0.0", - "graphql-ws": "^5.5.5 || ^6.0.3", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", - "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" - }, - "peerDependenciesMeta": { - "graphql-ws": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "subscriptions-transport-ws": { - "optional": true - } - } - }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -1035,15 +987,6 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "dev": true, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1096,29 +1039,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1421,9 +1341,9 @@ } }, "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -1528,9 +1448,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.0.tgz", - "integrity": "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", "cpu": [ "arm" ], @@ -1542,9 +1462,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.0.tgz", - "integrity": "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", "cpu": [ "arm64" ], @@ -1556,9 +1476,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.0.tgz", - "integrity": "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", "cpu": [ "arm64" ], @@ -1570,9 +1490,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.0.tgz", - "integrity": "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", "cpu": [ "x64" ], @@ -1584,9 +1504,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.0.tgz", - "integrity": "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", "cpu": [ "arm64" ], @@ -1598,9 +1518,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.0.tgz", - "integrity": "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", "cpu": [ "x64" ], @@ -1612,13 +1532,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.0.tgz", - "integrity": "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1626,13 +1549,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.0.tgz", - "integrity": "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1640,13 +1566,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.0.tgz", - "integrity": "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1654,13 +1583,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.0.tgz", - "integrity": "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1668,13 +1600,33 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.0.tgz", - "integrity": "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1682,13 +1634,33 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.0.tgz", - "integrity": "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1696,13 +1668,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.0.tgz", - "integrity": "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1710,13 +1685,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.0.tgz", - "integrity": "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1724,13 +1702,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.0.tgz", - "integrity": "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1738,13 +1719,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.0.tgz", - "integrity": "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1752,23 +1736,40 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.0.tgz", - "integrity": "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.0.tgz", - "integrity": "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", "cpu": [ "arm64" ], @@ -1780,9 +1781,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.0.tgz", - "integrity": "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", "cpu": [ "arm64" ], @@ -1794,9 +1795,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.0.tgz", - "integrity": "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", "cpu": [ "ia32" ], @@ -1808,9 +1809,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.0.tgz", - "integrity": "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", "cpu": [ "x64" ], @@ -1822,9 +1823,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.0.tgz", - "integrity": "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", "cpu": [ "x64" ], @@ -2194,9 +2195,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -2204,13 +2205,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2420,54 +2421,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@wry/caches": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", - "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wry/context": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", - "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wry/equality": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", - "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wry/trie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", - "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2506,9 +2459,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2690,30 +2643,34 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -3333,6 +3290,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -3809,6 +3777,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -3895,9 +3880,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4081,15 +4066,16 @@ } }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -4117,17 +4103,40 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4226,21 +4235,6 @@ "graphql-middleware": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^6.0.0" } }, - "node_modules/graphql-tag": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, "node_modules/graphql-ws": { "version": "5.16.2", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.2.tgz", @@ -4337,16 +4331,6 @@ "node": ">= 0.4" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -4387,9 +4371,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -4397,6 +4381,10 @@ }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -4825,10 +4813,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4944,16 +4933,18 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", @@ -4974,19 +4965,6 @@ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -5131,9 +5109,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5415,16 +5393,6 @@ "which": "bin/which" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -5496,30 +5464,6 @@ "wrappy": "1" } }, - "node_modules/optimism": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.0.tgz", - "integrity": "sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ==", - "dev": true, - "dependencies": { - "@wry/caches": "^1.0.0", - "@wry/context": "^0.7.0", - "@wry/trie": "^0.4.3", - "tslib": "^2.3.0" - } - }, - "node_modules/optimism/node_modules/@wry/trie": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.4.3.tgz", - "integrity": "sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5669,9 +5613,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", "funding": { @@ -5704,10 +5648,11 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5819,18 +5764,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/property-expr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", @@ -5860,9 +5793,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5939,13 +5872,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -6015,25 +5941,6 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/rehackt": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", - "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "*" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6120,9 +6027,9 @@ } }, "node_modules/rollup": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.0.tgz", - "integrity": "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6136,28 +6043,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.0", - "@rollup/rollup-android-arm64": "4.52.0", - "@rollup/rollup-darwin-arm64": "4.52.0", - "@rollup/rollup-darwin-x64": "4.52.0", - "@rollup/rollup-freebsd-arm64": "4.52.0", - "@rollup/rollup-freebsd-x64": "4.52.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", - "@rollup/rollup-linux-arm-musleabihf": "4.52.0", - "@rollup/rollup-linux-arm64-gnu": "4.52.0", - "@rollup/rollup-linux-arm64-musl": "4.52.0", - "@rollup/rollup-linux-loong64-gnu": "4.52.0", - "@rollup/rollup-linux-ppc64-gnu": "4.52.0", - "@rollup/rollup-linux-riscv64-gnu": "4.52.0", - "@rollup/rollup-linux-riscv64-musl": "4.52.0", - "@rollup/rollup-linux-s390x-gnu": "4.52.0", - "@rollup/rollup-linux-x64-gnu": "4.52.0", - "@rollup/rollup-linux-x64-musl": "4.52.0", - "@rollup/rollup-openharmony-arm64": "4.52.0", - "@rollup/rollup-win32-arm64-msvc": "4.52.0", - "@rollup/rollup-win32-ia32-msvc": "4.52.0", - "@rollup/rollup-win32-x64-gnu": "4.52.0", - "@rollup/rollup-win32-x64-msvc": "4.52.0", + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" } }, @@ -6761,16 +6671,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-observable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", - "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -6804,15 +6704,16 @@ } }, "node_modules/table/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -6841,9 +6742,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -6851,9 +6752,10 @@ } }, "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -6895,13 +6797,13 @@ "license": "ISC" }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7023,9 +6925,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7107,19 +7009,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-invariant": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", - "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -7346,13 +7235,13 @@ } }, "node_modules/vite": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", - "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -7443,6 +7332,490 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -7462,9 +7835,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7548,9 +7921,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7680,9 +8053,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "dev": true, "license": "MIT", "engines": { @@ -7776,23 +8149,6 @@ "engines": { "node": ">=10" } - }, - "node_modules/zen-observable": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/zen-observable-ts": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", - "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "zen-observable": "0.8.15" - } } } } diff --git a/package.json b/package.json index 16cbfd9..4ba4ea9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makerx/graphql-core", - "version": "2.3.0", + "version": "3.0.0-beta.0", "private": false, "description": "A set of core GraphQL utilities that MakerX uses to build GraphQL APIs", "author": "MakerX", @@ -38,18 +38,14 @@ "@makerx/node-common": "^1.5.0" }, "peerDependencies": { - "@apollo/client": "*", + "es-toolkit": ">=1", "express": "*", "graphql": "*", "graphql-shield": "*", "graphql-ws": "*", - "lodash": "*", "ws": "*" }, "peerDependenciesMeta": { - "@apollo/client": { - "optional": true - }, "graphql-ws": { "optional": true }, @@ -60,38 +56,37 @@ "devDependencies": { "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.36.0", - "@apollo/client": "^3.8.10", "@makerx/eslint-config": "4.2.0", "@makerx/prettier-config": "2.0.1", "@makerx/ts-toolkit": "^4.0.0-beta.24", + "@rollup/plugin-commonjs": "28.0.6", + "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-typescript": "^12.1.4", + "@tsconfig/node20": "^20.1.6", "@types/express": "^5.0.3", + "@types/node": "20.19.17", "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.44.1", "@typescript-eslint/parser": "^8.44.1", + "@vitest/coverage-v8": "3.2.4", "better-npm-audit": "^3.11.0", "copyfiles": "^2.4.1", + "es-toolkit": "^1.39.10", "eslint": "^9.36.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.5.4", "express": "^5.1.0", "graphql-shield": "^7.6.5", - "graphql-ws": "^5.14.3", - "lodash": "^4.17.21", + "graphql-ws": "^5.16.2", "npm-run-all": "^4.1.5", - "rimraf": "^6.0.1", - "rollup": "4.52.0", - "typescript": "^5.9.2", - "ws": "^8.18.3", "prettier": "3.6.2", - "@types/node": "20.19.17", - "@tsconfig/node20": "^20.1.6", - "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-commonjs": "28.0.6", + "rimraf": "^6.0.1", + "rollup": "^4.60.2", "tsx": "4.20.5", + "typescript": "^5.9.2", "vitest": "3.2.4", - "@vitest/coverage-v8": "3.2.4" + "ws": "^8.20.0" } } diff --git a/rollup.config.ts b/rollup.config.ts index 2aed5ad..c340a4b 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -5,7 +5,7 @@ import json from '@rollup/plugin-json' import type { RollupOptions } from 'rollup' const config: RollupOptions = { - input: ['src/index.ts', 'src/testing.ts', 'src/subscriptions/index.ts'], + input: ['src/index.ts', 'src/shield.ts', 'src/subscriptions/index.ts'], output: [ { dir: 'dist', diff --git a/src/context.spec.ts b/src/context.spec.ts new file mode 100644 index 0000000..e3ed8df --- /dev/null +++ b/src/context.spec.ts @@ -0,0 +1,140 @@ +import type { Logger } from '@makerx/node-common' +import type { Request } from 'express' +import { describe, expect, it, vi } from 'vitest' +import { createContextFactory, type JwtPayload, type LambdaContext } from './context' +import { User } from './User' + +type Headers = Record + +const makeRequest = ( + options: { + headers?: Headers + protocol?: 'http' | 'https' + hostname?: string + method?: string + originalUrl?: string + remoteAddress?: string + } = {}, +): Request => { + const { headers = {}, protocol = 'http', hostname = 'example.com', method = 'GET', originalUrl = '/', remoteAddress } = options + return { + headers, + protocol, + hostname, + method, + originalUrl, + socket: { remoteAddress }, + } as unknown as Request +} + +const makeLogger = (): Logger => { + const fn = vi.fn() + return { info: fn, warn: fn, error: fn, debug: fn } as unknown as Logger +} + +const sampleClaims: JwtPayload = { + oid: 'oid-1', + iss: 'https://issuer.example', + sub: 'sub-1', + aud: 'api://app', + scp: 'read write', + roles: ['admin'], + email: 'jane@example.com', +} + +describe('createContextFactory', () => { + it('builds a default User when claims are provided and createUser is omitted', async () => { + const logger = makeLogger() + const createContext = createContextFactory({ requestLogger: logger }) + + const context = await createContext({ + req: makeRequest({ headers: { authorization: 'Bearer token-abc' } }), + claims: sampleClaims, + }) + + expect(context.user).toBeInstanceOf(User) + expect(context.user?.token).toBe('token-abc') + expect(context.user?.id).toBe('oid-1') + }) + + it('leaves user undefined when no claims and createUser is omitted', async () => { + const createContext = createContextFactory({ requestLogger: makeLogger() }) + const context = await createContext({ req: makeRequest() }) + expect(context.user).toBeUndefined() + }) + + it('uses a supplied createUser and passes its result to requestLogger', async () => { + type AppUser = { id: string; instance: string } + const requestLogger = vi.fn((_metadata: Record, _user: AppUser) => makeLogger()) + + const createContext = createContextFactory({ + requestLogger, + createUser: async (): Promise => ({ id: 'u-1', instance: 'tenant-a' }), + }) + + const context = await createContext({ req: makeRequest(), claims: sampleClaims }) + + expect(context.user).toEqual({ id: 'u-1', instance: 'tenant-a' }) + expect(requestLogger).toHaveBeenCalledTimes(1) + expect(requestLogger.mock.calls[0][1]).toEqual({ id: 'u-1', instance: 'tenant-a' }) + }) + + it('includes filtered request info and claims in the requestLogger metadata', async () => { + const requestLogger = vi.fn((_metadata: Record, _user: User | undefined) => makeLogger()) + + const createContext = createContextFactory({ + requestLogger, + claimsToLog: ['oid', 'iss'], + requestInfoToLog: ['requestId', 'origin'], + }) + + await createContext({ + req: makeRequest({ headers: { origin: 'https://app.example.com', 'x-request-id': 'req-1' } }), + claims: sampleClaims, + }) + + const metadata = requestLogger.mock.calls[0][0] as { + request: Record + user: Record + } + expect(metadata.request).toEqual({ requestId: 'req-1', origin: 'https://app.example.com' }) + expect(metadata.user).toEqual({ oid: 'oid-1', iss: 'https://issuer.example' }) + }) + + it('uses a Logger instance directly without invoking it as a factory', async () => { + const logger = makeLogger() + const createContext = createContextFactory({ requestLogger: logger }) + const context = await createContext({ req: makeRequest() }) + expect(context.logger).toBe(logger) + }) + + it('merges augmentContext output onto the context', async () => { + const logger = makeLogger() + const createContext = createContextFactory({ + requestLogger: logger, + augmentContext: (context) => ({ tag: 'augmented', startedMirror: context.started }), + }) + + const context = await createContext({ req: makeRequest() }) + expect(context.tag).toBe('augmented') + expect(context.startedMirror).toBe(context.started) + }) + + it('merges augmentRequestInfo output onto requestInfo', async () => { + const createContext = createContextFactory({ + requestLogger: makeLogger(), + augmentRequestInfo: () => ({ tenant: 'acme' }), + }) + + const context = await createContext({ req: makeRequest() }) + expect((context.requestInfo as Record).tenant).toBe('acme') + }) + + it('adds lambda fields to requestInfo when a LambdaContext is supplied', async () => { + const lambdaContext: LambdaContext = { awsRequestId: 'aws-1', functionName: 'my-fn' } + const createContext = createContextFactory({ requestLogger: makeLogger() }) + + const context = await createContext({ req: makeRequest(), context: lambdaContext }) + expect(context.requestInfo).toMatchObject({ awsRequestId: 'aws-1', functionName: 'my-fn' }) + }) +}) diff --git a/src/context.ts b/src/context.ts index 141c26c..79e85de 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,7 +1,7 @@ import type { Logger } from '@makerx/node-common' -import { randomUUID } from 'crypto' +import { pick } from 'es-toolkit/compat' import type { Request } from 'express' -import { pick } from 'lodash' +import { buildBaseRequestInfo, type BaseRequestInfo } from './request-info' import { User } from './User' export interface GraphQLContext< @@ -17,20 +17,6 @@ export interface GraphQLContext< export type AnyGraphqlContext = GraphQLContext -export interface BaseRequestInfo extends Record { - requestId: string - protocol: 'http' | 'https' | 'ws' - host: string - method: string - url: string - origin: string - referer?: string - correlationId?: string - arrLogId?: string - clientIp?: string - userAgent?: string -} - export interface LambdaContext { functionName?: string awsRequestId?: string @@ -39,6 +25,13 @@ export type LambdaEvent = never export type LambdaRequestInfo = BaseRequestInfo & LambdaContext export type RequestInfo = BaseRequestInfo | LambdaRequestInfo +// Strips the `[key: string]: unknown` index signature so `keyof` returns only the declared keys. +type KnownKeys = keyof { + [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] +} +// Known keys for autocomplete, plus `(string & {})` to keep arbitrary augmented fields assignable. +export type RequestInfoLogKey = KnownKeys | keyof LambdaContext | (string & {}) + // standard claims https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 export interface JwtPayload { [key: string]: unknown @@ -71,41 +64,38 @@ export interface ContextInput { event?: LambdaEvent } export type CreateContext = (input: ContextInput) => Promise -export type CreateRequestLogger = (requestMetadata: Record) => Logger +export type CreateRequestLogger = ( + requestMetadata: Record, + user: TUser, +) => TLogger export type AugmentRequestInfo = (input: ContextInput) => Record -export interface CreateContextConfig { - requestLogger: CreateRequestLogger | Logger +// `createUser` is optional when TUser is compatible with the default `User | undefined` +// (i.e. `defaultCreateUser` can satisfy it), and required when TUser is narrower. +export type CreateContextConfig< + TUser = User | undefined, + TAugment extends Record = Record, + TLogger extends Logger = Logger, +> = { + requestLogger: CreateRequestLogger | TLogger augmentRequestInfo?: AugmentRequestInfo claimsToLog?: string[] - createUser: CreateUser> - requestInfoToLog?: Array - augmentContext?: (context: TContext) => Record | Promise> -} + requestInfoToLog?: Array + augmentContext?: (context: GraphQLContext) => TAugment | Promise +} & ([User | undefined] extends [TUser] ? { createUser?: CreateUser } : { createUser: CreateUser }) + +export const createContextFactory = < + TUser = User | undefined, + TAugment extends Record = Record, + TLogger extends Logger = Logger, +>( + config: CreateContextConfig, +): CreateContext & TAugment> => { + const { requestLogger, augmentRequestInfo, claimsToLog, requestInfoToLog, augmentContext } = config + // The conditional type on CreateContextConfig guarantees `createUser` is provided when TUser is + // narrower than `User | undefined`, so defaulting to defaultCreateUser is sound here. + const createUser = (config.createUser ?? defaultCreateUser) as CreateUser -export const buildBaseRequestInfo = (req: Request): BaseRequestInfo => ({ - requestId: req.headers['x-request-id']?.toString() ?? randomUUID(), - protocol: req.protocol as 'http' | 'https', - host: req.hostname ?? '', - method: req.method ?? '', - url: req.originalUrl, - origin: req.get('Origin') ?? '', - referer: req.headers.referer?.toString() ?? '', - arrLogId: req.headers['x-arr-log-id']?.toString() ?? undefined, - clientIp: req.headers['x-forwarded-for']?.toString() ?? req.socket.remoteAddress, - correlationId: req.headers['x-correlation-id']?.toString() ?? undefined, - userAgent: req.headers['user-agent']?.toString() ?? undefined, -}) - -export const createContextFactory = ({ - requestLogger, - augmentRequestInfo, - claimsToLog, - createUser, - requestInfoToLog, - augmentContext, -}: CreateContextConfig): CreateContext => { - // the function that creates the GraphQL context return async (input: ContextInput) => { const { req, claims, context } = input @@ -131,8 +121,10 @@ export const createContextFactory = = {} @@ -141,21 +133,19 @@ export const createContextFactory = = { requestInfo, logger, - user: createUser ? await createUser(input) : undefined, + user, started: Date.now(), } - const augmentedGraphQLContext = augmentContext - ? { ...graphqlContext, ...(await augmentContext(graphqlContext as TContext)) } - : graphqlContext + const augmentedGraphQLContext = augmentContext ? { ...graphqlContext, ...(await augmentContext(graphqlContext)) } : graphqlContext - return augmentedGraphQLContext as TContext + return augmentedGraphQLContext as GraphQLContext & TAugment } } diff --git a/src/index.ts b/src/index.ts index 303ccd6..49b5693 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ export * from './context' -export * from './User' -export * from './utils' export * from './logging' +export * from './request-info' export * from './schema-util' -export * from './shield' -export * from './models' +export * from './type-utils' +export * from './User' +export * from './utils' diff --git a/src/logging.ts b/src/logging.ts index 9d7766d..d1ddfb4 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -3,7 +3,7 @@ import { isLocalDev } from '@makerx/node-common' import type { ExecutionArgs, GraphQLFormattedError } from 'graphql' import { OperationTypeNode, print } from 'graphql' import type { ExecutionResult } from 'graphql-ws' -import { omitBy } from 'lodash' +import { omitBy } from 'es-toolkit/compat' import type { GraphQLContext } from './context' import { isIntrospectionQuery, isNil } from './utils' diff --git a/src/request-info.spec.ts b/src/request-info.spec.ts new file mode 100644 index 0000000..25cdd5d --- /dev/null +++ b/src/request-info.spec.ts @@ -0,0 +1,390 @@ +import type { Request } from 'express' +import type { IncomingMessage } from 'http' +import { describe, expect, it } from 'vitest' +import { buildBaseRequestInfo, buildConnectRequestInfo, connectRequestBaseUrl, requestBaseUrl } from './request-info' + +type Headers = Record + +const makeExpressRequest = ( + options: { + headers?: Headers + protocol?: 'http' | 'https' + hostname?: string + method?: string + originalUrl?: string + remoteAddress?: string + } = {}, +): Request => { + const { headers = {}, protocol = 'http', hostname = 'example.com', method = 'GET', originalUrl = '/', remoteAddress } = options + return { + headers, + protocol, + hostname, + method, + originalUrl, + socket: { remoteAddress }, + } as unknown as Request +} + +const makeIncomingMessage = ( + options: { + headers?: Headers + method?: string + url?: string + remoteAddress?: string + encrypted?: boolean + } = {}, +): IncomingMessage => { + const { headers = {}, method = 'GET', url = '/', remoteAddress, encrypted } = options + const socket: Record = { remoteAddress } + if (encrypted !== undefined) socket.encrypted = encrypted + return { + headers, + method, + url, + socket, + } as unknown as IncomingMessage +} + +describe('requestBaseUrl', () => { + it('prefers x-forwarded-host over host header', () => { + const req = makeExpressRequest({ + protocol: 'https', + headers: { 'x-forwarded-host': 'public.example.com', host: 'internal:8080' }, + }) + expect(requestBaseUrl(req)).toBe('https://public.example.com') + }) + + it('uses first value when x-forwarded-host is comma-separated', () => { + const req = makeExpressRequest({ + protocol: 'https', + headers: { 'x-forwarded-host': 'first.example.com, second.example.com' }, + }) + expect(requestBaseUrl(req)).toBe('https://first.example.com') + }) + + it('uses first value when x-forwarded-host is an array', () => { + const req = makeExpressRequest({ + protocol: 'http', + headers: { 'x-forwarded-host': ['one.example.com', 'two.example.com'] }, + }) + expect(requestBaseUrl(req)).toBe('http://one.example.com') + }) + + it('falls back to host header when x-forwarded-host absent', () => { + const req = makeExpressRequest({ + protocol: 'http', + headers: { host: 'example.com:3000' }, + }) + expect(requestBaseUrl(req)).toBe('http://example.com:3000') + }) + + it('falls back to req.hostname when no host header available', () => { + const req = makeExpressRequest({ protocol: 'https', hostname: 'fallback.example.com' }) + expect(requestBaseUrl(req)).toBe('https://fallback.example.com') + }) + + it('strips default http port 80', () => { + const req = makeExpressRequest({ protocol: 'http', headers: { host: 'example.com:80' } }) + expect(requestBaseUrl(req)).toBe('http://example.com') + }) + + it('strips default https port 443', () => { + const req = makeExpressRequest({ protocol: 'https', headers: { host: 'example.com:443' } }) + expect(requestBaseUrl(req)).toBe('https://example.com') + }) + + it('keeps non-default port', () => { + const req = makeExpressRequest({ protocol: 'https', headers: { host: 'example.com:8443' } }) + expect(requestBaseUrl(req)).toBe('https://example.com:8443') + }) + + it('handles bracketed IPv6 host with port', () => { + const req = makeExpressRequest({ protocol: 'http', headers: { host: '[::1]:8080' } }) + expect(requestBaseUrl(req)).toBe('http://[::1]:8080') + }) + + it('builds localhost URL for typical local dev (http, port 4000, no proxy)', () => { + const req = makeExpressRequest({ protocol: 'http', hostname: 'localhost', headers: { host: 'localhost:4000' } }) + expect(requestBaseUrl(req)).toBe('http://localhost:4000') + }) +}) + +describe('connectRequestBaseUrl', () => { + it('prefers x-forwarded-proto over socket encryption', () => { + const req = makeIncomingMessage({ + encrypted: true, + headers: { 'x-forwarded-proto': 'http', host: 'example.com' }, + }) + expect(connectRequestBaseUrl(req)).toBe('http://example.com') + }) + + it('detects https via TLS socket encrypted flag', () => { + const req = makeIncomingMessage({ + encrypted: true, + headers: { host: 'example.com' }, + }) + expect(connectRequestBaseUrl(req)).toBe('https://example.com') + }) + + it('defaults to http when no proto signal', () => { + const req = makeIncomingMessage({ headers: { host: 'example.com:3000' } }) + expect(connectRequestBaseUrl(req)).toBe('http://example.com:3000') + }) + + it('prefers x-forwarded-host over host header', () => { + const req = makeIncomingMessage({ + headers: { 'x-forwarded-proto': 'https', 'x-forwarded-host': 'public.example.com', host: 'internal:8080' }, + }) + expect(connectRequestBaseUrl(req)).toBe('https://public.example.com') + }) + + it('strips default port', () => { + const req = makeIncomingMessage({ + headers: { 'x-forwarded-proto': 'https', host: 'example.com:443' }, + }) + expect(connectRequestBaseUrl(req)).toBe('https://example.com') + }) + + it('throws when no host header available', () => { + const req = makeIncomingMessage() + expect(() => connectRequestBaseUrl(req)).toThrow(/Cannot determine base URL/) + }) + + it('builds localhost URL for typical local dev (ws, port 4000, no proxy, unencrypted socket)', () => { + const req = makeIncomingMessage({ encrypted: false, headers: { host: 'localhost:4000' } }) + expect(connectRequestBaseUrl(req)).toBe('http://localhost:4000') + }) +}) + +describe('buildBaseRequestInfo', () => { + it('builds full request info with all headers populated', () => { + const req = makeExpressRequest({ + protocol: 'https', + hostname: 'api.example.com', + method: 'POST', + originalUrl: '/graphql?q=1', + headers: { + 'x-request-id': 'req-abc', + 'x-forwarded-host': 'api.example.com:8443', + host: 'internal:8080', + origin: 'https://app.example.com', + referer: 'https://app.example.com/page', + 'x-arr-log-id': 'arr-123', + 'x-forwarded-for': '203.0.113.5, 10.0.0.1', + 'x-correlation-id': 'corr-xyz', + 'user-agent': 'test-agent/1.0', + }, + }) + + expect(buildBaseRequestInfo(req)).toEqual({ + requestId: 'req-abc', + source: 'http', + protocol: 'https', + host: 'api.example.com', + port: 8443, + method: 'POST', + baseUrl: 'https://api.example.com:8443', + url: '/graphql?q=1', + origin: 'https://app.example.com', + referer: 'https://app.example.com/page', + arrLogId: 'arr-123', + clientIp: '203.0.113.5', + correlationId: 'corr-xyz', + userAgent: 'test-agent/1.0', + }) + }) + + it('sets source to "http"', () => { + expect(buildBaseRequestInfo(makeExpressRequest()).source).toBe('http') + }) + + it('generates a uuid for requestId when x-request-id absent', () => { + const req = makeExpressRequest() + const info = buildBaseRequestInfo(req) + expect(info.requestId).toMatch(/^[0-9a-f-]{36}$/i) + }) + + it('takes first value from x-forwarded-for chain for clientIp', () => { + const req = makeExpressRequest({ + headers: { 'x-forwarded-for': '203.0.113.5, 198.51.100.2, 10.0.0.1' }, + }) + expect(buildBaseRequestInfo(req).clientIp).toBe('203.0.113.5') + }) + + it('falls back to socket.remoteAddress for clientIp when no x-forwarded-for', () => { + const req = makeExpressRequest({ remoteAddress: '127.0.0.1' }) + expect(buildBaseRequestInfo(req).clientIp).toBe('127.0.0.1') + }) + + it('returns undefined for absent optional fields', () => { + const req = makeExpressRequest() + const info = buildBaseRequestInfo(req) + expect(info.arrLogId).toBeUndefined() + expect(info.correlationId).toBeUndefined() + expect(info.userAgent).toBeUndefined() + }) + + it('splits host header into host + port when x-forwarded-host absent', () => { + const req = makeExpressRequest({ headers: { host: 'direct.example.com:8080' } }) + const info = buildBaseRequestInfo(req) + expect(info.host).toBe('direct.example.com') + expect(info.port).toBe(8080) + }) + + it('falls back to req.hostname for host when no host header available', () => { + const req = makeExpressRequest({ hostname: 'fallback.example.com' }) + const info = buildBaseRequestInfo(req) + expect(info.host).toBe('fallback.example.com') + expect(info.port).toBeUndefined() + }) + + it('splits x-forwarded-host into host + port', () => { + const req = makeExpressRequest({ + protocol: 'https', + headers: { 'x-forwarded-host': 'public.example.com:8443' }, + }) + const info = buildBaseRequestInfo(req) + expect(info.host).toBe('public.example.com') + expect(info.port).toBe(8443) + }) + + it('builds local dev request info (http, localhost:4000, no proxy headers)', () => { + const req = makeExpressRequest({ + protocol: 'http', + hostname: 'localhost', + method: 'POST', + originalUrl: '/graphql', + remoteAddress: '::1', + headers: { + host: 'localhost:4000', + origin: 'http://localhost:4000', + 'user-agent': 'curl/8.0.0', + }, + }) + + const info = buildBaseRequestInfo(req) + expect(info).toMatchObject({ + source: 'http', + protocol: 'http', + host: 'localhost', + port: 4000, + baseUrl: 'http://localhost:4000', + url: '/graphql', + method: 'POST', + origin: 'http://localhost:4000', + clientIp: '::1', + userAgent: 'curl/8.0.0', + arrLogId: undefined, + correlationId: undefined, + }) + expect(info.requestId).toMatch(/^[0-9a-f-]{36}$/i) + }) +}) + +describe('buildConnectRequestInfo', () => { + it('builds full request info with wss protocol when encrypted', () => { + const req = makeIncomingMessage({ + method: 'GET', + url: '/graphql', + encrypted: true, + headers: { + 'x-request-id': 'req-ws', + 'x-forwarded-host': 'api.example.com', + host: 'internal:8080', + origin: 'https://app.example.com', + referer: 'https://app.example.com/page', + 'x-arr-log-id': 'arr-456', + 'x-forwarded-for': '203.0.113.5', + 'x-correlation-id': 'corr-ws', + 'user-agent': 'ws-client/1.0', + }, + }) + + expect(buildConnectRequestInfo(req)).toEqual({ + requestId: 'req-ws', + source: 'subscription', + protocol: 'wss', + host: 'api.example.com', + port: undefined, + method: 'GET', + baseUrl: 'https://api.example.com', + url: '/graphql', + origin: 'https://app.example.com', + referer: 'https://app.example.com/page', + arrLogId: 'arr-456', + clientIp: '203.0.113.5', + correlationId: 'corr-ws', + userAgent: 'ws-client/1.0', + }) + }) + + it('sets source to "subscription"', () => { + const req = makeIncomingMessage({ headers: { host: 'example.com' } }) + expect(buildConnectRequestInfo(req).source).toBe('subscription') + }) + + it('emits ws protocol when socket is unencrypted and no x-forwarded-proto', () => { + const req = makeIncomingMessage({ encrypted: false, headers: { host: 'example.com' } }) + expect(buildConnectRequestInfo(req).protocol).toBe('ws') + }) + + it('emits wss protocol when x-forwarded-proto is https (normalizing to wss)', () => { + const req = makeIncomingMessage({ headers: { 'x-forwarded-proto': 'https', host: 'example.com' } }) + expect(buildConnectRequestInfo(req).protocol).toBe('wss') + }) + + it('emits wss protocol when x-forwarded-proto is wss', () => { + const req = makeIncomingMessage({ headers: { 'x-forwarded-proto': 'wss', host: 'example.com' } }) + expect(buildConnectRequestInfo(req).protocol).toBe('wss') + }) + + it('baseUrl uses http(s) scheme even when protocol is ws/wss', () => { + const req = makeIncomingMessage({ encrypted: true, headers: { host: 'example.com' } }) + const info = buildConnectRequestInfo(req) + expect(info.protocol).toBe('wss') + expect(info.baseUrl).toBe('https://example.com') + }) + + it('splits headers.host into host + port when x-forwarded-host absent', () => { + const req = makeIncomingMessage({ headers: { host: 'direct.example.com:8080' } }) + const info = buildConnectRequestInfo(req) + expect(info.host).toBe('direct.example.com') + expect(info.port).toBe(8080) + }) + + it('throws when no host header available', () => { + const req = makeIncomingMessage() + expect(() => buildConnectRequestInfo(req)).toThrow(/Cannot determine base URL/) + }) + + it('builds local dev ws request info (localhost:4000, no proxy headers, unencrypted socket)', () => { + const req = makeIncomingMessage({ + method: 'GET', + url: '/graphql', + encrypted: false, + remoteAddress: '::1', + headers: { + host: 'localhost:4000', + origin: 'http://localhost:4000', + 'user-agent': 'ws-client/1.0', + }, + }) + + const info = buildConnectRequestInfo(req) + expect(info).toMatchObject({ + source: 'subscription', + protocol: 'ws', + host: 'localhost', + port: 4000, + baseUrl: 'http://localhost:4000', + url: '/graphql', + origin: 'http://localhost:4000', + clientIp: '::1', + userAgent: 'ws-client/1.0', + arrLogId: undefined, + correlationId: undefined, + }) + expect(info.requestId).toMatch(/^[0-9a-f-]{36}$/i) + }) +}) diff --git a/src/request-info.ts b/src/request-info.ts new file mode 100644 index 0000000..70789c5 --- /dev/null +++ b/src/request-info.ts @@ -0,0 +1,107 @@ +import { randomUUID } from 'crypto' +import type { Request } from 'express' +import type { IncomingMessage } from 'http' +import type { TLSSocket } from 'tls' + +export interface BaseRequestInfo extends Record { + requestId: string + source: 'http' | 'subscription' + protocol: 'http' | 'https' | 'ws' | 'wss' + host: string + port?: number + method: string + baseUrl: string + url: string + origin: string + referer?: string + correlationId?: string + arrLogId?: string + clientIp?: string + userAgent?: string +} + +const isDefaultPort = (protocol: string, port: number | undefined): boolean => + port == null || (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) + +const formatBaseUrl = (protocol: string, hostname: string, port: number | undefined): string => + isDefaultPort(protocol, port) ? `${protocol}://${hostname}` : `${protocol}://${hostname}:${port}` + +const firstHeaderValue = (value: string | string[] | undefined): string | undefined => { + const raw = Array.isArray(value) ? value[0] : value + return raw?.split(',')[0]?.trim() || undefined +} + +const parseHostHeader = (hostHeader: string): { hostname: string; port: number | undefined } => { + const url = new URL(`http://${hostHeader}`) + return { hostname: url.hostname, port: url.port ? Number(url.port) : undefined } +} + +const isEncryptedSocket = (req: IncomingMessage): boolean => 'encrypted' in req.socket && (req.socket as TLSSocket).encrypted === true + +const isEncryptedConnect = (req: IncomingMessage): boolean => { + const forwarded = firstHeaderValue(req.headers['x-forwarded-proto'])?.toLowerCase() + if (forwarded === 'https' || forwarded === 'wss') return true + if (forwarded === 'http' || forwarded === 'ws') return false + return isEncryptedSocket(req) +} + +const resolveForwardedHost = (req: IncomingMessage): string | undefined => firstHeaderValue(req.headers['x-forwarded-host']) + +const resolveHostAndPort = (req: IncomingMessage, fallbackHostname?: string): { host: string; port: number | undefined } => { + const headerValue = resolveForwardedHost(req) ?? req.headers.host + if (headerValue) { + const { hostname, port } = parseHostHeader(headerValue) + return { host: hostname, port } + } + return { host: fallbackHostname ?? '', port: undefined } +} + +export const requestBaseUrl = (req: Request): string => { + const { host, port } = resolveHostAndPort(req, req.hostname) + return formatBaseUrl(req.protocol, host, port) +} + +export const connectRequestBaseUrl = (req: IncomingMessage): string => { + const { host, port } = resolveHostAndPort(req) + if (!host) throw new Error('Cannot determine base URL from websocket connect request') + return formatBaseUrl(isEncryptedConnect(req) ? 'https' : 'http', host, port) +} + +const buildSharedRequestInfo = (req: IncomingMessage) => ({ + requestId: req.headers['x-request-id']?.toString() ?? randomUUID(), + method: req.method ?? '', + origin: req.headers.origin ?? '', + referer: req.headers.referer?.toString() ?? '', + arrLogId: req.headers['x-arr-log-id']?.toString() ?? undefined, + clientIp: firstHeaderValue(req.headers['x-forwarded-for']) ?? req.socket.remoteAddress, + correlationId: req.headers['x-correlation-id']?.toString() ?? undefined, + userAgent: req.headers['user-agent']?.toString() ?? undefined, +}) + +export const buildBaseRequestInfo = (req: Request): BaseRequestInfo => { + const { host, port } = resolveHostAndPort(req, req.hostname) + return { + ...buildSharedRequestInfo(req), + source: 'http', + protocol: req.protocol as 'http' | 'https', + host, + port, + baseUrl: formatBaseUrl(req.protocol, host, port), + url: req.originalUrl, + } +} + +export const buildConnectRequestInfo = (req: IncomingMessage): BaseRequestInfo => { + const { host, port } = resolveHostAndPort(req) + const protocol = isEncryptedConnect(req) ? 'wss' : 'ws' + if (!host) throw new Error('Cannot determine base URL from websocket connect request') + return { + ...buildSharedRequestInfo(req), + source: 'subscription', + protocol, + host, + port, + baseUrl: formatBaseUrl(protocol === 'wss' ? 'https' : 'http', host, port), + url: req.url ?? '', + } +} diff --git a/src/shield.ts b/src/shield.ts index 384aed1..f9c0d2d 100644 --- a/src/shield.ts +++ b/src/shield.ts @@ -1,7 +1,6 @@ -import type { and, chain, or, race } from 'graphql-shield' +import type { and, chain, IRules, or, race } from 'graphql-shield' import { allow, rule, shield } from 'graphql-shield' -import type { IRules } from 'graphql-shield' -import type { Primitive } from './models' +import type { Primitive } from './type-utils' type RuleCombinator = typeof chain | typeof race | typeof or | typeof and // For whatever reason, graphql-shield doesn't export this type, but we can extract if from diff --git a/src/subscriptions/context.spec.ts b/src/subscriptions/context.spec.ts new file mode 100644 index 0000000..b3d378a --- /dev/null +++ b/src/subscriptions/context.spec.ts @@ -0,0 +1,127 @@ +import type { Logger } from '@makerx/node-common' +import type { IncomingMessage } from 'http' +import { describe, expect, it, vi } from 'vitest' +import type { JwtPayload } from '../context' +import { User } from '../User' +import { createSubscriptionContextFactory } from './context' + +type Headers = Record + +const makeConnectRequest = ( + options: { + headers?: Headers + method?: string + url?: string + remoteAddress?: string + encrypted?: boolean + } = {}, +): IncomingMessage => { + const { headers = { host: 'example.com' }, method = 'GET', url = '/graphql', remoteAddress, encrypted } = options + const socket: Record = { remoteAddress } + if (encrypted !== undefined) socket.encrypted = encrypted + return { headers, method, url, socket } as unknown as IncomingMessage +} + +const makeLogger = (): Logger => { + const fn = vi.fn() + return { info: fn, warn: fn, error: fn, debug: fn } as unknown as Logger +} + +const sampleClaims: JwtPayload = { + oid: 'oid-1', + iss: 'https://issuer.example', + sub: 'sub-1', + scp: 'read', +} + +describe('createSubscriptionContextFactory', () => { + it('builds a default User from claims and the bearer token in connectionParams', async () => { + const createContext = createSubscriptionContextFactory({ requestLogger: makeLogger() }) + + const context = await createContext({ + connectRequest: makeConnectRequest(), + claims: sampleClaims, + connectionParams: { authorization: 'Bearer token-ws' }, + }) + + expect(context.user).toBeInstanceOf(User) + expect(context.user?.token).toBe('token-ws') + expect(context.user?.id).toBe('oid-1') + }) + + it('leaves user undefined when no claims are provided', async () => { + const createContext = createSubscriptionContextFactory({ requestLogger: makeLogger() }) + const context = await createContext({ connectRequest: makeConnectRequest() }) + expect(context.user).toBeUndefined() + }) + + it('uses a supplied createUser and passes its result to requestLogger', async () => { + type AppUser = { id: string; instance: string } + const requestLogger = vi.fn((_metadata: Record, _user: AppUser) => makeLogger()) + + const createContext = createSubscriptionContextFactory({ + requestLogger, + createUser: async (): Promise => ({ id: 'u-1', instance: 'tenant-a' }), + }) + + const context = await createContext({ connectRequest: makeConnectRequest(), claims: sampleClaims }) + + expect(context.user).toEqual({ id: 'u-1', instance: 'tenant-a' }) + expect(requestLogger.mock.calls[0][1]).toEqual({ id: 'u-1', instance: 'tenant-a' }) + }) + + it('includes filtered request info and claims in the requestLogger metadata', async () => { + const requestLogger = vi.fn((_metadata: Record, _user: User | undefined) => makeLogger()) + + const createContext = createSubscriptionContextFactory({ + requestLogger, + claimsToLog: ['oid', 'iss'], + requestInfoToLog: ['requestId', 'protocol'], + }) + + await createContext({ + connectRequest: makeConnectRequest({ headers: { host: 'example.com', 'x-request-id': 'req-ws' } }), + claims: sampleClaims, + }) + + const metadata = requestLogger.mock.calls[0][0] as { + request: Record + user: Record + } + expect(metadata.request).toEqual({ requestId: 'req-ws', protocol: 'ws' }) + expect(metadata.user).toEqual({ oid: 'oid-1', iss: 'https://issuer.example' }) + }) + + it('uses a Logger instance directly without invoking it as a factory', async () => { + const logger = makeLogger() + const createContext = createSubscriptionContextFactory({ requestLogger: logger }) + const context = await createContext({ connectRequest: makeConnectRequest() }) + expect(context.logger).toBe(logger) + }) + + it('merges augmentContext output onto the context', async () => { + const createContext = createSubscriptionContextFactory({ + requestLogger: makeLogger(), + augmentContext: () => ({ channel: 'subs' }), + }) + + const context = await createContext({ connectRequest: makeConnectRequest() }) + expect(context.channel).toBe('subs') + }) + + it('merges augmentRequestInfo output onto requestInfo', async () => { + const createContext = createSubscriptionContextFactory({ + requestLogger: makeLogger(), + augmentRequestInfo: () => ({ tenant: 'acme' }), + }) + + const context = await createContext({ connectRequest: makeConnectRequest() }) + expect((context.requestInfo as Record).tenant).toBe('acme') + }) + + it('marks requestInfo.source as "subscription"', async () => { + const createContext = createSubscriptionContextFactory({ requestLogger: makeLogger() }) + const context = await createContext({ connectRequest: makeConnectRequest() }) + expect(context.requestInfo.source).toBe('subscription') + }) +}) diff --git a/src/subscriptions/context.ts b/src/subscriptions/context.ts index 225fb3c..c6282a4 100644 --- a/src/subscriptions/context.ts +++ b/src/subscriptions/context.ts @@ -1,9 +1,9 @@ import type { Logger } from '@makerx/node-common' -import { randomUUID } from 'crypto' +import { pick } from 'es-toolkit/compat' import type { IncomingMessage } from 'http' -import { pick } from 'lodash' import { User } from '../User' -import type { CreateRequestLogger, GraphQLContext, JwtPayload, RequestInfo } from '../context' +import type { CreateRequestLogger, GraphQLContext, JwtPayload, RequestInfo, RequestInfoLogKey } from '../context' +import { buildConnectRequestInfo } from '../request-info' import { extractTokenFromConnectionParams } from './utils' export interface SubscriptionContextInput { @@ -12,51 +12,49 @@ export interface SubscriptionContextInput { claims?: JwtPayload } -export type CreateSubscriptionUser = (input: SubscriptionContextInput) => Promise +export type CreateSubscriptionUser = (input: SubscriptionContextInput) => Promise | T export type CreateSubscriptionContext = (input: SubscriptionContextInput) => Promise export type AugmentSubscriptionRequestInfo = (input: SubscriptionContextInput) => Record -export interface CreateSubscriptionContextConfig { - requestLogger: CreateRequestLogger | Logger +// `createUser` is optional when TUser is compatible with the default `User | undefined` +// (i.e. defaultCreateUser can satisfy it), and required when TUser is narrower. +export type CreateSubscriptionContextConfig< + TUser = User | undefined, + TAugment extends Record = Record, + TLogger extends Logger = Logger, +> = { + requestLogger: CreateRequestLogger | TLogger augmentRequestInfo?: AugmentSubscriptionRequestInfo claimsToLog?: string[] - createUser?: CreateSubscriptionUser - requestInfoToLog?: Array - augmentContext?: (context: TContext) => Record | Promise> -} + requestInfoToLog?: Array + augmentContext?: (context: GraphQLContext) => TAugment | Promise +} & ([User | undefined] extends [TUser] ? { createUser?: CreateSubscriptionUser } : { createUser: CreateSubscriptionUser }) + +export const createSubscriptionContextFactory = < + TUser = User | undefined, + TAugment extends Record = Record, + TLogger extends Logger = Logger, +>( + config: CreateSubscriptionContextConfig, +): CreateSubscriptionContext & TAugment> => { + const { requestLogger, augmentRequestInfo, claimsToLog, requestInfoToLog, augmentContext } = config + // The conditional type on CreateSubscriptionContextConfig guarantees `createUser` is provided + // when TUser is narrower than `User | undefined`, so defaulting is sound here. + const createUser = (config.createUser ?? defaultCreateUser) as CreateSubscriptionUser -export const createSubscriptionContextFactory = ({ - requestLogger, - augmentRequestInfo, - claimsToLog, - createUser = defaultCreateUser, - requestInfoToLog, - augmentContext, -}: CreateSubscriptionContextConfig): CreateSubscriptionContext => { - // the function that creates the GraphQL context return async (input: SubscriptionContextInput) => { const { connectRequest: req, claims } = input - const xForwardedFor = req.headers['x-forwarded-for'] - const host = Array.isArray(xForwardedFor) ? xForwardedFor[0] : (xForwardedFor ?? req.headers.host) - // build request info from the connect request and socket const requestInfo: RequestInfo = { - requestId: req.headers['x-request-id']?.toString() ?? randomUUID(), - protocol: 'ws', - host: host ?? '', - method: req.method ?? '', - url: req.url ?? '', - origin: req.headers['origin'] ?? '', - referer: req.headers.referer?.toString() ?? '', - arrLogId: req.headers['x-arr-log-id']?.toString() ?? undefined, - clientIp: req.headers['x-forwarded-for']?.toString() ?? req.socket.remoteAddress, - correlationId: req.headers['x-correlation-id']?.toString() ?? undefined, + ...buildConnectRequestInfo(req), ...augmentRequestInfo?.(input), } + const user = await createUser(input) + // create request logger - let logger: Logger + let logger: TLogger if (typeof requestLogger === 'function') { // build request logger metadata const requestLoggerMetadata: Record = {} @@ -65,21 +63,19 @@ export const createSubscriptionContextFactory = = { requestInfo, logger, - user: await createUser(input), + user, started: Date.now(), } - const augmentedGraphQLContext = augmentContext - ? { ...graphqlContext, ...(await augmentContext(graphqlContext as TContext)) } - : graphqlContext + const augmentedGraphQLContext = augmentContext ? { ...graphqlContext, ...(await augmentContext(graphqlContext)) } : graphqlContext - return augmentedGraphQLContext as TContext + return augmentedGraphQLContext as GraphQLContext & TAugment } } diff --git a/src/subscriptions/server.ts b/src/subscriptions/server.ts index abed05b..6477148 100644 --- a/src/subscriptions/server.ts +++ b/src/subscriptions/server.ts @@ -3,7 +3,7 @@ import type { GraphQLSchema } from 'graphql' import { CloseCode } from 'graphql-ws' import { useServer } from 'graphql-ws/lib/use/ws' import type { Server } from 'http' -import { pick } from 'lodash' +import { pick } from 'es-toolkit/compat' import { WebSocketServer } from 'ws' import type { GraphQLContext, JwtPayload } from '../context' import { logSubscriptionOperation } from '../logging' diff --git a/src/testing.ts b/src/testing.ts deleted file mode 100644 index e0d7a07..0000000 --- a/src/testing.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { NormalizedCacheObject } from '@apollo/client/core' -import { ApolloClient, createHttpLink, from, InMemoryCache } from '@apollo/client/core' -import type { ApolloClientOptions } from '@apollo/client/core/ApolloClient' -import { setContext } from '@apollo/client/link/context' -import type { AccessTokenResponse, ClientCredentialsConfig } from '@makerx/node-common' -import { getClientCredentialsToken } from '@makerx/node-common' - -export * from '@apollo/client/core' - -const bearerTokenLink = (accessToken?: string) => - setContext((_, { headers }) => { - if (accessToken) - return { - headers: { - ...headers, - authorization: `Bearer ${accessToken}`, - }, - } - return { - headers, - } - }) - -const clientCredentialsLink = (clientCredentialsConfig: ClientCredentialsConfig) => { - let promise: Promise | undefined - return setContext(async () => { - if (!promise) promise = getClientCredentialsToken(clientCredentialsConfig) - let tokenResponse = await promise - if (tokenResponse.isExpired) { - promise = getClientCredentialsToken(clientCredentialsConfig) - tokenResponse = await promise - } - return { - headers: { - authorization: `Bearer ${tokenResponse.access_token}`, - }, - } - }) -} - -const httpLink = (url: string) => - createHttpLink({ - uri: url, - }) - -export const createTestClient = ( - url: string, - accessToken?: string, - options?: Partial>, -): ApolloClient => - new ApolloClient({ - link: from([bearerTokenLink(accessToken), httpLink(url)]), - cache: new InMemoryCache(), - defaultOptions: { - query: { - errorPolicy: 'all', - }, - }, - ...options, - }) - -export const createTestClientWithClientCredentials = ( - url: string, - clientCredentialsConfig: ClientCredentialsConfig, - options?: Partial>, -): ApolloClient => - new ApolloClient({ - link: from([clientCredentialsLink(clientCredentialsConfig), httpLink(url)]), - cache: new InMemoryCache(), - defaultOptions: { - query: { - errorPolicy: 'all', - }, - }, - ...options, - }) diff --git a/src/models.ts b/src/type-utils.ts similarity index 100% rename from src/models.ts rename to src/type-utils.ts