From f5bcb423eb4745ef4c6fb9b3bc5b5f33009c3386 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 05:30:23 +0000 Subject: [PATCH 01/14] Swap lodash for es-toolkit Replace lodash imports with es-toolkit/compat across source files and update peer/dev dependencies in package.json accordingly. https://claude.ai/code/session_01JuZopuWcLH9C9jCkiUtkQz --- package-lock.json | 15 +++++++++++++-- package.json | 4 ++-- src/context.ts | 2 +- src/logging.ts | 2 +- src/subscriptions/context.ts | 2 +- src/subscriptions/server.ts | 2 +- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17621aa..19c0984 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@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", @@ -38,7 +39,6 @@ "express": "^5.1.0", "graphql-shield": "^7.6.5", "graphql-ws": "^5.14.3", - "lodash": "^4.17.21", "npm-run-all": "^4.1.5", "prettier": "3.6.2", "rimraf": "^6.0.1", @@ -53,11 +53,11 @@ }, "peerDependencies": { "@apollo/client": "*", + "es-toolkit": "*", "express": "*", "graphql": "*", "graphql-shield": "*", "graphql-ws": "*", - "lodash": "*", "ws": "*" }, "peerDependenciesMeta": { @@ -3333,6 +3333,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", diff --git a/package.json b/package.json index 16cbfd9..64c875f 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "graphql": "*", "graphql-shield": "*", "graphql-ws": "*", - "lodash": "*", + "es-toolkit": "*", "ws": "*" }, "peerDependenciesMeta": { @@ -79,7 +79,7 @@ "express": "^5.1.0", "graphql-shield": "^7.6.5", "graphql-ws": "^5.14.3", - "lodash": "^4.17.21", + "es-toolkit": "^1.39.10", "npm-run-all": "^4.1.5", "rimraf": "^6.0.1", "rollup": "4.52.0", diff --git a/src/context.ts b/src/context.ts index 141c26c..f46ee97 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 type { Request } from 'express' -import { pick } from 'lodash' +import { pick } from 'es-toolkit/compat' import { User } from './User' export interface GraphQLContext< 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/subscriptions/context.ts b/src/subscriptions/context.ts index 225fb3c..d4d936f 100644 --- a/src/subscriptions/context.ts +++ b/src/subscriptions/context.ts @@ -1,7 +1,7 @@ import type { Logger } from '@makerx/node-common' import { randomUUID } from 'crypto' import type { IncomingMessage } from 'http' -import { pick } from 'lodash' +import { pick } from 'es-toolkit/compat' import { User } from '../User' import type { CreateRequestLogger, GraphQLContext, JwtPayload, RequestInfo } from '../context' import { extractTokenFromConnectionParams } from './utils' 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' From f13a77ab942529307e07db1d916a8576fe685266 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Sun, 19 Apr 2026 14:05:45 +0800 Subject: [PATCH 02/14] bump major version, fix vulns --- package-lock.json | 1028 +++++++++++++++++++++++++++++++++++---------- package.json | 28 +- 2 files changed, 828 insertions(+), 228 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19c0984..bed093f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,22 +38,22 @@ "eslint-plugin-prettier": "^5.5.4", "express": "^5.1.0", "graphql-shield": "^7.6.5", - "graphql-ws": "^5.14.3", + "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": "*", + "es-toolkit": ">=1", "express": "*", "graphql": "*", "graphql-shield": "*", @@ -1096,29 +1096,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 +1398,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 +1505,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 +1519,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 +1533,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 +1547,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 +1561,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 +1575,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 +1589,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 +1606,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 +1623,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 +1640,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 +1657,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 +1691,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 +1725,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 +1742,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 +1759,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 +1776,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 +1793,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 +1838,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 +1852,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 +1866,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 +1880,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 +2252,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 +2262,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" @@ -2506,9 +2564,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 +2748,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": { @@ -3820,6 +3882,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", @@ -3906,9 +3985,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" }, @@ -4092,15 +4171,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" @@ -4128,17 +4208,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" @@ -4398,9 +4501,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": { @@ -4408,6 +4511,10 @@ }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -4836,10 +4943,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" }, @@ -4955,16 +5063,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", @@ -5142,9 +5252,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": { @@ -5680,9 +5790,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": { @@ -5715,10 +5825,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" }, @@ -5871,9 +5982,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": { @@ -6131,9 +6242,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": { @@ -6147,28 +6258,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" } }, @@ -6815,15 +6929,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", @@ -6852,9 +6967,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": { @@ -6862,9 +6977,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": { @@ -6906,13 +7022,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" @@ -7034,9 +7150,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": { @@ -7357,13 +7473,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", @@ -7454,6 +7570,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", @@ -7473,9 +8073,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": { @@ -7559,9 +8159,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": { @@ -7691,9 +8291,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": { diff --git a/package.json b/package.json index 64c875f..1226fc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makerx/graphql-core", - "version": "2.3.0", + "version": "3.0.0", "private": false, "description": "A set of core GraphQL utilities that MakerX uses to build GraphQL APIs", "author": "MakerX", @@ -39,11 +39,11 @@ }, "peerDependencies": { "@apollo/client": "*", + "es-toolkit": ">=1", "express": "*", "graphql": "*", "graphql-shield": "*", "graphql-ws": "*", - "es-toolkit": "*", "ws": "*" }, "peerDependenciesMeta": { @@ -58,40 +58,40 @@ } }, "devDependencies": { + "@apollo/client": "^3.8.10", "@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", - "es-toolkit": "^1.39.10", + "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" } } From 11de1e4d941f6aefc4316edadc7643bc1e24792b Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Mon, 20 Apr 2026 11:58:00 +0800 Subject: [PATCH 03/14] switch to federeated npm publish --- .github/workflows/publish.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b9f1898..c851d1c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,20 +8,18 @@ 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 From 675accf6df202e8d4b3cc3a508c3cccf4403f771 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 08:41:38 +0800 Subject: [PATCH 04/14] Add second user parameter to CreateRequestLogger --- src/context.ts | 8 +++++--- src/subscriptions/context.ts | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/context.ts b/src/context.ts index f46ee97..c897984 100644 --- a/src/context.ts +++ b/src/context.ts @@ -71,7 +71,7 @@ export interface ContextInput { event?: LambdaEvent } export type CreateContext = (input: ContextInput) => Promise -export type CreateRequestLogger = (requestMetadata: Record) => Logger +export type CreateRequestLogger = (requestMetadata: Record, user: User | undefined) => Logger export type AugmentRequestInfo = (input: ContextInput) => Record export interface CreateContextConfig { @@ -131,6 +131,8 @@ export const createContextFactory = Date: Tue, 21 Apr 2026 09:58:36 +0800 Subject: [PATCH 05/14] refactor request handling for Express Request + http subscription IncomingMessage --- src/context.ts | 19 +- src/index.ts | 7 +- src/request-utils.spec.ts | 330 +++++++++++++++++++++++++++++++ src/request-utils.ts | 66 +++++++ src/shield.ts | 5 +- src/subscriptions/context.ts | 16 +- src/{models.ts => type-utils.ts} | 0 7 files changed, 407 insertions(+), 36 deletions(-) create mode 100644 src/request-utils.spec.ts create mode 100644 src/request-utils.ts rename src/{models.ts => type-utils.ts} (100%) diff --git a/src/context.ts b/src/context.ts index c897984..b3096d1 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 type { Request } from 'express' import { pick } from 'es-toolkit/compat' +import type { Request } from 'express' +import { buildBaseRequestInfo } from './request-utils' import { User } from './User' export interface GraphQLContext< @@ -22,6 +22,7 @@ export interface BaseRequestInfo extends Record { protocol: 'http' | 'https' | 'ws' host: string method: string + baseUrl: string url: string origin: string referer?: string @@ -83,20 +84,6 @@ export interface CreateContextConfig Record | Promise> } -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, diff --git a/src/index.ts b/src/index.ts index 303ccd6..dc74edf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ export * from './context' -export * from './User' -export * from './utils' export * from './logging' +export * from './request-utils' export * from './schema-util' export * from './shield' -export * from './models' +export * from './type-utils' +export * from './User' +export * from './utils' diff --git a/src/request-utils.spec.ts b/src/request-utils.spec.ts new file mode 100644 index 0000000..da6029e --- /dev/null +++ b/src/request-utils.spec.ts @@ -0,0 +1,330 @@ +import type { Request } from 'express' +import type { IncomingMessage } from 'http' +import { describe, expect, it } from 'vitest' +import { buildBaseRequestInfo, buildConnectRequestInfo, connectRequestBaseUrl, requestBaseUrl } from './request-utils' + +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', + protocol: 'https', + host: 'api.example.com: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('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('falls back to req.hostname for host when x-forwarded-host absent', () => { + const req = makeExpressRequest({ hostname: 'fallback.example.com' }) + expect(buildBaseRequestInfo(req).host).toBe('fallback.example.com') + }) + + 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({ + protocol: 'http', + host: 'localhost', + 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 ws protocol', () => { + 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', + protocol: 'ws', + host: 'api.example.com', + 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('falls back to headers.host for host field when x-forwarded-host absent', () => { + const req = makeIncomingMessage({ headers: { host: 'direct.example.com:8080' } }) + expect(buildConnectRequestInfo(req).host).toBe('direct.example.com: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({ + protocol: 'ws', + host: 'localhost: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-utils.ts b/src/request-utils.ts new file mode 100644 index 0000000..a287f9e --- /dev/null +++ b/src/request-utils.ts @@ -0,0 +1,66 @@ +import { randomUUID } from 'crypto' +import type { Request } from 'express' +import type { IncomingMessage } from 'http' +import type { TLSSocket } from 'tls' +import type { BaseRequestInfo } from './context' + +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 resolveForwardedHost = (req: IncomingMessage): string | undefined => firstHeaderValue(req.headers['x-forwarded-host']) + +export const requestBaseUrl = (req: Request): string => { + const hostHeader = resolveForwardedHost(req) ?? req.headers.host + const { hostname, port } = hostHeader ? parseHostHeader(hostHeader) : { hostname: req.hostname, port: undefined } + return formatBaseUrl(req.protocol, hostname, port) +} + +export const connectRequestBaseUrl = (req: IncomingMessage): string => { + const protocol = firstHeaderValue(req.headers['x-forwarded-proto']) ?? (isEncryptedSocket(req) ? 'https' : 'http') + const hostHeader = resolveForwardedHost(req) ?? req.headers.host + if (!hostHeader) throw new Error('Cannot determine base URL from websocket connect request') + const { hostname, port } = parseHostHeader(hostHeader) + return formatBaseUrl(protocol, hostname, 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 => ({ + ...buildSharedRequestInfo(req), + protocol: req.protocol as 'http' | 'https', + host: resolveForwardedHost(req) ?? req.hostname ?? '', + baseUrl: requestBaseUrl(req), + url: req.originalUrl, +}) + +export const buildConnectRequestInfo = (req: IncomingMessage): BaseRequestInfo => ({ + ...buildSharedRequestInfo(req), + protocol: 'ws', + host: resolveForwardedHost(req) ?? req.headers.host ?? '', + baseUrl: connectRequestBaseUrl(req), + 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.ts b/src/subscriptions/context.ts index bc0cbb5..93c43a5 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 type { IncomingMessage } from 'http' import { pick } from 'es-toolkit/compat' import { User } from '../User' import type { CreateRequestLogger, GraphQLContext, JwtPayload, RequestInfo } from '../context' +import { buildConnectRequestInfo } from '../request-utils' import { extractTokenFromConnectionParams } from './utils' export interface SubscriptionContextInput { @@ -37,21 +37,9 @@ export const createSubscriptionContextFactory = { 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), } 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 From 0a55322ee9c9456f95944a2d9a6834d4c48e23d3 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 10:01:12 +0800 Subject: [PATCH 06/14] add source (http or subscription) and widen protocol to include ws/wss --- src/context.ts | 17 +---------------- src/request-utils.spec.ts | 39 +++++++++++++++++++++++++++++++++++++-- src/request-utils.ts | 31 +++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/context.ts b/src/context.ts index b3096d1..7313059 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,7 +1,7 @@ import type { Logger } from '@makerx/node-common' import { pick } from 'es-toolkit/compat' import type { Request } from 'express' -import { buildBaseRequestInfo } from './request-utils' +import { buildBaseRequestInfo, type BaseRequestInfo } from './request-utils' import { User } from './User' export interface GraphQLContext< @@ -17,21 +17,6 @@ export interface GraphQLContext< export type AnyGraphqlContext = GraphQLContext -export interface BaseRequestInfo extends Record { - requestId: string - protocol: 'http' | 'https' | 'ws' - host: string - method: string - baseUrl: string - url: string - origin: string - referer?: string - correlationId?: string - arrLogId?: string - clientIp?: string - userAgent?: string -} - export interface LambdaContext { functionName?: string awsRequestId?: string diff --git a/src/request-utils.spec.ts b/src/request-utils.spec.ts index da6029e..a86f299 100644 --- a/src/request-utils.spec.ts +++ b/src/request-utils.spec.ts @@ -179,6 +179,7 @@ describe('buildBaseRequestInfo', () => { expect(buildBaseRequestInfo(req)).toEqual({ requestId: 'req-abc', + source: 'http', protocol: 'https', host: 'api.example.com:8443', method: 'POST', @@ -193,6 +194,10 @@ describe('buildBaseRequestInfo', () => { }) }) + 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) @@ -240,6 +245,7 @@ describe('buildBaseRequestInfo', () => { const info = buildBaseRequestInfo(req) expect(info).toMatchObject({ + source: 'http', protocol: 'http', host: 'localhost', baseUrl: 'http://localhost:4000', @@ -256,7 +262,7 @@ describe('buildBaseRequestInfo', () => { }) describe('buildConnectRequestInfo', () => { - it('builds full request info with ws protocol', () => { + it('builds full request info with wss protocol when encrypted', () => { const req = makeIncomingMessage({ method: 'GET', url: '/graphql', @@ -276,7 +282,8 @@ describe('buildConnectRequestInfo', () => { expect(buildConnectRequestInfo(req)).toEqual({ requestId: 'req-ws', - protocol: 'ws', + source: 'subscription', + protocol: 'wss', host: 'api.example.com', method: 'GET', baseUrl: 'https://api.example.com', @@ -290,6 +297,33 @@ describe('buildConnectRequestInfo', () => { }) }) + 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('falls back to headers.host for host field when x-forwarded-host absent', () => { const req = makeIncomingMessage({ headers: { host: 'direct.example.com:8080' } }) expect(buildConnectRequestInfo(req).host).toBe('direct.example.com:8080') @@ -315,6 +349,7 @@ describe('buildConnectRequestInfo', () => { const info = buildConnectRequestInfo(req) expect(info).toMatchObject({ + source: 'subscription', protocol: 'ws', host: 'localhost:4000', baseUrl: 'http://localhost:4000', diff --git a/src/request-utils.ts b/src/request-utils.ts index a287f9e..b5a50a8 100644 --- a/src/request-utils.ts +++ b/src/request-utils.ts @@ -2,7 +2,22 @@ import { randomUUID } from 'crypto' import type { Request } from 'express' import type { IncomingMessage } from 'http' import type { TLSSocket } from 'tls' -import type { BaseRequestInfo } from './context' + +export interface BaseRequestInfo extends Record { + requestId: string + source: 'http' | 'subscription' + protocol: 'http' | 'https' | 'ws' | 'wss' + host: string + 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) @@ -22,6 +37,13 @@ const parseHostHeader = (hostHeader: string): { hostname: string; port: number | 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']) export const requestBaseUrl = (req: Request): string => { @@ -31,11 +53,10 @@ export const requestBaseUrl = (req: Request): string => { } export const connectRequestBaseUrl = (req: IncomingMessage): string => { - const protocol = firstHeaderValue(req.headers['x-forwarded-proto']) ?? (isEncryptedSocket(req) ? 'https' : 'http') const hostHeader = resolveForwardedHost(req) ?? req.headers.host if (!hostHeader) throw new Error('Cannot determine base URL from websocket connect request') const { hostname, port } = parseHostHeader(hostHeader) - return formatBaseUrl(protocol, hostname, port) + return formatBaseUrl(isEncryptedConnect(req) ? 'https' : 'http', hostname, port) } const buildSharedRequestInfo = (req: IncomingMessage) => ({ @@ -51,6 +72,7 @@ const buildSharedRequestInfo = (req: IncomingMessage) => ({ export const buildBaseRequestInfo = (req: Request): BaseRequestInfo => ({ ...buildSharedRequestInfo(req), + source: 'http', protocol: req.protocol as 'http' | 'https', host: resolveForwardedHost(req) ?? req.hostname ?? '', baseUrl: requestBaseUrl(req), @@ -59,7 +81,8 @@ export const buildBaseRequestInfo = (req: Request): BaseRequestInfo => ({ export const buildConnectRequestInfo = (req: IncomingMessage): BaseRequestInfo => ({ ...buildSharedRequestInfo(req), - protocol: 'ws', + source: 'subscription', + protocol: isEncryptedConnect(req) ? 'wss' : 'ws', host: resolveForwardedHost(req) ?? req.headers.host ?? '', baseUrl: connectRequestBaseUrl(req), url: req.url ?? '', From 346afcd7f73a08ff723b64752d23ce0673c787a6 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 10:01:49 +0800 Subject: [PATCH 07/14] rename --- src/context.ts | 2 +- src/index.ts | 2 +- src/{request-utils.spec.ts => request-info.spec.ts} | 2 +- src/{request-utils.ts => request-info.ts} | 0 src/subscriptions/context.ts | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{request-utils.spec.ts => request-info.spec.ts} (99%) rename src/{request-utils.ts => request-info.ts} (100%) diff --git a/src/context.ts b/src/context.ts index 7313059..2613243 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,7 +1,7 @@ import type { Logger } from '@makerx/node-common' import { pick } from 'es-toolkit/compat' import type { Request } from 'express' -import { buildBaseRequestInfo, type BaseRequestInfo } from './request-utils' +import { buildBaseRequestInfo, type BaseRequestInfo } from './request-info' import { User } from './User' export interface GraphQLContext< diff --git a/src/index.ts b/src/index.ts index dc74edf..d356dc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export * from './context' export * from './logging' -export * from './request-utils' +export * from './request-info' export * from './schema-util' export * from './shield' export * from './type-utils' diff --git a/src/request-utils.spec.ts b/src/request-info.spec.ts similarity index 99% rename from src/request-utils.spec.ts rename to src/request-info.spec.ts index a86f299..7327f84 100644 --- a/src/request-utils.spec.ts +++ b/src/request-info.spec.ts @@ -1,7 +1,7 @@ import type { Request } from 'express' import type { IncomingMessage } from 'http' import { describe, expect, it } from 'vitest' -import { buildBaseRequestInfo, buildConnectRequestInfo, connectRequestBaseUrl, requestBaseUrl } from './request-utils' +import { buildBaseRequestInfo, buildConnectRequestInfo, connectRequestBaseUrl, requestBaseUrl } from './request-info' type Headers = Record diff --git a/src/request-utils.ts b/src/request-info.ts similarity index 100% rename from src/request-utils.ts rename to src/request-info.ts diff --git a/src/subscriptions/context.ts b/src/subscriptions/context.ts index 93c43a5..6c4eadf 100644 --- a/src/subscriptions/context.ts +++ b/src/subscriptions/context.ts @@ -1,9 +1,9 @@ import type { Logger } from '@makerx/node-common' -import type { IncomingMessage } from 'http' import { pick } from 'es-toolkit/compat' +import type { IncomingMessage } from 'http' import { User } from '../User' import type { CreateRequestLogger, GraphQLContext, JwtPayload, RequestInfo } from '../context' -import { buildConnectRequestInfo } from '../request-utils' +import { buildConnectRequestInfo } from '../request-info' import { extractTokenFromConnectionParams } from './utils' export interface SubscriptionContextInput { From 60a8d75be5e32880a125084c7f9d63d80894823c Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 10:47:45 +0800 Subject: [PATCH 08/14] update types to consider user arg for requestLogger and augmented vs non-augmented context --- README.md | 29 ++++++++++++++++++----------- src/context.ts | 24 +++++++++++------------- src/subscriptions/context.ts | 28 ++++++++++++++++------------ 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 2ca168f..8d73f14 100644 --- a/README.md +++ b/README.md @@ -20,31 +20,33 @@ Note: See explanation on \*Express peer dependency below. 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({ +// TUser is inferred from `createUser`, TAugment is inferred from `augmentContext`'s return type +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 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 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 + // provide a createUser function (or omit to use the default User based on JWT claims) + createUser: defaultCreateUser, + // 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 @@ -140,14 +142,16 @@ 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 } + + const augmentContext = (context: GraphQLContextBase): 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), @@ -156,13 +160,16 @@ This library includes a `subscriptions` module to provide simple setup using the }) // create a context using graphql-ws Server#context callback input - const createSubscriptionContext = createSubscriptionContextFactory({ - claimsToLog + const createSubscriptionContext = createSubscriptionContextFactory({ + claimsToLog, requestInfoToLog, requestLogger: (requestMetadata) => logger.child(requestMetadata), 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. diff --git a/src/context.ts b/src/context.ts index 2613243..c20595e 100644 --- a/src/context.ts +++ b/src/context.ts @@ -57,26 +57,26 @@ export interface ContextInput { event?: LambdaEvent } export type CreateContext = (input: ContextInput) => Promise -export type CreateRequestLogger = (requestMetadata: Record, user: User | undefined) => Logger +export type CreateRequestLogger = (requestMetadata: Record, user: TUser) => Logger export type AugmentRequestInfo = (input: ContextInput) => Record -export interface CreateContextConfig { - requestLogger: CreateRequestLogger | Logger +export interface CreateContextConfig = Record> { + requestLogger: CreateRequestLogger | Logger augmentRequestInfo?: AugmentRequestInfo claimsToLog?: string[] - createUser: CreateUser> + createUser: CreateUser requestInfoToLog?: Array - augmentContext?: (context: TContext) => Record | Promise> + augmentContext?: (context: GraphQLContext) => TAugment | Promise } -export const createContextFactory = ({ +export const createContextFactory = = Record>({ requestLogger, augmentRequestInfo, claimsToLog, createUser, requestInfoToLog, augmentContext, -}: CreateContextConfig): CreateContext => { +}: CreateContextConfig): CreateContext & TAugment> => { // the function that creates the GraphQL context return async (input: ContextInput) => { const { req, claims, context } = input @@ -103,7 +103,7 @@ export const createContextFactory = = { requestInfo, logger, 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/context.ts b/src/subscriptions/context.ts index 6c4eadf..374b57f 100644 --- a/src/subscriptions/context.ts +++ b/src/subscriptions/context.ts @@ -16,23 +16,29 @@ export type CreateSubscriptionUser = (input: SubscriptionC export type CreateSubscriptionContext = (input: SubscriptionContextInput) => Promise export type AugmentSubscriptionRequestInfo = (input: SubscriptionContextInput) => Record -export interface CreateSubscriptionContextConfig { - requestLogger: CreateRequestLogger | Logger +export interface CreateSubscriptionContextConfig< + TUser = User | undefined, + TAugment extends Record = Record, +> { + requestLogger: CreateRequestLogger | Logger augmentRequestInfo?: AugmentSubscriptionRequestInfo claimsToLog?: string[] - createUser?: CreateSubscriptionUser + createUser?: CreateSubscriptionUser requestInfoToLog?: Array - augmentContext?: (context: TContext) => Record | Promise> + augmentContext?: (context: GraphQLContext) => TAugment | Promise } -export const createSubscriptionContextFactory = ({ +export const createSubscriptionContextFactory = < + TUser = User | undefined, + TAugment extends Record = Record, +>({ requestLogger, augmentRequestInfo, claimsToLog, - createUser = defaultCreateUser, + createUser = defaultCreateUser as CreateSubscriptionUser, requestInfoToLog, augmentContext, -}: CreateSubscriptionContextConfig): CreateSubscriptionContext => { +}: CreateSubscriptionContextConfig): CreateSubscriptionContext & TAugment> => { // the function that creates the GraphQL context return async (input: SubscriptionContextInput) => { const { connectRequest: req, claims } = input @@ -58,18 +64,16 @@ export const createSubscriptionContextFactory = = { requestInfo, logger, 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 } } From 172aef71d0d5b79c9cc6b1bfbd308426589dd873 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 11:36:23 +0800 Subject: [PATCH 09/14] more refine --- README.md | 75 +++++++++++++--- src/context.spec.ts | 140 ++++++++++++++++++++++++++++++ src/context.ts | 47 ++++++---- src/subscriptions/context.spec.ts | 127 +++++++++++++++++++++++++++ src/subscriptions/context.ts | 40 +++++---- 5 files changed, 379 insertions(+), 50 deletions(-) create mode 100644 src/context.spec.ts create mode 100644 src/subscriptions/context.spec.ts diff --git a/README.md b/README.md index 8d73f14..69e47be 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ 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 you opt in with `defaultCreateUser`) +- anything else you wish to add to the context via `augmentContext` ### Step 1 - Define your context + creation @@ -29,14 +29,16 @@ type ExtraContext = { // configure the createContext function // TUser is inferred from `createUser`, TAugment is inferred from `augmentContext`'s return type 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 + // 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), - // provide a createUser function (or omit to use the default User based on JWT claims) - createUser: defaultCreateUser, + // 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) @@ -85,6 +87,51 @@ 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` | Prefers `x-forwarded-host`, falls back to the `Host` header / `req.hostname`. | +| `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. + +> **Note on `host` in v3:** `requestInfo.host` now resolves consistently across Express `Request` and the websocket connect `IncomingMessage` — both paths always trust the `x-forwarded-host` / `Host` headers rather than Express's `req.hostname`. As a result, when a proxy forwards a port in the `Host` / `x-forwarded-host` header, that port now appears on `host` (pre-v3 Express requests used `req.hostname`, which strips the port). + +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. @@ -144,7 +191,9 @@ This library includes a `subscriptions` module to provide simple setup using the ```ts type ExtraContext = { services: Services; dataSource: DataSource; dataLoaders: DataLoaders } - const augmentContext = (context: GraphQLContextBase): ExtraContext => { + // 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 } @@ -154,7 +203,7 @@ This library includes a `subscriptions` module to provide simple setup using the 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, }) @@ -163,7 +212,7 @@ This library includes a `subscriptions` module to provide simple setup using the 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, }) 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 c20595e..f906e69 100644 --- a/src/context.ts +++ b/src/context.ts @@ -57,27 +57,38 @@ export interface ContextInput { event?: LambdaEvent } export type CreateContext = (input: ContextInput) => Promise -export type CreateRequestLogger = (requestMetadata: Record, user: TUser) => Logger +export type CreateRequestLogger = ( + requestMetadata: Record, + user: TUser, +) => TLogger export type AugmentRequestInfo = (input: ContextInput) => Record -export interface CreateContextConfig = Record> { - 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: GraphQLContext) => TAugment | Promise -} + 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 createContextFactory = = Record>({ - requestLogger, - augmentRequestInfo, - claimsToLog, - createUser, - requestInfoToLog, - augmentContext, -}: CreateContextConfig): CreateContext & TAugment> => { - // the function that creates the GraphQL context return async (input: ContextInput) => { const { req, claims, context } = input @@ -106,7 +117,7 @@ export const createContextFactory = = {} @@ -118,7 +129,7 @@ export const createContextFactory = = { + const graphqlContext: GraphQLContext = { requestInfo, logger, user, @@ -127,7 +138,7 @@ export const createContextFactory = & TAugment + return augmentedGraphQLContext as GraphQLContext & TAugment } } 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 374b57f..8cc85f7 100644 --- a/src/subscriptions/context.ts +++ b/src/subscriptions/context.ts @@ -12,34 +12,36 @@ 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< +// `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, -> { - requestLogger: CreateRequestLogger | Logger + TLogger extends Logger = Logger, +> = { + requestLogger: CreateRequestLogger | TLogger augmentRequestInfo?: AugmentSubscriptionRequestInfo claimsToLog?: string[] - createUser?: CreateSubscriptionUser requestInfoToLog?: Array - augmentContext?: (context: GraphQLContext) => TAugment | Promise -} + augmentContext?: (context: GraphQLContext) => TAugment | Promise +} & ([User | undefined] extends [TUser] ? { createUser?: CreateSubscriptionUser } : { createUser: CreateSubscriptionUser }) export const createSubscriptionContextFactory = < TUser = User | undefined, TAugment extends Record = Record, ->({ - requestLogger, - augmentRequestInfo, - claimsToLog, - createUser = defaultCreateUser as CreateSubscriptionUser, - requestInfoToLog, - augmentContext, -}: CreateSubscriptionContextConfig): CreateSubscriptionContext & TAugment> => { - // the function that creates the GraphQL context + 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 + return async (input: SubscriptionContextInput) => { const { connectRequest: req, claims } = input @@ -52,7 +54,7 @@ export const createSubscriptionContextFactory = < 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 = {} @@ -64,7 +66,7 @@ export const createSubscriptionContextFactory = < logger = requestLogger(requestLoggerMetadata, user) } else logger = requestLogger - const graphqlContext: GraphQLContext = { + const graphqlContext: GraphQLContext = { requestInfo, logger, user, @@ -73,7 +75,7 @@ export const createSubscriptionContextFactory = < const augmentedGraphQLContext = augmentContext ? { ...graphqlContext, ...(await augmentContext(graphqlContext)) } : graphqlContext - return augmentedGraphQLContext as GraphQLContext & TAugment + return augmentedGraphQLContext as GraphQLContext & TAugment } } From d72e058c0eec801ad839a057ed144ded5db94c79 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 11:37:04 +0800 Subject: [PATCH 10/14] make v3 beta --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bed093f..e459edf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "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" diff --git a/package.json b/package.json index 1226fc9..a0d3b0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makerx/graphql-core", - "version": "3.0.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", From 3c997a7befb5290a16b58bfc477534e0d290cf09 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 12:39:25 +0800 Subject: [PATCH 11/14] add beta tag for v3 beta --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c851d1c..6de4db5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,3 +23,4 @@ jobs: uses: makerxstudio/shared-config/.github/workflows/node-trusted-publish.yml@main with: access: public + tags: beta From 163538457f8147107b4756d4dea4fa65b39f2608 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 12:40:12 +0800 Subject: [PATCH 12/14] address review comments, add separate port member to request info keeping backwards compat for host to exclude the port --- README.md | 7 ++--- src/request-info.spec.ts | 37 +++++++++++++++++++---- src/request-info.ts | 64 +++++++++++++++++++++++++--------------- 3 files changed, 75 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 69e47be..bc1708e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Note: See explanation on \*Express peer dependency below. - `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 you opt in with `defaultCreateUser`) +- `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 @@ -96,7 +96,8 @@ const graphqlServer = createServer({ | `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` | Prefers `x-forwarded-host`, falls back to the `Host` header / `req.hostname`. | +| `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. | @@ -108,8 +109,6 @@ const graphqlServer = createServer({ You can add more via `augmentRequestInfo(input)`. Lambda deployments also get `functionName` and `awsRequestId` when a `LambdaContext` is supplied. -> **Note on `host` in v3:** `requestInfo.host` now resolves consistently across Express `Request` and the websocket connect `IncomingMessage` — both paths always trust the `x-forwarded-host` / `Host` headers rather than Express's `req.hostname`. As a result, when a proxy forwards a port in the `Host` / `x-forwarded-host` header, that port now appears on `host` (pre-v3 Express requests used `req.hostname`, which strips the port). - Helpers are exported for custom wiring: `buildBaseRequestInfo(req)` (Express), `buildConnectRequestInfo(req)` (websocket `IncomingMessage`), and `requestBaseUrl` / `connectRequestBaseUrl`. ## Request logger diff --git a/src/request-info.spec.ts b/src/request-info.spec.ts index 7327f84..25cdd5d 100644 --- a/src/request-info.spec.ts +++ b/src/request-info.spec.ts @@ -181,7 +181,8 @@ describe('buildBaseRequestInfo', () => { requestId: 'req-abc', source: 'http', protocol: 'https', - host: 'api.example.com:8443', + host: 'api.example.com', + port: 8443, method: 'POST', baseUrl: 'https://api.example.com:8443', url: '/graphql?q=1', @@ -224,9 +225,28 @@ describe('buildBaseRequestInfo', () => { expect(info.userAgent).toBeUndefined() }) - it('falls back to req.hostname for host when x-forwarded-host absent', () => { + 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' }) - expect(buildBaseRequestInfo(req).host).toBe('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)', () => { @@ -248,6 +268,7 @@ describe('buildBaseRequestInfo', () => { source: 'http', protocol: 'http', host: 'localhost', + port: 4000, baseUrl: 'http://localhost:4000', url: '/graphql', method: 'POST', @@ -285,6 +306,7 @@ describe('buildConnectRequestInfo', () => { source: 'subscription', protocol: 'wss', host: 'api.example.com', + port: undefined, method: 'GET', baseUrl: 'https://api.example.com', url: '/graphql', @@ -324,9 +346,11 @@ describe('buildConnectRequestInfo', () => { expect(info.baseUrl).toBe('https://example.com') }) - it('falls back to headers.host for host field when x-forwarded-host absent', () => { + it('splits headers.host into host + port when x-forwarded-host absent', () => { const req = makeIncomingMessage({ headers: { host: 'direct.example.com:8080' } }) - expect(buildConnectRequestInfo(req).host).toBe('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', () => { @@ -351,7 +375,8 @@ describe('buildConnectRequestInfo', () => { expect(info).toMatchObject({ source: 'subscription', protocol: 'ws', - host: 'localhost:4000', + host: 'localhost', + port: 4000, baseUrl: 'http://localhost:4000', url: '/graphql', origin: 'http://localhost:4000', diff --git a/src/request-info.ts b/src/request-info.ts index b5a50a8..70789c5 100644 --- a/src/request-info.ts +++ b/src/request-info.ts @@ -8,6 +8,7 @@ export interface BaseRequestInfo extends Record { source: 'http' | 'subscription' protocol: 'http' | 'https' | 'ws' | 'wss' host: string + port?: number method: string baseUrl: string url: string @@ -46,17 +47,24 @@ const isEncryptedConnect = (req: IncomingMessage): boolean => { 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 hostHeader = resolveForwardedHost(req) ?? req.headers.host - const { hostname, port } = hostHeader ? parseHostHeader(hostHeader) : { hostname: req.hostname, port: undefined } - return formatBaseUrl(req.protocol, hostname, port) + const { host, port } = resolveHostAndPort(req, req.hostname) + return formatBaseUrl(req.protocol, host, port) } export const connectRequestBaseUrl = (req: IncomingMessage): string => { - const hostHeader = resolveForwardedHost(req) ?? req.headers.host - if (!hostHeader) throw new Error('Cannot determine base URL from websocket connect request') - const { hostname, port } = parseHostHeader(hostHeader) - return formatBaseUrl(isEncryptedConnect(req) ? 'https' : 'http', hostname, port) + 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) => ({ @@ -70,20 +78,30 @@ const buildSharedRequestInfo = (req: IncomingMessage) => ({ userAgent: req.headers['user-agent']?.toString() ?? undefined, }) -export const buildBaseRequestInfo = (req: Request): BaseRequestInfo => ({ - ...buildSharedRequestInfo(req), - source: 'http', - protocol: req.protocol as 'http' | 'https', - host: resolveForwardedHost(req) ?? req.hostname ?? '', - baseUrl: requestBaseUrl(req), - url: req.originalUrl, -}) +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 => ({ - ...buildSharedRequestInfo(req), - source: 'subscription', - protocol: isEncryptedConnect(req) ? 'wss' : 'ws', - host: resolveForwardedHost(req) ?? req.headers.host ?? '', - baseUrl: connectRequestBaseUrl(req), - url: req.url ?? '', -}) +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 ?? '', + } +} From e432f8c1b5c967483ec29d51ac135ae277375c51 Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 12:45:22 +0800 Subject: [PATCH 13/14] improve requestInfoToLog typing --- src/context.ts | 9 ++++++++- src/subscriptions/context.ts | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/context.ts b/src/context.ts index f906e69..79e85de 100644 --- a/src/context.ts +++ b/src/context.ts @@ -25,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 @@ -73,7 +80,7 @@ export type CreateContextConfig< requestLogger: CreateRequestLogger | TLogger augmentRequestInfo?: AugmentRequestInfo claimsToLog?: string[] - requestInfoToLog?: Array + requestInfoToLog?: Array augmentContext?: (context: GraphQLContext) => TAugment | Promise } & ([User | undefined] extends [TUser] ? { createUser?: CreateUser } : { createUser: CreateUser }) diff --git a/src/subscriptions/context.ts b/src/subscriptions/context.ts index 8cc85f7..c6282a4 100644 --- a/src/subscriptions/context.ts +++ b/src/subscriptions/context.ts @@ -2,7 +2,7 @@ import type { Logger } from '@makerx/node-common' import { pick } from 'es-toolkit/compat' import type { IncomingMessage } from 'http' 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' @@ -26,7 +26,7 @@ export type CreateSubscriptionContextConfig< requestLogger: CreateRequestLogger | TLogger augmentRequestInfo?: AugmentSubscriptionRequestInfo claimsToLog?: string[] - requestInfoToLog?: Array + requestInfoToLog?: Array augmentContext?: (context: GraphQLContext) => TAugment | Promise } & ([User | undefined] extends [TUser] ? { createUser?: CreateSubscriptionUser } : { createUser: CreateSubscriptionUser }) From b3972ed8093939d61e094b90206be143f8174b9a Mon Sep 17 00:00:00 2001 From: Sam Curry Date: Tue, 21 Apr 2026 13:07:08 +0800 Subject: [PATCH 14/14] fix for ESM, remove /testing module and apollo client deps --- .tstoolkitrc.ts | 10 +- README.md | 79 -------------- package-lock.json | 255 ---------------------------------------------- package.json | 5 - rollup.config.ts | 2 +- src/index.ts | 1 - src/testing.ts | 76 -------------- 7 files changed, 6 insertions(+), 422 deletions(-) delete mode 100644 src/testing.ts 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 bc1708e..239d5af 100644 --- a/README.md +++ b/README.md @@ -259,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 e459edf..9426db8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@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", @@ -52,7 +51,6 @@ "node": ">=20.0" }, "peerDependencies": { - "@apollo/client": "*", "es-toolkit": ">=1", "express": "*", "graphql": "*", @@ -61,9 +59,6 @@ "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", @@ -2478,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", @@ -4340,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", @@ -4451,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", @@ -5095,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", @@ -5536,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", @@ -5617,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", @@ -5941,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", @@ -6061,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", @@ -6137,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", @@ -6886,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", @@ -7234,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", @@ -8387,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 a0d3b0c..4ba4ea9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "@makerx/node-common": "^1.5.0" }, "peerDependencies": { - "@apollo/client": "*", "es-toolkit": ">=1", "express": "*", "graphql": "*", @@ -47,9 +46,6 @@ "ws": "*" }, "peerDependenciesMeta": { - "@apollo/client": { - "optional": true - }, "graphql-ws": { "optional": true }, @@ -58,7 +54,6 @@ } }, "devDependencies": { - "@apollo/client": "^3.8.10", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.36.0", "@makerx/eslint-config": "4.2.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/index.ts b/src/index.ts index d356dc2..49b5693 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ export * from './context' export * from './logging' export * from './request-info' export * from './schema-util' -export * from './shield' export * from './type-utils' export * from './User' export * from './utils' 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, - })