From 172789d8da2f335ee2acd082100ae47a07dc5a25 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 20:47:02 +0100 Subject: [PATCH 01/11] feat!: modernize project for Node 24 + ESM + TypeScript --- .changeset/config.json | 11 + .changeset/major-modernization.md | 11 + .github/workflows/ci.yml | 39 + .github/workflows/release.yml | 34 + .gitignore | 10 + .yarnrc.yml | 1 + README.md | 70 +- bin/notify-url-proxy.ts | 83 + biome.json | 25 + example.js | 4 - index.js | 158 -- package-lock.json | 380 ----- package.json | 43 +- src/index.ts | 243 +++ test/index.test.ts | 39 + tsconfig.json | 19 + vitest.config.ts | 8 + yarn.lock | 2560 +++++++++++++++++++++++++++++ 18 files changed, 3189 insertions(+), 549 deletions(-) create mode 100644 .changeset/config.json create mode 100644 .changeset/major-modernization.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .yarnrc.yml create mode 100755 bin/notify-url-proxy.ts create mode 100644 biome.json delete mode 100644 example.js delete mode 100644 index.js delete mode 100644 package-lock.json create mode 100644 src/index.ts create mode 100644 test/index.test.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts create mode 100644 yarn.lock diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..2be13d4 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/major-modernization.md b/.changeset/major-modernization.md new file mode 100644 index 0000000..5ea99dd --- /dev/null +++ b/.changeset/major-modernization.md @@ -0,0 +1,11 @@ +--- +"transloadit-notify-url-proxy": major +--- + +Modernize the package with breaking runtime and tooling changes: + +- switch to ESM and TypeScript source in `src/` +- require Node.js 24+ with native TypeScript execution +- add CLI bin entrypoint (`notify-url-proxy`) +- replace deprecated dependencies (`request`, `underscore`) with native APIs +- migrate project tooling to Yarn 4, Biome, Vitest, GitHub Actions, and Changesets diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..10013be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + check: + name: Lint + Typecheck + Unit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: corepack enable + - run: yarn install --immutable + - run: yarn check + + pack: + name: Package Smoke Test + runs-on: ubuntu-latest + needs: + - check + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: corepack enable + - run: yarn install --immutable + - run: yarn pack + - uses: actions/upload-artifact@v4 + with: + name: package + path: '*.tgz' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..18d43c8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + release: + name: Changesets Release + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + - run: npm install -g npm@11.5.1 + - run: corepack enable + - run: corepack yarn + - uses: changesets/action@v1 + with: + version: corepack yarn changeset version + publish: corepack yarn changeset publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_CONFIG_PROVENANCE: 'true' + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 8ff1c40..b2561b4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,13 @@ node-flame.svg npm-debug.log npm-debug.log* tmp/ + +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +.pnp.* +coverage/ +dist/ diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/README.md b/README.md index ab9675c..ee4c7e5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,69 @@ -# notify-url-proxy -Allows testing your notify_url locally. +# transloadit-notify-url-proxy +Local `notify_url` proxy for Transloadit assemblies. -This project is heavily inspired by the work from @substantial's [node-transloadit-development_notify_url_proxy](https://github.com/substantial/node-transloadit-development_notify_url_proxy) project. +This version is modernized for: + +- Node.js 24+ +- Native TypeScript execution (type stripping) +- ESM +- Yarn 4 +- Biome + Vitest + GitHub Actions + Changesets + +## Install + +```bash +yarn add transloadit-notify-url-proxy +``` + +## CLI usage + +```bash +notify-url-proxy \ + --secret "$TRANSLOADIT_SECRET" \ + --notifyUrl "http://127.0.0.1:3000/transloadit" \ + --port 8888 +``` + +Run `notify-url-proxy --help` for all options. + +## Programmatic usage + +```ts +import TransloaditNotifyUrlProxy from 'transloadit-notify-url-proxy'; + +const proxy = new TransloaditNotifyUrlProxy( + process.env.TRANSLOADIT_SECRET ?? '', + 'http://127.0.0.1:3000/transloadit' +); + +proxy.run({ + port: 8888, + target: 'https://api2.transloadit.com/assemblies/', + pollIntervalMs: 2000, + maxPollAttempts: 10 +}); +``` + +## Development + +```bash +corepack enable +yarn install + +yarn lint +yarn typecheck +yarn test +yarn check +``` + +## Releases + +Changesets drives releases: + +```bash +yarn changeset +yarn changeset:version +``` + +On pushes to `main`, `.github/workflows/release.yml` runs `changesets/action` to publish. diff --git a/bin/notify-url-proxy.ts b/bin/notify-url-proxy.ts new file mode 100755 index 0000000..4cfab5e --- /dev/null +++ b/bin/notify-url-proxy.ts @@ -0,0 +1,83 @@ +#!/usr/bin/env node + +import { parseArgs } from 'node:util'; + +import TransloaditNotifyUrlProxy, { type ProxySettings } from '../src/index.ts'; + +function parsePositiveIntOption( + name: string, + value: string, + max = Number.MAX_SAFE_INTEGER, +): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) { + console.error(`Invalid ${name}: ${value}`); + process.exit(1); + } + return parsed; +} + +const { values } = parseArgs({ + options: { + secret: { type: 'string' }, + notifyUrl: { type: 'string' }, + target: { type: 'string' }, + port: { type: 'string' }, + pollIntervalMs: { type: 'string' }, + maxPollAttempts: { type: 'string' }, + help: { type: 'boolean', short: 'h' }, + }, +}); + +if (values.help) { + console.log(`Usage: notify-url-proxy [options] + +Options: + --secret HMAC secret for notification signatures (required) + --notifyUrl URL to send notifications to + --target Transloadit assemblies endpoint to proxy to + --port Local listen port + --pollIntervalMs Poll interval in milliseconds + --maxPollAttempts Max number of poll attempts + -h, --help Show this help + +Environment fallback: + TRANSLOADIT_SECRET, TRANSLOADIT_NOTIFY_URL +`); + process.exit(0); +} + +const secret = values.secret ?? process.env.TRANSLOADIT_SECRET; +if (!secret) { + console.error('Missing secret. Provide --secret or TRANSLOADIT_SECRET.'); + process.exit(1); +} + +const settings: Partial = {}; + +if (values.target) { + settings.target = values.target; +} +if (values.port) { + settings.port = parsePositiveIntOption('port', values.port, 65_535); +} +if (values.pollIntervalMs) { + settings.pollIntervalMs = parsePositiveIntOption('pollIntervalMs', values.pollIntervalMs); +} +if (values.maxPollAttempts) { + settings.maxPollAttempts = parsePositiveIntOption('maxPollAttempts', values.maxPollAttempts); +} + +const proxy = new TransloaditNotifyUrlProxy( + secret, + values.notifyUrl ?? process.env.TRANSLOADIT_NOTIFY_URL, +); +proxy.run(settings); + +const close = () => { + proxy.close(); + process.exit(0); +}; + +process.on('SIGINT', close); +process.on('SIGTERM', close); diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..80e6b1b --- /dev/null +++ b/biome.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "files": { + "includes": ["**", "!**/dist", "!**/.next"] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + } +} diff --git a/example.js b/example.js deleted file mode 100644 index cc1166d..0000000 --- a/example.js +++ /dev/null @@ -1,4 +0,0 @@ -const TransloaditNotifyUrlProxy = require('./index.js') - -const proxy = new TransloaditNotifyUrlProxy('foo_secret', 'http://127.0.0.1:3000/transloadit') -proxy.run() diff --git a/index.js b/index.js deleted file mode 100644 index e4e38e1..0000000 --- a/index.js +++ /dev/null @@ -1,158 +0,0 @@ -const http = require('http') -const httpProxy = require('http-proxy') -const request = require('request') -const crypto = require('crypto') -const _ = require('underscore') - -class TransloaditNotifyUrlProxy { - constructor (secret, notifyUrl) { - this._server = null - this._proxy = null - - this._secret = secret | '' - this._notifyUrl = notifyUrl || 'http://127.0.0.1:3000/transloadit' - - this._defaults = { - target: 'https://api2.transloadit.com/assemblies/', - port: 8888, - pollInterval: 2000, - } - this._settings = {} - } - - run (opts) { - if (opts === undefined) { - opts = {} - } - this._settings = _.extend(this._defaults, opts) - - this._createProxy() - this._createServer() - } - - close () { - if (this._server !== null) { - this._server.close() - this._server = null - } - - if (this._proxy !== null) { - this._proxy.close() - this._proxy = null - } - } - - _createProxy () { - this._proxy = httpProxy.createProxyServer({ - target: this._settings.target - }) - } - - _createServer () { - this._server = http.createServer((req, res) => { - this._proxy.web(req, res) - this._proxy.on('proxyRes', (res) => { - let body = '' - - res.on('data', (chunk) => { - body += chunk - }) - - res.on('end', () => { - const assemblyUrl = JSON.parse(body).assembly_url - this._out("Received proxy response, polling assemblyUrl: %s", assemblyUrl) - - this._pollAssembly(assemblyUrl) - }) - }) - }).listen(this._settings.port) - - this._out("Listening on http://localhost:%d, forwarding to %s, notifying %s", - this._settings.port, - this._settings.target, - this._notifyUrl - ) - } - - _pollAssembly (assemblyUrl) { - const opts = { - retries: 10, - minTimeout: this._settings.pollInterval, - maxTimeout: this._settings.pollInterval, - } - const operation = retry.operation(opts) - - operation.attempt((currentAttempt) => { - this._checkAssembly(assemblyUrl, (err, response) => { - if (!err && response) { - console.debug("%s valid response, notifying.", assemblyUrl) - return this._notify(response) - } - - console.debug("%s not completed, checking again.", assemblyUrl) - if (operation.retry(err)) { - return - } - - this._out("No attempts left, giving up on checking assemblyUrl: %s", assemblyUrl) - }) - }) - } - - _checkAssembly (assemblyUrl, cb) { - request.get(assemblyUrl, (err, res, body) => { - let response = JSON.parse(body) - let err = null - let msg = '' - - if (!response || !response.ok) { - err = new Error('No ok field found in Assembly response.') - return cb(err) - } - - if (response.ok == 'ASSEMBLY_COMPLETED') { - this._out('%s completed.', assemblyUrl) - return cb(null, response) - } - - if (response.ok == 'ASSEMBLY_UPLOADING'){ - msg = `${assemblyUrl} is still uploading.` - this._out(msg) - return cb(new Error(msg)) - } - - if (response.ok == 'ASSEMBLY_EXECUTING'){ - msg = `${assemblyUrl} is still executing.` - this._out(msg) - return cb(new Error(msg)) - } - - cb(new Error(`${assemblyUrl} - unknown Assembly state found.`)) - }) - } - - _notify (response) { - const stringified = JSON.stringify(response) - const signature = this._getSignature(stringified) - - request.post(this._notifyUrl, { - form: { - transloadit: stringified, - signatture: signature, - } - }) - } - - _getSignature (toSign) { - return crypto - .createHmac('sha1', this._secret) - .update(Buffer.from(toSign, 'utf-8')) - .digest('hex') - } - - _out (msg, ...args) { - console.log(msg, ...args) - } -} - -module.exports = TransloaditNotifyUrlProxy diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index c2e133d..0000000 --- a/package-lock.json +++ /dev/null @@ -1,380 +0,0 @@ -{ - "name": "transloadit-notify-url-proxy", - "version": "0.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "dependencies": { - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - } - } - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "dependencies": { - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - } - } - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "requires": { - "mime-db": "1.40.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "psl": { - "version": "1.1.32", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", - "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - } - } -} diff --git a/package.json b/package.json index 3b5f94a..7d51752 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,45 @@ { - "author": "@tim-kos", "name": "transloadit-notify-url-proxy", + "description": "Local notify_url proxy for Transloadit assemblies.", "version": "0.0.1", + "type": "module", + "packageManager": "yarn@4.10.3", + "author": "@tim-kos", + "engines": { + "node": ">=24" + }, + "bin": { + "notify-url-proxy": "./bin/notify-url-proxy.ts" + }, + "files": [ + "bin", + "src", + "README.md" + ], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + }, + "scripts": { + "start": "node ./bin/notify-url-proxy.ts", + "lint": "biome check .", + "format": "biome format . --write", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "check": "yarn lint && yarn typecheck && yarn test", + "changeset": "changeset", + "changeset:version": "changeset version" + }, "dependencies": { - "http-proxy": "1.18.1", - "request": "2.88.0", - "underscore": "1.12.1" + "http-proxy": "^1.18.1" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.5", + "@changesets/cli": "^2.30.0", + "@types/http-proxy": "^1.17.17", + "@types/node": "^25.3.3", + "typescript": "^5.9.3", + "vitest": "^4.0.18" } } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..bf7c42f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,243 @@ +import { createHmac } from 'node:crypto'; +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; +import { setTimeout as delay } from 'node:timers/promises'; + +import httpProxy from 'http-proxy'; + +export interface ProxySettings { + target: string; + port: number; + pollIntervalMs: number; + maxPollAttempts: number; +} + +export interface AssemblyResponse { + ok: string; + [key: string]: unknown; +} + +const DEFAULT_SETTINGS: ProxySettings = { + target: 'https://api2.transloadit.com/assemblies/', + port: 8888, + pollIntervalMs: 2_000, + maxPollAttempts: 10, +}; + +const KNOWN_STATES = new Set(['ASSEMBLY_COMPLETED', 'ASSEMBLY_UPLOADING', 'ASSEMBLY_EXECUTING']); + +type KnownAssemblyState = 'ASSEMBLY_COMPLETED' | 'ASSEMBLY_UPLOADING' | 'ASSEMBLY_EXECUTING'; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function toErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return String(error); +} + +export function extractAssemblyUrl(body: string): string | null { + try { + const payload = JSON.parse(body) as unknown; + if (!isRecord(payload)) { + return null; + } + + const assemblyUrl = payload.assembly_url; + return typeof assemblyUrl === 'string' && assemblyUrl.length > 0 ? assemblyUrl : null; + } catch { + return null; + } +} + +export function getAssemblyState(payload: unknown): KnownAssemblyState { + if (!isRecord(payload) || typeof payload.ok !== 'string') { + throw new Error('No ok field found in Assembly response.'); + } + + if (!KNOWN_STATES.has(payload.ok)) { + throw new Error(`Unknown Assembly state found: ${payload.ok}`); + } + + return payload.ok as KnownAssemblyState; +} + +export function getSignature(secret: string, toSign: string): string { + return createHmac('sha1', secret).update(Buffer.from(toSign, 'utf-8')).digest('hex'); +} + +export default class TransloaditNotifyUrlProxy { + private server: Server | null = null; + private proxy: httpProxy | null = null; + + private readonly secret: string; + private readonly notifyUrl: string; + private readonly defaults: ProxySettings; + private settings: ProxySettings; + + constructor(secret: string, notifyUrl = 'http://127.0.0.1:3000/transloadit') { + this.secret = secret || ''; + this.notifyUrl = notifyUrl; + + this.defaults = { ...DEFAULT_SETTINGS }; + this.settings = { ...DEFAULT_SETTINGS }; + } + + run(opts: Partial = {}): void { + if (this.server !== null || this.proxy !== null) { + this.close(); + } + + this.settings = { ...this.defaults, ...opts }; + + this.createProxy(); + this.createServer(); + } + + close(): void { + this.server?.close(); + this.server = null; + + this.proxy?.close(); + this.proxy = null; + } + + private createProxy(): void { + this.proxy = httpProxy.createProxyServer({ + target: this.settings.target, + changeOrigin: true, + }); + + this.proxy.on('error', (error, _req, res) => { + if ('writeHead' in res) { + if (!res.headersSent) { + res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' }); + } + res.end('Proxy error'); + } else { + res.end(); + } + this.out('Proxy error: %s', toErrorMessage(error)); + }); + + this.proxy.on('proxyRes', (proxyRes) => { + void this.handleProxyResponse(proxyRes); + }); + } + + private createServer(): void { + if (this.proxy === null) { + throw new Error('Proxy is not initialized.'); + } + + this.server = createServer((req, res) => { + this.proxy?.web(req, res); + }); + + this.server.listen(this.settings.port); + + this.out( + 'Listening on http://localhost:%d, forwarding to %s, notifying %s', + this.settings.port, + this.settings.target, + this.notifyUrl, + ); + } + + private async handleProxyResponse(proxyRes: IncomingMessage): Promise { + const body = await this.readResponseBody(proxyRes); + const assemblyUrl = extractAssemblyUrl(body); + + if (assemblyUrl === null) { + return; + } + + this.out('Received proxy response, polling assemblyUrl: %s', assemblyUrl); + await this.pollAssembly(assemblyUrl); + } + + private async readResponseBody(response: IncomingMessage): Promise { + const chunks: Buffer[] = []; + + for await (const chunk of response) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + + return Buffer.concat(chunks).toString('utf-8'); + } + + private async pollAssembly(assemblyUrl: string): Promise { + for (let attempt = 1; attempt <= this.settings.maxPollAttempts; attempt += 1) { + try { + const response = await this.checkAssembly(assemblyUrl); + await this.notify(response); + return; + } catch (error) { + if (attempt === this.settings.maxPollAttempts) { + this.out('No attempts left, giving up on checking assemblyUrl: %s', assemblyUrl); + return; + } + + this.out( + 'Attempt %d/%d failed for %s: %s', + attempt, + this.settings.maxPollAttempts, + assemblyUrl, + toErrorMessage(error), + ); + + await delay(this.settings.pollIntervalMs); + } + } + } + + private async checkAssembly(assemblyUrl: string): Promise { + const response = await fetch(assemblyUrl); + if (!response.ok) { + throw new Error(`Assembly poll returned HTTP ${response.status}`); + } + + const payload = (await response.json()) as unknown; + const state = getAssemblyState(payload); + + if (state === 'ASSEMBLY_COMPLETED') { + this.out('%s completed.', assemblyUrl); + return payload as AssemblyResponse; + } + + if (state === 'ASSEMBLY_UPLOADING') { + throw new Error(`${assemblyUrl} is still uploading.`); + } + + throw new Error(`${assemblyUrl} is still executing.`); + } + + private async notify(response: AssemblyResponse): Promise { + const transloadit = JSON.stringify(response); + const signature = getSignature(this.secret, transloadit); + + const notifyResponse = await fetch(this.notifyUrl, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', + }, + body: new URLSearchParams({ + transloadit, + signature, + }), + }); + + if (!notifyResponse.ok) { + throw new Error(`Notify URL returned HTTP ${notifyResponse.status}`); + } + + this.out('Notify payload sent to %s', this.notifyUrl); + } + + private out(message: string, ...args: unknown[]): void { + console.log(message, ...args); + } +} diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..dcd956b --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; + +import { extractAssemblyUrl, getAssemblyState, getSignature } from '../src/index.ts'; + +describe('getSignature', () => { + it('creates a sha1 hmac signature', () => { + const signature = getSignature('foo_secret', '{"ok":"ASSEMBLY_COMPLETED"}'); + expect(signature).toBe('9c31e806b2f3ac4d7cf69d7c29ccf6806b9ee073'); + }); +}); + +describe('extractAssemblyUrl', () => { + it('extracts assembly_url from proxy payload', () => { + expect(extractAssemblyUrl('{"assembly_url":"https://example.test/a/123"}')).toBe( + 'https://example.test/a/123', + ); + }); + + it('returns null for invalid payloads', () => { + expect(extractAssemblyUrl('nope')).toBeNull(); + expect(extractAssemblyUrl('{"foo":"bar"}')).toBeNull(); + }); +}); + +describe('getAssemblyState', () => { + it('accepts known states', () => { + expect(getAssemblyState({ ok: 'ASSEMBLY_COMPLETED' })).toBe('ASSEMBLY_COMPLETED'); + expect(getAssemblyState({ ok: 'ASSEMBLY_UPLOADING' })).toBe('ASSEMBLY_UPLOADING'); + expect(getAssemblyState({ ok: 'ASSEMBLY_EXECUTING' })).toBe('ASSEMBLY_EXECUTING'); + }); + + it('rejects unknown states', () => { + expect(() => getAssemblyState({ ok: 'UNKNOWN' })).toThrow('Unknown Assembly state found'); + }); + + it('rejects malformed payloads', () => { + expect(() => getAssemblyState(null)).toThrow('No ok field found'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..37d9957 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "allowImportingTsExtensions": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "bin/**/*.ts", "test/**/*.ts", "vitest.config.ts"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a71174d --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['test/**/*.test.ts'], + }, +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..e762976 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2560 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@babel/runtime@npm:^7.5.5": + version: 7.28.6 + resolution: "@babel/runtime@npm:7.28.6" + checksum: 10c0/358cf2429992ac1c466df1a21c1601d595c46930a13c1d4662fde908d44ee78ec3c183aaff513ecb01ef8c55c3624afe0309eeeb34715672dbfadb7feedb2c0d + languageName: node + linkType: hard + +"@biomejs/biome@npm:^2.4.5": + version: 2.4.5 + resolution: "@biomejs/biome@npm:2.4.5" + dependencies: + "@biomejs/cli-darwin-arm64": "npm:2.4.5" + "@biomejs/cli-darwin-x64": "npm:2.4.5" + "@biomejs/cli-linux-arm64": "npm:2.4.5" + "@biomejs/cli-linux-arm64-musl": "npm:2.4.5" + "@biomejs/cli-linux-x64": "npm:2.4.5" + "@biomejs/cli-linux-x64-musl": "npm:2.4.5" + "@biomejs/cli-win32-arm64": "npm:2.4.5" + "@biomejs/cli-win32-x64": "npm:2.4.5" + dependenciesMeta: + "@biomejs/cli-darwin-arm64": + optional: true + "@biomejs/cli-darwin-x64": + optional: true + "@biomejs/cli-linux-arm64": + optional: true + "@biomejs/cli-linux-arm64-musl": + optional: true + "@biomejs/cli-linux-x64": + optional: true + "@biomejs/cli-linux-x64-musl": + optional: true + "@biomejs/cli-win32-arm64": + optional: true + "@biomejs/cli-win32-x64": + optional: true + bin: + biome: bin/biome + checksum: 10c0/5466d0f69c30bbe1912f3f5f307dd31293e054c8b307c21f1df22edad415de34cf46842b0bd57e150cfc6dc6d698e848f6cd26fc7155d685aa8323e4ead7538e + languageName: node + linkType: hard + +"@biomejs/cli-darwin-arm64@npm:2.4.5": + version: 2.4.5 + resolution: "@biomejs/cli-darwin-arm64@npm:2.4.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-darwin-x64@npm:2.4.5": + version: 2.4.5 + resolution: "@biomejs/cli-darwin-x64@npm:2.4.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64-musl@npm:2.4.5": + version: 2.4.5 + resolution: "@biomejs/cli-linux-arm64-musl@npm:2.4.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64@npm:2.4.5": + version: 2.4.5 + resolution: "@biomejs/cli-linux-arm64@npm:2.4.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64-musl@npm:2.4.5": + version: 2.4.5 + resolution: "@biomejs/cli-linux-x64-musl@npm:2.4.5" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64@npm:2.4.5": + version: 2.4.5 + resolution: "@biomejs/cli-linux-x64@npm:2.4.5" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-win32-arm64@npm:2.4.5": + version: 2.4.5 + resolution: "@biomejs/cli-win32-arm64@npm:2.4.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-win32-x64@npm:2.4.5": + version: 2.4.5 + resolution: "@biomejs/cli-win32-x64@npm:2.4.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@changesets/apply-release-plan@npm:^7.1.0": + version: 7.1.0 + resolution: "@changesets/apply-release-plan@npm:7.1.0" + dependencies: + "@changesets/config": "npm:^3.1.3" + "@changesets/get-version-range-type": "npm:^0.4.0" + "@changesets/git": "npm:^3.0.4" + "@changesets/should-skip-package": "npm:^0.1.2" + "@changesets/types": "npm:^6.1.0" + "@manypkg/get-packages": "npm:^1.1.3" + detect-indent: "npm:^6.0.0" + fs-extra: "npm:^7.0.1" + lodash.startcase: "npm:^4.4.0" + outdent: "npm:^0.5.0" + prettier: "npm:^2.7.1" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.5.3" + checksum: 10c0/c8b4fa55f204a0c343c450ca44ae32a892752eaa81b594fb8941e9d1eb8675aba6245c8d80e5e9726e915d2643c542d22cba40d430c76a71ff438ad368d91f5c + languageName: node + linkType: hard + +"@changesets/assemble-release-plan@npm:^6.0.9": + version: 6.0.9 + resolution: "@changesets/assemble-release-plan@npm:6.0.9" + dependencies: + "@changesets/errors": "npm:^0.2.0" + "@changesets/get-dependents-graph": "npm:^2.1.3" + "@changesets/should-skip-package": "npm:^0.1.2" + "@changesets/types": "npm:^6.1.0" + "@manypkg/get-packages": "npm:^1.1.3" + semver: "npm:^7.5.3" + checksum: 10c0/128f87975f65d9ceb2c997df186a5deae8637fd3868098bb4fb9772f35fdd3b47883ccbdc2761d0468e60a83ef4e2c1561a8e58f8052bfe2daf1ea046803fe1a + languageName: node + linkType: hard + +"@changesets/changelog-git@npm:^0.2.1": + version: 0.2.1 + resolution: "@changesets/changelog-git@npm:0.2.1" + dependencies: + "@changesets/types": "npm:^6.1.0" + checksum: 10c0/6a6fb315ffb2266fcb8f32ae9a60ccdb5436e52350a2f53beacf9822d3355f9052aba5001a718e12af472b4a8fabd69b408d0b11c02ac909ba7a183d27a9f7fd + languageName: node + linkType: hard + +"@changesets/cli@npm:^2.30.0": + version: 2.30.0 + resolution: "@changesets/cli@npm:2.30.0" + dependencies: + "@changesets/apply-release-plan": "npm:^7.1.0" + "@changesets/assemble-release-plan": "npm:^6.0.9" + "@changesets/changelog-git": "npm:^0.2.1" + "@changesets/config": "npm:^3.1.3" + "@changesets/errors": "npm:^0.2.0" + "@changesets/get-dependents-graph": "npm:^2.1.3" + "@changesets/get-release-plan": "npm:^4.0.15" + "@changesets/git": "npm:^3.0.4" + "@changesets/logger": "npm:^0.1.1" + "@changesets/pre": "npm:^2.0.2" + "@changesets/read": "npm:^0.6.7" + "@changesets/should-skip-package": "npm:^0.1.2" + "@changesets/types": "npm:^6.1.0" + "@changesets/write": "npm:^0.4.0" + "@inquirer/external-editor": "npm:^1.0.2" + "@manypkg/get-packages": "npm:^1.1.3" + ansi-colors: "npm:^4.1.3" + enquirer: "npm:^2.4.1" + fs-extra: "npm:^7.0.1" + mri: "npm:^1.2.0" + package-manager-detector: "npm:^0.2.0" + picocolors: "npm:^1.1.0" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.5.3" + spawndamnit: "npm:^3.0.1" + term-size: "npm:^2.1.0" + bin: + changeset: bin.js + checksum: 10c0/2b06343ae6df20b627ee89027f4078c074bdd758f82bb5dbf16ef7c4900138f733b59ceeb1c960fca1e9e59cf6973bb4d5984e4c7dd6d50a3949b39c490f31e0 + languageName: node + linkType: hard + +"@changesets/config@npm:^3.1.3": + version: 3.1.3 + resolution: "@changesets/config@npm:3.1.3" + dependencies: + "@changesets/errors": "npm:^0.2.0" + "@changesets/get-dependents-graph": "npm:^2.1.3" + "@changesets/logger": "npm:^0.1.1" + "@changesets/should-skip-package": "npm:^0.1.2" + "@changesets/types": "npm:^6.1.0" + "@manypkg/get-packages": "npm:^1.1.3" + fs-extra: "npm:^7.0.1" + micromatch: "npm:^4.0.8" + checksum: 10c0/68764135cbd014aca24b20429ffaf6f90e440286c7d233c33ddc968f0ab54b9e6e5dd5015a619dd0e0dd2eb172f028064a229fa610c260b779ff5315a840be1e + languageName: node + linkType: hard + +"@changesets/errors@npm:^0.2.0": + version: 0.2.0 + resolution: "@changesets/errors@npm:0.2.0" + dependencies: + extendable-error: "npm:^0.1.5" + checksum: 10c0/f2757c752ab04e9733b0dfd7903f1caf873f9e603794c4d9ea2294af4f937c73d07273c24be864ad0c30b6a98424360d5b96a6eab14f97f3cf2cbfd3763b95c1 + languageName: node + linkType: hard + +"@changesets/get-dependents-graph@npm:^2.1.3": + version: 2.1.3 + resolution: "@changesets/get-dependents-graph@npm:2.1.3" + dependencies: + "@changesets/types": "npm:^6.1.0" + "@manypkg/get-packages": "npm:^1.1.3" + picocolors: "npm:^1.1.0" + semver: "npm:^7.5.3" + checksum: 10c0/b9d9992440b7e09dcaf22f57d28f1d8e0e31996e1bc44dbbfa1801e44f93fa49ebba6f9356c60f6ff0bd85cd0f0d0b8602f7e0f2addc5be647b686e6f8985f70 + languageName: node + linkType: hard + +"@changesets/get-release-plan@npm:^4.0.15": + version: 4.0.15 + resolution: "@changesets/get-release-plan@npm:4.0.15" + dependencies: + "@changesets/assemble-release-plan": "npm:^6.0.9" + "@changesets/config": "npm:^3.1.3" + "@changesets/pre": "npm:^2.0.2" + "@changesets/read": "npm:^0.6.7" + "@changesets/types": "npm:^6.1.0" + "@manypkg/get-packages": "npm:^1.1.3" + checksum: 10c0/d059c18ef5aab1c1aa1dd4f68d74e2fc351d965e28a76ab7f7c63c3290787d645f887d89c50b92f9f6bb63148a5d17329cfbb9cdea8e02c669a47768ec3456bc + languageName: node + linkType: hard + +"@changesets/get-version-range-type@npm:^0.4.0": + version: 0.4.0 + resolution: "@changesets/get-version-range-type@npm:0.4.0" + checksum: 10c0/e466208c8383489a383f37958d8b5b9aed38539f9287b47fe155a2e8855973f6960fb1724a1ee33b11580d65e1011059045ee654e8ef51e4783017d8989c9d3f + languageName: node + linkType: hard + +"@changesets/git@npm:^3.0.4": + version: 3.0.4 + resolution: "@changesets/git@npm:3.0.4" + dependencies: + "@changesets/errors": "npm:^0.2.0" + "@manypkg/get-packages": "npm:^1.1.3" + is-subdir: "npm:^1.1.1" + micromatch: "npm:^4.0.8" + spawndamnit: "npm:^3.0.1" + checksum: 10c0/4abbdc1dec6ddc50b6ad927d9eba4f23acd775fdff615415813099befb0cecd1b0f56ceea5e18a5a3cbbb919d68179366074b02a954fbf4016501e5fd125d2b5 + languageName: node + linkType: hard + +"@changesets/logger@npm:^0.1.1": + version: 0.1.1 + resolution: "@changesets/logger@npm:0.1.1" + dependencies: + picocolors: "npm:^1.1.0" + checksum: 10c0/a0933b5bd4d99e10730b22612dc1bdfd25b8804c5b48f8cada050bf5c7a89b2ae9a61687f846a5e9e5d379a95b59fef795c8d5d91e49a251f8da2be76133f83f + languageName: node + linkType: hard + +"@changesets/parse@npm:^0.4.3": + version: 0.4.3 + resolution: "@changesets/parse@npm:0.4.3" + dependencies: + "@changesets/types": "npm:^6.1.0" + js-yaml: "npm:^4.1.1" + checksum: 10c0/4d8488eaf224974ae335fec964dc1dc486abcfa9f96856cf4267c2765b02ed6af1778375ec03d38252ebab9e191aa4a11c5f37a6ad42e907e08290fed2b9690c + languageName: node + linkType: hard + +"@changesets/pre@npm:^2.0.2": + version: 2.0.2 + resolution: "@changesets/pre@npm:2.0.2" + dependencies: + "@changesets/errors": "npm:^0.2.0" + "@changesets/types": "npm:^6.1.0" + "@manypkg/get-packages": "npm:^1.1.3" + fs-extra: "npm:^7.0.1" + checksum: 10c0/0af9396d84c47a88d79b757e9db4e3579b6620260f92c243b8349e7fcefca3c2652583f6d215c13115bed5d5cdc30c975f307fd6acbb89d205b1ba2ae403b918 + languageName: node + linkType: hard + +"@changesets/read@npm:^0.6.7": + version: 0.6.7 + resolution: "@changesets/read@npm:0.6.7" + dependencies: + "@changesets/git": "npm:^3.0.4" + "@changesets/logger": "npm:^0.1.1" + "@changesets/parse": "npm:^0.4.3" + "@changesets/types": "npm:^6.1.0" + fs-extra: "npm:^7.0.1" + p-filter: "npm:^2.1.0" + picocolors: "npm:^1.1.0" + checksum: 10c0/eebda5f5cea8684b9cb470e74cd5e67043a62ca54452ac88bb1a998bebeee1a2e3a642dc76818155a145863551c65f10f9c4ff85378b0419179fc60049edbbc6 + languageName: node + linkType: hard + +"@changesets/should-skip-package@npm:^0.1.2": + version: 0.1.2 + resolution: "@changesets/should-skip-package@npm:0.1.2" + dependencies: + "@changesets/types": "npm:^6.1.0" + "@manypkg/get-packages": "npm:^1.1.3" + checksum: 10c0/484e339e7d6e6950e12bff4eda6e8eccb077c0fbb1f09dd95d2ae948b715226a838c71eaf50cd2d7e0e631ce3bfb1ca93ac752436e6feae5b87aece2e917b440 + languageName: node + linkType: hard + +"@changesets/types@npm:^4.0.1": + version: 4.1.0 + resolution: "@changesets/types@npm:4.1.0" + checksum: 10c0/a372ad21f6a1e0d4ce6c19573c1ca269eef1ad53c26751ad9515a24f003e7c49dcd859dbb1fedb6badaf7be956c1559e8798304039e0ec0da2d9a68583f13464 + languageName: node + linkType: hard + +"@changesets/types@npm:^6.1.0": + version: 6.1.0 + resolution: "@changesets/types@npm:6.1.0" + checksum: 10c0/b4cea3a4465d1eaf0bbd7be1e404aca5a055a61d4cc72aadcb73bbbda1670b4022736b8d3052616cbf1f451afa0637545d077697f4b923236539af9cd5abce6c + languageName: node + linkType: hard + +"@changesets/write@npm:^0.4.0": + version: 0.4.0 + resolution: "@changesets/write@npm:0.4.0" + dependencies: + "@changesets/types": "npm:^6.1.0" + fs-extra: "npm:^7.0.1" + human-id: "npm:^4.1.1" + prettier: "npm:^2.7.1" + checksum: 10c0/311f4d0e536d1b5f2d3f9053537d62b2d4cdbd51e1d2767807ac9d1e0f380367f915d2ad370e5c73902d5a54bffd282d53fff5418c8ad31df51751d652bea826 + languageName: node + linkType: hard + +"@esbuild/aix-ppc64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/aix-ppc64@npm:0.27.3" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-arm64@npm:0.27.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-arm@npm:0.27.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-x64@npm:0.27.3" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/darwin-arm64@npm:0.27.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/darwin-x64@npm:0.27.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/freebsd-arm64@npm:0.27.3" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/freebsd-x64@npm:0.27.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-arm64@npm:0.27.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-arm@npm:0.27.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-ia32@npm:0.27.3" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-loong64@npm:0.27.3" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-mips64el@npm:0.27.3" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-ppc64@npm:0.27.3" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-riscv64@npm:0.27.3" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-s390x@npm:0.27.3" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-x64@npm:0.27.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/netbsd-arm64@npm:0.27.3" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/netbsd-x64@npm:0.27.3" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openbsd-arm64@npm:0.27.3" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openbsd-x64@npm:0.27.3" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openharmony-arm64@npm:0.27.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/sunos-x64@npm:0.27.3" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-arm64@npm:0.27.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-ia32@npm:0.27.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-x64@npm:0.27.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@gar/promise-retry@npm:^1.0.0": + version: 1.0.2 + resolution: "@gar/promise-retry@npm:1.0.2" + dependencies: + retry: "npm:^0.13.1" + checksum: 10c0/748a84fb0ab962f7867966f21dc24d1872c53c1656dd3352320fe69ad3b2043f2dfdb3be024c7636ce4904c5ba1da22d0f3558e489c3de578f5bb520f062d0fd + languageName: node + linkType: hard + +"@inquirer/external-editor@npm:^1.0.2": + version: 1.0.3 + resolution: "@inquirer/external-editor@npm:1.0.3" + dependencies: + chardet: "npm:^2.1.1" + iconv-lite: "npm:^0.7.0" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10c0/82951cb7f3762dd78cca2ea291396841e3f4adfe26004b5badfed1cec4b6a04bb567dff94d0e41b35c61bdd7957317c64c22f58074d14b238d44e44d9e420019 + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 + languageName: node + linkType: hard + +"@manypkg/find-root@npm:^1.1.0": + version: 1.1.0 + resolution: "@manypkg/find-root@npm:1.1.0" + dependencies: + "@babel/runtime": "npm:^7.5.5" + "@types/node": "npm:^12.7.1" + find-up: "npm:^4.1.0" + fs-extra: "npm:^8.1.0" + checksum: 10c0/0ee907698e6c73d6f1821ff630f3fec6dcf38260817c8752fec8991ac38b95ba431ab11c2773ddf9beb33d0e057f1122b00e8ffc9b8411b3fd24151413626fa6 + languageName: node + linkType: hard + +"@manypkg/get-packages@npm:^1.1.3": + version: 1.1.3 + resolution: "@manypkg/get-packages@npm:1.1.3" + dependencies: + "@babel/runtime": "npm:^7.5.5" + "@changesets/types": "npm:^4.0.1" + "@manypkg/find-root": "npm:^1.1.0" + fs-extra: "npm:^8.1.0" + globby: "npm:^11.0.0" + read-yaml-file: "npm:^1.1.0" + checksum: 10c0/f05907d1174ae28861eaa06d0efdc144f773d9a4b8b65e1e7cdc01eb93361d335351b4a336e05c6aac02661be39e8809a3f7ad28bc67b6b338071434ab442130 + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^11.2.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/f7b5ce0f3dd42c3f8c6546e8433573d8049f67ef11ec22aa4704bc41483122f68bf97752e06302c455ead667af5cb753e6a09bff06632bc465c1cfd4c4b75a53 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/26e376d780f60ff16e874a0ac9bc3399186846baae0b6e1352286385ac134d900cc5dafaded77f38d77f86898fc923ae1cee9d7399f0275b1aa24878915d722b + languageName: node + linkType: hard + +"@rollup/rollup-android-arm-eabi@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.59.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm64@npm:4.59.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.59.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.59.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.59.0" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.59.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.59.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.59.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.59.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.59.0" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.59.0" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.59.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.59.0" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.59.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.59.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.59.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.59.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.59.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-openbsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openbsd-x64@npm:4.59.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.59.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.59.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.59.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.59.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.59.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@standard-schema/spec@npm:^1.0.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 + languageName: node + linkType: hard + +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f + languageName: node + linkType: hard + +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + languageName: node + linkType: hard + +"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 + languageName: node + linkType: hard + +"@types/http-proxy@npm:^1.17.17": + version: 1.17.17 + resolution: "@types/http-proxy@npm:1.17.17" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/547e322a5eecf0b50d08f6a46bd89c8c8663d67dbdcd472da5daf968b03e63a82f6b3650443378abe6c10a46475dac52015f30e8c74ba2ea5820dd4e9cdef2d4 + languageName: node + linkType: hard + +"@types/node@npm:*, @types/node@npm:^25.3.3": + version: 25.3.3 + resolution: "@types/node@npm:25.3.3" + dependencies: + undici-types: "npm:~7.18.0" + checksum: 10c0/63e1d3816a9f4a706ab5d588d18cb98aa824b97748ff585537d327528e9438f58f69f45c7762e7cd3a1ab32c1619f551aabe8075d13172f9273cf10f6d83ab91 + languageName: node + linkType: hard + +"@types/node@npm:^12.7.1": + version: 12.20.55 + resolution: "@types/node@npm:12.20.55" + checksum: 10c0/3b190bb0410047d489c49bbaab592d2e6630de6a50f00ba3d7d513d59401d279972a8f5a598b5bb8ddc1702f8a2f4ec57a65d93852f9c329639738e7053637d1 + languageName: node + linkType: hard + +"@vitest/expect@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/expect@npm:4.0.18" + dependencies: + "@standard-schema/spec": "npm:^1.0.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.0.18" + "@vitest/utils": "npm:4.0.18" + chai: "npm:^6.2.1" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/123b0aa111682e82ec5289186df18037b1a1768700e468ee0f9879709aaa320cf790463c15c0d8ee10df92b402f4394baf5d27797e604d78e674766d87bcaadc + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/mocker@npm:4.0.18" + dependencies: + "@vitest/spy": "npm:4.0.18" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/fb0a257e7e167759d4ad228d53fa7bad2267586459c4a62188f2043dd7163b4b02e1e496dc3c227837f776e7d73d6c4343613e89e7da379d9d30de8260f1ee4b + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/pretty-format@npm:4.0.18" + dependencies: + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/0086b8c88eeca896d8e4b98fcdef452c8041a1b63eb9e85d3e0bcc96c8aa76d8e9e0b6990ebb0bb0a697c4ebab347e7735888b24f507dbff2742ddce7723fd94 + languageName: node + linkType: hard + +"@vitest/runner@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/runner@npm:4.0.18" + dependencies: + "@vitest/utils": "npm:4.0.18" + pathe: "npm:^2.0.3" + checksum: 10c0/fdb4afa411475133c05ba266c8092eaf1e56cbd5fb601f92ec6ccb9bab7ca52e06733ee8626599355cba4ee71cb3a8f28c84d3b69dc972e41047edc50229bc01 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/snapshot@npm:4.0.18" + dependencies: + "@vitest/pretty-format": "npm:4.0.18" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10c0/d3bfefa558db9a69a66886ace6575eb96903a5ba59f4d9a5d0fecb4acc2bb8dbb443ef409f5ac1475f2e1add30bd1d71280f98912da35e89c75829df9e84ea43 + languageName: node + linkType: hard + +"@vitest/spy@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/spy@npm:4.0.18" + checksum: 10c0/6de537890b3994fcadb8e8d8ac05942320ae184f071ec395d978a5fba7fa928cbb0c5de85af86a1c165706c466e840de8779eaff8c93450c511c7abaeb9b8a4e + languageName: node + linkType: hard + +"@vitest/utils@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/utils@npm:4.0.18" + dependencies: + "@vitest/pretty-format": "npm:4.0.18" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/4a3c43c1421eb90f38576926496f6c80056167ba111e63f77cf118983902673737a1a38880b890d7c06ec0a12475024587344ee502b3c43093781533022f2aeb + languageName: node + linkType: hard + +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5 + languageName: node + linkType: hard + +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 10c0/c2c9ab7599692d594b6a161559ada307b7a624fa4c7b03e3afdb5a5e31cd0e53269115b620fcab024c5ac6a6f37fa5eb2e004f076ad30f5f7e6b8b671f7b35fe + languageName: node + linkType: hard + +"ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": + version: 4.1.3 + resolution: "ansi-colors@npm:4.1.3" + checksum: 10c0/ec87a2f59902f74e61eada7f6e6fe20094a628dab765cfdbd03c3477599368768cffccdb5d3bb19a1b6c99126783a143b1fee31aab729b31ffe5836c7e5e28b9 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"argparse@npm:^1.0.7": + version: 1.0.10 + resolution: "argparse@npm:1.0.10" + dependencies: + sprintf-js: "npm:~1.0.2" + checksum: 10c0/b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e + languageName: node + linkType: hard + +"array-union@npm:^2.1.0": + version: 2.1.0 + resolution: "array-union@npm:2.1.0" + checksum: 10c0/429897e68110374f39b771ec47a7161fc6a8fc33e196857c0a396dc75df0b5f65e4d046674db764330b6bb66b39ef48dd7c53b6a2ee75cfb0681e0c1a7033962 + languageName: node + linkType: hard + +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b + languageName: node + linkType: hard + +"better-path-resolve@npm:1.0.0": + version: 1.0.0 + resolution: "better-path-resolve@npm:1.0.0" + dependencies: + is-windows: "npm:^1.0.0" + checksum: 10c0/7335130729d59a14b8e4753fea180ca84e287cccc20cb5f2438a95667abc5810327c414eee7b3c79ed1b5a348a40284ea872958f50caba69432c40405eb0acce + languageName: node + linkType: hard + +"brace-expansion@npm:^5.0.2": + version: 5.0.4 + resolution: "brace-expansion@npm:5.0.4" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10c0/359cbcfa80b2eb914ca1f3440e92313fbfe7919ee6b274c35db55bec555aded69dac5ee78f102cec90c35f98c20fa43d10936d0cd9978158823c249257e1643a + languageName: node + linkType: hard + +"braces@npm:^3.0.3": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: "npm:^7.1.1" + checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 + languageName: node + linkType: hard + +"cacache@npm:^20.0.1": + version: 20.0.3 + resolution: "cacache@npm:20.0.3" + dependencies: + "@npmcli/fs": "npm:^5.0.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^13.0.0" + lru-cache: "npm:^11.1.0" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^7.0.2" + ssri: "npm:^13.0.0" + unique-filename: "npm:^5.0.0" + checksum: 10c0/c7da1ca694d20e8f8aedabd21dc11518f809a7d2b59aa76a1fc655db5a9e62379e465c157ddd2afe34b19230808882288effa6911b2de26a088a6d5645123462 + languageName: node + linkType: hard + +"chai@npm:^6.2.1": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 + languageName: node + linkType: hard + +"chardet@npm:^2.1.1": + version: 2.1.1 + resolution: "chardet@npm:2.1.1" + checksum: 10c0/d8391dd412338442b3de0d3a488aa9327f8bcf74b62b8723d6bd0b85c4084d50b731320e0a7c710edb1d44de75969995d2784b80e4c13b004a6c7a0db4c6e793 + languageName: node + linkType: hard + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.5": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + +"detect-indent@npm:^6.0.0": + version: 6.1.0 + resolution: "detect-indent@npm:6.1.0" + checksum: 10c0/dd83cdeda9af219cf77f5e9a0dc31d828c045337386cfb55ce04fad94ba872ee7957336834154f7647b89b899c3c7acc977c57a79b7c776b506240993f97acc7 + languageName: node + linkType: hard + +"dir-glob@npm:^3.0.1": + version: 3.0.1 + resolution: "dir-glob@npm:3.0.1" + dependencies: + path-type: "npm:^4.0.0" + checksum: 10c0/dcac00920a4d503e38bb64001acb19df4efc14536ada475725e12f52c16777afdee4db827f55f13a908ee7efc0cb282e2e3dbaeeb98c0993dd93d1802d3bf00c + languageName: node + linkType: hard + +"enquirer@npm:^2.4.1": + version: 2.4.1 + resolution: "enquirer@npm:2.4.1" + dependencies: + ansi-colors: "npm:^4.1.1" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/43850479d7a51d36a9c924b518dcdc6373b5a8ae3401097d336b7b7e258324749d0ad37a1fcaa5706f04799baa05585cd7af19ebdf7667673e7694435fcea918 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"es-module-lexer@npm:^1.7.0": + version: 1.7.0 + resolution: "es-module-lexer@npm:1.7.0" + checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b + languageName: node + linkType: hard + +"esbuild@npm:^0.27.0": + version: 0.27.3 + resolution: "esbuild@npm:0.27.3" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.3" + "@esbuild/android-arm": "npm:0.27.3" + "@esbuild/android-arm64": "npm:0.27.3" + "@esbuild/android-x64": "npm:0.27.3" + "@esbuild/darwin-arm64": "npm:0.27.3" + "@esbuild/darwin-x64": "npm:0.27.3" + "@esbuild/freebsd-arm64": "npm:0.27.3" + "@esbuild/freebsd-x64": "npm:0.27.3" + "@esbuild/linux-arm": "npm:0.27.3" + "@esbuild/linux-arm64": "npm:0.27.3" + "@esbuild/linux-ia32": "npm:0.27.3" + "@esbuild/linux-loong64": "npm:0.27.3" + "@esbuild/linux-mips64el": "npm:0.27.3" + "@esbuild/linux-ppc64": "npm:0.27.3" + "@esbuild/linux-riscv64": "npm:0.27.3" + "@esbuild/linux-s390x": "npm:0.27.3" + "@esbuild/linux-x64": "npm:0.27.3" + "@esbuild/netbsd-arm64": "npm:0.27.3" + "@esbuild/netbsd-x64": "npm:0.27.3" + "@esbuild/openbsd-arm64": "npm:0.27.3" + "@esbuild/openbsd-x64": "npm:0.27.3" + "@esbuild/openharmony-arm64": "npm:0.27.3" + "@esbuild/sunos-x64": "npm:0.27.3" + "@esbuild/win32-arm64": "npm:0.27.3" + "@esbuild/win32-ia32": "npm:0.27.3" + "@esbuild/win32-x64": "npm:0.27.3" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/fdc3f87a3f08b3ef98362f37377136c389a0d180fda4b8d073b26ba930cf245521db0a368f119cc7624bc619248fff1439f5811f062d853576f8ffa3df8ee5f1 + languageName: node + linkType: hard + +"esprima@npm:^4.0.0": + version: 4.0.1 + resolution: "esprima@npm:4.0.1" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 10c0/ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 + languageName: node + linkType: hard + +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + +"eventemitter3@npm:^4.0.0": + version: 4.0.7 + resolution: "eventemitter3@npm:4.0.7" + checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b + languageName: node + linkType: hard + +"expect-type@npm:^1.2.2": + version: 1.3.0 + resolution: "expect-type@npm:1.3.0" + checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 + languageName: node + linkType: hard + +"extendable-error@npm:^0.1.5": + version: 0.1.7 + resolution: "extendable-error@npm:0.1.7" + checksum: 10c0/c46648b7682448428f81b157cbfe480170fd96359c55db477a839ddeaa34905a18cba0b989bafe5e83f93c2491a3fcc7cc536063ea326ba9d72e9c6e2fe736a7 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.9": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.8" + checksum: 10c0/f6aaa141d0d3384cf73cbcdfc52f475ed293f6d5b65bfc5def368b09163a9f7e5ec2b3014d80f733c405f58e470ee0cc451c2937685045cddcdeaa24199c43fe + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.20.1 + resolution: "fastq@npm:1.20.1" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10c0/e5dd725884decb1f11e5c822221d76136f239d0236f176fab80b7b8f9e7619ae57e6b4e5b73defc21e6b9ef99437ee7b545cff8e6c2c337819633712fa9d352e + languageName: node + linkType: hard + +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f + languageName: node + linkType: hard + +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 + languageName: node + linkType: hard + +"find-up@npm:^4.1.0": + version: 4.1.0 + resolution: "find-up@npm:4.1.0" + dependencies: + locate-path: "npm:^5.0.0" + path-exists: "npm:^4.0.0" + checksum: 10c0/0406ee89ebeefa2d507feb07ec366bebd8a6167ae74aa4e34fb4c4abd06cf782a3ce26ae4194d70706f72182841733f00551c209fe575cb00bd92104056e78c1 + languageName: node + linkType: hard + +"follow-redirects@npm:^1.0.0": + version: 1.15.11 + resolution: "follow-redirects@npm:1.15.11" + peerDependenciesMeta: + debug: + optional: true + checksum: 10c0/d301f430542520a54058d4aeeb453233c564aaccac835d29d15e050beb33f339ad67d9bddbce01739c5dc46a6716dbe3d9d0d5134b1ca203effa11a7ef092343 + languageName: node + linkType: hard + +"fs-extra@npm:^7.0.1": + version: 7.0.1 + resolution: "fs-extra@npm:7.0.1" + dependencies: + graceful-fs: "npm:^4.1.2" + jsonfile: "npm:^4.0.0" + universalify: "npm:^0.1.0" + checksum: 10c0/1943bb2150007e3739921b8d13d4109abdc3cc481e53b97b7ea7f77eda1c3c642e27ae49eac3af074e3496ea02fde30f411ef410c760c70a38b92e656e5da784 + languageName: node + linkType: hard + +"fs-extra@npm:^8.1.0": + version: 8.1.0 + resolution: "fs-extra@npm:8.1.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^4.0.0" + universalify: "npm:^0.1.0" + checksum: 10c0/259f7b814d9e50d686899550c4f9ded85c46c643f7fe19be69504888e007fcbc08f306fae8ec495b8b998635e997c9e3e175ff2eeed230524ef1c1684cc96423 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + +"glob@npm:^13.0.0": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10c0/269c236f11a9b50357fe7a8c6aadac667e01deb5242b19c84975628f05f4438d8ee1354bb62c5d6c10f37fd59911b54d7799730633a2786660d8c69f1d18120a + languageName: node + linkType: hard + +"globby@npm:^11.0.0": + version: 11.1.0 + resolution: "globby@npm:11.1.0" + dependencies: + array-union: "npm:^2.1.0" + dir-glob: "npm:^3.0.1" + fast-glob: "npm:^3.2.9" + ignore: "npm:^5.2.0" + merge2: "npm:^1.4.1" + slash: "npm:^3.0.0" + checksum: 10c0/b39511b4afe4bd8a7aead3a27c4ade2b9968649abab0a6c28b1a90141b96ca68ca5db1302f7c7bd29eab66bf51e13916b8e0a3d0ac08f75e1e84a39b35691189 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"http-proxy@npm:^1.18.1": + version: 1.18.1 + resolution: "http-proxy@npm:1.18.1" + dependencies: + eventemitter3: "npm:^4.0.0" + follow-redirects: "npm:^1.0.0" + requires-port: "npm:^1.0.0" + checksum: 10c0/148dfa700a03fb421e383aaaf88ac1d94521dfc34072f6c59770528c65250983c2e4ec996f2f03aa9f3fe46cd1270a593126068319311e3e8d9e610a37533e94 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:4" + checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac + languageName: node + linkType: hard + +"human-id@npm:^4.1.1": + version: 4.1.3 + resolution: "human-id@npm:4.1.3" + bin: + human-id: dist/cli.js + checksum: 10c0/c0e6aacfa71adff6e9783fc209493a7f8de92da041bea32deb3a9cd1244a4d2b89f32d5e90130e8e22da4e6fe15b61cf4e533f114927384de1418460c92b5a7a + languageName: node + linkType: hard + +"iconv-lite@npm:^0.7.0, iconv-lite@npm:^0.7.2": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 + languageName: node + linkType: hard + +"ignore@npm:^5.2.0": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"ip-address@npm:^10.0.1": + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-glob@npm:^4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + languageName: node + linkType: hard + +"is-subdir@npm:^1.1.1": + version: 1.2.0 + resolution: "is-subdir@npm:1.2.0" + dependencies: + better-path-resolve: "npm:1.0.0" + checksum: 10c0/03a03ee2ee6578ce589b1cfaf00e65c86b20fd1b82c1660625557c535439a7477cda77e20c62cda6d4c99e7fd908b4619355ae2d989f4a524a35350a44353032 + languageName: node + linkType: hard + +"is-windows@npm:^1.0.0": + version: 1.0.2 + resolution: "is-windows@npm:1.0.2" + checksum: 10c0/b32f418ab3385604a66f1b7a3ce39d25e8881dee0bd30816dc8344ef6ff9df473a732bcc1ec4e84fe99b2f229ae474f7133e8e93f9241686cfcf7eebe53ba7a5 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^4.0.0": + version: 4.0.0 + resolution: "isexe@npm:4.0.0" + checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce + languageName: node + linkType: hard + +"js-yaml@npm:^3.6.1": + version: 3.14.2 + resolution: "js-yaml@npm:3.14.2" + dependencies: + argparse: "npm:^1.0.7" + esprima: "npm:^4.0.0" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/3261f25912f5dd76605e5993d0a126c2b6c346311885d3c483706cd722efe34f697ea0331f654ce27c00a42b426e524518ec89d65ed02ea47df8ad26dcc8ce69 + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.1": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7 + languageName: node + linkType: hard + +"jsonfile@npm:^4.0.0": + version: 4.0.0 + resolution: "jsonfile@npm:4.0.0" + dependencies: + graceful-fs: "npm:^4.1.6" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10c0/7dc94b628d57a66b71fb1b79510d460d662eb975b5f876d723f81549c2e9cd316d58a2ddf742b2b93a4fa6b17b2accaf1a738a0e2ea114bdfb13a32e5377e480 + languageName: node + linkType: hard + +"locate-path@npm:^5.0.0": + version: 5.0.0 + resolution: "locate-path@npm:5.0.0" + dependencies: + p-locate: "npm:^4.1.0" + checksum: 10c0/33a1c5247e87e022f9713e6213a744557a3e9ec32c5d0b5efb10aa3a38177615bf90221a5592674857039c1a0fd2063b82f285702d37b792d973e9e72ace6c59 + languageName: node + linkType: hard + +"lodash.startcase@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.startcase@npm:4.4.0" + checksum: 10c0/bd82aa87a45de8080e1c5ee61128c7aee77bf7f1d86f4ff94f4a6d7438fc9e15e5f03374b947be577a93804c8ad6241f0251beaf1452bf716064eeb657b3a9f0 + languageName: node + linkType: hard + +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": + version: 11.2.6 + resolution: "lru-cache@npm:11.2.6" + checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd + languageName: node + linkType: hard + +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a + languageName: node + linkType: hard + +"make-fetch-happen@npm:^15.0.0": + version: 15.0.4 + resolution: "make-fetch-happen@npm:15.0.4" + dependencies: + "@gar/promise-retry": "npm:^1.0.0" + "@npmcli/agent": "npm:^4.0.0" + cacache: "npm:^20.0.1" + http-cache-semantics: "npm:^4.1.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^5.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^1.0.0" + proc-log: "npm:^6.0.0" + ssri: "npm:^13.0.0" + checksum: 10c0/b874bf6879fc0b8ef3a3cafdddadea4d956acf94790f8dede1a9d3c74c7886b6cd3eb992616b8e5935e6fd550016a465f10ba51bf6723a0c6f4d98883ae2926b + languageName: node + linkType: hard + +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb + languageName: node + linkType: hard + +"micromatch@npm:^4.0.8": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8 + languageName: node + linkType: hard + +"minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" + dependencies: + brace-expansion: "npm:^5.0.2" + checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^5.0.0": + version: 5.0.2 + resolution: "minipass-fetch@npm:5.0.2" + dependencies: + iconv-lite: "npm:^0.7.2" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^2.0.0" + minizlib: "npm:^3.0.1" + dependenciesMeta: + iconv-lite: + optional: true + checksum: 10c0/ce4ab9f21cfabaead2097d95dd33f485af8072fbc6b19611bce694965393453a1639d641c2bcf1c48f2ea7d41ea7fab8278373f1d0bee4e63b0a5b2cdd0ef649 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^2.0.0": + version: 2.0.0 + resolution: "minipass-sized@npm:2.0.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/f9201696a6f6d68610d04c9c83e3d2e5cb9c026aae1c8cbf7e17f386105cb79c1bb088dbc21bf0b1eb4f3fb5df384fd1e7aa3bf1f33868c416ae8c8a92679db8 + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec + languageName: node + linkType: hard + +"mri@npm:^1.2.0": + version: 1.2.0 + resolution: "mri@npm:1.2.0" + checksum: 10c0/a3d32379c2554cf7351db6237ddc18dc9e54e4214953f3da105b97dc3babe0deb3ffe99cf409b38ea47cc29f9430561ba6b53b24ab8f9ce97a4b50409e4a50e7 + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"nanoid@npm:^3.3.11": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" + bin: + nanoid: bin/nanoid.cjs + checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b + languageName: node + linkType: hard + +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 12.2.0 + resolution: "node-gyp@npm:12.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^15.0.0" + nopt: "npm:^9.0.0" + proc-log: "npm:^6.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.5.4" + tinyglobby: "npm:^0.2.12" + which: "npm:^6.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/3ed046746a5a7d90950cd8b0547332b06598443f31fe213ef4332a7174c7b7d259e1704835feda79b87d3f02e59d7791842aac60642ede4396ab25fdf0f8f759 + languageName: node + linkType: hard + +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" + dependencies: + abbrev: "npm:^4.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd + languageName: node + linkType: hard + +"obug@npm:^2.1.1": + version: 2.1.1 + resolution: "obug@npm:2.1.1" + checksum: 10c0/59dccd7de72a047e08f8649e94c1015ec72f94eefb6ddb57fb4812c4b425a813bc7e7cd30c9aca20db3c59abc3c85cc7a62bb656a968741d770f4e8e02bc2e78 + languageName: node + linkType: hard + +"outdent@npm:^0.5.0": + version: 0.5.0 + resolution: "outdent@npm:0.5.0" + checksum: 10c0/e216a4498889ba1babae06af84cdc4091f7cac86da49d22d0163b3be202a5f52efcd2bcd3dfca60a361eb3a27b4299f185c5655061b6b402552d7fcd1d040cff + languageName: node + linkType: hard + +"p-filter@npm:^2.1.0": + version: 2.1.0 + resolution: "p-filter@npm:2.1.0" + dependencies: + p-map: "npm:^2.0.0" + checksum: 10c0/5ac34b74b3b691c04212d5dd2319ed484f591c557a850a3ffc93a08cb38c4f5540be059c6b10a185773c479ca583a91ea00c7d6c9958c815e6b74d052f356645 + languageName: node + linkType: hard + +"p-limit@npm:^2.2.0": + version: 2.3.0 + resolution: "p-limit@npm:2.3.0" + dependencies: + p-try: "npm:^2.0.0" + checksum: 10c0/8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 + languageName: node + linkType: hard + +"p-locate@npm:^4.1.0": + version: 4.1.0 + resolution: "p-locate@npm:4.1.0" + dependencies: + p-limit: "npm:^2.2.0" + checksum: 10c0/1b476ad69ad7f6059744f343b26d51ce091508935c1dbb80c4e0a2f397ffce0ca3a1f9f5cd3c7ce19d7929a09719d5c65fe70d8ee289c3f267cd36f2881813e9 + languageName: node + linkType: hard + +"p-map@npm:^2.0.0": + version: 2.1.0 + resolution: "p-map@npm:2.1.0" + checksum: 10c0/735dae87badd4737a2dd582b6d8f93e49a1b79eabbc9815a4d63a528d5e3523e978e127a21d784cccb637010e32103a40d2aaa3ab23ae60250b1a820ca752043 + languageName: node + linkType: hard + +"p-map@npm:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 10c0/a5030935d3cb2919d7e89454d1ce82141e6f9955413658b8c9403cfe379283770ed3048146b44cde168aa9e8c716505f196d5689db0ae3ce9a71521a2fef3abd + languageName: node + linkType: hard + +"p-try@npm:^2.0.0": + version: 2.2.0 + resolution: "p-try@npm:2.2.0" + checksum: 10c0/c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f + languageName: node + linkType: hard + +"package-manager-detector@npm:^0.2.0": + version: 0.2.11 + resolution: "package-manager-detector@npm:0.2.11" + dependencies: + quansync: "npm:^0.2.7" + checksum: 10c0/247991de461b9e731f3463b7dae9ce187e53095b7b94d7d96eec039abf418b61ccf74464bec1d0c11d97311f33472e77baccd4c5898f77358da4b5b33395e0b1 + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: "npm:^11.0.0" + minipass: "npm:^7.1.2" + checksum: 10c0/b35ad37cf6557a87fd057121ce2be7695380c9138d93e87ae928609da259ea0a170fac6f3ef1eb3ece8a068e8b7f2f3adf5bb2374cf4d4a57fe484954fcc9482 + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c + languageName: node + linkType: hard + +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 + languageName: node + linkType: hard + +"picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 + languageName: node + linkType: hard + +"picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + languageName: node + linkType: hard + +"picomatch@npm:^4.0.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 + languageName: node + linkType: hard + +"pify@npm:^4.0.1": + version: 4.0.1 + resolution: "pify@npm:4.0.1" + checksum: 10c0/6f9d404b0d47a965437403c9b90eca8bb2536407f03de165940e62e72c8c8b75adda5516c6b9b23675a5877cc0bcac6bdfb0ef0e39414cd2476d5495da40e7cf + languageName: node + linkType: hard + +"postcss@npm:^8.5.6": + version: 8.5.8 + resolution: "postcss@npm:8.5.8" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/dd918f7127ee7c60a0295bae2e72b3787892296e1d1c3c564d7a2a00c68d8df83cadc3178491259daa19ccc54804fb71ed8c937c6787e08d8bd4bedf8d17044c + languageName: node + linkType: hard + +"prettier@npm:^2.7.1": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: 10c0/463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a + languageName: node + linkType: hard + +"proc-log@npm:^6.0.0": + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: 10c0/4f178d4062733ead9d71a9b1ab24ebcecdfe2250916a5b1555f04fe2eda972a0ec76fbaa8df1ad9c02707add6749219d118a4fc46dc56bdfe4dde4b47d80bb82 + languageName: node + linkType: hard + +"quansync@npm:^0.2.7": + version: 0.2.11 + resolution: "quansync@npm:0.2.11" + checksum: 10c0/cb9a1f8ebce074069f2f6a78578873ffedd9de9f6aa212039b44c0870955c04a71c3b1311b5d97f8ac2f2ec476de202d0a5c01160cb12bc0a11b7ef36d22ef56 + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 + languageName: node + linkType: hard + +"read-yaml-file@npm:^1.1.0": + version: 1.1.0 + resolution: "read-yaml-file@npm:1.1.0" + dependencies: + graceful-fs: "npm:^4.1.5" + js-yaml: "npm:^3.6.1" + pify: "npm:^4.0.1" + strip-bom: "npm:^3.0.0" + checksum: 10c0/85a9ba08bb93f3c91089bab4f1603995ec7156ee595f8ce40ae9f49d841cbb586511508bd47b7cf78c97f678c679b2c6e2c0092e63f124214af41b6f8a25ca31 + languageName: node + linkType: hard + +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: 10c0/b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267 + languageName: node + linkType: hard + +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 10c0/b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 + languageName: node + linkType: hard + +"retry@npm:^0.13.1": + version: 0.13.1 + resolution: "retry@npm:0.13.1" + checksum: 10c0/9ae822ee19db2163497e074ea919780b1efa00431d197c7afdb950e42bf109196774b92a49fc9821f0b8b328a98eea6017410bfc5e8a0fc19c85c6d11adb3772 + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.1.0 + resolution: "reusify@npm:1.1.0" + checksum: 10c0/4eff0d4a5f9383566c7d7ec437b671cc51b25963bd61bf127c3f3d3f68e44a026d99b8d2f1ad344afff8d278a8fe70a8ea092650a716d22287e8bef7126bb2fa + languageName: node + linkType: hard + +"rollup@npm:^4.43.0": + version: 4.59.0 + resolution: "rollup@npm:4.59.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.59.0" + "@rollup/rollup-android-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-x64": "npm:4.59.0" + "@rollup/rollup-freebsd-arm64": "npm:4.59.0" + "@rollup/rollup-freebsd-x64": "npm:4.59.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.59.0" + "@rollup/rollup-linux-loong64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-loong64-musl": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-musl": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.59.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-musl": "npm:4.59.0" + "@rollup/rollup-openbsd-x64": "npm:4.59.0" + "@rollup/rollup-openharmony-arm64": "npm:4.59.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.59.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.59.0" + "@rollup/rollup-win32-x64-gnu": "npm:4.59.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.59.0" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/f38742da34cfee5e899302615fa157aa77cb6a2a1495e5e3ce4cc9c540d3262e235bbe60caa31562bbfe492b01fdb3e7a8c43c39d842d3293bcf843123b766fc + languageName: node + linkType: hard + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 10c0/200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.5.3": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"slash@npm:^3.0.0": + version: 3.0.0 + resolution: "slash@npm:3.0.0" + checksum: 10c0/e18488c6a42bdfd4ac5be85b2ced3ccd0224773baae6ad42cfbb9ec74fc07f9fa8396bd35ee638084ead7a2a0818eb5e7151111544d4731ce843019dab4be47b + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10c0/5d2c6cecba6821389aabf18728325730504bf9bb1d9e342e7987a5d13badd7a98838cc9a55b8ed3cb866ad37cc23e1086f09c4d72d93105ce9dfe76330e9d2a6 + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.7 + resolution: "socks@npm:2.8.7" + dependencies: + ip-address: "npm:^10.0.1" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/2805a43a1c4bcf9ebf6e018268d87b32b32b06fbbc1f9282573583acc155860dc361500f89c73bfbb157caa1b4ac78059eac0ef15d1811eb0ca75e0bdadbc9d2 + languageName: node + linkType: hard + +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf + languageName: node + linkType: hard + +"spawndamnit@npm:^3.0.1": + version: 3.0.1 + resolution: "spawndamnit@npm:3.0.1" + dependencies: + cross-spawn: "npm:^7.0.5" + signal-exit: "npm:^4.0.1" + checksum: 10c0/a9821a59bc78a665bd44718dea8f4f4010bb1a374972b0a6a1633b9186cda6d6fd93f22d1e49d9944d6bb175ba23ce29036a4bd624884fb157d981842c3682f3 + languageName: node + linkType: hard + +"sprintf-js@npm:~1.0.2": + version: 1.0.3 + resolution: "sprintf-js@npm:1.0.3" + checksum: 10c0/ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb + languageName: node + linkType: hard + +"ssri@npm:^13.0.0": + version: 13.0.1 + resolution: "ssri@npm:13.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/cf6408a18676c57ff2ed06b8a20dc64bb3e748e5c7e095332e6aecaa2b8422b1e94a739a8453bf65156a8a47afe23757ba4ab52d3ea3b62322dc40875763e17a + languageName: node + linkType: hard + +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + +"std-env@npm:^3.10.0": + version: 3.10.0 + resolution: "std-env@npm:3.10.0" + checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f + languageName: node + linkType: hard + +"strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-bom@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-bom@npm:3.0.0" + checksum: 10c0/51201f50e021ef16672593d7434ca239441b7b760e905d9f33df6e4f3954ff54ec0e0a06f100d028af0982d6f25c35cd5cda2ce34eaebccd0250b8befb90d8f1 + languageName: node + linkType: hard + +"tar@npm:^7.5.4": + version: 7.5.9 + resolution: "tar@npm:7.5.9" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.1.0" + yallist: "npm:^5.0.0" + checksum: 10c0/e870beb1b2477135ca2abe86b2d18f7b35d0a4e3a37bbc523d3b8f7adca268dfab543f26528a431d569897f8c53a7cac745cdfbc4411c2f89aeeacc652b81b0a + languageName: node + linkType: hard + +"term-size@npm:^2.1.0": + version: 2.2.1 + resolution: "term-size@npm:2.2.1" + checksum: 10c0/89f6bba1d05d425156c0910982f9344d9e4aebf12d64bfa1f460d93c24baa7bc4c4a21d355fbd7153c316433df0538f64d0ae6e336cc4a69fdda4f85d62bc79d + languageName: node + linkType: hard + +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + +"tinyexec@npm:^1.0.2": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10c0/1261a8e34c9b539a9aae3b7f0bb5372045ff28ee1eba035a2a059e532198fe1a182ec61ac60fa0b4a4129f0c4c4b1d2d57355b5cb9aa2d17ac9454ecace502ee + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 + languageName: node + linkType: hard + +"tinyrainbow@npm:^3.0.3": + version: 3.0.3 + resolution: "tinyrainbow@npm:3.0.3" + checksum: 10c0/1e799d35cd23cabe02e22550985a3051dc88814a979be02dc632a159c393a998628eacfc558e4c746b3006606d54b00bcdea0c39301133956d10a27aa27e988c + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + languageName: node + linkType: hard + +"transloadit-notify-url-proxy@workspace:.": + version: 0.0.0-use.local + resolution: "transloadit-notify-url-proxy@workspace:." + dependencies: + "@biomejs/biome": "npm:^2.4.5" + "@changesets/cli": "npm:^2.30.0" + "@types/http-proxy": "npm:^1.17.17" + "@types/node": "npm:^25.3.3" + http-proxy: "npm:^1.18.1" + typescript: "npm:^5.9.3" + vitest: "npm:^4.0.18" + bin: + notify-url-proxy: ./bin/notify-url-proxy.ts + languageName: unknown + linkType: soft + +"typescript@npm:^5.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 + languageName: node + linkType: hard + +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d + languageName: node + linkType: hard + +"unique-filename@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-filename@npm:5.0.0" + dependencies: + unique-slug: "npm:^6.0.0" + checksum: 10c0/afb897e9cf4c2fb622ea716f7c2bb462001928fc5f437972213afdf1cc32101a230c0f1e9d96fc91ee5185eca0f2feb34127145874975f347be52eb91d6ccc2c + languageName: node + linkType: hard + +"unique-slug@npm:^6.0.0": + version: 6.0.0 + resolution: "unique-slug@npm:6.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/da7ade4cb04eb33ad0499861f82fe95ce9c7c878b7139dc54d140ecfb6a6541c18a5c8dac16188b8b379fe62c0c1f1b710814baac910cde5f4fec06212126c6a + languageName: node + linkType: hard + +"universalify@npm:^0.1.0": + version: 0.1.2 + resolution: "universalify@npm:0.1.2" + checksum: 10c0/e70e0339f6b36f34c9816f6bf9662372bd241714dc77508d231d08386d94f2c4aa1ba1318614f92015f40d45aae1b9075cd30bd490efbe39387b60a76ca3f045 + languageName: node + linkType: hard + +"vite@npm:^6.0.0 || ^7.0.0": + version: 7.3.1 + resolution: "vite@npm:7.3.1" + dependencies: + esbuild: "npm:^0.27.0" + fdir: "npm:^6.5.0" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.15" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/5c7548f5f43a23533e53324304db4ad85f1896b1bfd3ee32ae9b866bac2933782c77b350eb2b52a02c625c8ad1ddd4c000df077419410650c982cd97fde8d014 + languageName: node + linkType: hard + +"vitest@npm:^4.0.18": + version: 4.0.18 + resolution: "vitest@npm:4.0.18" + dependencies: + "@vitest/expect": "npm:4.0.18" + "@vitest/mocker": "npm:4.0.18" + "@vitest/pretty-format": "npm:4.0.18" + "@vitest/runner": "npm:4.0.18" + "@vitest/snapshot": "npm:4.0.18" + "@vitest/spy": "npm:4.0.18" + "@vitest/utils": "npm:4.0.18" + es-module-lexer: "npm:^1.7.0" + expect-type: "npm:^1.2.2" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^3.10.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.0.3" + vite: "npm:^6.0.0 || ^7.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.0.18 + "@vitest/browser-preview": 4.0.18 + "@vitest/browser-webdriverio": 4.0.18 + "@vitest/ui": 4.0.18 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/b913cd32032c95f29ff08c931f4b4c6fd6d2da498908d6770952c561a1b8d75c62499a1f04cadf82fb89cc0f9a33f29fb5dfdb899f6dbb27686a9d91571be5fa + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^6.0.0": + version: 6.0.1 + resolution: "which@npm:6.0.1" + dependencies: + isexe: "npm:^4.0.0" + bin: + node-which: bin/which.js + checksum: 10c0/7e710e54ea36d2d6183bee2f9caa27a3b47b9baf8dee55a199b736fcf85eab3b9df7556fca3d02b50af7f3dfba5ea3a45644189836df06267df457e354da66d5 + languageName: node + linkType: hard + +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 + languageName: node + linkType: hard From cc893f50db7d1a7ad1c3ade1a00c14b58ea73d90 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 20:49:39 +0100 Subject: [PATCH 02/11] refactor(cli): require TRANSLOADIT_SECRET env var --- README.md | 3 ++- bin/notify-url-proxy.ts | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ee4c7e5..09aad80 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ yarn add transloadit-notify-url-proxy ## CLI usage ```bash +export TRANSLOADIT_SECRET="your-secret" + notify-url-proxy \ - --secret "$TRANSLOADIT_SECRET" \ --notifyUrl "http://127.0.0.1:3000/transloadit" \ --port 8888 ``` diff --git a/bin/notify-url-proxy.ts b/bin/notify-url-proxy.ts index 4cfab5e..908ea03 100755 --- a/bin/notify-url-proxy.ts +++ b/bin/notify-url-proxy.ts @@ -19,7 +19,6 @@ function parsePositiveIntOption( const { values } = parseArgs({ options: { - secret: { type: 'string' }, notifyUrl: { type: 'string' }, target: { type: 'string' }, port: { type: 'string' }, @@ -33,7 +32,6 @@ if (values.help) { console.log(`Usage: notify-url-proxy [options] Options: - --secret HMAC secret for notification signatures (required) --notifyUrl URL to send notifications to --target Transloadit assemblies endpoint to proxy to --port Local listen port @@ -47,9 +45,9 @@ Environment fallback: process.exit(0); } -const secret = values.secret ?? process.env.TRANSLOADIT_SECRET; +const secret = process.env.TRANSLOADIT_SECRET; if (!secret) { - console.error('Missing secret. Provide --secret or TRANSLOADIT_SECRET.'); + console.error('Missing secret. Set TRANSLOADIT_SECRET.'); process.exit(1); } From 97214914a7cdd68180c5f5bd18b3261b63170339 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 20:55:28 +0100 Subject: [PATCH 03/11] refactor: use zod assembly states and no-semicolon style --- bin/notify-url-proxy.ts | 48 +++++----- biome.json | 3 +- package.json | 1 + src/index.ts | 190 +++++++++++++++++++++------------------- test/index.test.ts | 45 +++++----- vitest.config.ts | 4 +- yarn.lock | 25 ++++++ 7 files changed, 178 insertions(+), 138 deletions(-) diff --git a/bin/notify-url-proxy.ts b/bin/notify-url-proxy.ts index 908ea03..6abde57 100755 --- a/bin/notify-url-proxy.ts +++ b/bin/notify-url-proxy.ts @@ -1,20 +1,20 @@ #!/usr/bin/env node -import { parseArgs } from 'node:util'; +import { parseArgs } from 'node:util' -import TransloaditNotifyUrlProxy, { type ProxySettings } from '../src/index.ts'; +import TransloaditNotifyUrlProxy, { type ProxySettings } from '../src/index.ts' function parsePositiveIntOption( name: string, value: string, max = Number.MAX_SAFE_INTEGER, ): number { - const parsed = Number.parseInt(value, 10); + const parsed = Number.parseInt(value, 10) if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) { - console.error(`Invalid ${name}: ${value}`); - process.exit(1); + console.error(`Invalid ${name}: ${value}`) + process.exit(1) } - return parsed; + return parsed } const { values } = parseArgs({ @@ -26,7 +26,7 @@ const { values } = parseArgs({ maxPollAttempts: { type: 'string' }, help: { type: 'boolean', short: 'h' }, }, -}); +}) if (values.help) { console.log(`Usage: notify-url-proxy [options] @@ -41,41 +41,41 @@ Options: Environment fallback: TRANSLOADIT_SECRET, TRANSLOADIT_NOTIFY_URL -`); - process.exit(0); +`) + process.exit(0) } -const secret = process.env.TRANSLOADIT_SECRET; +const secret = process.env.TRANSLOADIT_SECRET if (!secret) { - console.error('Missing secret. Set TRANSLOADIT_SECRET.'); - process.exit(1); + console.error('Missing secret. Set TRANSLOADIT_SECRET.') + process.exit(1) } -const settings: Partial = {}; +const settings: Partial = {} if (values.target) { - settings.target = values.target; + settings.target = values.target } if (values.port) { - settings.port = parsePositiveIntOption('port', values.port, 65_535); + settings.port = parsePositiveIntOption('port', values.port, 65_535) } if (values.pollIntervalMs) { - settings.pollIntervalMs = parsePositiveIntOption('pollIntervalMs', values.pollIntervalMs); + settings.pollIntervalMs = parsePositiveIntOption('pollIntervalMs', values.pollIntervalMs) } if (values.maxPollAttempts) { - settings.maxPollAttempts = parsePositiveIntOption('maxPollAttempts', values.maxPollAttempts); + settings.maxPollAttempts = parsePositiveIntOption('maxPollAttempts', values.maxPollAttempts) } const proxy = new TransloaditNotifyUrlProxy( secret, values.notifyUrl ?? process.env.TRANSLOADIT_NOTIFY_URL, -); -proxy.run(settings); +) +proxy.run(settings) const close = () => { - proxy.close(); - process.exit(0); -}; + proxy.close() + process.exit(0) +} -process.on('SIGINT', close); -process.on('SIGTERM', close); +process.on('SIGINT', close) +process.on('SIGTERM', close) diff --git a/biome.json b/biome.json index 80e6b1b..d633f59 100644 --- a/biome.json +++ b/biome.json @@ -19,7 +19,8 @@ }, "javascript": { "formatter": { - "quoteStyle": "single" + "quoteStyle": "single", + "semicolons": "asNeeded" } } } diff --git a/package.json b/package.json index 7d51752..f48aa28 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "changeset:version": "changeset version" }, "dependencies": { + "@transloadit/zod": "^4.3.0", "http-proxy": "^1.18.1" }, "devDependencies": { diff --git a/src/index.ts b/src/index.ts index bf7c42f..75d3784 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,26 @@ -import { createHmac } from 'node:crypto'; -import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; -import { setTimeout as delay } from 'node:timers/promises'; +import { createHmac } from 'node:crypto' +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { setTimeout as delay } from 'node:timers/promises' -import httpProxy from 'http-proxy'; +import { + assemblyStatusOkCodeSchema, + isAssemblyBusyStatus, + isAssemblyTerminalOkStatus, +} from '@transloadit/zod/v4' +import httpProxy from 'http-proxy' export interface ProxySettings { - target: string; - port: number; - pollIntervalMs: number; - maxPollAttempts: number; + target: string + port: number + pollIntervalMs: number + maxPollAttempts: number } +type KnownAssemblyState = (typeof assemblyStatusOkCodeSchema.options)[number] + export interface AssemblyResponse { - ok: string; - [key: string]: unknown; + ok: KnownAssemblyState + [key: string]: unknown } const DEFAULT_SETTINGS: ProxySettings = { @@ -21,164 +28,161 @@ const DEFAULT_SETTINGS: ProxySettings = { port: 8888, pollIntervalMs: 2_000, maxPollAttempts: 10, -}; - -const KNOWN_STATES = new Set(['ASSEMBLY_COMPLETED', 'ASSEMBLY_UPLOADING', 'ASSEMBLY_EXECUTING']); - -type KnownAssemblyState = 'ASSEMBLY_COMPLETED' | 'ASSEMBLY_UPLOADING' | 'ASSEMBLY_EXECUTING'; +} function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null; + return typeof value === 'object' && value !== null } function toErrorMessage(error: unknown): string { if (error instanceof Error) { - return error.message; + return error.message } - return String(error); + return String(error) } export function extractAssemblyUrl(body: string): string | null { try { - const payload = JSON.parse(body) as unknown; + const payload = JSON.parse(body) as unknown if (!isRecord(payload)) { - return null; + return null } - const assemblyUrl = payload.assembly_url; - return typeof assemblyUrl === 'string' && assemblyUrl.length > 0 ? assemblyUrl : null; + const assemblyUrl = payload.assembly_url + return typeof assemblyUrl === 'string' && assemblyUrl.length > 0 ? assemblyUrl : null } catch { - return null; + return null } } export function getAssemblyState(payload: unknown): KnownAssemblyState { if (!isRecord(payload) || typeof payload.ok !== 'string') { - throw new Error('No ok field found in Assembly response.'); + throw new Error('No ok field found in Assembly response.') } - if (!KNOWN_STATES.has(payload.ok)) { - throw new Error(`Unknown Assembly state found: ${payload.ok}`); + const parsedState = assemblyStatusOkCodeSchema.safeParse(payload.ok) + if (!parsedState.success) { + throw new Error(`Unknown Assembly state found: ${payload.ok}`) } - return payload.ok as KnownAssemblyState; + return parsedState.data } export function getSignature(secret: string, toSign: string): string { - return createHmac('sha1', secret).update(Buffer.from(toSign, 'utf-8')).digest('hex'); + return createHmac('sha1', secret).update(Buffer.from(toSign, 'utf-8')).digest('hex') } export default class TransloaditNotifyUrlProxy { - private server: Server | null = null; - private proxy: httpProxy | null = null; + private server: Server | null = null + private proxy: httpProxy | null = null - private readonly secret: string; - private readonly notifyUrl: string; - private readonly defaults: ProxySettings; - private settings: ProxySettings; + private readonly secret: string + private readonly notifyUrl: string + private readonly defaults: ProxySettings + private settings: ProxySettings constructor(secret: string, notifyUrl = 'http://127.0.0.1:3000/transloadit') { - this.secret = secret || ''; - this.notifyUrl = notifyUrl; + this.secret = secret || '' + this.notifyUrl = notifyUrl - this.defaults = { ...DEFAULT_SETTINGS }; - this.settings = { ...DEFAULT_SETTINGS }; + this.defaults = { ...DEFAULT_SETTINGS } + this.settings = { ...DEFAULT_SETTINGS } } run(opts: Partial = {}): void { if (this.server !== null || this.proxy !== null) { - this.close(); + this.close() } - this.settings = { ...this.defaults, ...opts }; + this.settings = { ...this.defaults, ...opts } - this.createProxy(); - this.createServer(); + this.createProxy() + this.createServer() } close(): void { - this.server?.close(); - this.server = null; + this.server?.close() + this.server = null - this.proxy?.close(); - this.proxy = null; + this.proxy?.close() + this.proxy = null } private createProxy(): void { this.proxy = httpProxy.createProxyServer({ target: this.settings.target, changeOrigin: true, - }); + }) this.proxy.on('error', (error, _req, res) => { if ('writeHead' in res) { if (!res.headersSent) { - res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' }); + res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' }) } - res.end('Proxy error'); + res.end('Proxy error') } else { - res.end(); + res.end() } - this.out('Proxy error: %s', toErrorMessage(error)); - }); + this.out('Proxy error: %s', toErrorMessage(error)) + }) this.proxy.on('proxyRes', (proxyRes) => { - void this.handleProxyResponse(proxyRes); - }); + void this.handleProxyResponse(proxyRes) + }) } private createServer(): void { if (this.proxy === null) { - throw new Error('Proxy is not initialized.'); + throw new Error('Proxy is not initialized.') } this.server = createServer((req, res) => { - this.proxy?.web(req, res); - }); + this.proxy?.web(req, res) + }) - this.server.listen(this.settings.port); + this.server.listen(this.settings.port) this.out( 'Listening on http://localhost:%d, forwarding to %s, notifying %s', this.settings.port, this.settings.target, this.notifyUrl, - ); + ) } private async handleProxyResponse(proxyRes: IncomingMessage): Promise { - const body = await this.readResponseBody(proxyRes); - const assemblyUrl = extractAssemblyUrl(body); + const body = await this.readResponseBody(proxyRes) + const assemblyUrl = extractAssemblyUrl(body) if (assemblyUrl === null) { - return; + return } - this.out('Received proxy response, polling assemblyUrl: %s', assemblyUrl); - await this.pollAssembly(assemblyUrl); + this.out('Received proxy response, polling assemblyUrl: %s', assemblyUrl) + await this.pollAssembly(assemblyUrl) } private async readResponseBody(response: IncomingMessage): Promise { - const chunks: Buffer[] = []; + const chunks: Buffer[] = [] for await (const chunk of response) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) } - return Buffer.concat(chunks).toString('utf-8'); + return Buffer.concat(chunks).toString('utf-8') } private async pollAssembly(assemblyUrl: string): Promise { for (let attempt = 1; attempt <= this.settings.maxPollAttempts; attempt += 1) { try { - const response = await this.checkAssembly(assemblyUrl); - await this.notify(response); - return; + const response = await this.checkAssembly(assemblyUrl) + await this.notify(response) + return } catch (error) { if (attempt === this.settings.maxPollAttempts) { - this.out('No attempts left, giving up on checking assemblyUrl: %s', assemblyUrl); - return; + this.out('No attempts left, giving up on checking assemblyUrl: %s', assemblyUrl) + return } this.out( @@ -187,37 +191,43 @@ export default class TransloaditNotifyUrlProxy { this.settings.maxPollAttempts, assemblyUrl, toErrorMessage(error), - ); + ) - await delay(this.settings.pollIntervalMs); + await delay(this.settings.pollIntervalMs) } } } private async checkAssembly(assemblyUrl: string): Promise { - const response = await fetch(assemblyUrl); + const response = await fetch(assemblyUrl) if (!response.ok) { - throw new Error(`Assembly poll returned HTTP ${response.status}`); + throw new Error(`Assembly poll returned HTTP ${response.status}`) } - const payload = (await response.json()) as unknown; - const state = getAssemblyState(payload); + const payload = (await response.json()) as unknown + const state = getAssemblyState(payload) - if (state === 'ASSEMBLY_COMPLETED') { - this.out('%s completed.', assemblyUrl); - return payload as AssemblyResponse; + if (isAssemblyTerminalOkStatus(state)) { + this.out('%s reached terminal state %s.', assemblyUrl, state) + return payload as AssemblyResponse } - if (state === 'ASSEMBLY_UPLOADING') { - throw new Error(`${assemblyUrl} is still uploading.`); + if (isAssemblyBusyStatus(state)) { + if (state === 'ASSEMBLY_UPLOADING') { + throw new Error(`${assemblyUrl} is still uploading.`) + } + if (state === 'ASSEMBLY_EXECUTING') { + throw new Error(`${assemblyUrl} is still executing.`) + } + throw new Error(`${assemblyUrl} is still replaying.`) } - throw new Error(`${assemblyUrl} is still executing.`); + throw new Error(`${assemblyUrl} is in non-terminal state ${state}.`) } private async notify(response: AssemblyResponse): Promise { - const transloadit = JSON.stringify(response); - const signature = getSignature(this.secret, transloadit); + const transloadit = JSON.stringify(response) + const signature = getSignature(this.secret, transloadit) const notifyResponse = await fetch(this.notifyUrl, { method: 'POST', @@ -228,16 +238,16 @@ export default class TransloaditNotifyUrlProxy { transloadit, signature, }), - }); + }) if (!notifyResponse.ok) { - throw new Error(`Notify URL returned HTTP ${notifyResponse.status}`); + throw new Error(`Notify URL returned HTTP ${notifyResponse.status}`) } - this.out('Notify payload sent to %s', this.notifyUrl); + this.out('Notify payload sent to %s', this.notifyUrl) } private out(message: string, ...args: unknown[]): void { - console.log(message, ...args); + console.log(message, ...args) } } diff --git a/test/index.test.ts b/test/index.test.ts index dcd956b..e261a6c 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,39 +1,42 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest' -import { extractAssemblyUrl, getAssemblyState, getSignature } from '../src/index.ts'; +import { extractAssemblyUrl, getAssemblyState, getSignature } from '../src/index.ts' describe('getSignature', () => { it('creates a sha1 hmac signature', () => { - const signature = getSignature('foo_secret', '{"ok":"ASSEMBLY_COMPLETED"}'); - expect(signature).toBe('9c31e806b2f3ac4d7cf69d7c29ccf6806b9ee073'); - }); -}); + const signature = getSignature('foo_secret', '{"ok":"ASSEMBLY_COMPLETED"}') + expect(signature).toBe('9c31e806b2f3ac4d7cf69d7c29ccf6806b9ee073') + }) +}) describe('extractAssemblyUrl', () => { it('extracts assembly_url from proxy payload', () => { expect(extractAssemblyUrl('{"assembly_url":"https://example.test/a/123"}')).toBe( 'https://example.test/a/123', - ); - }); + ) + }) it('returns null for invalid payloads', () => { - expect(extractAssemblyUrl('nope')).toBeNull(); - expect(extractAssemblyUrl('{"foo":"bar"}')).toBeNull(); - }); -}); + expect(extractAssemblyUrl('nope')).toBeNull() + expect(extractAssemblyUrl('{"foo":"bar"}')).toBeNull() + }) +}) describe('getAssemblyState', () => { it('accepts known states', () => { - expect(getAssemblyState({ ok: 'ASSEMBLY_COMPLETED' })).toBe('ASSEMBLY_COMPLETED'); - expect(getAssemblyState({ ok: 'ASSEMBLY_UPLOADING' })).toBe('ASSEMBLY_UPLOADING'); - expect(getAssemblyState({ ok: 'ASSEMBLY_EXECUTING' })).toBe('ASSEMBLY_EXECUTING'); - }); + expect(getAssemblyState({ ok: 'ASSEMBLY_COMPLETED' })).toBe('ASSEMBLY_COMPLETED') + expect(getAssemblyState({ ok: 'ASSEMBLY_CANCELED' })).toBe('ASSEMBLY_CANCELED') + expect(getAssemblyState({ ok: 'REQUEST_ABORTED' })).toBe('REQUEST_ABORTED') + expect(getAssemblyState({ ok: 'ASSEMBLY_UPLOADING' })).toBe('ASSEMBLY_UPLOADING') + expect(getAssemblyState({ ok: 'ASSEMBLY_EXECUTING' })).toBe('ASSEMBLY_EXECUTING') + expect(getAssemblyState({ ok: 'ASSEMBLY_REPLAYING' })).toBe('ASSEMBLY_REPLAYING') + }) it('rejects unknown states', () => { - expect(() => getAssemblyState({ ok: 'UNKNOWN' })).toThrow('Unknown Assembly state found'); - }); + expect(() => getAssemblyState({ ok: 'UNKNOWN' })).toThrow('Unknown Assembly state found') + }) it('rejects malformed payloads', () => { - expect(() => getAssemblyState(null)).toThrow('No ok field found'); - }); -}); + expect(() => getAssemblyState(null)).toThrow('No ok field found') + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index a71174d..f19fc42 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,8 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from 'vitest/config' export default defineConfig({ test: { environment: 'node', include: ['test/**/*.test.ts'], }, -}); +}) diff --git a/yarn.lock b/yarn.lock index e762976..0eb164a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -815,6 +815,16 @@ __metadata: languageName: node linkType: hard +"@transloadit/zod@npm:^4.3.0": + version: 4.3.0 + resolution: "@transloadit/zod@npm:4.3.0" + dependencies: + type-fest: "npm:^4.41.0" + zod: "npm:^4.0.0" + checksum: 10c0/7bbf4fb70dcfd1417a1ec0c65fae0713504f0f19f4a6b737d5082aa9e6eec073af8f3448bd3358d0dcda36e59d862261cef9177e95eef77951edc5441b9d1617 + languageName: node + linkType: hard + "@types/chai@npm:^5.2.2": version: 5.2.3 resolution: "@types/chai@npm:5.2.3" @@ -2335,6 +2345,7 @@ __metadata: dependencies: "@biomejs/biome": "npm:^2.4.5" "@changesets/cli": "npm:^2.30.0" + "@transloadit/zod": "npm:^4.3.0" "@types/http-proxy": "npm:^1.17.17" "@types/node": "npm:^25.3.3" http-proxy: "npm:^1.18.1" @@ -2345,6 +2356,13 @@ __metadata: languageName: unknown linkType: soft +"type-fest@npm:^4.41.0": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 + languageName: node + linkType: hard + "typescript@npm:^5.9.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" @@ -2558,3 +2576,10 @@ __metadata: checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 languageName: node linkType: hard + +"zod@npm:^4.0.0": + version: 4.3.6 + resolution: "zod@npm:4.3.6" + checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307 + languageName: node + linkType: hard From 900465a65435b3afab722938db9c299970b2493f Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 23:25:08 +0100 Subject: [PATCH 04/11] refactor: use zod assembly schema and utils sha384 signing --- README.md | 2 ++ package.json | 1 + src/index.ts | 67 +++++++++++++++++++++++++++++----------------- test/index.test.ts | 12 +++++++-- yarn.lock | 8 ++++++ 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 09aad80..b0b1a47 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ This version is modernized for: - Yarn 4 - Biome + Vitest + GitHub Actions + Changesets +Notify payloads are signed via `@transloadit/utils` using prefixed `sha384` signatures. + ## Install ```bash diff --git a/package.json b/package.json index f48aa28..6ce089e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "changeset:version": "changeset version" }, "dependencies": { + "@transloadit/utils": "^4.3.0", "@transloadit/zod": "^4.3.0", "http-proxy": "^1.18.1" }, diff --git a/src/index.ts b/src/index.ts index 75d3784..943a000 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,17 @@ -import { createHmac } from 'node:crypto' import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' import { setTimeout as delay } from 'node:timers/promises' - +import { signParamsSync } from '@transloadit/utils/node' import { + type AssemblyStatus, assemblyStatusOkCodeSchema, - isAssemblyBusyStatus, - isAssemblyTerminalOkStatus, + assemblyStatusSchema, + getAssemblyStage, + getError, + getOk, + isAssemblyBusy, + isAssemblyTerminalError, + isAssemblyTerminalOk, + parseAssemblyUrls, } from '@transloadit/zod/v4' import httpProxy from 'http-proxy' @@ -18,10 +24,7 @@ export interface ProxySettings { type KnownAssemblyState = (typeof assemblyStatusOkCodeSchema.options)[number] -export interface AssemblyResponse { - ok: KnownAssemblyState - [key: string]: unknown -} +export type AssemblyResponse = AssemblyStatus const DEFAULT_SETTINGS: ProxySettings = { target: 'https://api2.transloadit.com/assemblies/', @@ -34,6 +37,8 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } +class TerminalAssemblyError extends Error {} + function toErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message @@ -45,12 +50,7 @@ function toErrorMessage(error: unknown): string { export function extractAssemblyUrl(body: string): string | null { try { const payload = JSON.parse(body) as unknown - if (!isRecord(payload)) { - return null - } - - const assemblyUrl = payload.assembly_url - return typeof assemblyUrl === 'string' && assemblyUrl.length > 0 ? assemblyUrl : null + return parseAssemblyUrls(payload).assemblyUrl } catch { return null } @@ -70,7 +70,16 @@ export function getAssemblyState(payload: unknown): KnownAssemblyState { } export function getSignature(secret: string, toSign: string): string { - return createHmac('sha1', secret).update(Buffer.from(toSign, 'utf-8')).digest('hex') + return signParamsSync(toSign, secret, 'sha384') +} + +export function parseAssemblyResponse(payload: unknown): AssemblyResponse { + const parsed = assemblyStatusSchema.safeParse(payload) + if (!parsed.success) { + throw new Error('Invalid assembly response payload.') + } + + return parsed.data } export default class TransloaditNotifyUrlProxy { @@ -180,6 +189,11 @@ export default class TransloaditNotifyUrlProxy { await this.notify(response) return } catch (error) { + if (error instanceof TerminalAssemblyError) { + this.out('%s', error.message) + return + } + if (attempt === this.settings.maxPollAttempts) { this.out('No attempts left, giving up on checking assemblyUrl: %s', assemblyUrl) return @@ -204,25 +218,30 @@ export default class TransloaditNotifyUrlProxy { throw new Error(`Assembly poll returned HTTP ${response.status}`) } - const payload = (await response.json()) as unknown - const state = getAssemblyState(payload) + const assembly = parseAssemblyResponse((await response.json()) as unknown) + + if (isAssemblyTerminalError(assembly)) { + const errorCode = getError(assembly) ?? 'UNKNOWN_ERROR' + throw new TerminalAssemblyError(`${assemblyUrl} reached terminal error state ${errorCode}.`) + } - if (isAssemblyTerminalOkStatus(state)) { - this.out('%s reached terminal state %s.', assemblyUrl, state) - return payload as AssemblyResponse + if (isAssemblyTerminalOk(assembly)) { + this.out('%s reached terminal state %s.', assemblyUrl, getOk(assembly)) + return assembly } - if (isAssemblyBusyStatus(state)) { - if (state === 'ASSEMBLY_UPLOADING') { + if (isAssemblyBusy(assembly)) { + const stage = getAssemblyStage(assembly) + if (stage === 'uploading') { throw new Error(`${assemblyUrl} is still uploading.`) } - if (state === 'ASSEMBLY_EXECUTING') { + if (stage === 'processing') { throw new Error(`${assemblyUrl} is still executing.`) } throw new Error(`${assemblyUrl} is still replaying.`) } - throw new Error(`${assemblyUrl} is in non-terminal state ${state}.`) + throw new Error(`${assemblyUrl} returned a non-terminal assembly state.`) } private async notify(response: AssemblyResponse): Promise { diff --git a/test/index.test.ts b/test/index.test.ts index e261a6c..6b4827e 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest' import { extractAssemblyUrl, getAssemblyState, getSignature } from '../src/index.ts' describe('getSignature', () => { - it('creates a sha1 hmac signature', () => { + it('creates a sha384 prefixed hmac signature', () => { const signature = getSignature('foo_secret', '{"ok":"ASSEMBLY_COMPLETED"}') - expect(signature).toBe('9c31e806b2f3ac4d7cf69d7c29ccf6806b9ee073') + expect(signature).toBe( + 'sha384:bf26800c5256b38bbf9375c76894d5b649751903973f99d0f036c8e52f6cda287bed711b73c21dbde4d4df6c8fc540a1', + ) }) }) @@ -16,6 +18,12 @@ describe('extractAssemblyUrl', () => { ) }) + it('extracts assembly_ssl_url when present', () => { + expect(extractAssemblyUrl('{"assembly_ssl_url":"https://secure.example.test/a/123"}')).toBe( + 'https://secure.example.test/a/123', + ) + }) + it('returns null for invalid payloads', () => { expect(extractAssemblyUrl('nope')).toBeNull() expect(extractAssemblyUrl('{"foo":"bar"}')).toBeNull() diff --git a/yarn.lock b/yarn.lock index 0eb164a..4934f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -815,6 +815,13 @@ __metadata: languageName: node linkType: hard +"@transloadit/utils@npm:^4.3.0": + version: 4.3.0 + resolution: "@transloadit/utils@npm:4.3.0" + checksum: 10c0/57c2326a2851cc984093170ae888ca205ef84bc2d13a2d5009cac5a73e6f3a2b5afe899725756e0accc5d4a3e640497571381dc6b374685600665dfacb771d7c + languageName: node + linkType: hard + "@transloadit/zod@npm:^4.3.0": version: 4.3.0 resolution: "@transloadit/zod@npm:4.3.0" @@ -2345,6 +2352,7 @@ __metadata: dependencies: "@biomejs/biome": "npm:^2.4.5" "@changesets/cli": "npm:^2.30.0" + "@transloadit/utils": "npm:^4.3.0" "@transloadit/zod": "npm:^4.3.0" "@types/http-proxy": "npm:^1.17.17" "@types/node": "npm:^25.3.3" From d362449c9e29b2a7663535565a783a64678069de Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 3 Mar 2026 23:28:39 +0100 Subject: [PATCH 05/11] test: add local-server e2e flow for proxy poll and notify --- test/e2e.test.ts | 144 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 test/e2e.test.ts diff --git a/test/e2e.test.ts b/test/e2e.test.ts new file mode 100644 index 0000000..be156d6 --- /dev/null +++ b/test/e2e.test.ts @@ -0,0 +1,144 @@ +import { once } from 'node:events' +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { setTimeout as delay } from 'node:timers/promises' + +import { describe, expect, it } from 'vitest' + +import TransloaditNotifyUrlProxy, { getSignature } from '../src/index.ts' + +async function readBody(request: IncomingMessage): Promise { + const chunks: Buffer[] = [] + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + + return Buffer.concat(chunks).toString('utf-8') +} + +async function listen(server: Server): Promise { + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') { + throw new Error('Could not resolve server address') + } + return address.port +} + +async function closeServer(server: Server): Promise { + await new Promise((resolve) => { + server.close(() => resolve()) + }) +} + +async function getFreePort(): Promise { + const server = createServer() + const port = await listen(server) + await closeServer(server) + return port +} + +function json(response: ServerResponse, statusCode: number, payload: unknown): void { + response.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' }) + response.end(JSON.stringify(payload)) +} + +describe('proxy e2e', () => { + it('proxies assembly creation, polls assembly status, and notifies target', async () => { + const secret = 'foo_secret' + let upstreamPort = 0 + let pollCount = 0 + + let notifyTransloadit: string | null = null + let notifySignature: string | null = null + let resolveNotify: (() => void) | null = null + const notifyReceived = new Promise((resolve) => { + resolveNotify = resolve + }) + + const notifyServer = createServer(async (request, response) => { + if (request.method !== 'POST' || request.url !== '/transloadit') { + response.writeHead(404) + response.end() + return + } + + const payload = new URLSearchParams(await readBody(request)) + notifyTransloadit = payload.get('transloadit') + notifySignature = payload.get('signature') + response.writeHead(200) + response.end('ok') + resolveNotify?.() + }) + + const upstreamServer = createServer((request, response) => { + if (request.method === 'POST' && request.url === '/assemblies') { + json(response, 200, { + assembly_url: `http://127.0.0.1:${upstreamPort}/assembly/123`, + }) + return + } + + if (request.method === 'GET' && request.url === '/assembly/123') { + pollCount += 1 + if (pollCount === 1) { + json(response, 200, { ok: 'ASSEMBLY_EXECUTING' }) + return + } + + json(response, 200, { ok: 'ASSEMBLY_COMPLETED', assembly_id: '123' }) + return + } + + response.writeHead(404) + response.end() + }) + + const notifyPort = await listen(notifyServer) + upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy( + secret, + `http://127.0.0.1:${notifyPort}/transloadit`, + ) + proxy.run({ + target: `http://127.0.0.1:${upstreamPort}`, + port: proxyPort, + pollIntervalMs: 5, + maxPollAttempts: 5, + }) + + try { + const createResponse = await fetch(`http://127.0.0.1:${proxyPort}/assemblies`, { + method: 'POST', + body: new URLSearchParams({ params: '{}' }), + }) + expect(createResponse.status).toBe(200) + + await Promise.race([ + notifyReceived, + delay(3_000).then(() => { + throw new Error('Timed out waiting for notify request') + }), + ]) + + expect(pollCount).toBe(2) + expect(notifyTransloadit).toBeTypeOf('string') + expect(notifySignature).toBeTypeOf('string') + + if (notifyTransloadit === null || notifySignature === null) { + throw new Error('Notify payload did not include transloadit + signature fields') + } + + expect(notifySignature).toBe(getSignature(secret, notifyTransloadit)) + + const body = JSON.parse(notifyTransloadit) as { ok?: string } + expect(body.ok).toBe('ASSEMBLY_COMPLETED') + } finally { + proxy.close() + await closeServer(notifyServer) + await closeServer(upstreamServer) + } + }, 10_000) +}) From c089b4579bcf773501dad8000f533ac3e8534341 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 4 Mar 2026 07:40:17 +0100 Subject: [PATCH 06/11] test: add opt-in real API e2e via node-sdk credentials --- .env.example | 7 + .gitignore | 3 + README.md | 12 + package.json | 3 + scripts/sync-node-sdk-env.sh | 32 + test/real.e2e.test.ts | 149 +++ yarn.lock | 2068 +++++++++++++++++++++++++++++++++- 7 files changed, 2270 insertions(+), 4 deletions(-) create mode 100644 .env.example create mode 100755 scripts/sync-node-sdk-env.sh create mode 100644 test/real.e2e.test.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..41c4d12 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Copy real credentials from ~/code/node-sdk/.env via: +# yarn env:sync:node-sdk + +TRANSLOADIT_KEY=your-key +TRANSLOADIT_SECRET=your-secret +# Optional (defaults to https://api2.transloadit.com) +TRANSLOADIT_ENDPOINT=https://api2.transloadit.com diff --git a/.gitignore b/.gitignore index b2561b4..d587504 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ _bundle/ env.*.sh env.sh +.env +.env.* +!.env.example node_modules/ node-flame.htm node-flame.svg diff --git a/README.md b/README.md index b0b1a47..9d8662b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,18 @@ yarn test yarn check ``` +## Real API E2E + +Run an opt-in test against the real Transloadit API: + +```bash +yarn env:sync:node-sdk +yarn test:real +``` + +This copies `TRANSLOADIT_KEY` and `TRANSLOADIT_SECRET` from `~/code/node-sdk/.env` into a local +`.env` (gitignored), then runs `test/real.e2e.test.ts`. + ## Releases Changesets drives releases: diff --git a/package.json b/package.json index 6ce089e..a78eb64 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,12 @@ }, "scripts": { "start": "node ./bin/notify-url-proxy.ts", + "env:sync:node-sdk": "bash ./scripts/sync-node-sdk-env.sh", "lint": "biome check .", "format": "biome format . --write", "typecheck": "tsc --noEmit", "test": "vitest run", + "test:real": "RUN_REAL_E2E=1 vitest run test/real.e2e.test.ts", "test:watch": "vitest", "check": "yarn lint && yarn typecheck && yarn test", "changeset": "changeset", @@ -41,6 +43,7 @@ "@changesets/cli": "^2.30.0", "@types/http-proxy": "^1.17.17", "@types/node": "^25.3.3", + "transloadit": "^4.7.4", "typescript": "^5.9.3", "vitest": "^4.0.18" } diff --git a/scripts/sync-node-sdk-env.sh b/scripts/sync-node-sdk-env.sh new file mode 100755 index 0000000..a097462 --- /dev/null +++ b/scripts/sync-node-sdk-env.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +SOURCE="${1:-$HOME/code/node-sdk/.env}" +TARGET=".env" + +if [[ ! -f "$SOURCE" ]]; then + echo "Source env file not found: $SOURCE" >&2 + exit 1 +fi + +tmp_file="$(mktemp)" +{ + echo "# Synced from $SOURCE" + echo "# Generated at $(date -u +%Y-%m-%dT%H:%M:%SZ)" + grep -E '^(TRANSLOADIT_KEY|TRANSLOADIT_SECRET|TRANSLOADIT_ENDPOINT)=' "$SOURCE" || true +} > "$tmp_file" + +if ! grep -q '^TRANSLOADIT_KEY=' "$tmp_file"; then + echo "Missing TRANSLOADIT_KEY in $SOURCE" >&2 + rm -f "$tmp_file" + exit 1 +fi + +if ! grep -q '^TRANSLOADIT_SECRET=' "$tmp_file"; then + echo "Missing TRANSLOADIT_SECRET in $SOURCE" >&2 + rm -f "$tmp_file" + exit 1 +fi + +mv "$tmp_file" "$TARGET" +echo "Wrote $TARGET from $SOURCE" diff --git a/test/real.e2e.test.ts b/test/real.e2e.test.ts new file mode 100644 index 0000000..465f4b5 --- /dev/null +++ b/test/real.e2e.test.ts @@ -0,0 +1,149 @@ +import { once } from 'node:events' +import { existsSync } from 'node:fs' +import { createServer, type IncomingMessage, type Server } from 'node:http' +import { setTimeout as delay } from 'node:timers/promises' + +import { Transloadit } from 'transloadit' +import { describe, expect, it } from 'vitest' + +import TransloaditNotifyUrlProxy, { getSignature } from '../src/index.ts' + +if (typeof process.loadEnvFile === 'function' && existsSync('.env')) { + process.loadEnvFile('.env') +} + +async function readBody(request: IncomingMessage): Promise { + const chunks: Buffer[] = [] + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + + return Buffer.concat(chunks).toString('utf-8') +} + +async function listen(server: Server): Promise { + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') { + throw new Error('Could not resolve server address') + } + return address.port +} + +async function closeServer(server: Server): Promise { + await new Promise((resolve) => { + server.close(() => resolve()) + }) +} + +async function getFreePort(): Promise { + const server = createServer() + const port = await listen(server) + await closeServer(server) + return port +} + +const runReal = + process.env.RUN_REAL_E2E === '1' && + typeof process.env.TRANSLOADIT_KEY === 'string' && + process.env.TRANSLOADIT_KEY.length > 0 && + typeof process.env.TRANSLOADIT_SECRET === 'string' && + process.env.TRANSLOADIT_SECRET.length > 0 + +const describeReal = runReal ? describe : describe.skip + +describeReal('real api e2e', () => { + it('creates a real assembly through the proxy and receives signed notify callback', async () => { + const secret = process.env.TRANSLOADIT_SECRET as string + const authKey = process.env.TRANSLOADIT_KEY as string + const endpoint = (process.env.TRANSLOADIT_ENDPOINT || 'https://api2.transloadit.com').replace( + /\/$/, + '', + ) + + let notifyTransloadit: string | null = null + let notifySignature: string | null = null + let resolveNotify: (() => void) | null = null + const notifyReceived = new Promise((resolve) => { + resolveNotify = resolve + }) + + const notifyServer = createServer(async (request, response) => { + if (request.method !== 'POST' || request.url !== '/transloadit') { + response.writeHead(404) + response.end() + return + } + + const payload = new URLSearchParams(await readBody(request)) + notifyTransloadit = payload.get('transloadit') + notifySignature = payload.get('signature') + + response.writeHead(200) + response.end('ok') + resolveNotify?.() + }) + + const notifyPort = await listen(notifyServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy( + secret, + `http://127.0.0.1:${notifyPort}/transloadit`, + ) + proxy.run({ + target: endpoint, + port: proxyPort, + pollIntervalMs: 1_000, + maxPollAttempts: 120, + }) + + const client = new Transloadit({ + authKey, + authSecret: secret, + endpoint: `http://127.0.0.1:${proxyPort}`, + }) + + try { + const createPromise = client.createAssembly({ + uploads: { + probe: Buffer.from(`notify-url-proxy-real-e2e-${Date.now()}`, 'utf-8'), + }, + params: { + steps: { + ':original': { robot: '/upload/handle' }, + }, + }, + waitForCompletion: false, + timeout: 120_000, + }) + const createdAssemblyId = createPromise.assemblyId + expect(typeof createdAssemblyId).toBe('string') + expect(createdAssemblyId.length).toBeGreaterThan(0) + await createPromise + + await Promise.race([ + notifyReceived, + delay(180_000).then(() => { + throw new Error('Timed out waiting for notify callback from proxy') + }), + ]) + + expect(notifyTransloadit).toBeTypeOf('string') + expect(notifySignature).toBeTypeOf('string') + if (notifyTransloadit === null || notifySignature === null) { + throw new Error('Notify payload did not include transloadit + signature fields') + } + + expect(notifySignature).toBe(getSignature(secret, notifyTransloadit)) + + const payload = JSON.parse(notifyTransloadit) as { assembly_id?: string; ok?: string } + expect(payload.assembly_id).toBe(createdAssemblyId) + expect(payload.ok).toBe('ASSEMBLY_COMPLETED') + } finally { + proxy.close() + await closeServer(notifyServer) + } + }, 210_000) +}) diff --git a/yarn.lock b/yarn.lock index 4934f7d..a7c5290 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,665 @@ __metadata: version: 8 cacheKey: 10c0 +"@aws-crypto/crc32@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10c0/eab9581d3363af5ea498ae0e72de792f54d8890360e14a9d8261b7b5c55ebe080279fb2556e07994d785341cdaa99ab0b1ccf137832b53b5904cd6928f2b094b + languageName: node + linkType: hard + +"@aws-crypto/crc32c@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32c@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10c0/223efac396cdebaf5645568fa9a38cd0c322c960ae1f4276bedfe2e1031d0112e49d7d39225d386354680ecefae29f39af469a84b2ddfa77cb6692036188af77 + languageName: node + linkType: hard + +"@aws-crypto/sha1-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha1-browser@npm:5.2.0" + dependencies: + "@aws-crypto/supports-web-crypto": "npm:^5.2.0" + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + "@aws-sdk/util-locate-window": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10c0/51fed0bf078c10322d910af179871b7d299dde5b5897873ffbeeb036f427e5d11d23db9794439226544b73901920fd19f4d86bbc103ed73cc0cfdea47a83c6ac + languageName: node + linkType: hard + +"@aws-crypto/sha256-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-browser@npm:5.2.0" + dependencies: + "@aws-crypto/sha256-js": "npm:^5.2.0" + "@aws-crypto/supports-web-crypto": "npm:^5.2.0" + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + "@aws-sdk/util-locate-window": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10c0/05f6d256794df800fe9aef5f52f2ac7415f7f3117d461f85a6aecaa4e29e91527b6fd503681a17136fa89e9dd3d916e9c7e4cfb5eba222875cb6c077bdc1d00d + languageName: node + linkType: hard + +"@aws-crypto/sha256-js@npm:5.2.0, @aws-crypto/sha256-js@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-js@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10c0/6c48701f8336341bb104dfde3d0050c89c288051f6b5e9bdfeb8091cf3ffc86efcd5c9e6ff2a4a134406b019c07aca9db608128f8d9267c952578a3108db9fd1 + languageName: node + linkType: hard + +"@aws-crypto/supports-web-crypto@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/supports-web-crypto@npm:5.2.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/4d2118e29d68ca3f5947f1e37ce1fbb3239a0c569cc938cdc8ab8390d595609b5caf51a07c9e0535105b17bf5c52ea256fed705a07e9681118120ab64ee73af2 + languageName: node + linkType: hard + +"@aws-crypto/util@npm:5.2.0, @aws-crypto/util@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/util@npm:5.2.0" + dependencies: + "@aws-sdk/types": "npm:^3.222.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10c0/0362d4c197b1fd64b423966945130207d1fe23e1bb2878a18e361f7743c8d339dad3f8729895a29aa34fff6a86c65f281cf5167c4bf253f21627ae80b6dd2951 + languageName: node + linkType: hard + +"@aws-sdk/client-s3@npm:^3.891.0": + version: 3.1001.0 + resolution: "@aws-sdk/client-s3@npm:3.1001.0" + dependencies: + "@aws-crypto/sha1-browser": "npm:5.2.0" + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/credential-provider-node": "npm:^3.972.15" + "@aws-sdk/middleware-bucket-endpoint": "npm:^3.972.6" + "@aws-sdk/middleware-expect-continue": "npm:^3.972.6" + "@aws-sdk/middleware-flexible-checksums": "npm:^3.973.2" + "@aws-sdk/middleware-host-header": "npm:^3.972.6" + "@aws-sdk/middleware-location-constraint": "npm:^3.972.6" + "@aws-sdk/middleware-logger": "npm:^3.972.6" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.6" + "@aws-sdk/middleware-sdk-s3": "npm:^3.972.16" + "@aws-sdk/middleware-ssec": "npm:^3.972.6" + "@aws-sdk/middleware-user-agent": "npm:^3.972.16" + "@aws-sdk/region-config-resolver": "npm:^3.972.6" + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.4" + "@aws-sdk/types": "npm:^3.973.4" + "@aws-sdk/util-endpoints": "npm:^3.996.3" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.6" + "@aws-sdk/util-user-agent-node": "npm:^3.973.1" + "@smithy/config-resolver": "npm:^4.4.9" + "@smithy/core": "npm:^3.23.7" + "@smithy/eventstream-serde-browser": "npm:^4.2.10" + "@smithy/eventstream-serde-config-resolver": "npm:^4.3.10" + "@smithy/eventstream-serde-node": "npm:^4.2.10" + "@smithy/fetch-http-handler": "npm:^5.3.12" + "@smithy/hash-blob-browser": "npm:^4.2.11" + "@smithy/hash-node": "npm:^4.2.10" + "@smithy/hash-stream-node": "npm:^4.2.10" + "@smithy/invalid-dependency": "npm:^4.2.10" + "@smithy/md5-js": "npm:^4.2.10" + "@smithy/middleware-content-length": "npm:^4.2.10" + "@smithy/middleware-endpoint": "npm:^4.4.21" + "@smithy/middleware-retry": "npm:^4.4.38" + "@smithy/middleware-serde": "npm:^4.2.11" + "@smithy/middleware-stack": "npm:^4.2.10" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/node-http-handler": "npm:^4.4.13" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.10" + "@smithy/util-base64": "npm:^4.3.1" + "@smithy/util-body-length-browser": "npm:^4.2.1" + "@smithy/util-body-length-node": "npm:^4.2.2" + "@smithy/util-defaults-mode-browser": "npm:^4.3.37" + "@smithy/util-defaults-mode-node": "npm:^4.2.40" + "@smithy/util-endpoints": "npm:^3.3.1" + "@smithy/util-middleware": "npm:^4.2.10" + "@smithy/util-retry": "npm:^4.2.10" + "@smithy/util-stream": "npm:^4.5.16" + "@smithy/util-utf8": "npm:^4.2.1" + "@smithy/util-waiter": "npm:^4.2.10" + tslib: "npm:^2.6.2" + checksum: 10c0/50e938cb59aa001bf235dde2cb077362aa60f371e59d33323a3ab4a0912ebbb459917d4e11ea9132a17f85d0ad937b74e4a5978ea079431d51dc423cb0ab0b1a + languageName: node + linkType: hard + +"@aws-sdk/core@npm:^3.973.16": + version: 3.973.16 + resolution: "@aws-sdk/core@npm:3.973.16" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@aws-sdk/xml-builder": "npm:^3.972.9" + "@smithy/core": "npm:^3.23.7" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/signature-v4": "npm:^5.3.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-base64": "npm:^4.3.1" + "@smithy/util-middleware": "npm:^4.2.10" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/02f4391ef4343d16843810d99fb879f1cf4be9cfcad2b70260b01528498587384f2840d0d6fd8b217f826f9012a3d59c18f4c495254ea6e3175db28c21b1c3ea + languageName: node + linkType: hard + +"@aws-sdk/crc64-nvme@npm:^3.972.3": + version: 3.972.3 + resolution: "@aws-sdk/crc64-nvme@npm:3.972.3" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/9390ce9e5b9e444022e5acadeee2c8bf2a9ef726527d5cd37dd4dde76fc4b51abee66558f8cb5d6c50cc58645a9006898d83688120dbaf2e90465437459cf813 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/credential-provider-env@npm:3.972.14" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/ec4b0a6c2ca27344b0cb3e82cdcdd8d0a2fc430c6e73a9c317bf7cfea74a0266d62d88c6554719d3620c84f2be25aaee0f46c78ae7a94704bec5aedf0cd866b0 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-http@npm:^3.972.16": + version: 3.972.16 + resolution: "@aws-sdk/credential-provider-http@npm:3.972.16" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/fetch-http-handler": "npm:^5.3.12" + "@smithy/node-http-handler": "npm:^4.4.13" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-stream": "npm:^4.5.16" + tslib: "npm:^2.6.2" + checksum: 10c0/76ca13e1a3760b17af14b111a7589a87be88b0631b9ed7745c51ff183a6397ad3397b7bd54178781a4a88dd500330e889b5ccf0d7e31b9348100857f7dad7623 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/credential-provider-ini@npm:3.972.14" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/credential-provider-env": "npm:^3.972.14" + "@aws-sdk/credential-provider-http": "npm:^3.972.16" + "@aws-sdk/credential-provider-login": "npm:^3.972.14" + "@aws-sdk/credential-provider-process": "npm:^3.972.14" + "@aws-sdk/credential-provider-sso": "npm:^3.972.14" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.14" + "@aws-sdk/nested-clients": "npm:^3.996.4" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/credential-provider-imds": "npm:^4.2.10" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/a241f110fb39c65cc50a90d7c23d71d934a8d4f807741d5d411e36ead603c19fb62a8518b1abe8c95a164b46914a4c31f30bfc8d5518cfbd8a219b83b781f84a + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-login@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/credential-provider-login@npm:3.972.14" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/nested-clients": "npm:^3.996.4" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/b31b3717203ccb8edfae1c81d98575a48165deb69418a0f129a1f5690be24cf64b158bfa0b4c7a5cb0e2487fe4a2e28702213985cb8cf24a5cfb45211eff99d9 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-node@npm:^3.972.15": + version: 3.972.15 + resolution: "@aws-sdk/credential-provider-node@npm:3.972.15" + dependencies: + "@aws-sdk/credential-provider-env": "npm:^3.972.14" + "@aws-sdk/credential-provider-http": "npm:^3.972.16" + "@aws-sdk/credential-provider-ini": "npm:^3.972.14" + "@aws-sdk/credential-provider-process": "npm:^3.972.14" + "@aws-sdk/credential-provider-sso": "npm:^3.972.14" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.14" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/credential-provider-imds": "npm:^4.2.10" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/ef7d1426cddd20f5e1d340fdaea79d3c1f1c3da6894b8401f76047ec3af025207af7f379e5b4f103cf8233f2b8c4fc14f264d5978fc0ba76b965b95496fa8435 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-process@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/credential-provider-process@npm:3.972.14" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/9239454c983c86c25549b0c138309991313ae36e0e3b978a1df56b2dc41904bd6290e718bae98e92a32abce3d78755194d524e90ada4df3cc3df43f59db7cbff + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-sso@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/credential-provider-sso@npm:3.972.14" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/nested-clients": "npm:^3.996.4" + "@aws-sdk/token-providers": "npm:3.1001.0" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/ad4e557223be206afea59f43287a3fadc530574f206faa325ba64f048d1ecd6e0ba3a78b48fbe37c6c485b52ad875e5b758952f358270802735c53a32b0e98f6 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-web-identity@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.14" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/nested-clients": "npm:^3.996.4" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/c01ac229014c9ac632f2150f469619a83a5806faee6519080b5b613a064f342632bac8496472a868e44849be304bea6fa96ea62d661a24e52a641eabb0dc51a8 + languageName: node + linkType: hard + +"@aws-sdk/middleware-bucket-endpoint@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@aws-sdk/util-arn-parser": "npm:^3.972.2" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-config-provider": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/c0ac2400ffc62744fbcfd3a99170f2218e9bd3294a3375bbef8a1bbe06168014415e6010fb43052dccb18fbde4e66a705ba0ea11462c8aad94e663b68aba4168 + languageName: node + linkType: hard + +"@aws-sdk/middleware-expect-continue@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/98878b37ee46911194807c5ee7d277994cd038f0e18456bec9b8396b90bd21bcf244465b343b9f6c7dce408a5f6189d9509e2346307769ee9f4471e520089eb9 + languageName: node + linkType: hard + +"@aws-sdk/middleware-flexible-checksums@npm:^3.973.2": + version: 3.973.2 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.973.2" + dependencies: + "@aws-crypto/crc32": "npm:5.2.0" + "@aws-crypto/crc32c": "npm:5.2.0" + "@aws-crypto/util": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/crc64-nvme": "npm:^3.972.3" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/is-array-buffer": "npm:^4.2.1" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-middleware": "npm:^4.2.10" + "@smithy/util-stream": "npm:^4.5.16" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/861aa18de99d0b1f522280b6afbdc26d9c65308c09da31eadd23294b401d3c03ff7942d17b0cf40667a051ea73626e648ac00ee5ab20bfd22a95f69dfffa132b + languageName: node + linkType: hard + +"@aws-sdk/middleware-host-header@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/middleware-host-header@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/53ee2ee8eeb5cf37080562312c05ee811f2185cf058b4ee0e39c7826e3823d5bf46b77db4fc272a32029a0e770c74f256edfed589ae74346af38fe1d2ca27685 + languageName: node + linkType: hard + +"@aws-sdk/middleware-location-constraint@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/391d425ad97b8d838ed51338f25409db282268f9f04e3ef289342e321b5cb07a48ef366d23b44a7004cb70edbd913d25dca4f1f27a43c871f096d182b15a835e + languageName: node + linkType: hard + +"@aws-sdk/middleware-logger@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/middleware-logger@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/5f8e7a681ab7cecb531f844b67c52a377d3fe472fd5526177eb28b49882c89474e7fd219f62c6ffaaec197fabc9beed5595cb52d8b93fe4109b98a4c289a76f9 + languageName: node + linkType: hard + +"@aws-sdk/middleware-recursion-detection@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@aws/lambda-invoke-store": "npm:^0.2.2" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/27105c07565e2866ef677f4ed40eab818ecafdc41fe2b9e7c846839131c8e8d77ae0d3f95440bf35d8262408c329b354b1510ebf5d1f93478ece21c1c009fa5b + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-s3@npm:^3.972.16": + version: 3.972.16 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.972.16" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/types": "npm:^3.973.4" + "@aws-sdk/util-arn-parser": "npm:^3.972.2" + "@smithy/core": "npm:^3.23.7" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/signature-v4": "npm:^5.3.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-config-provider": "npm:^4.2.1" + "@smithy/util-middleware": "npm:^4.2.10" + "@smithy/util-stream": "npm:^4.5.16" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/3a11260b98cfd78f0a2528a468a1331d66d4dc3f815f548cf1a9a66b93e0353d2ec4d921ad33e137a616851e57cb179ad3cc1a07c12d0ab74b5ad2daee737d89 + languageName: node + linkType: hard + +"@aws-sdk/middleware-ssec@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/middleware-ssec@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/13af32722f45374e7aa04295331b49d9d894476f82afa832c892e3f85a5d70d3806b36975200e8b279a5b706b5c1eb4864eaa528a8da4b62dc3754e8fc71c123 + languageName: node + linkType: hard + +"@aws-sdk/middleware-user-agent@npm:^3.972.16": + version: 3.972.16 + resolution: "@aws-sdk/middleware-user-agent@npm:3.972.16" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/types": "npm:^3.973.4" + "@aws-sdk/util-endpoints": "npm:^3.996.3" + "@smithy/core": "npm:^3.23.7" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/728944012a5f5edd0d8fdcfcb6d1c1dd28f2272e390a035a255366364f536e21d0c7e84f328ccf3b3dc5c8f906ce6bd80899fb46419522a481b23aba165135fd + languageName: node + linkType: hard + +"@aws-sdk/nested-clients@npm:^3.996.4": + version: 3.996.4 + resolution: "@aws-sdk/nested-clients@npm:3.996.4" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/middleware-host-header": "npm:^3.972.6" + "@aws-sdk/middleware-logger": "npm:^3.972.6" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.6" + "@aws-sdk/middleware-user-agent": "npm:^3.972.16" + "@aws-sdk/region-config-resolver": "npm:^3.972.6" + "@aws-sdk/types": "npm:^3.973.4" + "@aws-sdk/util-endpoints": "npm:^3.996.3" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.6" + "@aws-sdk/util-user-agent-node": "npm:^3.973.1" + "@smithy/config-resolver": "npm:^4.4.9" + "@smithy/core": "npm:^3.23.7" + "@smithy/fetch-http-handler": "npm:^5.3.12" + "@smithy/hash-node": "npm:^4.2.10" + "@smithy/invalid-dependency": "npm:^4.2.10" + "@smithy/middleware-content-length": "npm:^4.2.10" + "@smithy/middleware-endpoint": "npm:^4.4.21" + "@smithy/middleware-retry": "npm:^4.4.38" + "@smithy/middleware-serde": "npm:^4.2.11" + "@smithy/middleware-stack": "npm:^4.2.10" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/node-http-handler": "npm:^4.4.13" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.10" + "@smithy/util-base64": "npm:^4.3.1" + "@smithy/util-body-length-browser": "npm:^4.2.1" + "@smithy/util-body-length-node": "npm:^4.2.2" + "@smithy/util-defaults-mode-browser": "npm:^4.3.37" + "@smithy/util-defaults-mode-node": "npm:^4.2.40" + "@smithy/util-endpoints": "npm:^3.3.1" + "@smithy/util-middleware": "npm:^4.2.10" + "@smithy/util-retry": "npm:^4.2.10" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/2a3a50194dfee036ea0b444f56140ff08af43d4053588fbc579c6f6c3620fe5fbeee71aa0851cbfa07868e546b6fb78c5778285bb3d4079c9cec57ffc484e6ac + languageName: node + linkType: hard + +"@aws-sdk/region-config-resolver@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/region-config-resolver@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/config-resolver": "npm:^4.4.9" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/01da247b934cb4bc4bb5e213a450d8c07b5a59faeee453c22cf909b0492e6ba4afe9082e169a9382a3587a2cd82bbf0bfd9288f8892da04c7ccb5bde43853eeb + languageName: node + linkType: hard + +"@aws-sdk/s3-request-presigner@npm:^3.891.0": + version: 3.1001.0 + resolution: "@aws-sdk/s3-request-presigner@npm:3.1001.0" + dependencies: + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.4" + "@aws-sdk/types": "npm:^3.973.4" + "@aws-sdk/util-format-url": "npm:^3.972.6" + "@smithy/middleware-endpoint": "npm:^4.4.21" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/51cbe9bcef780a391bf5e0a084490ff40bba317d6741ed14b3b5d5f6c761fc161e71de7241527e02f8b56e0b6ac4bce9045a43aeed8a79cb0418d4f78f2134dd + languageName: node + linkType: hard + +"@aws-sdk/signature-v4-multi-region@npm:^3.996.4": + version: 3.996.4 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.996.4" + dependencies: + "@aws-sdk/middleware-sdk-s3": "npm:^3.972.16" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/signature-v4": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/533aeba01f91acb9f626cf11d5cd2b177ea95768fe0a3e85c03defda4f6f88bea6d9ec3bc4f7c6c375ae26bb44b3ba475a871ecbb96ae8e9575ef1cfe82b197c + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.1001.0": + version: 3.1001.0 + resolution: "@aws-sdk/token-providers@npm:3.1001.0" + dependencies: + "@aws-sdk/core": "npm:^3.973.16" + "@aws-sdk/nested-clients": "npm:^3.996.4" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/56b7a1778c9e69beb7f8f170e33a5b8f9f28f55851d690c4938aa1e7a357f8c26ec83ca6120db84ed2d53841ec1b79343d820d09bb89c953a54fc32e9644fc9f + languageName: node + linkType: hard + +"@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.973.4": + version: 3.973.4 + resolution: "@aws-sdk/types@npm:3.973.4" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/7b6b3646842a43f5eaef29f2975b3c28f8357fd44b2f7f939fb5209db460289b7ebbbe26eb0845a6d87f5692f0466378d97c0ec1f795a4aa8b43b68327235229 + languageName: node + linkType: hard + +"@aws-sdk/util-arn-parser@npm:^3.972.2": + version: 3.972.2 + resolution: "@aws-sdk/util-arn-parser@npm:3.972.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/94aec6e0217da6add9d2334e8ec1c0c23955d279478e0161d00f66fd3527baf8a483e6fc41ecc2fb44e0b4116b52e85847a525ee7bdf43ff07d206f1e4ef03c9 + languageName: node + linkType: hard + +"@aws-sdk/util-endpoints@npm:^3.996.3": + version: 3.996.3 + resolution: "@aws-sdk/util-endpoints@npm:3.996.3" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.10" + "@smithy/util-endpoints": "npm:^3.3.1" + tslib: "npm:^2.6.2" + checksum: 10c0/a8fa65d58c66de68c71085eb622f16fcb86c37e558a1e7a1ceeb6eb3ca0495e3920e54bcc8f53045ef8931d6298dc2e10ea759cffe8d41965359988ea9d99346 + languageName: node + linkType: hard + +"@aws-sdk/util-format-url@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/util-format-url@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/querystring-builder": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/e752084629e5be6923623336e1d7c6c3a509fc98214cf405b993b0850841ffd4b2f903fb8b4ca1a73be3e9fb300b3e94ca63c358e7a7e5e855e11aa7dcdbaf7a + languageName: node + linkType: hard + +"@aws-sdk/util-locate-window@npm:^3.0.0": + version: 3.965.4 + resolution: "@aws-sdk/util-locate-window@npm:3.965.4" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/c87bef4cbeef564cb101f3d80b1dee008667e5a8c301b974dc25ec275e8d503a3226d3740bbfc4c1e17781719b7b8f06a25deea8196919caeaf87b0b8117286d + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-browser@npm:^3.972.6": + version: 3.972.6 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.972.6" + dependencies: + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/types": "npm:^4.13.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/84154d72bca9f11d29918f26b9d4063791717fadffa5d7b165463c481e9df75d617af749ea58c5a3fa7acd15a02dc47ba67445d57f18f48a99b7bcee80d6e28b + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-node@npm:^3.973.1": + version: 3.973.1 + resolution: "@aws-sdk/util-user-agent-node@npm:3.973.1" + dependencies: + "@aws-sdk/middleware-user-agent": "npm:^3.972.16" + "@aws-sdk/types": "npm:^3.973.4" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10c0/23cc136d534a23ceed9f5a2fc028ae0a697eab7433b7f01cb62affa78ea51e1ff6c1323dba3c09fb931192c29140ad3c5e9e2a55260a823fb58ffcb848913cef + languageName: node + linkType: hard + +"@aws-sdk/xml-builder@npm:^3.972.9": + version: 3.972.9 + resolution: "@aws-sdk/xml-builder@npm:3.972.9" + dependencies: + "@smithy/types": "npm:^4.13.0" + fast-xml-parser: "npm:5.4.1" + tslib: "npm:^2.6.2" + checksum: 10c0/b87469821b3c2e37d89c22ff6a9780fc17a906574c89d324e05f94c1d3b3ed3581ec35ce9f5d875d67454799ecf37655fe0d1cff8d7fbb64995d782019926632 + languageName: node + linkType: hard + +"@aws/lambda-invoke-store@npm:^0.2.2": + version: 0.2.3 + resolution: "@aws/lambda-invoke-store@npm:0.2.3" + checksum: 10c0/3869a5d2494ff81fba306d603c0f2e36c59f89c4efdffd1105a208a595da77059547209a163b6f0c1b716e9d273ce24f94dcbd5a08bad74b2602d13711b0cb3b + languageName: node + linkType: hard + "@babel/runtime@npm:^7.5.5": version: 7.28.6 resolution: "@babel/runtime@npm:7.28.6" @@ -808,6 +1467,628 @@ __metadata: languageName: node linkType: hard +"@sec-ant/readable-stream@npm:^0.4.1": + version: 0.4.1 + resolution: "@sec-ant/readable-stream@npm:0.4.1" + checksum: 10c0/64e9e9cf161e848067a5bf60cdc04d18495dc28bb63a8d9f8993e4dd99b91ad34e4b563c85de17d91ffb177ec17a0664991d2e115f6543e73236a906068987af + languageName: node + linkType: hard + +"@sindresorhus/is@npm:^7.0.1": + version: 7.2.0 + resolution: "@sindresorhus/is@npm:7.2.0" + checksum: 10c0/0040c17d7826414363f99f5d56077c200789d51e6dfe5542920bfb29ab3828ec0ebf2845e8bae796bee461debb646b5e4c0a623140131cf3143471e915b50b54 + languageName: node + linkType: hard + +"@smithy/abort-controller@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/abort-controller@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/c07517dd50c20282ae46e2d464fca7ba8db85f622cd70b168977f973eeedb48bf2cb73e106309a96b274144e7b519d2f19202e1d5fb894033589a290994f50d3 + languageName: node + linkType: hard + +"@smithy/chunked-blob-reader-native@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/chunked-blob-reader-native@npm:4.2.2" + dependencies: + "@smithy/util-base64": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10c0/86fbd6e5f021f58a2d0d157410e5eb37df2b5bfdf6b6eb0b2491276e9322649ab763bb87c5bcd2da99399ff5989aec2f10e15fe831c9d6f7b6efa75703bc2303 + languageName: node + linkType: hard + +"@smithy/chunked-blob-reader@npm:^5.2.1": + version: 5.2.1 + resolution: "@smithy/chunked-blob-reader@npm:5.2.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/44f477ffa90b12a981d81be066740db631f33807cf9de4e4b8fc2f6eaaecec200b80855f61369abe0090a7d50b845dbe2b8a1727b07235f293394e386dab6c66 + languageName: node + linkType: hard + +"@smithy/config-resolver@npm:^4.4.9": + version: 4.4.9 + resolution: "@smithy/config-resolver@npm:4.4.9" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-config-provider": "npm:^4.2.1" + "@smithy/util-endpoints": "npm:^3.3.1" + "@smithy/util-middleware": "npm:^4.2.10" + tslib: "npm:^2.6.2" + checksum: 10c0/efd294dc45960a5bf8a728d719e26f53d92796f3cbd0ef1ee66a65f9325ba14b689ce12aea18cf1e39048529db208feb2a2a4d85afafbfcd71dff1778ac7210d + languageName: node + linkType: hard + +"@smithy/core@npm:^3.23.7": + version: 3.23.7 + resolution: "@smithy/core@npm:3.23.7" + dependencies: + "@smithy/middleware-serde": "npm:^4.2.11" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-base64": "npm:^4.3.1" + "@smithy/util-body-length-browser": "npm:^4.2.1" + "@smithy/util-middleware": "npm:^4.2.10" + "@smithy/util-stream": "npm:^4.5.16" + "@smithy/util-utf8": "npm:^4.2.1" + "@smithy/uuid": "npm:^1.1.1" + tslib: "npm:^2.6.2" + checksum: 10c0/122eef4665a59ef827da8fd7d4ba0577972ef3e4a29747662ce08ce8609860f88cbd3242a4499229a5ae69b7ef0e903138fc1d2501cd0cf7a6d024404984a210 + languageName: node + linkType: hard + +"@smithy/credential-provider-imds@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/credential-provider-imds@npm:4.2.10" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.10" + tslib: "npm:^2.6.2" + checksum: 10c0/8e2b3f0b327a5ba476d0b6de2907ec4d37cd7d2efcf5203159fca3385d6bf9dd0a004b91c69206d504e6cc92c4616266493e51aacb7269628f89f951baafd855 + languageName: node + linkType: hard + +"@smithy/eventstream-codec@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/eventstream-codec@npm:4.2.10" + dependencies: + "@aws-crypto/crc32": "npm:5.2.0" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-hex-encoding": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/e6a4424e8c23e4d396b8fbc9c287bf73b4e5bef3b9f29065ce9803b1b8b5bda2bba05312721986bec83336e2c1e30560869ef1d1c91e3815081ce78d48287be1 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-browser@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/eventstream-serde-browser@npm:4.2.10" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/29c7ad443d2cef74ec5aaaa8d536bbcb2031ae40894c61244f2ff135dcc23d439c980f7109b28d2c2b80aaddfae80541331c4d7704c19f6a0a461af5c4df7a55 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-config-resolver@npm:^4.3.10": + version: 4.3.10 + resolution: "@smithy/eventstream-serde-config-resolver@npm:4.3.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/b53bd51fb6858aa44791eaff519504df708e4ccd78a3f980cd19a9c38642349410fc58feb4306c8160e7a94fd2f4f7b9058dfcd62800b7fff6abdcdc789f966e + languageName: node + linkType: hard + +"@smithy/eventstream-serde-node@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/eventstream-serde-node@npm:4.2.10" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/1d4a6d7bb33f721639edb198b4a5d4f5eb52f14ed89f932d370740c80636ed2bfbd989f438986fd4f4feaa6b08f96244216c6515f410df41bcc0a83c83d58eff + languageName: node + linkType: hard + +"@smithy/eventstream-serde-universal@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/eventstream-serde-universal@npm:4.2.10" + dependencies: + "@smithy/eventstream-codec": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/a970df4af9900b88ba7f8d1fe33f501674eb3a71b261e686be1596ce9c4ee8eacb88a7a33a01b3a2551c022600041ffe353782bde2d6b344c2107f480ae7ecbe + languageName: node + linkType: hard + +"@smithy/fetch-http-handler@npm:^5.3.12": + version: 5.3.12 + resolution: "@smithy/fetch-http-handler@npm:5.3.12" + dependencies: + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/querystring-builder": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-base64": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10c0/dbc5b29db4b8bb20a15aa13e77e524b7a316de5facec5a37eef052e76d196fe4b8504ca9d3aba36205b31498716164a89cc05535b1930b590ea07f9422f4fdfc + languageName: node + linkType: hard + +"@smithy/hash-blob-browser@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/hash-blob-browser@npm:4.2.11" + dependencies: + "@smithy/chunked-blob-reader": "npm:^5.2.1" + "@smithy/chunked-blob-reader-native": "npm:^4.2.2" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/52591638cccbc2a4e2016d345ae7db434641356bf5db8817972995b17e59321c9a4cb2cfe5902b5f45be7cd6e9ce24ac6c801bd04d88da5028d267e26bce875c + languageName: node + linkType: hard + +"@smithy/hash-node@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/hash-node@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + "@smithy/util-buffer-from": "npm:^4.2.1" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/b6fc9c4c3b8cc103f1a2de2a4cd389f7897c2b098fa217d3a3335cfad32051b436907a032ea3f8987b6fed5d1e917dd656261421806bb8cff8c678ab50282abc + languageName: node + linkType: hard + +"@smithy/hash-stream-node@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/hash-stream-node@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/8860c3365217cbf41f0e51380ec002fd5d5ab262f7f3ed2ea500159590f134f898ca53067c8e1e0dd69320ffedab70569d2b3f005d9874c784e57a23356b0622 + languageName: node + linkType: hard + +"@smithy/invalid-dependency@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/invalid-dependency@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/8d4a2b163eaaeb2e0c1af26dd39fd36b03ee319eac4fba2d329e99e2cd2849c65ed6bc510c9b58dd20e1f2ee7634d61ccc21b15c8a871fb2109e87c1337df057 + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/is-array-buffer@npm:2.2.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/2f2523cd8cc4538131e408eb31664983fecb0c8724956788b015aaf3ab85a0c976b50f4f09b176f1ed7bbe79f3edf80743be7a80a11f22cd9ce1285d77161aaf + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/is-array-buffer@npm:4.2.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/8e84ae3542ca85aade7a1d00809f29bd22e3dae3282df8a7d558e002d47b1245de55add7a5ca310170d8336f46c95c4f812a0641f44af95364a510d22eb4c4ea + languageName: node + linkType: hard + +"@smithy/md5-js@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/md5-js@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/5be25be720951546188f5ad34e279f3f72bce650c0cc16879b49025039ad504759f08bd7c98b3743c3354c126145875ef5d8557185be35b252c708a5dceda909 + languageName: node + linkType: hard + +"@smithy/middleware-content-length@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/middleware-content-length@npm:4.2.10" + dependencies: + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/f250d96f4b0c8cc28be5b842ac8538df5117a7f40e15652be39757b8817e929e0ddb41dd3e4f97e29799d3031efcb5b0aece4a0a263ac25ce23a1b10fcc87a70 + languageName: node + linkType: hard + +"@smithy/middleware-endpoint@npm:^4.4.21": + version: 4.4.21 + resolution: "@smithy/middleware-endpoint@npm:4.4.21" + dependencies: + "@smithy/core": "npm:^3.23.7" + "@smithy/middleware-serde": "npm:^4.2.11" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + "@smithy/url-parser": "npm:^4.2.10" + "@smithy/util-middleware": "npm:^4.2.10" + tslib: "npm:^2.6.2" + checksum: 10c0/5a8bf7e0ca046ac0d071cd46d383ffc1dade1480a0f77ea602e9a10e50b7df36f89bb699f91e40c9301b3e4c3bb422b9205ef99a67d6b177ef4b19f0581706ed + languageName: node + linkType: hard + +"@smithy/middleware-retry@npm:^4.4.38": + version: 4.4.38 + resolution: "@smithy/middleware-retry@npm:4.4.38" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/service-error-classification": "npm:^4.2.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-middleware": "npm:^4.2.10" + "@smithy/util-retry": "npm:^4.2.10" + "@smithy/uuid": "npm:^1.1.1" + tslib: "npm:^2.6.2" + checksum: 10c0/15271cbe521260a6ac85149efe59c7d5f36d6fb1201e546b6977a21777beeb11b86d417218b00f7558288a9048fe2f3a06974d145ecef01765db6d72b0201e11 + languageName: node + linkType: hard + +"@smithy/middleware-serde@npm:^4.2.11": + version: 4.2.11 + resolution: "@smithy/middleware-serde@npm:4.2.11" + dependencies: + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/fd62e4a68c497e225fc6ec63a7451180dfd8c64f1db3b7df8b9b99d90e43b6c5f4979a86c980530641964b7f9a938ad65ba440fd70a59806ee5c08cf711a3ea7 + languageName: node + linkType: hard + +"@smithy/middleware-stack@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/middleware-stack@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/79e83c21a79ffe649675f9dc85f8ee3d362f1f23352b8ddba183462c9b3fd7e8d6b8e7ee08581cffe7a94f046194ae7058d8c57f9ce834b5cc973cbb6bc80a21 + languageName: node + linkType: hard + +"@smithy/node-config-provider@npm:^4.3.10": + version: 4.3.10 + resolution: "@smithy/node-config-provider@npm:4.3.10" + dependencies: + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/shared-ini-file-loader": "npm:^4.4.5" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/c015b5d29c739d378630c43a1bbc91e23ad4c48ea71123fb69ebbd4dea12573b61339827061d588625e4897f03d0401da5bc5e6ead51e37764726ba1a7662bb3 + languageName: node + linkType: hard + +"@smithy/node-http-handler@npm:^4.4.13": + version: 4.4.13 + resolution: "@smithy/node-http-handler@npm:4.4.13" + dependencies: + "@smithy/abort-controller": "npm:^4.2.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/querystring-builder": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/7e80b3107eaf040a2db4238e88022d0b2b0ccbc2d03eda810bd2d077eb345ad77dc4cb46525fc71bab9f9b60748c8f1b83f05c31617b2fb7bc26943f16447787 + languageName: node + linkType: hard + +"@smithy/property-provider@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/property-provider@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/bb4775be0b766eca77ecf348739cd9466a74eec26df582f088b7aeb5888213135652b05b9cd0df281a720c1b5ea0cf8b720be261a654faf8aa52453cbd7da800 + languageName: node + linkType: hard + +"@smithy/protocol-http@npm:^5.3.10": + version: 5.3.10 + resolution: "@smithy/protocol-http@npm:5.3.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/9c801aa785a17a1904e88570ece5ee6e035b24ab9fb2231886f7ce444942954268849692c8b46909ad021bb75cfc23266f1baeecd530db6f9acef6dedd778f70 + languageName: node + linkType: hard + +"@smithy/querystring-builder@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/querystring-builder@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + "@smithy/util-uri-escape": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/f35460dd4a5e8757b8a872c41ee4734e5e053a9e8892b7c29bb805c1faee99ea7df06dd0f20b6c15ecba5e43549542d6862a3085d0dd146a335f53de5be58b4a + languageName: node + linkType: hard + +"@smithy/querystring-parser@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/querystring-parser@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/cf5c0302143d841c9996a4a91c898531d04b5ab187e17c7b881b5619506a61109c3125894a97be021f6638e561a8d59a4e389bbf47c20ed7ee7872af3657e5a2 + languageName: node + linkType: hard + +"@smithy/service-error-classification@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/service-error-classification@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + checksum: 10c0/c234383b5b9f78623537bebcfef1191855d8352c2dbc1d9df0592d01cce8372da6d26369ced29e31fac4e30c444f4918caacd96bdd5ee3fabed5492c0447fcfb + languageName: node + linkType: hard + +"@smithy/shared-ini-file-loader@npm:^4.4.5": + version: 4.4.5 + resolution: "@smithy/shared-ini-file-loader@npm:4.4.5" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/e4dfbb6e7265d07abe94a5b3f085c2191cfbb4cb1b4dff48f011dc148de651caf1bf47844dec2e7b35a52f9a4e58a4b09017edb5faa5ff551f3b4f270b63fffa + languageName: node + linkType: hard + +"@smithy/signature-v4@npm:^5.3.10": + version: 5.3.10 + resolution: "@smithy/signature-v4@npm:5.3.10" + dependencies: + "@smithy/is-array-buffer": "npm:^4.2.1" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-hex-encoding": "npm:^4.2.1" + "@smithy/util-middleware": "npm:^4.2.10" + "@smithy/util-uri-escape": "npm:^4.2.1" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/b6a03a1868b14c2ff6332ed42b05541ebd33f1362769c7e3e884b9de947da98ed5855b4a1626f16dc79184f61ce13a2145fd8583aedeff018ae0104623b83315 + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^4.12.1": + version: 4.12.1 + resolution: "@smithy/smithy-client@npm:4.12.1" + dependencies: + "@smithy/core": "npm:^3.23.7" + "@smithy/middleware-endpoint": "npm:^4.4.21" + "@smithy/middleware-stack": "npm:^4.2.10" + "@smithy/protocol-http": "npm:^5.3.10" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-stream": "npm:^4.5.16" + tslib: "npm:^2.6.2" + checksum: 10c0/dc3fd496a11b13f72cbe7f201d54dda9fae8390a4515c2179e844e9ff4939566838bc252e2fc11b254af9f99dd3bf29ada67727a334a3ad52bea35a4ed5e00ac + languageName: node + linkType: hard + +"@smithy/types@npm:^4.13.0": + version: 4.13.0 + resolution: "@smithy/types@npm:4.13.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/c6d8ab088214089e1e1b9bb3e1305fdaac0b0802b1772a6326821a671bc1a4fcd00c492928db455a022281f5a60471f0560402a74e0e4a0824ee8cb2163c0b73 + languageName: node + linkType: hard + +"@smithy/url-parser@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/url-parser@npm:4.2.10" + dependencies: + "@smithy/querystring-parser": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/dffabdfd18d214ff6b561c497f23ed1f2cf248178113e55721223061963c84b463ac0e1e2dfb94c8d7fe82c7c9acb62f7274d567e56334f1361928d451f17de5 + languageName: node + linkType: hard + +"@smithy/util-base64@npm:^4.3.1": + version: 4.3.1 + resolution: "@smithy/util-base64@npm:4.3.1" + dependencies: + "@smithy/util-buffer-from": "npm:^4.2.1" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/050c2b22f231c3c6f46e1c2ae5c322f0f8adaafe1d82c9bba54c429099dfb1f81947f99309a0e32e77c5d471054370d784d9b8eb7bb1a96a25449a780e29bddd + languageName: node + linkType: hard + +"@smithy/util-body-length-browser@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/util-body-length-browser@npm:4.2.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/5c3ba12d980ad9bbfc4b00eff11f928e111e32f59ef55d3ecd4baf67389a531614d1ee21d23bfef347b86e735d1e0b14ebdd4cd2a33ada61b949f17cf7fe628b + languageName: node + linkType: hard + +"@smithy/util-body-length-node@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-body-length-node@npm:4.2.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/683d9d01dd12a3caad11304edacefe131de585a665b354bfc6965ad873e71e5ee105934c19a3e25871df2390a653a602b5e87805bb9961b181b390cca3530f3e + languageName: node + linkType: hard + +"@smithy/util-buffer-from@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/util-buffer-from@npm:2.2.0" + dependencies: + "@smithy/is-array-buffer": "npm:^2.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/223d6a508b52ff236eea01cddc062b7652d859dd01d457a4e50365af3de1e24a05f756e19433f6ccf1538544076b4215469e21a4ea83dc1d58d829725b0dbc5a + languageName: node + linkType: hard + +"@smithy/util-buffer-from@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/util-buffer-from@npm:4.2.1" + dependencies: + "@smithy/is-array-buffer": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/9784ead40c7d2aa312a7d690f329c84be5f0107b9bb42e5c8cd377aac9e62e6cc03a5f543961d0367fa1c1418f32eb6abbc6ba18916372a4d06ce2ed50f82c78 + languageName: node + linkType: hard + +"@smithy/util-config-provider@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/util-config-provider@npm:4.2.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/7c17ec9b5f9e28eb44abdb25cf8046abd646aef6988c87c1640a504b9ef9f5eeac792ab2289d6c2fcff56c3a40a36b45632846b276b00ff3b91ee593bbbd78c9 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^4.3.37": + version: 4.3.37 + resolution: "@smithy/util-defaults-mode-browser@npm:4.3.37" + dependencies: + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/fdcf2084faefcbe66a6c188dcf73bead14e27e52bb0d854cda642857e73ee6b19ea749506353967466bafc6de148040590ad3af93557500324c32bf1aa295ae0 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-node@npm:^4.2.40": + version: 4.2.40 + resolution: "@smithy/util-defaults-mode-node@npm:4.2.40" + dependencies: + "@smithy/config-resolver": "npm:^4.4.9" + "@smithy/credential-provider-imds": "npm:^4.2.10" + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/property-provider": "npm:^4.2.10" + "@smithy/smithy-client": "npm:^4.12.1" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/9a761c1daebad71cac01505d3966de7ba2f7beeab359b56df1e464cf565100d3231e5e399510cefec2aff8949784b7a06c4988f499b8203520d4e417e25dde69 + languageName: node + linkType: hard + +"@smithy/util-endpoints@npm:^3.3.1": + version: 3.3.1 + resolution: "@smithy/util-endpoints@npm:3.3.1" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/6b339f5e372bbea377c935d35ca90e9933e34dc0621efe2ba045041fc2f0fb921cb81f619ffe8d090f46e9e774af04be633ad2fbb4dcb4e4339aa9d0ddf7250b + languageName: node + linkType: hard + +"@smithy/util-hex-encoding@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/util-hex-encoding@npm:4.2.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/3e007c57e75a828b2933f354d11a9a0035f5b25053c0391c402a24b32b5f86082db937cbd22f997a22c0b55e875b905521686ea414fa32ee6b0a5a7f52aabb74 + languageName: node + linkType: hard + +"@smithy/util-middleware@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/util-middleware@npm:4.2.10" + dependencies: + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/28eaca81bf2041e78287d660c97793dc0c5a000079e4956a9ba0e7d8f5354d3890408cb427a42585a05b1c0797fc71684b2b4d463280e234254bb5a9eac08dc4 + languageName: node + linkType: hard + +"@smithy/util-retry@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/util-retry@npm:4.2.10" + dependencies: + "@smithy/service-error-classification": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/b4a2478e7a156e3718a10424ee5c266205e52ff3f935190c4fbb96cdbe66a26c5554ed1650113befc918d62fa3d973e82b0499cc47b89c54726952debc293e0f + languageName: node + linkType: hard + +"@smithy/util-stream@npm:^4.5.16": + version: 4.5.16 + resolution: "@smithy/util-stream@npm:4.5.16" + dependencies: + "@smithy/fetch-http-handler": "npm:^5.3.12" + "@smithy/node-http-handler": "npm:^4.4.13" + "@smithy/types": "npm:^4.13.0" + "@smithy/util-base64": "npm:^4.3.1" + "@smithy/util-buffer-from": "npm:^4.2.1" + "@smithy/util-hex-encoding": "npm:^4.2.1" + "@smithy/util-utf8": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/266fbed834b0d6d1daccaf89eb24cab93feb6d2ee904689ceaf5cd15c9e9170f4f46632ab5dc630bc09dd8a3465d7e72aeda43f4b4150f4eebf21f00027ed015 + languageName: node + linkType: hard + +"@smithy/util-uri-escape@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/util-uri-escape@npm:4.2.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/cad0a1a589be437718bff36466add86c14e87da3f241dc706d7b6f13cde0eeafc0ab3837e866279a42cd468574e0ecba6480133f91853b086b9c02151c6dad85 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^2.0.0": + version: 2.3.0 + resolution: "@smithy/util-utf8@npm:2.3.0" + dependencies: + "@smithy/util-buffer-from": "npm:^2.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/e18840c58cc507ca57fdd624302aefd13337ee982754c9aa688463ffcae598c08461e8620e9852a424d662ffa948fc64919e852508028d09e89ced459bd506ab + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/util-utf8@npm:4.2.1" + dependencies: + "@smithy/util-buffer-from": "npm:^4.2.1" + tslib: "npm:^2.6.2" + checksum: 10c0/6f53ce67e79d7e1b6712b2fef0e042b63b1b0255ad390ca79b506f9e19f389cc6e5042814186454fef382ee3bb8f5af7819d0ce4f8d302906c2584e0675258cd + languageName: node + linkType: hard + +"@smithy/util-waiter@npm:^4.2.10": + version: 4.2.10 + resolution: "@smithy/util-waiter@npm:4.2.10" + dependencies: + "@smithy/abort-controller": "npm:^4.2.10" + "@smithy/types": "npm:^4.13.0" + tslib: "npm:^2.6.2" + checksum: 10c0/f47189255cf88fdb6b25df2d3d9ca2437402f55054d31a83ec5876f6f2b7265dc21b3f4cec2e0da8adb294d3ed36a71a760464cdf45ae8ccf88c1b612161e46f + languageName: node + linkType: hard + +"@smithy/uuid@npm:^1.1.1": + version: 1.1.1 + resolution: "@smithy/uuid@npm:1.1.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/083807ef29bdcc64ab692b50127d7ac98529ef5b564295ffec50d55515e8ba1b2257af113fdc813f9638c23897520e32dad4e9896613ebc012c5252531195ff8 + languageName: node + linkType: hard + "@standard-schema/spec@npm:^1.0.0": version: 1.1.0 resolution: "@standard-schema/spec@npm:1.1.0" @@ -815,6 +2096,31 @@ __metadata: languageName: node linkType: hard +"@szmarczak/http-timer@npm:^5.0.1": + version: 5.0.1 + resolution: "@szmarczak/http-timer@npm:5.0.1" + dependencies: + defer-to-connect: "npm:^2.0.1" + checksum: 10c0/4629d2fbb2ea67c2e9dc03af235c0991c79ebdddcbc19aed5d5732fb29ce01c13331e9b1a491584b9069bd6ecde6581dcbf871f11b7eefdebbab34de6cf2197e + languageName: node + linkType: hard + +"@transloadit/abbr@npm:^1.0.0": + version: 1.0.0 + resolution: "@transloadit/abbr@npm:1.0.0" + checksum: 10c0/4394a4c144cd6426676e0755394f04e18ddacd32d58866e4a3862aaf149f95a60e6809861aede7fb11a146b4a760c81e57eb423dafde7bd7c92f9a7406ab8cd3 + languageName: node + linkType: hard + +"@transloadit/sev-logger@npm:^0.1.9": + version: 0.1.9 + resolution: "@transloadit/sev-logger@npm:0.1.9" + dependencies: + "@transloadit/abbr": "npm:^1.0.0" + checksum: 10c0/704e4fcafcdf93da5d4fc7ecdef13407ea0f291b23b269c2b815c6ac20410503223b662a5b1637bf95f6a62955e8b8676ad5ff3ececb17a4fabbeb9ba22e4ca6 + languageName: node + linkType: hard + "@transloadit/utils@npm:^4.3.0": version: 4.3.0 resolution: "@transloadit/utils@npm:4.3.0" @@ -856,6 +2162,13 @@ __metadata: languageName: node linkType: hard +"@types/http-cache-semantics@npm:^4.0.4": + version: 4.2.0 + resolution: "@types/http-cache-semantics@npm:4.2.0" + checksum: 10c0/82dd33cbe7d4843f1e884a251c6a12d385b62274353b9db167462e7fbffdbb3a83606f9952203017c5b8cabbd7b9eef0cf240a3a9dedd20f69875c9701939415 + languageName: node + linkType: hard + "@types/http-proxy@npm:^1.17.17": version: 1.17.17 resolution: "@types/http-proxy@npm:1.17.17" @@ -1019,6 +2332,34 @@ __metadata: languageName: node linkType: hard +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73 + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + "balanced-match@npm:^4.0.2": version: 4.0.4 resolution: "balanced-match@npm:4.0.4" @@ -1035,6 +2376,23 @@ __metadata: languageName: node linkType: hard +"bowser@npm:^2.11.0": + version: 2.14.1 + resolution: "bowser@npm:2.14.1" + checksum: 10c0/bb69b55ba7f0456e3dc07d0cfd9467f985581f640ba8fd426b08754a6737ee0d6cf3b50607941e5255f04c83075b952ece0599f978dd4d20f1e95461104c5ffd + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.12 + resolution: "brace-expansion@npm:1.1.12" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: 10c0/975fecac2bb7758c062c20d0b3b6288c7cc895219ee25f0a64a9de662dbac981ff0b6e89909c3897c1f84fa353113a721923afdec5f8b2350255b097f12b1f73 + languageName: node + linkType: hard + "brace-expansion@npm:^5.0.2": version: 5.0.4 resolution: "brace-expansion@npm:5.0.4" @@ -1053,6 +2411,13 @@ __metadata: languageName: node linkType: hard +"buffer-from@npm:^1.1.2": + version: 1.1.2 + resolution: "buffer-from@npm:1.1.2" + checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + languageName: node + linkType: hard + "cacache@npm:^20.0.1": version: 20.0.3 resolution: "cacache@npm:20.0.3" @@ -1072,6 +2437,38 @@ __metadata: languageName: node linkType: hard +"cacheable-lookup@npm:^7.0.0": + version: 7.0.0 + resolution: "cacheable-lookup@npm:7.0.0" + checksum: 10c0/63a9c144c5b45cb5549251e3ea774c04d63063b29e469f7584171d059d3a88f650f47869a974e2d07de62116463d742c287a81a625e791539d987115cb081635 + languageName: node + linkType: hard + +"cacheable-request@npm:^12.0.1": + version: 12.0.1 + resolution: "cacheable-request@npm:12.0.1" + dependencies: + "@types/http-cache-semantics": "npm:^4.0.4" + get-stream: "npm:^9.0.1" + http-cache-semantics: "npm:^4.1.1" + keyv: "npm:^4.5.4" + mimic-response: "npm:^4.0.0" + normalize-url: "npm:^8.0.1" + responselike: "npm:^3.0.0" + checksum: 10c0/3ccc26519c8dd0821fcb21fa00781e55f05ab6e1da1487fbbee9c8c03435a3cf72c29a710a991cebe398fb9a5274e2a772fc488546d402db8dc21310764ed83a + languageName: node + linkType: hard + +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 + languageName: node + linkType: hard + "chai@npm:^6.2.1": version: 6.2.2 resolution: "chai@npm:6.2.2" @@ -1093,6 +2490,50 @@ __metadata: languageName: node linkType: hard +"clipanion@npm:^4.0.0-rc.4": + version: 4.0.0-rc.4 + resolution: "clipanion@npm:4.0.0-rc.4" + dependencies: + typanion: "npm:^3.8.0" + peerDependencies: + typanion: "*" + checksum: 10c0/047b415b59a5e9777d00690fba563ccc850eca6bf27790a88d1deea3ecc8a89840ae9aed554ff284cc698a9f3f20256e43c25ff4a7c4c90a71e5e7d9dca61dd1 + languageName: node + linkType: hard + +"code-error-fragment@npm:0.0.230": + version: 0.0.230 + resolution: "code-error-fragment@npm:0.0.230" + checksum: 10c0/40164ec7ad40434fc05ed02d2cd085db3a974a2be9dc48b54b1d7f99f38c621a027dc3af29bda0208c7a6af46571208ae8b84e1ea5eb20fcecbbbf40403c9fd1 + languageName: node + linkType: hard + +"combine-errors@npm:^3.0.3": + version: 3.0.3 + resolution: "combine-errors@npm:3.0.3" + dependencies: + custom-error-instance: "npm:2.1.1" + lodash.uniqby: "npm:4.5.0" + checksum: 10c0/6e7c04102596dfc43ddb252c8390e869e358fb6df3b027567b33f5d998fdb2ad84573ce30a3861feb84fdee1fc3da3c465d68e23f7ab39f98bbe846ca9f70c6d + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.5": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -1104,7 +2545,14 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.4": +"custom-error-instance@npm:2.1.1": + version: 2.1.1 + resolution: "custom-error-instance@npm:2.1.1" + checksum: 10c0/c4f68550ae88426c49810846c62651438f04a10462c4d8667c089fb9264bcf1723c9b188e3f1d7791d727b0e5aa48ed1de0415eef50d43ea078517844296b85e + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.4, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -1116,6 +2564,29 @@ __metadata: languageName: node linkType: hard +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e + languageName: node + linkType: hard + +"defer-to-connect@npm:^2.0.1": + version: 2.0.1 + resolution: "defer-to-connect@npm:2.0.1" + checksum: 10c0/625ce28e1b5ad10cf77057b9a6a727bf84780c17660f6644dab61dd34c23de3001f03cedc401f7d30a4ed9965c2e8a7336e220a329146f2cf85d4eddea429782 + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 + languageName: node + linkType: hard + "detect-indent@npm:^6.0.0": version: 6.1.0 resolution: "detect-indent@npm:6.1.0" @@ -1132,6 +2603,24 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^17.2.3": + version: 17.3.1 + resolution: "dotenv@npm:17.3.1" + checksum: 10c0/c78e0c2d5a549c751e544cc60e2b95e7cb67e0c551f42e094d161c6b297aa44b630a3c2dcacf5569e529a6c2a6b84e2ab9be8d37b299d425df5a18b81ce4a35f + languageName: node + linkType: hard + +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 + languageName: node + linkType: hard + "enquirer@npm:^2.4.1": version: 2.4.1 resolution: "enquirer@npm:2.4.1" @@ -1149,6 +2638,20 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 + languageName: node + linkType: hard + "es-module-lexer@npm:^1.7.0": version: 1.7.0 resolution: "es-module-lexer@npm:1.7.0" @@ -1156,6 +2659,27 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10c0/65364812ca4daf48eb76e2a3b7a89b3f6a2e62a1c420766ce9f692665a29d94fe41fe88b65f24106f449859549711e4b40d9fb8002d862dfd7eb1c512d10be0c + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af + languageName: node + linkType: hard + "esbuild@npm:^0.27.0": version: 0.27.3 resolution: "esbuild@npm:0.27.3" @@ -1271,6 +2795,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^5.0.1": + version: 5.0.4 + resolution: "eventemitter3@npm:5.0.4" + checksum: 10c0/575b8cac8d709e1473da46f8f15ef311b57ff7609445a7c71af5cd42598583eee6f098fa7a593e30f27e94b8865642baa0689e8fa97c016f742abdb3b1bf6d9a + languageName: node + linkType: hard + "expect-type@npm:^1.2.2": version: 1.3.0 resolution: "expect-type@npm:1.3.0" @@ -1305,6 +2836,25 @@ __metadata: languageName: node linkType: hard +"fast-xml-builder@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-xml-builder@npm:1.0.0" + checksum: 10c0/2631fda265c81e8008884d08944eeed4e284430116faa5b8b7a43a3602af367223b7bf01c933215c9ad2358b8666e45041bc038d64877156a2f88821841b3014 + languageName: node + linkType: hard + +"fast-xml-parser@npm:5.4.1": + version: 5.4.1 + resolution: "fast-xml-parser@npm:5.4.1" + dependencies: + fast-xml-builder: "npm:^1.0.0" + strnum: "npm:^2.1.2" + bin: + fxparser: src/cli/cli.js + checksum: 10c0/8c696438a0c64135faf93ea6a93879208d649b7c9a3293d30d6eb750dc7f766fd083c0df5a82786b60809c3ead64fad155f28dbed25efea91017aaf9f64c91e5 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.20.1 resolution: "fastq@npm:1.20.1" @@ -1355,6 +2905,26 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:^4.0.2": + version: 4.1.0 + resolution: "form-data-encoder@npm:4.1.0" + checksum: 10c0/cbd655aa8ffff6f7c2733b1d8e95fa9a2fe8a88a90bde29fb54b8e02c9406e51f32a014bfe8297d67fbac9f77614d14a8b4bbc4fd0352838e67e97a881d06332 + languageName: node + linkType: hard + +"form-data@npm:^4.0.4": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10c0/dd6b767ee0bbd6d84039db12a0fa5a2028160ffbfaba1800695713b46ae974a5f6e08b3356c3195137f8530dcd9dfcb5d5ae1eeff53d0db1e5aad863b619ce3b + languageName: node + linkType: hard + "fs-extra@npm:^7.0.1": version: 7.0.1 resolution: "fs-extra@npm:7.0.1" @@ -1405,6 +2975,61 @@ __metadata: languageName: node linkType: hard +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 + languageName: node + linkType: hard + +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8 + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.2.6": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" + dependencies: + async-function: "npm:^1.0.0" + async-generator-function: "npm:^1.0.0" + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + function-bind: "npm:^1.1.2" + generator-function: "npm:^2.0.0" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d + languageName: node + linkType: hard + +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c + languageName: node + linkType: hard + +"get-stream@npm:^9.0.1": + version: 9.0.1 + resolution: "get-stream@npm:9.0.1" + dependencies: + "@sec-ant/readable-stream": "npm:^0.4.1" + is-stream: "npm:^4.0.1" + checksum: 10c0/d70e73857f2eea1826ac570c3a912757dcfbe8a718a033fa0c23e12ac8e7d633195b01710e0559af574cbb5af101009b42df7b6f6b29ceec8dbdf7291931b948 + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -1439,13 +3064,71 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead + languageName: node + linkType: hard + +"got@npm:14.4.9": + version: 14.4.9 + resolution: "got@npm:14.4.9" + dependencies: + "@sindresorhus/is": "npm:^7.0.1" + "@szmarczak/http-timer": "npm:^5.0.1" + cacheable-lookup: "npm:^7.0.0" + cacheable-request: "npm:^12.0.1" + decompress-response: "npm:^6.0.0" + form-data-encoder: "npm:^4.0.2" + http2-wrapper: "npm:^2.2.1" + lowercase-keys: "npm:^3.0.0" + p-cancelable: "npm:^4.0.1" + responselike: "npm:^3.0.0" + type-fest: "npm:^4.26.1" + checksum: 10c0/bc4b0991c114947b54681d96d2ee998638bb824a6ccb2e95ab09660aaf0f514bac383a81755fe7b6b89e3a23fa1bf05c1e5ae9be102f50b37d654f29c967c8d8 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 languageName: node linkType: hard +"grapheme-splitter@npm:^1.0.4": + version: 1.0.4 + resolution: "grapheme-splitter@npm:1.0.4" + checksum: 10c0/108415fb07ac913f17040dc336607772fcea68c7f495ef91887edddb0b0f5ff7bc1d1ab181b125ecb2f0505669ef12c9a178a3bbd2dd8e042d8c5f1d7c90331a + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.2": + version: 1.0.2 + resolution: "has-tostringtag@npm:1.0.2" + dependencies: + has-symbols: "npm:^1.0.3" + checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c + languageName: node + linkType: hard + +"hasown@npm:^2.0.2": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" @@ -1474,6 +3157,16 @@ __metadata: languageName: node linkType: hard +"http2-wrapper@npm:^2.2.1": + version: 2.2.1 + resolution: "http2-wrapper@npm:2.2.1" + dependencies: + quick-lru: "npm:^5.1.1" + resolve-alpn: "npm:^1.2.0" + checksum: 10c0/7207201d3c6e53e72e510c9b8912e4f3e468d3ecc0cf3bf52682f2aac9cd99358b896d1da4467380adc151cf97c412bedc59dc13dae90c523f42053a7449eedb + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -1516,6 +3209,13 @@ __metadata: languageName: node linkType: hard +"into-stream@npm:^9.0.0": + version: 9.1.0 + resolution: "into-stream@npm:9.1.0" + checksum: 10c0/843849ab3255e1edda3eb6a70a72d1267ac7aa3dcfe13cd5c88a3a90882bb141107c182bd7832aa6e5e1c69e55f70d6768da61ad1250ad7b9f132115c5d1364a + languageName: node + linkType: hard + "ip-address@npm:^10.0.1": version: 10.1.0 resolution: "ip-address@npm:10.1.0" @@ -1546,6 +3246,20 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^2.0.0": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 + languageName: node + linkType: hard + +"is-stream@npm:^4.0.1": + version: 4.0.1 + resolution: "is-stream@npm:4.0.1" + checksum: 10c0/2706c7f19b851327ba374687bc4a3940805e14ca496dc672b9629e744d143b1ad9c6f1b162dece81c7bfbc0f83b32b61ccc19ad2e05aad2dd7af347408f60c7f + languageName: node + linkType: hard + "is-subdir@npm:^1.1.1": version: 1.2.0 resolution: "is-subdir@npm:1.2.0" @@ -1576,6 +3290,13 @@ __metadata: languageName: node linkType: hard +"js-base64@npm:^3.7.2": + version: 3.7.8 + resolution: "js-base64@npm:3.7.8" + checksum: 10c0/a4452a7e7f32b0ef568a344157efec00c14593bbb1cf0c113f008dddff7ec515b35147af0cd70a7735adb69a2a2bdee921adffea2ea465e2c856ba50d649b11e + languageName: node + linkType: hard + "js-yaml@npm:^3.6.1": version: 3.14.2 resolution: "js-yaml@npm:3.14.2" @@ -1599,6 +3320,23 @@ __metadata: languageName: node linkType: hard +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 10c0/0d1c91569d9588e7eef2b49b59851f297f3ab93c7b35c7c221e288099322be6b562767d11e4821da500f3219542b9afd2e54c5dc573107c1126ed1080f8e96d7 + languageName: node + linkType: hard + +"json-to-ast@npm:^2.1.0": + version: 2.1.0 + resolution: "json-to-ast@npm:2.1.0" + dependencies: + code-error-fragment: "npm:0.0.230" + grapheme-splitter: "npm:^1.0.4" + checksum: 10c0/28f37650df38cf61002e8c4454b6fcf471a13c19c9b300491b2240cb0e1ef71f5e29480b9f0d10a7462c5e17c9bca4eda4446776b425805d7a4bb8cd2aeef74a + languageName: node + linkType: hard + "jsonfile@npm:^4.0.0": version: 4.0.0 resolution: "jsonfile@npm:4.0.0" @@ -1611,6 +3349,15 @@ __metadata: languageName: node linkType: hard +"keyv@npm:^4.5.4": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: "npm:3.0.1" + checksum: 10c0/aa52f3c5e18e16bb6324876bb8b59dd02acf782a4b789c7b2ae21107fab95fab3890ed448d4f8dba80ce05391eeac4bfabb4f02a20221342982f806fa2cf271e + languageName: node + linkType: hard + "locate-path@npm:^5.0.0": version: 5.0.0 resolution: "locate-path@npm:5.0.0" @@ -1620,6 +3367,62 @@ __metadata: languageName: node linkType: hard +"lodash-es@npm:^4.17.21": + version: 4.17.23 + resolution: "lodash-es@npm:4.17.23" + checksum: 10c0/3150fb6660c14c7a6b5f23bd11597d884b140c0e862a17fdb415aaa5ef7741523182904a6b7929f04e5f60a11edb5a79499eb448734381c99ffb3c4734beeddd + languageName: node + linkType: hard + +"lodash._baseiteratee@npm:~4.7.0": + version: 4.7.0 + resolution: "lodash._baseiteratee@npm:4.7.0" + dependencies: + lodash._stringtopath: "npm:~4.8.0" + checksum: 10c0/67f80e6878444e44e3cc6e779941c78da2bfa02413e305b5bfbe612e6ddedbbb05bda025e673014d734b79d69db911c7cfaf25dbc05abc56356eccc90e1e59e3 + languageName: node + linkType: hard + +"lodash._basetostring@npm:~4.12.0": + version: 4.12.0 + resolution: "lodash._basetostring@npm:4.12.0" + checksum: 10c0/08d4f8affb3cfed7cc50bbe35d35605597bd72c05e6ed0696122b0a4da242de1b5f5396e986645f46a97e976d6f7b4943a001b002a4e2b3b8ea087949dc9eb98 + languageName: node + linkType: hard + +"lodash._baseuniq@npm:~4.6.0": + version: 4.6.0 + resolution: "lodash._baseuniq@npm:4.6.0" + dependencies: + lodash._createset: "npm:~4.0.0" + lodash._root: "npm:~3.0.0" + checksum: 10c0/07e2ac63efde634685ed12b16664ee04931b258cd2511b703fddd9ddfc624a85542e3f03a6c2fdac369c00559296198daa8efffaf90d71c7a35f5c89f94ebb14 + languageName: node + linkType: hard + +"lodash._createset@npm:~4.0.0": + version: 4.0.3 + resolution: "lodash._createset@npm:4.0.3" + checksum: 10c0/6144f59a63cedb8bf2840970579dc7dfdad55970741a80f9a6e5c6b29b629fc204c846e54e29266ec24b41e3f680bcbeb9fe9332fb9f2bab71eb9c70cacd26d8 + languageName: node + linkType: hard + +"lodash._root@npm:~3.0.0": + version: 3.0.1 + resolution: "lodash._root@npm:3.0.1" + checksum: 10c0/679c8a570381795b6953ec8c680442acb5472c5b399263ed4ea6839630cf4dd5d65aa8c2a0f6934507fc5bc70f2af20bc430cbd847728b8c1672db95555edb51 + languageName: node + linkType: hard + +"lodash._stringtopath@npm:~4.8.0": + version: 4.8.0 + resolution: "lodash._stringtopath@npm:4.8.0" + dependencies: + lodash._basetostring: "npm:~4.12.0" + checksum: 10c0/5e3a58a97b481a9069fc43ca6ddeace83891e3d9cff8d36400064c85977c225621a9c52131224d64632a054a7c19d8ca7b2f0f5bfc983402564bd2a5e49d8672 + languageName: node + linkType: hard + "lodash.startcase@npm:^4.4.0": version: 4.4.0 resolution: "lodash.startcase@npm:4.4.0" @@ -1627,6 +3430,30 @@ __metadata: languageName: node linkType: hard +"lodash.throttle@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.throttle@npm:4.1.1" + checksum: 10c0/14628013e9e7f65ac904fc82fd8ecb0e55a9c4c2416434b1dd9cf64ae70a8937f0b15376a39a68248530adc64887ed0fe2b75204b2c9ec3eea1cb2d66ddd125d + languageName: node + linkType: hard + +"lodash.uniqby@npm:4.5.0": + version: 4.5.0 + resolution: "lodash.uniqby@npm:4.5.0" + dependencies: + lodash._baseiteratee: "npm:~4.7.0" + lodash._baseuniq: "npm:~4.6.0" + checksum: 10c0/4f479ec2dd92825c2a308986d3613e32fe02b13dffc5d0998cbb1ef4eeee56835f13a88e093c7a88473fa72cf5ffd37f807f5ae2d9496e738d776c1911fdabc8 + languageName: node + linkType: hard + +"lowercase-keys@npm:^3.0.0": + version: 3.0.0 + resolution: "lowercase-keys@npm:3.0.0" + checksum: 10c0/ef62b9fa5690ab0a6e4ef40c94efce68e3ed124f583cc3be38b26ff871da0178a28b9a84ce0c209653bb25ca135520ab87fea7cd411a54ac4899cb2f30501430 + languageName: node + linkType: hard + "lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": version: 11.2.6 resolution: "lru-cache@npm:11.2.6" @@ -1662,6 +3489,13 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f + languageName: node + linkType: hard + "merge2@npm:^1.3.0, merge2@npm:^1.4.1": version: 1.4.1 resolution: "merge2@npm:1.4.1" @@ -1679,6 +3513,36 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 + languageName: node + linkType: hard + +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362 + languageName: node + linkType: hard + +"mimic-response@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-response@npm:4.0.0" + checksum: 10c0/761d788d2668ae9292c489605ffd4fad220f442fbae6832adce5ebad086d691e906a6d5240c290293c7a11e99fbdbbef04abbbed498bf8699a4ee0f31315e3fb + languageName: node + linkType: hard + "minimatch@npm:^10.2.2": version: 10.2.4 resolution: "minimatch@npm:10.2.4" @@ -1688,6 +3552,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^3.0.5": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70 + languageName: node + linkType: hard + "minipass-collect@npm:^2.0.1": version: 2.0.1 resolution: "minipass-collect@npm:2.0.1" @@ -1814,6 +3687,13 @@ __metadata: languageName: node linkType: hard +"node-watch@npm:^0.7.4": + version: 0.7.4 + resolution: "node-watch@npm:0.7.4" + checksum: 10c0/05c3e66e7b5013d64c31a6dd96b55d87c14c8c0515d05d73554d706a1f8b962fe31781dce74740db29c0ec7c9a1f33a6bac07ef1e8aecc0d38c5ab4eef4c7ac0 + languageName: node + linkType: hard + "nopt@npm:^9.0.0": version: 9.0.0 resolution: "nopt@npm:9.0.0" @@ -1825,6 +3705,13 @@ __metadata: languageName: node linkType: hard +"normalize-url@npm:^8.0.1": + version: 8.1.1 + resolution: "normalize-url@npm:8.1.1" + checksum: 10c0/1beb700ce42acb2288f39453cdf8001eead55bbf046d407936a40404af420b8c1c6be97a869884ae9e659d7b1c744e40e905c875ac9290644eec2e3e6fb0b370 + languageName: node + linkType: hard + "obug@npm:^2.1.1": version: 2.1.1 resolution: "obug@npm:2.1.1" @@ -1839,6 +3726,13 @@ __metadata: languageName: node linkType: hard +"p-cancelable@npm:^4.0.1": + version: 4.0.1 + resolution: "p-cancelable@npm:4.0.1" + checksum: 10c0/12636623f46784ba962b6fe7a1f34d021f1d9a2cc12c43e270baa715ea872d5c8c7d9f086ed420b8b9817e91d9bbe92c14c90e5dddd4a9968c81a2a7aef7089d + languageName: node + linkType: hard + "p-filter@npm:^2.1.0": version: 2.1.0 resolution: "p-filter@npm:2.1.0" @@ -1873,13 +3767,30 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^7.0.2": +"p-map@npm:^7.0.2, p-map@npm:^7.0.3": version: 7.0.4 resolution: "p-map@npm:7.0.4" checksum: 10c0/a5030935d3cb2919d7e89454d1ce82141e6f9955413658b8c9403cfe379283770ed3048146b44cde168aa9e8c716505f196d5689db0ae3ce9a71521a2fef3abd languageName: node linkType: hard +"p-queue@npm:^9.0.1": + version: 9.1.0 + resolution: "p-queue@npm:9.1.0" + dependencies: + eventemitter3: "npm:^5.0.1" + p-timeout: "npm:^7.0.0" + checksum: 10c0/f6bb4644997c20cbbf68c0c88208283697c6c9b1e1879f2073791b1ffcb2d2eb0a9fe35c9631e0c74bd6562ef159b87b418d48df7e7b30e5ddb4d99055bb5c92 + languageName: node + linkType: hard + +"p-timeout@npm:^7.0.0": + version: 7.0.1 + resolution: "p-timeout@npm:7.0.1" + checksum: 10c0/87d96529d1096d506607218dba6f9ec077c6dbedd0c2e2788c748e33bcd05faae8a81009fd9d22ec0b3c95fc83f4717306baba223f6e464737d8b99294c3e863 + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -1989,6 +3900,17 @@ __metadata: languageName: node linkType: hard +"proper-lockfile@npm:^4.1.2": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" + dependencies: + graceful-fs: "npm:^4.2.4" + retry: "npm:^0.12.0" + signal-exit: "npm:^3.0.2" + checksum: 10c0/2f265dbad15897a43110a02dae55105c04d356ec4ed560723dcb9f0d34bc4fb2f13f79bb930e7561be10278e2314db5aca2527d5d3dcbbdee5e6b331d1571f6d + languageName: node + linkType: hard + "quansync@npm:^0.2.7": version: 0.2.11 resolution: "quansync@npm:0.2.11" @@ -1996,6 +3918,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 10c0/3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -2003,6 +3932,13 @@ __metadata: languageName: node linkType: hard +"quick-lru@npm:^5.1.1": + version: 5.1.1 + resolution: "quick-lru@npm:5.1.1" + checksum: 10c0/a24cba5da8cec30d70d2484be37622580f64765fb6390a928b17f60cd69e8dbd32a954b3ff9176fa1b86d86ff2ba05252fae55dc4d40d0291c60412b0ad096da + languageName: node + linkType: hard + "read-yaml-file@npm:^1.1.0": version: 1.1.0 resolution: "read-yaml-file@npm:1.1.0" @@ -2015,6 +3951,15 @@ __metadata: languageName: node linkType: hard +"recursive-readdir@npm:^2.2.3": + version: 2.2.3 + resolution: "recursive-readdir@npm:2.2.3" + dependencies: + minimatch: "npm:^3.0.5" + checksum: 10c0/d0238f137b03af9cd645e1e0b40ae78b6cda13846e3ca57f626fcb58a66c79ae018a10e926b13b3a460f1285acc946a4e512ea8daa2e35df4b76a105709930d1 + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -2022,6 +3967,13 @@ __metadata: languageName: node linkType: hard +"resolve-alpn@npm:^1.2.0": + version: 1.2.1 + resolution: "resolve-alpn@npm:1.2.1" + checksum: 10c0/b70b29c1843bc39781ef946c8cd4482e6d425976599c0f9c138cec8209e4e0736161bf39319b01676a847000085dfdaf63583c6fb4427bf751a10635bd2aa0c4 + languageName: node + linkType: hard + "resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" @@ -2029,6 +3981,22 @@ __metadata: languageName: node linkType: hard +"responselike@npm:^3.0.0": + version: 3.0.0 + resolution: "responselike@npm:3.0.0" + dependencies: + lowercase-keys: "npm:^3.0.0" + checksum: 10c0/8af27153f7e47aa2c07a5f2d538cb1e5872995f0e9ff77def858ecce5c3fe677d42b824a62cde502e56d275ab832b0a8bd350d5cd6b467ac0425214ac12ae658 + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + "retry@npm:^0.13.1": version: 0.13.1 resolution: "retry@npm:0.13.1" @@ -2181,6 +4149,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^3.0.2": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -2286,6 +4261,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^2.1.2": + version: 2.2.0 + resolution: "strnum@npm:2.2.0" + checksum: 10c0/9a656f5048047abff8d10d0bb57761a01916e368a71e95d4f5a962b57f64b738e20672e68ba10b7de3dc78e861c77bc0566bdeed7017abdda1caf0303c929a3f + languageName: node + linkType: hard + "tar@npm:^7.5.4": version: 7.5.9 resolution: "tar@npm:7.5.9" @@ -2357,6 +4339,7 @@ __metadata: "@types/http-proxy": "npm:^1.17.17" "@types/node": "npm:^25.3.3" http-proxy: "npm:^1.18.1" + transloadit: "npm:^4.7.4" typescript: "npm:^5.9.3" vitest: "npm:^4.0.18" bin: @@ -2364,7 +4347,67 @@ __metadata: languageName: unknown linkType: soft -"type-fest@npm:^4.41.0": +"transloadit@npm:^4.7.4": + version: 4.7.4 + resolution: "transloadit@npm:4.7.4" + dependencies: + "@aws-sdk/client-s3": "npm:^3.891.0" + "@aws-sdk/s3-request-presigner": "npm:^3.891.0" + "@transloadit/sev-logger": "npm:^0.1.9" + "@transloadit/utils": "npm:^4.3.0" + clipanion: "npm:^4.0.0-rc.4" + debug: "npm:^4.4.3" + dotenv: "npm:^17.2.3" + form-data: "npm:^4.0.4" + got: "npm:14.4.9" + into-stream: "npm:^9.0.0" + is-stream: "npm:^4.0.1" + json-to-ast: "npm:^2.1.0" + lodash-es: "npm:^4.17.21" + node-watch: "npm:^0.7.4" + p-map: "npm:^7.0.3" + p-queue: "npm:^9.0.1" + recursive-readdir: "npm:^2.2.3" + tus-js-client: "npm:^4.3.1" + typanion: "npm:^3.14.0" + type-fest: "npm:^4.41.0" + zod: "npm:3.25.76" + bin: + transloadit: dist/cli.js + checksum: 10c0/d7a3b47cd38cba26cf67a55e36854e5862ea6dafdf181111582eeb8afef671587fb53fcf005952cd60ef9a0f3aad248904e3fbbcf20eb0ebf41600786cc7fb56 + languageName: node + linkType: hard + +"tslib@npm:^2.6.2": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 + languageName: node + linkType: hard + +"tus-js-client@npm:^4.3.1": + version: 4.3.1 + resolution: "tus-js-client@npm:4.3.1" + dependencies: + buffer-from: "npm:^1.1.2" + combine-errors: "npm:^3.0.3" + is-stream: "npm:^2.0.0" + js-base64: "npm:^3.7.2" + lodash.throttle: "npm:^4.1.1" + proper-lockfile: "npm:^4.1.2" + url-parse: "npm:^1.5.7" + checksum: 10c0/db36ec17cff0217f1199b84f355f8aa07abab47d98d003926f67d9bad08e15e77c3ef61f98aa360133f27ad4944cf519170ff5b5eb5b93a8a762c28dd49df59d + languageName: node + linkType: hard + +"typanion@npm:^3.14.0, typanion@npm:^3.8.0": + version: 3.14.0 + resolution: "typanion@npm:3.14.0" + checksum: 10c0/8b03b19844e6955bfd906c31dc781bae6d7f1fb3ce4fe24b7501557013d4889ae5cefe671dafe98d87ead0adceb8afcb8bc16df7dc0bd2b7331bac96f3a7cae2 + languageName: node + linkType: hard + +"type-fest@npm:^4.26.1, type-fest@npm:^4.41.0": version: 4.41.0 resolution: "type-fest@npm:4.41.0" checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 @@ -2423,6 +4466,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:^1.5.7": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: 10c0/bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87 + languageName: node + linkType: hard + "vite@npm:^6.0.0 || ^7.0.0": version: 7.3.1 resolution: "vite@npm:7.3.1" @@ -2585,6 +4638,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:3.25.76": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c + languageName: node + linkType: hard + "zod@npm:^4.0.0": version: 4.3.6 resolution: "zod@npm:4.3.6" From b6cd69fd7af78d53bb7f290200882765a74f10cb Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 4 Mar 2026 07:45:31 +0100 Subject: [PATCH 07/11] ci: run trusted real e2e only when secrets are configured --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ README.md | 14 +++++++++++--- package.json | 1 - scripts/sync-node-sdk-env.sh | 32 -------------------------------- 4 files changed, 37 insertions(+), 36 deletions(-) delete mode 100755 scripts/sync-node-sdk-env.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10013be..bb1bd26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,32 @@ jobs: - run: yarn install --immutable - run: yarn check + real-e2e: + name: Real API E2E (trusted runs) + if: > + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + needs: + - check + env: + TRANSLOADIT_KEY: ${{ secrets.TRANSLOADIT_KEY }} + TRANSLOADIT_SECRET: ${{ secrets.TRANSLOADIT_SECRET }} + TRANSLOADIT_ENDPOINT: ${{ secrets.TRANSLOADIT_ENDPOINT }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: corepack enable + - run: yarn install --immutable + - name: Skip if secrets are not configured + if: env.TRANSLOADIT_KEY == '' || env.TRANSLOADIT_SECRET == '' + run: echo "Skipping real E2E; TRANSLOADIT_KEY/TRANSLOADIT_SECRET are not configured." + - name: Run real API E2E + if: env.TRANSLOADIT_KEY != '' && env.TRANSLOADIT_SECRET != '' + run: yarn test:real + pack: name: Package Smoke Test runs-on: ubuntu-latest diff --git a/README.md b/README.md index 9d8662b..95df21d 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,20 @@ yarn check Run an opt-in test against the real Transloadit API: ```bash -yarn env:sync:node-sdk +# set locally (for example in .env) +export TRANSLOADIT_KEY="your-key" +export TRANSLOADIT_SECRET="your-secret" +# optional +export TRANSLOADIT_ENDPOINT="https://api2.transloadit.com" + yarn test:real ``` -This copies `TRANSLOADIT_KEY` and `TRANSLOADIT_SECRET` from `~/code/node-sdk/.env` into a local -`.env` (gitignored), then runs `test/real.e2e.test.ts`. +For CI, configure repository secrets: + +- `TRANSLOADIT_KEY` +- `TRANSLOADIT_SECRET` +- `TRANSLOADIT_ENDPOINT` (optional) ## Releases diff --git a/package.json b/package.json index a78eb64..78819f6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ }, "scripts": { "start": "node ./bin/notify-url-proxy.ts", - "env:sync:node-sdk": "bash ./scripts/sync-node-sdk-env.sh", "lint": "biome check .", "format": "biome format . --write", "typecheck": "tsc --noEmit", diff --git a/scripts/sync-node-sdk-env.sh b/scripts/sync-node-sdk-env.sh deleted file mode 100755 index a097462..0000000 --- a/scripts/sync-node-sdk-env.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SOURCE="${1:-$HOME/code/node-sdk/.env}" -TARGET=".env" - -if [[ ! -f "$SOURCE" ]]; then - echo "Source env file not found: $SOURCE" >&2 - exit 1 -fi - -tmp_file="$(mktemp)" -{ - echo "# Synced from $SOURCE" - echo "# Generated at $(date -u +%Y-%m-%dT%H:%M:%SZ)" - grep -E '^(TRANSLOADIT_KEY|TRANSLOADIT_SECRET|TRANSLOADIT_ENDPOINT)=' "$SOURCE" || true -} > "$tmp_file" - -if ! grep -q '^TRANSLOADIT_KEY=' "$tmp_file"; then - echo "Missing TRANSLOADIT_KEY in $SOURCE" >&2 - rm -f "$tmp_file" - exit 1 -fi - -if ! grep -q '^TRANSLOADIT_SECRET=' "$tmp_file"; then - echo "Missing TRANSLOADIT_SECRET in $SOURCE" >&2 - rm -f "$tmp_file" - exit 1 -fi - -mv "$tmp_file" "$TARGET" -echo "Wrote $TARGET from $SOURCE" From 404e1fdf37e66143d3e4d65964650d2c00166412 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 4 Mar 2026 08:41:50 +0100 Subject: [PATCH 08/11] feat: add sev logger, retry strategy, and cli log level --- .env.example | 5 +- README.md | 8 ++- bin/notify-url-proxy.ts | 48 ++++++++++++++++- package.json | 4 +- src/index.ts | 115 +++++++++++++++++++++++----------------- yarn.lock | 18 +++++++ 6 files changed, 144 insertions(+), 54 deletions(-) diff --git a/.env.example b/.env.example index 41c4d12..e7f3835 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ -# Copy real credentials from ~/code/node-sdk/.env via: -# yarn env:sync:node-sdk - TRANSLOADIT_KEY=your-key TRANSLOADIT_SECRET=your-secret # Optional (defaults to https://api2.transloadit.com) TRANSLOADIT_ENDPOINT=https://api2.transloadit.com +# Optional (0-8 or emerg/alert/crit/err/warn/notice/info/debug/trace) +TRANSLOADIT_LOG_LEVEL=info diff --git a/README.md b/README.md index 95df21d..5535980 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This version is modernized for: - Biome + Vitest + GitHub Actions + Changesets Notify payloads are signed via `@transloadit/utils` using prefixed `sha384` signatures. +Polling retries use `p-retry`, and logs are emitted via `@transloadit/sev-logger`. ## Install @@ -25,11 +26,16 @@ export TRANSLOADIT_SECRET="your-secret" notify-url-proxy \ --notifyUrl "http://127.0.0.1:3000/transloadit" \ - --port 8888 + --port 8888 \ + --log-level info ``` Run `notify-url-proxy --help` for all options. +Log level accepts `0-8` or names: +`emerg`, `alert`, `crit`, `err`, `warn`, `notice`, `info`, `debug`, `trace`. +You can also set `TRANSLOADIT_LOG_LEVEL`. + ## Programmatic usage ```ts diff --git a/bin/notify-url-proxy.ts b/bin/notify-url-proxy.ts index 6abde57..5f398bc 100755 --- a/bin/notify-url-proxy.ts +++ b/bin/notify-url-proxy.ts @@ -2,6 +2,7 @@ import { parseArgs } from 'node:util' +import { SevLogger } from '@transloadit/sev-logger' import TransloaditNotifyUrlProxy, { type ProxySettings } from '../src/index.ts' function parsePositiveIntOption( @@ -17,6 +18,43 @@ function parsePositiveIntOption( return parsed } +const LOG_LEVEL_BY_NAME = { + emerg: SevLogger.LEVEL.EMERG, + alert: SevLogger.LEVEL.ALERT, + crit: SevLogger.LEVEL.CRIT, + err: SevLogger.LEVEL.ERR, + error: SevLogger.LEVEL.ERR, + warn: SevLogger.LEVEL.WARN, + warning: SevLogger.LEVEL.WARN, + notice: SevLogger.LEVEL.NOTICE, + info: SevLogger.LEVEL.INFO, + debug: SevLogger.LEVEL.DEBUG, + trace: SevLogger.LEVEL.TRACE, +} as const + +function parseLogLevelOption(value: string): number { + const normalized = value.trim().toLowerCase() + const parsedNumeric = Number.parseInt(normalized, 10) + + if ( + Number.isInteger(parsedNumeric) && + parsedNumeric >= SevLogger.LEVEL.EMERG && + parsedNumeric <= SevLogger.LEVEL.TRACE + ) { + return parsedNumeric + } + + const parsedNamed = LOG_LEVEL_BY_NAME[normalized as keyof typeof LOG_LEVEL_BY_NAME] + if (typeof parsedNamed === 'number') { + return parsedNamed + } + + console.error( + `Invalid log level: ${value}. Use 0-8 or one of ${Object.keys(LOG_LEVEL_BY_NAME).join(', ')}.`, + ) + process.exit(1) +} + const { values } = parseArgs({ options: { notifyUrl: { type: 'string' }, @@ -24,6 +62,8 @@ const { values } = parseArgs({ port: { type: 'string' }, pollIntervalMs: { type: 'string' }, maxPollAttempts: { type: 'string' }, + logLevel: { type: 'string', short: 'l' }, + 'log-level': { type: 'string' }, help: { type: 'boolean', short: 'h' }, }, }) @@ -37,10 +77,11 @@ Options: --port Local listen port --pollIntervalMs Poll interval in milliseconds --maxPollAttempts Max number of poll attempts + -l, --log-level Log level (0-8 or emerg/alert/crit/err/warn/notice/info/debug/trace) -h, --help Show this help Environment fallback: - TRANSLOADIT_SECRET, TRANSLOADIT_NOTIFY_URL + TRANSLOADIT_SECRET, TRANSLOADIT_NOTIFY_URL, TRANSLOADIT_LOG_LEVEL `) process.exit(0) } @@ -66,9 +107,14 @@ if (values.maxPollAttempts) { settings.maxPollAttempts = parsePositiveIntOption('maxPollAttempts', values.maxPollAttempts) } +const rawLogLevel = values['log-level'] ?? values.logLevel ?? process.env.TRANSLOADIT_LOG_LEVEL +const logLevel = rawLogLevel ? parseLogLevelOption(rawLogLevel) : undefined +const loggerOptions = typeof logLevel === 'number' ? { logLevel } : {} + const proxy = new TransloaditNotifyUrlProxy( secret, values.notifyUrl ?? process.env.TRANSLOADIT_NOTIFY_URL, + loggerOptions, ) proxy.run(settings) diff --git a/package.json b/package.json index 78819f6..5cf2c62 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,11 @@ "changeset:version": "changeset version" }, "dependencies": { + "@transloadit/sev-logger": "^0.1.9", "@transloadit/utils": "^4.3.0", "@transloadit/zod": "^4.3.0", - "http-proxy": "^1.18.1" + "http-proxy": "^1.18.1", + "p-retry": "^7.1.1" }, "devDependencies": { "@biomejs/biome": "^2.4.5", diff --git a/src/index.ts b/src/index.ts index 943a000..82dcee0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,21 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' -import { setTimeout as delay } from 'node:timers/promises' +import { SevLogger } from '@transloadit/sev-logger' import { signParamsSync } from '@transloadit/utils/node' import { type AssemblyStatus, - assemblyStatusOkCodeSchema, + type assemblyStatusOkCodeSchema, assemblyStatusSchema, getAssemblyStage, getError, getOk, isAssemblyBusy, + isAssemblyOkStatus, isAssemblyTerminalError, isAssemblyTerminalOk, parseAssemblyUrls, } from '@transloadit/zod/v4' import httpProxy from 'http-proxy' +import pRetry, { AbortError, type RetryContext } from 'p-retry' export interface ProxySettings { target: string @@ -22,6 +24,11 @@ export interface ProxySettings { maxPollAttempts: number } +export interface ProxyLoggerOptions { + logger?: SevLogger + logLevel?: number +} + type KnownAssemblyState = (typeof assemblyStatusOkCodeSchema.options)[number] export type AssemblyResponse = AssemblyStatus @@ -33,12 +40,12 @@ const DEFAULT_SETTINGS: ProxySettings = { maxPollAttempts: 10, } +const DEFAULT_LOG_LEVEL = SevLogger.LEVEL.INFO + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } -class TerminalAssemblyError extends Error {} - function toErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message @@ -57,16 +64,16 @@ export function extractAssemblyUrl(body: string): string | null { } export function getAssemblyState(payload: unknown): KnownAssemblyState { - if (!isRecord(payload) || typeof payload.ok !== 'string') { + if (!isRecord(payload)) { throw new Error('No ok field found in Assembly response.') } - const parsedState = assemblyStatusOkCodeSchema.safeParse(payload.ok) - if (!parsedState.success) { - throw new Error(`Unknown Assembly state found: ${payload.ok}`) + const ok = typeof payload.ok === 'string' ? payload.ok : undefined + if (!isAssemblyOkStatus(ok)) { + throw new Error(`Unknown Assembly state found: ${String(payload.ok)}`) } - return parsedState.data + return ok } export function getSignature(secret: string, toSign: string): string { @@ -88,15 +95,30 @@ export default class TransloaditNotifyUrlProxy { private readonly secret: string private readonly notifyUrl: string + private readonly logger: SevLogger private readonly defaults: ProxySettings private settings: ProxySettings - constructor(secret: string, notifyUrl = 'http://127.0.0.1:3000/transloadit') { + constructor( + secret: string, + notifyUrl = 'http://127.0.0.1:3000/transloadit', + loggerOptions: ProxyLoggerOptions = {}, + ) { this.secret = secret || '' this.notifyUrl = notifyUrl this.defaults = { ...DEFAULT_SETTINGS } this.settings = { ...DEFAULT_SETTINGS } + this.logger = + loggerOptions.logger ?? + new SevLogger({ + breadcrumbs: ['notify-url-proxy'], + level: loggerOptions.logLevel ?? DEFAULT_LOG_LEVEL, + }) + + if (loggerOptions.logger && typeof loggerOptions.logLevel === 'number') { + this.logger.update({ level: loggerOptions.logLevel }) + } } run(opts: Partial = {}): void { @@ -133,7 +155,7 @@ export default class TransloaditNotifyUrlProxy { } else { res.end() } - this.out('Proxy error: %s', toErrorMessage(error)) + this.logger.err(`Proxy error: ${toErrorMessage(error)}`) }) this.proxy.on('proxyRes', (proxyRes) => { @@ -152,11 +174,8 @@ export default class TransloaditNotifyUrlProxy { this.server.listen(this.settings.port) - this.out( - 'Listening on http://localhost:%d, forwarding to %s, notifying %s', - this.settings.port, - this.settings.target, - this.notifyUrl, + this.logger.notice( + `Listening on http://localhost:${this.settings.port}, forwarding to ${this.settings.target}, notifying ${this.notifyUrl}`, ) } @@ -168,7 +187,7 @@ export default class TransloaditNotifyUrlProxy { return } - this.out('Received proxy response, polling assemblyUrl: %s', assemblyUrl) + this.logger.info(`Received proxy response, polling assemblyUrl: ${assemblyUrl}`) await this.pollAssembly(assemblyUrl) } @@ -183,32 +202,36 @@ export default class TransloaditNotifyUrlProxy { } private async pollAssembly(assemblyUrl: string): Promise { - for (let attempt = 1; attempt <= this.settings.maxPollAttempts; attempt += 1) { - try { - const response = await this.checkAssembly(assemblyUrl) - await this.notify(response) + const retries = Math.max(this.settings.maxPollAttempts - 1, 0) + + try { + const response = await pRetry(() => this.checkAssembly(assemblyUrl), { + retries, + minTimeout: this.settings.pollIntervalMs, + maxTimeout: this.settings.pollIntervalMs, + factor: 1, + randomize: false, + onFailedAttempt: (retryContext: RetryContext) => { + if (retryContext.retriesLeft <= 0) { + return + } + + this.logger.warn( + `Attempt ${retryContext.attemptNumber}/${this.settings.maxPollAttempts} failed for ${assemblyUrl}: ${retryContext.error.message}`, + ) + }, + }) + + await this.notify(response) + } catch (error) { + if (error instanceof AbortError) { + this.logger.notice(error.message) return - } catch (error) { - if (error instanceof TerminalAssemblyError) { - this.out('%s', error.message) - return - } - - if (attempt === this.settings.maxPollAttempts) { - this.out('No attempts left, giving up on checking assemblyUrl: %s', assemblyUrl) - return - } - - this.out( - 'Attempt %d/%d failed for %s: %s', - attempt, - this.settings.maxPollAttempts, - assemblyUrl, - toErrorMessage(error), - ) - - await delay(this.settings.pollIntervalMs) } + + this.logger.err( + `No attempts left, giving up on checking assemblyUrl: ${assemblyUrl} (${toErrorMessage(error)})`, + ) } } @@ -222,11 +245,11 @@ export default class TransloaditNotifyUrlProxy { if (isAssemblyTerminalError(assembly)) { const errorCode = getError(assembly) ?? 'UNKNOWN_ERROR' - throw new TerminalAssemblyError(`${assemblyUrl} reached terminal error state ${errorCode}.`) + throw new AbortError(`${assemblyUrl} reached terminal error state ${errorCode}.`) } if (isAssemblyTerminalOk(assembly)) { - this.out('%s reached terminal state %s.', assemblyUrl, getOk(assembly)) + this.logger.info(`${assemblyUrl} reached terminal state ${getOk(assembly)}.`) return assembly } @@ -263,10 +286,6 @@ export default class TransloaditNotifyUrlProxy { throw new Error(`Notify URL returned HTTP ${notifyResponse.status}`) } - this.out('Notify payload sent to %s', this.notifyUrl) - } - - private out(message: string, ...args: unknown[]): void { - console.log(message, ...args) + this.logger.notice(`Notify payload sent to ${this.notifyUrl}`) } } diff --git a/yarn.lock b/yarn.lock index a7c5290..22e7ec2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3239,6 +3239,13 @@ __metadata: languageName: node linkType: hard +"is-network-error@npm:^1.1.0": + version: 1.3.1 + resolution: "is-network-error@npm:1.3.1" + checksum: 10c0/389b4a4cc6838bc5764c1d4ab8af11ec68c63825d53f7ce9f5a31aa4d2c9e5d33896c052f4c44100911e8db47bcf854c4aae6c03d6b1d84700f7c6aa72d16693 + languageName: node + linkType: hard + "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -3784,6 +3791,15 @@ __metadata: languageName: node linkType: hard +"p-retry@npm:^7.1.1": + version: 7.1.1 + resolution: "p-retry@npm:7.1.1" + dependencies: + is-network-error: "npm:^1.1.0" + checksum: 10c0/d72fb15dace25b8bf72c97a13c8a630ad1deb4667e708955e8806ee38f1d70e9611598ebe57bd9677349256e024a5599292f99aabb203143e0e13f1735e30818 + languageName: node + linkType: hard + "p-timeout@npm:^7.0.0": version: 7.0.1 resolution: "p-timeout@npm:7.0.1" @@ -4334,11 +4350,13 @@ __metadata: dependencies: "@biomejs/biome": "npm:^2.4.5" "@changesets/cli": "npm:^2.30.0" + "@transloadit/sev-logger": "npm:^0.1.9" "@transloadit/utils": "npm:^4.3.0" "@transloadit/zod": "npm:^4.3.0" "@types/http-proxy": "npm:^1.17.17" "@types/node": "npm:^25.3.3" http-proxy: "npm:^1.18.1" + p-retry: "npm:^7.1.1" transloadit: "npm:^4.7.4" typescript: "npm:^5.9.3" vitest: "npm:^4.0.18" From 7997346ada48d95d96375c831c5047b148e952d1 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 4 Mar 2026 08:54:04 +0100 Subject: [PATCH 09/11] feat: finish 12-point modernization sweep --- .github/workflows/ci.yml | 38 +++ README.md | 14 +- bin/notify-url-proxy.ts | 130 +++++++-- docs/prompts/2026-03-04-modernize.md | 32 +++ package.json | 7 +- src/index.ts | 298 +++++++++++++++++---- test/behavior.test.ts | 384 +++++++++++++++++++++++++++ test/real.e2e.test.ts | 2 +- vitest.config.ts | 3 + yarn.lock | 49 +--- 10 files changed, 823 insertions(+), 134 deletions(-) create mode 100644 docs/prompts/2026-03-04-modernize.md create mode 100644 test/behavior.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb1bd26..fa8da86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,49 @@ on: branches: - main +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: check: name: Lint + Typecheck + Unit runs-on: ubuntu-latest steps: + - name: Changeset policy (PR only) + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request + const labels = (pr.labels || []).map((label) => label.name) + const hasBypassLabel = labels.includes('no-changeset') || labels.includes('skip-changeset') + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + per_page: 100, + }) + + const touchesReleaseSurface = files.some((file) => + /^src\/|^bin\/|^package\.json$|^README\.md$/.test(file.filename), + ) + + const hasChangeset = files.some((file) => + file.filename.startsWith('.changeset/') && + file.filename.endsWith('.md') && + file.filename !== '.changeset/README.md', + ) + + if (touchesReleaseSurface && !hasChangeset && !hasBypassLabel) { + core.setFailed( + 'This PR changes package behavior/release surface. Add a changeset or apply the no-changeset/skip-changeset label.', + ) + return + } + + core.info('Changeset policy passed.') - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: diff --git a/README.md b/README.md index 5535980..af1dbd3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This version is modernized for: - Biome + Vitest + GitHub Actions + Changesets Notify payloads are signed via `@transloadit/utils` using prefixed `sha384` signatures. -Polling retries use `p-retry`, and logs are emitted via `@transloadit/sev-logger`. +Forwarding uses native `fetch`, polling retries use `p-retry`, and logs are emitted via `@transloadit/sev-logger`. ## Install @@ -27,6 +27,7 @@ export TRANSLOADIT_SECRET="your-secret" notify-url-proxy \ --notifyUrl "http://127.0.0.1:3000/transloadit" \ --port 8888 \ + --notifyOnTerminalError \ --log-level info ``` @@ -48,9 +49,13 @@ const proxy = new TransloaditNotifyUrlProxy( proxy.run({ port: 8888, - target: 'https://api2.transloadit.com/assemblies/', + target: 'https://api2.transloadit.com', pollIntervalMs: 2000, - maxPollAttempts: 10 + pollMaxIntervalMs: 30000, + pollBackoffFactor: 2, + maxPollAttempts: 10, + maxInFlightPolls: 4, + notifyOnTerminalError: false }); ``` @@ -63,12 +68,13 @@ yarn install yarn lint yarn typecheck yarn test +yarn test:real yarn check ``` ## Real API E2E -Run an opt-in test against the real Transloadit API: +Run an opt-in test against the real Transloadit API (default `yarn test` excludes this test): ```bash # set locally (for example in .env) diff --git a/bin/notify-url-proxy.ts b/bin/notify-url-proxy.ts index 5f398bc..ce73222 100755 --- a/bin/notify-url-proxy.ts +++ b/bin/notify-url-proxy.ts @@ -5,18 +5,7 @@ import { parseArgs } from 'node:util' import { SevLogger } from '@transloadit/sev-logger' import TransloaditNotifyUrlProxy, { type ProxySettings } from '../src/index.ts' -function parsePositiveIntOption( - name: string, - value: string, - max = Number.MAX_SAFE_INTEGER, -): number { - const parsed = Number.parseInt(value, 10) - if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) { - console.error(`Invalid ${name}: ${value}`) - process.exit(1) - } - return parsed -} +const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']) const LOG_LEVEL_BY_NAME = { emerg: SevLogger.LEVEL.EMERG, @@ -32,6 +21,36 @@ const LOG_LEVEL_BY_NAME = { trace: SevLogger.LEVEL.TRACE, } as const +function fail(message: string): never { + console.error(message) + process.exit(1) +} + +function parsePositiveIntOption( + name: string, + value: string, + max = Number.MAX_SAFE_INTEGER, +): number { + const parsed = Number.parseInt(value, 10) + if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) { + fail(`Invalid ${name}: ${value}`) + } + return parsed +} + +function parsePositiveFloatOption( + name: string, + value: string, + min = Number.MIN_VALUE, + max = Number.MAX_SAFE_INTEGER, +): number { + const parsed = Number.parseFloat(value) + if (!Number.isFinite(parsed) || parsed < min || parsed > max) { + fail(`Invalid ${name}: ${value}`) + } + return parsed +} + function parseLogLevelOption(value: string): number { const normalized = value.trim().toLowerCase() const parsedNumeric = Number.parseInt(normalized, 10) @@ -49,10 +68,37 @@ function parseLogLevelOption(value: string): number { return parsedNamed } - console.error( + fail( `Invalid log level: ${value}. Use 0-8 or one of ${Object.keys(LOG_LEVEL_BY_NAME).join(', ')}.`, ) - process.exit(1) +} + +function parseHttpUrlOption(name: string, value: string): URL { + let parsed: URL + + try { + parsed = new URL(value) + } catch { + fail(`Invalid ${name}: ${value}`) + } + + if (!['http:', 'https:'].includes(parsed.protocol)) { + fail(`Invalid ${name} protocol: ${parsed.protocol}. Use http or https.`) + } + if (!parsed.hostname) { + fail(`Invalid ${name}: missing hostname.`) + } + + return parsed +} + +function parseNotifyUrlOption(value: string): string { + const parsed = parseHttpUrlOption('notifyUrl', value) + if (parsed.protocol === 'http:' && !LOCAL_HOSTS.has(parsed.hostname.toLowerCase())) { + fail('Insecure notifyUrl over HTTP is only allowed for localhost/127.0.0.1/::1.') + } + + return parsed.toString() } const { values } = parseArgs({ @@ -61,7 +107,12 @@ const { values } = parseArgs({ target: { type: 'string' }, port: { type: 'string' }, pollIntervalMs: { type: 'string' }, + pollMaxIntervalMs: { type: 'string' }, + pollBackoffFactor: { type: 'string' }, maxPollAttempts: { type: 'string' }, + maxInFlightPolls: { type: 'string' }, + notifyOnTerminalError: { type: 'boolean' }, + 'notify-on-terminal-error': { type: 'boolean' }, logLevel: { type: 'string', short: 'l' }, 'log-level': { type: 'string' }, help: { type: 'boolean', short: 'h' }, @@ -72,13 +123,17 @@ if (values.help) { console.log(`Usage: notify-url-proxy [options] Options: - --notifyUrl URL to send notifications to - --target Transloadit assemblies endpoint to proxy to - --port Local listen port - --pollIntervalMs Poll interval in milliseconds - --maxPollAttempts Max number of poll attempts - -l, --log-level Log level (0-8 or emerg/alert/crit/err/warn/notice/info/debug/trace) - -h, --help Show this help + --notifyUrl URL to send notifications to (http://localhost allowed, otherwise https) + --target Transloadit endpoint base URL + --port Local listen port + --pollIntervalMs Base poll interval in milliseconds + --pollMaxIntervalMs Max poll backoff interval in milliseconds + --pollBackoffFactor Poll backoff factor (>= 1) + --maxPollAttempts Max number of poll attempts + --maxInFlightPolls Max number of active assembly pollers + --notifyOnTerminalError Send notify payload when terminal error is reached + -l, --log-level Log level (0-8 or emerg/alert/crit/err/warn/notice/info/debug/trace) + -h, --help Show this help Environment fallback: TRANSLOADIT_SECRET, TRANSLOADIT_NOTIFY_URL, TRANSLOADIT_LOG_LEVEL @@ -88,14 +143,13 @@ Environment fallback: const secret = process.env.TRANSLOADIT_SECRET if (!secret) { - console.error('Missing secret. Set TRANSLOADIT_SECRET.') - process.exit(1) + fail('Missing secret. Set TRANSLOADIT_SECRET.') } const settings: Partial = {} if (values.target) { - settings.target = values.target + settings.target = parseHttpUrlOption('target', values.target).toString() } if (values.port) { settings.port = parsePositiveIntOption('port', values.port, 65_535) @@ -103,19 +157,37 @@ if (values.port) { if (values.pollIntervalMs) { settings.pollIntervalMs = parsePositiveIntOption('pollIntervalMs', values.pollIntervalMs) } +if (values.pollMaxIntervalMs) { + settings.pollMaxIntervalMs = parsePositiveIntOption('pollMaxIntervalMs', values.pollMaxIntervalMs) +} +if (values.pollBackoffFactor) { + settings.pollBackoffFactor = parsePositiveFloatOption( + 'pollBackoffFactor', + values.pollBackoffFactor, + 1, + ) +} if (values.maxPollAttempts) { settings.maxPollAttempts = parsePositiveIntOption('maxPollAttempts', values.maxPollAttempts) } +if (values.maxInFlightPolls) { + settings.maxInFlightPolls = parsePositiveIntOption('maxInFlightPolls', values.maxInFlightPolls) +} + +const notifyOnTerminalError = + values.notifyOnTerminalError === true || values['notify-on-terminal-error'] === true +if (notifyOnTerminalError) { + settings.notifyOnTerminalError = true +} const rawLogLevel = values['log-level'] ?? values.logLevel ?? process.env.TRANSLOADIT_LOG_LEVEL const logLevel = rawLogLevel ? parseLogLevelOption(rawLogLevel) : undefined const loggerOptions = typeof logLevel === 'number' ? { logLevel } : {} -const proxy = new TransloaditNotifyUrlProxy( - secret, - values.notifyUrl ?? process.env.TRANSLOADIT_NOTIFY_URL, - loggerOptions, -) +const notifyUrlRaw = values.notifyUrl ?? process.env.TRANSLOADIT_NOTIFY_URL +const notifyUrl = notifyUrlRaw ? parseNotifyUrlOption(notifyUrlRaw) : undefined + +const proxy = new TransloaditNotifyUrlProxy(secret, notifyUrl, loggerOptions) proxy.run(settings) const close = () => { diff --git a/docs/prompts/2026-03-04-modernize.md b/docs/prompts/2026-03-04-modernize.md new file mode 100644 index 0000000..ad7dd1f --- /dev/null +++ b/docs/prompts/2026-03-04-modernize.md @@ -0,0 +1,32 @@ +# 2026-03-04 Modernization Checklist + +Goal: finish the 12-item modernization/refactor sweep for `notify-url-proxy` and keep each change independently verifiable with tests and CI checks. + +Context: +- Branch: `chore/modernize-node24-ts-esm` +- Node runtime target: `>=24` +- Existing known gap: `http-proxy` still present and causing `util._extend` deprecation warnings. +- Existing real API e2e exists and must remain opt-in and secret-gated. + +Checklist: +- [x] 1. Replace `http-proxy` with native `fetch`-based forwarding. +- [x] 2. Exclude real e2e from default `yarn test`. +- [x] 3. Add polling dedupe + max in-flight concurrency guard. +- [x] 4. Add cancellation plumbing for pollers (`AbortController`, cancel on `close()`). +- [x] 5. Switch retry behavior to exponential backoff + jitter. +- [x] 6. Add `notifyOnTerminalError` option and behavior. +- [x] 7. Harden CLI config validation for `notifyUrl` and `target`. +- [x] 8. Keep structured logging hooks/injection (`@transloadit/sev-logger`). +- [x] 9. Add GitHub Actions workflow concurrency cancellation. +- [x] 10. Add CI changeset policy check (changeset or explicit bypass label). +- [x] 11. Add regression tests: + - [x] duplicate assembly URL dedupe behavior + - [x] poll cancellation on `close()` + - [x] terminal error behavior path +- [x] 12. Add publish hardening (`publishConfig` intent/registry/provenance). + +Verification plan: +1. `yarn format` +2. `yarn check` +3. `yarn test:real` (when env/secrets are present) +4. push branch, confirm GitHub Actions run status diff --git a/package.json b/package.json index 5cf2c62..d3ac1ec 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,11 @@ "engines": { "node": ">=24" }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/", + "provenance": true + }, "bin": { "notify-url-proxy": "./bin/notify-url-proxy.ts" }, @@ -36,13 +41,11 @@ "@transloadit/sev-logger": "^0.1.9", "@transloadit/utils": "^4.3.0", "@transloadit/zod": "^4.3.0", - "http-proxy": "^1.18.1", "p-retry": "^7.1.1" }, "devDependencies": { "@biomejs/biome": "^2.4.5", "@changesets/cli": "^2.30.0", - "@types/http-proxy": "^1.17.17", "@types/node": "^25.3.3", "transloadit": "^4.7.4", "typescript": "^5.9.3", diff --git a/src/index.ts b/src/index.ts index 82dcee0..ee3363c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { Readable } from 'node:stream' import { SevLogger } from '@transloadit/sev-logger' import { signParamsSync } from '@transloadit/utils/node' import { @@ -14,14 +15,28 @@ import { isAssemblyTerminalOk, parseAssemblyUrls, } from '@transloadit/zod/v4' -import httpProxy from 'http-proxy' import pRetry, { AbortError, type RetryContext } from 'p-retry' +const HOP_BY_HOP_HEADERS = new Set([ + 'connection', + 'keep-alive', + 'proxy-authenticate', + 'proxy-authorization', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', +]) + export interface ProxySettings { target: string port: number pollIntervalMs: number + pollMaxIntervalMs: number + pollBackoffFactor: number maxPollAttempts: number + maxInFlightPolls: number + notifyOnTerminalError: boolean } export interface ProxyLoggerOptions { @@ -34,10 +49,14 @@ type KnownAssemblyState = (typeof assemblyStatusOkCodeSchema.options)[number] export type AssemblyResponse = AssemblyStatus const DEFAULT_SETTINGS: ProxySettings = { - target: 'https://api2.transloadit.com/assemblies/', + target: 'https://api2.transloadit.com', port: 8888, pollIntervalMs: 2_000, + pollMaxIntervalMs: 30_000, + pollBackoffFactor: 2, maxPollAttempts: 10, + maxInFlightPolls: 4, + notifyOnTerminalError: false, } const DEFAULT_LOG_LEVEL = SevLogger.LEVEL.INFO @@ -54,6 +73,34 @@ function toErrorMessage(error: unknown): string { return String(error) } +function supportsBody(method: string | undefined): boolean { + const normalized = (method ?? 'GET').toUpperCase() + return normalized !== 'GET' && normalized !== 'HEAD' +} + +function isAbortLikeError(error: unknown): boolean { + if (error instanceof DOMException && error.name === 'AbortError') { + return true + } + + return error instanceof Error && error.name === 'AbortError' +} + +function getHeaderValues(name: string, headers: Headers): string[] { + const normalized = name.toLowerCase() + if (normalized !== 'set-cookie') { + return [] + } + + const withGetSetCookie = headers as Headers & { getSetCookie?: () => string[] } + if (typeof withGetSetCookie.getSetCookie === 'function') { + return withGetSetCookie.getSetCookie() + } + + const fallback = headers.get('set-cookie') + return fallback ? [fallback] : [] +} + export function extractAssemblyUrl(body: string): string | null { try { const payload = JSON.parse(body) as unknown @@ -91,7 +138,7 @@ export function parseAssemblyResponse(payload: unknown): AssemblyResponse { export default class TransloaditNotifyUrlProxy { private server: Server | null = null - private proxy: httpProxy | null = null + private isClosing = false private readonly secret: string private readonly notifyUrl: string @@ -99,6 +146,11 @@ export default class TransloaditNotifyUrlProxy { private readonly defaults: ProxySettings private settings: ProxySettings + private readonly pendingAssemblyUrls = new Set() + private readonly activePolls = new Map>() + private readonly pollControllers = new Map() + private activePollCount = 0 + constructor( secret: string, notifyUrl = 'http://127.0.0.1:3000/transloadit', @@ -122,95 +174,220 @@ export default class TransloaditNotifyUrlProxy { } run(opts: Partial = {}): void { - if (this.server !== null || this.proxy !== null) { + if (this.server !== null) { this.close() } + this.isClosing = false this.settings = { ...this.defaults, ...opts } - - this.createProxy() this.createServer() } close(): void { + this.isClosing = true + this.server?.close() this.server = null - this.proxy?.close() - this.proxy = null + for (const [assemblyUrl, controller] of this.pollControllers) { + controller.abort(new Error(`Proxy closed while polling ${assemblyUrl}`)) + } + + this.pollControllers.clear() + this.pendingAssemblyUrls.clear() + this.activePolls.clear() + this.activePollCount = 0 + } + + private createServer(): void { + this.server = createServer((request, response) => { + void this.handleForward(request, response) + }) + + this.server.listen(this.settings.port) + + this.logger.notice( + `Listening on http://localhost:${this.settings.port}, forwarding to ${this.settings.target}, notifying ${this.notifyUrl}`, + ) } - private createProxy(): void { - this.proxy = httpProxy.createProxyServer({ - target: this.settings.target, - changeOrigin: true, + private async handleForward(request: IncomingMessage, response: ServerResponse): Promise { + const proxyController = new AbortController() + request.on('aborted', () => { + proxyController.abort(new Error('Client aborted request')) }) - this.proxy.on('error', (error, _req, res) => { - if ('writeHead' in res) { - if (!res.headersSent) { - res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' }) + try { + const targetUrl = this.resolveTargetUrl(request.url) + const requestBody = supportsBody(request.method) + ? (Readable.toWeb(request) as ReadableStream) + : undefined + const fetchInit: RequestInit = { + method: request.method ?? 'GET', + headers: this.createForwardHeaders(request), + redirect: 'manual', + signal: proxyController.signal, + } + if (requestBody) { + fetchInit.body = requestBody + ;(fetchInit as RequestInit & { duplex: 'half' }).duplex = 'half' + } + + const upstreamResponse = await fetch(targetUrl, fetchInit) + + const body = Buffer.from(await upstreamResponse.arrayBuffer()) + this.writeForwardedResponse(response, upstreamResponse, body) + this.maybePollAssemblyFromBody(body) + } catch (error) { + if (isAbortLikeError(error)) { + return + } + + if (!response.headersSent) { + response.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' }) + } + if (!response.writableEnded) { + response.end('Proxy error') + } + + this.logger.err(`Proxy error: ${toErrorMessage(error)}`) + } + } + + private resolveTargetUrl(requestUrl: string | undefined): string { + const path = requestUrl ?? '/' + if (/^https?:\/\//i.test(path)) { + throw new Error(`Absolute request URL is not supported: ${path}`) + } + + return new URL(path, this.settings.target).toString() + } + + private createForwardHeaders(request: IncomingMessage): Headers { + const headers = new Headers() + + for (const [name, value] of Object.entries(request.headers)) { + const headerName = name.toLowerCase() + if (HOP_BY_HOP_HEADERS.has(headerName) || headerName === 'host') { + continue + } + if (value === undefined) { + continue + } + + if (Array.isArray(value)) { + for (const item of value) { + headers.append(name, item) } - res.end('Proxy error') } else { - res.end() + headers.set(name, value) } - this.logger.err(`Proxy error: ${toErrorMessage(error)}`) - }) + } - this.proxy.on('proxyRes', (proxyRes) => { - void this.handleProxyResponse(proxyRes) - }) + if (request.socket.remoteAddress) { + headers.set('x-forwarded-for', request.socket.remoteAddress) + } + if (typeof request.headers.host === 'string') { + headers.set('x-forwarded-host', request.headers.host) + } + + return headers } - private createServer(): void { - if (this.proxy === null) { - throw new Error('Proxy is not initialized.') + private writeForwardedResponse( + response: ServerResponse, + upstreamResponse: Response, + body: Buffer, + ): void { + response.statusCode = upstreamResponse.status + response.statusMessage = upstreamResponse.statusText + + for (const [name, value] of upstreamResponse.headers) { + const headerName = name.toLowerCase() + if (HOP_BY_HOP_HEADERS.has(headerName) || headerName === 'set-cookie') { + continue + } + response.setHeader(name, value) } - this.server = createServer((req, res) => { - this.proxy?.web(req, res) - }) + const setCookies = getHeaderValues('set-cookie', upstreamResponse.headers) + if (setCookies.length > 0) { + response.setHeader('set-cookie', setCookies) + } - this.server.listen(this.settings.port) + response.end(body) + } - this.logger.notice( - `Listening on http://localhost:${this.settings.port}, forwarding to ${this.settings.target}, notifying ${this.notifyUrl}`, - ) + private maybePollAssemblyFromBody(body: Buffer): void { + const assemblyUrl = extractAssemblyUrl(body.toString('utf-8')) + if (!assemblyUrl) { + return + } + + this.enqueueAssemblyPoll(assemblyUrl) } - private async handleProxyResponse(proxyRes: IncomingMessage): Promise { - const body = await this.readResponseBody(proxyRes) - const assemblyUrl = extractAssemblyUrl(body) + private enqueueAssemblyPoll(assemblyUrl: string): void { + if (this.isClosing) { + return + } - if (assemblyUrl === null) { + if (this.pendingAssemblyUrls.has(assemblyUrl) || this.activePolls.has(assemblyUrl)) { + this.logger.debug(`Skipping duplicate poll registration for ${assemblyUrl}`) return } - this.logger.info(`Received proxy response, polling assemblyUrl: ${assemblyUrl}`) - await this.pollAssembly(assemblyUrl) + this.pendingAssemblyUrls.add(assemblyUrl) + this.logger.info(`Queued poll for ${assemblyUrl}`) + this.drainPollQueue() } - private async readResponseBody(response: IncomingMessage): Promise { - const chunks: Buffer[] = [] - - for await (const chunk of response) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + private drainPollQueue(): void { + if (this.isClosing) { + return } - return Buffer.concat(chunks).toString('utf-8') + while (this.activePollCount < this.settings.maxInFlightPolls) { + const next = this.pendingAssemblyUrls.values().next().value as string | undefined + if (!next) { + break + } + + this.pendingAssemblyUrls.delete(next) + + const controller = new AbortController() + this.pollControllers.set(next, controller) + this.activePollCount += 1 + + const pollPromise = this.pollAssembly(next, controller.signal).finally(() => { + if (this.activePolls.get(next) !== pollPromise) { + return + } + + this.activePolls.delete(next) + this.pollControllers.delete(next) + this.activePollCount = Math.max(0, this.activePollCount - 1) + + if (!this.isClosing) { + this.drainPollQueue() + } + }) + + this.activePolls.set(next, pollPromise) + } } - private async pollAssembly(assemblyUrl: string): Promise { + private async pollAssembly(assemblyUrl: string, signal: AbortSignal): Promise { const retries = Math.max(this.settings.maxPollAttempts - 1, 0) try { - const response = await pRetry(() => this.checkAssembly(assemblyUrl), { + const response = await pRetry(() => this.checkAssembly(assemblyUrl, signal), { retries, minTimeout: this.settings.pollIntervalMs, - maxTimeout: this.settings.pollIntervalMs, - factor: 1, - randomize: false, + maxTimeout: this.settings.pollMaxIntervalMs, + factor: this.settings.pollBackoffFactor, + randomize: true, + signal, onFailedAttempt: (retryContext: RetryContext) => { if (retryContext.retriesLeft <= 0) { return @@ -222,21 +399,26 @@ export default class TransloaditNotifyUrlProxy { }, }) - await this.notify(response) + await this.notify(response, signal) } catch (error) { if (error instanceof AbortError) { this.logger.notice(error.message) return } + if (signal.aborted || this.isClosing || isAbortLikeError(error)) { + this.logger.debug(`Polling cancelled for ${assemblyUrl}`) + return + } + this.logger.err( `No attempts left, giving up on checking assemblyUrl: ${assemblyUrl} (${toErrorMessage(error)})`, ) } } - private async checkAssembly(assemblyUrl: string): Promise { - const response = await fetch(assemblyUrl) + private async checkAssembly(assemblyUrl: string, signal: AbortSignal): Promise { + const response = await fetch(assemblyUrl, { signal }) if (!response.ok) { throw new Error(`Assembly poll returned HTTP ${response.status}`) } @@ -245,6 +427,13 @@ export default class TransloaditNotifyUrlProxy { if (isAssemblyTerminalError(assembly)) { const errorCode = getError(assembly) ?? 'UNKNOWN_ERROR' + if (this.settings.notifyOnTerminalError) { + this.logger.notice( + `${assemblyUrl} reached terminal error state ${errorCode}; notifying because notifyOnTerminalError=true.`, + ) + return assembly + } + throw new AbortError(`${assemblyUrl} reached terminal error state ${errorCode}.`) } @@ -267,7 +456,7 @@ export default class TransloaditNotifyUrlProxy { throw new Error(`${assemblyUrl} returned a non-terminal assembly state.`) } - private async notify(response: AssemblyResponse): Promise { + private async notify(response: AssemblyResponse, signal: AbortSignal): Promise { const transloadit = JSON.stringify(response) const signature = getSignature(this.secret, transloadit) @@ -280,6 +469,7 @@ export default class TransloaditNotifyUrlProxy { transloadit, signature, }), + signal, }) if (!notifyResponse.ok) { diff --git a/test/behavior.test.ts b/test/behavior.test.ts new file mode 100644 index 0000000..c426c9e --- /dev/null +++ b/test/behavior.test.ts @@ -0,0 +1,384 @@ +import { once } from 'node:events' +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { setTimeout as delay } from 'node:timers/promises' + +import { describe, expect, it } from 'vitest' + +import TransloaditNotifyUrlProxy, { getSignature } from '../src/index.ts' + +async function readBody(request: IncomingMessage): Promise { + const chunks: Buffer[] = [] + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + + return Buffer.concat(chunks).toString('utf-8') +} + +async function listen(server: Server): Promise { + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') { + throw new Error('Could not resolve server address') + } + return address.port +} + +async function closeServer(server: Server): Promise { + await new Promise((resolve) => { + server.close(() => resolve()) + }) +} + +async function getFreePort(): Promise { + const server = createServer() + const port = await listen(server) + await closeServer(server) + return port +} + +function json(response: ServerResponse, statusCode: number, payload: unknown): void { + response.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' }) + response.end(JSON.stringify(payload)) +} + +async function waitFor( + fn: () => boolean, + timeoutMs: number, + intervalMs = 5, + errorMessage = 'Timed out', +): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (fn()) { + return + } + await delay(intervalMs) + } + + throw new Error(errorMessage) +} + +describe('proxy behavior guards', () => { + it('dedupes duplicate assembly URLs and avoids duplicate poll loops', async () => { + const secret = 'foo_secret' + let upstreamPort = 0 + let pollCount = 0 + let notifyCount = 0 + let resolveNotify: (() => void) | null = null + const notifyReceived = new Promise((resolve) => { + resolveNotify = resolve + }) + + const notifyServer = createServer(async (request, response) => { + if (request.method !== 'POST' || request.url !== '/transloadit') { + response.writeHead(404) + response.end() + return + } + + notifyCount += 1 + await readBody(request) + response.writeHead(200) + response.end('ok') + resolveNotify?.() + }) + + const upstreamServer = createServer((request, response) => { + if (request.method === 'POST' && request.url === '/assemblies') { + json(response, 200, { + assembly_url: `http://127.0.0.1:${upstreamPort}/assembly/dupe`, + }) + return + } + + if (request.method === 'GET' && request.url === '/assembly/dupe') { + pollCount += 1 + if (pollCount === 1) { + json(response, 200, { ok: 'ASSEMBLY_EXECUTING' }) + return + } + + json(response, 200, { ok: 'ASSEMBLY_COMPLETED', assembly_id: 'dupe' }) + return + } + + response.writeHead(404) + response.end() + }) + + const notifyPort = await listen(notifyServer) + upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy( + secret, + `http://127.0.0.1:${notifyPort}/transloadit`, + { logLevel: 0 }, + ) + proxy.run({ + target: `http://127.0.0.1:${upstreamPort}`, + port: proxyPort, + pollIntervalMs: 10, + pollMaxIntervalMs: 100, + maxPollAttempts: 5, + maxInFlightPolls: 1, + }) + + try { + const [responseA, responseB] = await Promise.all([ + fetch(`http://127.0.0.1:${proxyPort}/assemblies`, { + method: 'POST', + body: new URLSearchParams({ params: '{}' }), + }), + fetch(`http://127.0.0.1:${proxyPort}/assemblies`, { + method: 'POST', + body: new URLSearchParams({ params: '{}' }), + }), + ]) + + expect(responseA.status).toBe(200) + expect(responseB.status).toBe(200) + + await Promise.race([ + notifyReceived, + delay(3_000).then(() => { + throw new Error('Timed out waiting for notify request') + }), + ]) + + expect(pollCount).toBe(2) + expect(notifyCount).toBe(1) + } finally { + proxy.close() + await closeServer(notifyServer) + await closeServer(upstreamServer) + } + }, 10_000) + + it('cancels polling on close()', async () => { + const secret = 'foo_secret' + let upstreamPort = 0 + let pollCount = 0 + + const notifyServer = createServer(async (request, response) => { + await readBody(request) + response.writeHead(200) + response.end('ok') + }) + + const upstreamServer = createServer((request, response) => { + if (request.method === 'POST' && request.url === '/assemblies') { + json(response, 200, { + assembly_url: `http://127.0.0.1:${upstreamPort}/assembly/slow`, + }) + return + } + + if (request.method === 'GET' && request.url === '/assembly/slow') { + pollCount += 1 + json(response, 200, { ok: 'ASSEMBLY_EXECUTING' }) + return + } + + response.writeHead(404) + response.end() + }) + + const notifyPort = await listen(notifyServer) + upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy( + secret, + `http://127.0.0.1:${notifyPort}/transloadit`, + { logLevel: 0 }, + ) + proxy.run({ + target: `http://127.0.0.1:${upstreamPort}`, + port: proxyPort, + pollIntervalMs: 20, + pollMaxIntervalMs: 40, + maxPollAttempts: 100, + maxInFlightPolls: 1, + }) + + try { + const createResponse = await fetch(`http://127.0.0.1:${proxyPort}/assemblies`, { + method: 'POST', + body: new URLSearchParams({ params: '{}' }), + }) + expect(createResponse.status).toBe(200) + + await waitFor(() => pollCount >= 1, 2_000, 5, 'Timed out waiting for first polling attempt') + + proxy.close() + const countAfterClose = pollCount + await delay(150) + + expect(pollCount).toBe(countAfterClose) + } finally { + proxy.close() + await closeServer(notifyServer) + await closeServer(upstreamServer) + } + }, 10_000) + + it('does not notify on terminal error by default', async () => { + const secret = 'foo_secret' + let upstreamPort = 0 + let notifyCount = 0 + + const notifyServer = createServer(async (request, response) => { + notifyCount += 1 + await readBody(request) + response.writeHead(200) + response.end('ok') + }) + + const upstreamServer = createServer((request, response) => { + if (request.method === 'POST' && request.url === '/assemblies') { + json(response, 200, { + assembly_url: `http://127.0.0.1:${upstreamPort}/assembly/error`, + }) + return + } + + if (request.method === 'GET' && request.url === '/assembly/error') { + json(response, 200, { + error: 'ASSEMBLY_CRASHED', + }) + return + } + + response.writeHead(404) + response.end() + }) + + const notifyPort = await listen(notifyServer) + upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy( + secret, + `http://127.0.0.1:${notifyPort}/transloadit`, + { logLevel: 0 }, + ) + proxy.run({ + target: `http://127.0.0.1:${upstreamPort}`, + port: proxyPort, + pollIntervalMs: 10, + pollMaxIntervalMs: 100, + maxPollAttempts: 5, + }) + + try { + const createResponse = await fetch(`http://127.0.0.1:${proxyPort}/assemblies`, { + method: 'POST', + body: new URLSearchParams({ params: '{}' }), + }) + expect(createResponse.status).toBe(200) + + await delay(200) + expect(notifyCount).toBe(0) + } finally { + proxy.close() + await closeServer(notifyServer) + await closeServer(upstreamServer) + } + }, 10_000) + + it('notifies when notifyOnTerminalError is enabled', async () => { + const secret = 'foo_secret' + let upstreamPort = 0 + let notifyTransloadit: string | null = null + let notifySignature: string | null = null + let resolveNotify: (() => void) | null = null + const notifyReceived = new Promise((resolve) => { + resolveNotify = resolve + }) + + const notifyServer = createServer(async (request, response) => { + if (request.method !== 'POST' || request.url !== '/transloadit') { + response.writeHead(404) + response.end() + return + } + + const payload = new URLSearchParams(await readBody(request)) + notifyTransloadit = payload.get('transloadit') + notifySignature = payload.get('signature') + response.writeHead(200) + response.end('ok') + resolveNotify?.() + }) + + const upstreamServer = createServer((request, response) => { + if (request.method === 'POST' && request.url === '/assemblies') { + json(response, 200, { + assembly_url: `http://127.0.0.1:${upstreamPort}/assembly/error`, + }) + return + } + + if (request.method === 'GET' && request.url === '/assembly/error') { + json(response, 200, { + error: 'ASSEMBLY_CRASHED', + }) + return + } + + response.writeHead(404) + response.end() + }) + + const notifyPort = await listen(notifyServer) + upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy( + secret, + `http://127.0.0.1:${notifyPort}/transloadit`, + { logLevel: 0 }, + ) + proxy.run({ + target: `http://127.0.0.1:${upstreamPort}`, + port: proxyPort, + pollIntervalMs: 10, + pollMaxIntervalMs: 100, + maxPollAttempts: 5, + notifyOnTerminalError: true, + }) + + try { + const createResponse = await fetch(`http://127.0.0.1:${proxyPort}/assemblies`, { + method: 'POST', + body: new URLSearchParams({ params: '{}' }), + }) + expect(createResponse.status).toBe(200) + + await Promise.race([ + notifyReceived, + delay(3_000).then(() => { + throw new Error('Timed out waiting for terminal-error notify request') + }), + ]) + + expect(notifyTransloadit).toBeTypeOf('string') + expect(notifySignature).toBeTypeOf('string') + + if (notifyTransloadit === null || notifySignature === null) { + throw new Error('Missing notify transloadit/signature payload') + } + + expect(notifySignature).toBe(getSignature(secret, notifyTransloadit)) + const payload = JSON.parse(notifyTransloadit) as { error?: string } + expect(payload.error).toBe('ASSEMBLY_CRASHED') + } finally { + proxy.close() + await closeServer(notifyServer) + await closeServer(upstreamServer) + } + }, 10_000) +}) diff --git a/test/real.e2e.test.ts b/test/real.e2e.test.ts index 465f4b5..d89071b 100644 --- a/test/real.e2e.test.ts +++ b/test/real.e2e.test.ts @@ -3,7 +3,6 @@ import { existsSync } from 'node:fs' import { createServer, type IncomingMessage, type Server } from 'node:http' import { setTimeout as delay } from 'node:timers/promises' -import { Transloadit } from 'transloadit' import { describe, expect, it } from 'vitest' import TransloaditNotifyUrlProxy, { getSignature } from '../src/index.ts' @@ -99,6 +98,7 @@ describeReal('real api e2e', () => { maxPollAttempts: 120, }) + const { Transloadit } = await import('transloadit') const client = new Transloadit({ authKey, authSecret: secret, diff --git a/vitest.config.ts b/vitest.config.ts index f19fc42..e046e5c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,11 @@ import { defineConfig } from 'vitest/config' +const includeRealE2E = process.env.RUN_REAL_E2E === '1' + export default defineConfig({ test: { environment: 'node', include: ['test/**/*.test.ts'], + exclude: includeRealE2E ? [] : ['test/real.e2e.test.ts'], }, }) diff --git a/yarn.lock b/yarn.lock index 22e7ec2..0bacaf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2169,16 +2169,14 @@ __metadata: languageName: node linkType: hard -"@types/http-proxy@npm:^1.17.17": - version: 1.17.17 - resolution: "@types/http-proxy@npm:1.17.17" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/547e322a5eecf0b50d08f6a46bd89c8c8663d67dbdcd472da5daf968b03e63a82f6b3650443378abe6c10a46475dac52015f30e8c74ba2ea5820dd4e9cdef2d4 +"@types/node@npm:^12.7.1": + version: 12.20.55 + resolution: "@types/node@npm:12.20.55" + checksum: 10c0/3b190bb0410047d489c49bbaab592d2e6630de6a50f00ba3d7d513d59401d279972a8f5a598b5bb8ddc1702f8a2f4ec57a65d93852f9c329639738e7053637d1 languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:^25.3.3": +"@types/node@npm:^25.3.3": version: 25.3.3 resolution: "@types/node@npm:25.3.3" dependencies: @@ -2187,13 +2185,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^12.7.1": - version: 12.20.55 - resolution: "@types/node@npm:12.20.55" - checksum: 10c0/3b190bb0410047d489c49bbaab592d2e6630de6a50f00ba3d7d513d59401d279972a8f5a598b5bb8ddc1702f8a2f4ec57a65d93852f9c329639738e7053637d1 - languageName: node - linkType: hard - "@vitest/expect@npm:4.0.18": version: 4.0.18 resolution: "@vitest/expect@npm:4.0.18" @@ -2788,13 +2779,6 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^4.0.0": - version: 4.0.7 - resolution: "eventemitter3@npm:4.0.7" - checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b - languageName: node - linkType: hard - "eventemitter3@npm:^5.0.1": version: 5.0.4 resolution: "eventemitter3@npm:5.0.4" @@ -2895,16 +2879,6 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0": - version: 1.15.11 - resolution: "follow-redirects@npm:1.15.11" - peerDependenciesMeta: - debug: - optional: true - checksum: 10c0/d301f430542520a54058d4aeeb453233c564aaccac835d29d15e050beb33f339ad67d9bddbce01739c5dc46a6716dbe3d9d0d5134b1ca203effa11a7ef092343 - languageName: node - linkType: hard - "form-data-encoder@npm:^4.0.2": version: 4.1.0 resolution: "form-data-encoder@npm:4.1.0" @@ -3146,17 +3120,6 @@ __metadata: languageName: node linkType: hard -"http-proxy@npm:^1.18.1": - version: 1.18.1 - resolution: "http-proxy@npm:1.18.1" - dependencies: - eventemitter3: "npm:^4.0.0" - follow-redirects: "npm:^1.0.0" - requires-port: "npm:^1.0.0" - checksum: 10c0/148dfa700a03fb421e383aaaf88ac1d94521dfc34072f6c59770528c65250983c2e4ec996f2f03aa9f3fe46cd1270a593126068319311e3e8d9e610a37533e94 - languageName: node - linkType: hard - "http2-wrapper@npm:^2.2.1": version: 2.2.1 resolution: "http2-wrapper@npm:2.2.1" @@ -4353,9 +4316,7 @@ __metadata: "@transloadit/sev-logger": "npm:^0.1.9" "@transloadit/utils": "npm:^4.3.0" "@transloadit/zod": "npm:^4.3.0" - "@types/http-proxy": "npm:^1.17.17" "@types/node": "npm:^25.3.3" - http-proxy: "npm:^1.18.1" p-retry: "npm:^7.1.1" transloadit: "npm:^4.7.4" typescript: "npm:^5.9.3" From dce92389e3d9927cfaa4ab83f0e509f2eebcf4aa Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 4 Mar 2026 10:35:39 +0100 Subject: [PATCH 10/11] feat: add timeout policy, metrics hooks, and reactive tui mode --- README.md | 23 +- bin/notify-url-proxy.ts | 249 ++++++++++++- docs/prompts/2026-03-04-modernize.md | 14 + src/index.ts | 519 ++++++++++++++++++++++++--- test/chaos.test.ts | 274 ++++++++++++++ test/network.test.ts | 196 ++++++++++ 6 files changed, 1206 insertions(+), 69 deletions(-) create mode 100644 test/chaos.test.ts create mode 100644 test/network.test.ts diff --git a/README.md b/README.md index af1dbd3..0885217 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This version is modernized for: Notify payloads are signed via `@transloadit/utils` using prefixed `sha384` signatures. Forwarding uses native `fetch`, polling retries use `p-retry`, and logs are emitted via `@transloadit/sev-logger`. +Metrics hooks are available for counters, gauges, and timings. ## Install @@ -37,6 +38,19 @@ Log level accepts `0-8` or names: `emerg`, `alert`, `crit`, `err`, `warn`, `notice`, `info`, `debug`, `trace`. You can also set `TRANSLOADIT_LOG_LEVEL`. +### Reactive TUI Mode + +```bash +notify-url-proxy --ui --log-level info +``` + +This opens a live terminal dashboard with: + +- throughput and retry counters +- in-flight queue gauges +- latency sparklines +- streaming logs + ## Programmatic usage ```ts @@ -50,12 +64,19 @@ const proxy = new TransloaditNotifyUrlProxy( proxy.run({ port: 8888, target: 'https://api2.transloadit.com', + forwardTimeoutMs: 15000, pollIntervalMs: 2000, pollMaxIntervalMs: 30000, pollBackoffFactor: 2, + pollRequestTimeoutMs: 15000, maxPollAttempts: 10, maxInFlightPolls: 4, - notifyOnTerminalError: false + notifyOnTerminalError: false, + notifyTimeoutMs: 15000, + notifyMaxAttempts: 3, + notifyIntervalMs: 500, + notifyMaxIntervalMs: 5000, + notifyBackoffFactor: 2 }); ``` diff --git a/bin/notify-url-proxy.ts b/bin/notify-url-proxy.ts index ce73222..0f385fd 100755 --- a/bin/notify-url-proxy.ts +++ b/bin/notify-url-proxy.ts @@ -3,7 +3,14 @@ import { parseArgs } from 'node:util' import { SevLogger } from '@transloadit/sev-logger' -import TransloaditNotifyUrlProxy, { type ProxySettings } from '../src/index.ts' +import TransloaditNotifyUrlProxy, { + type CounterMetricEvent, + type GaugeMetricEvent, + type ProxyLogEvent, + type ProxyRuntimeOptions, + type ProxySettings, + type TimingMetricEvent, +} from '../src/index.ts' const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']) @@ -101,18 +108,181 @@ function parseNotifyUrlOption(value: string): string { return parsed.toString() } +function sparkline(values: number[], width = 44): string { + const chars = '▁▂▃▄▅▆▇█' + const tail = values.slice(-width) + + if (tail.length === 0) { + return '·'.repeat(width) + } + + const max = Math.max(...tail, 1) + return tail + .map((value) => { + const ratio = value / max + const index = Math.max(0, Math.min(chars.length - 1, Math.round(ratio * (chars.length - 1)))) + return chars[index] + }) + .join('') + .padStart(width, '·') +} + +function createTuiMode(logLevel: number | undefined): { + runtimeOptions: ProxyRuntimeOptions + start: () => void + stop: () => void +} { + const counters: Record = {} + const gauges: Record = {} + const timings: Record = {} + const logs: ProxyLogEvent[] = [] + const series: { + forward: number[] + pollRetry: number[] + notifyOk: number[] + inflight: number[] + latencyForward: number[] + latencyNotify: number[] + } = { + forward: [], + pollRetry: [], + notifyOk: [], + inflight: [], + latencyForward: [], + latencyNotify: [], + } + + const startedAt = Date.now() + let timer: NodeJS.Timeout | null = null + + const pushSeries = (key: keyof typeof series, value: number): void => { + const bucket = series[key] + bucket.push(value) + if (bucket.length > 160) { + bucket.splice(0, bucket.length - 160) + } + } + + const onCounter = (event: CounterMetricEvent): void => { + counters[event.name] = event.total + if (event.name === 'forward.requests_total') pushSeries('forward', event.total) + if (event.name === 'poll.retry_total') pushSeries('pollRetry', event.total) + if (event.name === 'notify.success_total') pushSeries('notifyOk', event.total) + } + + const onGauge = (event: GaugeMetricEvent): void => { + gauges[event.name] = event.value + if (event.name === 'poll.in_flight') pushSeries('inflight', event.value) + } + + const onTiming = (event: TimingMetricEvent): void => { + timings[event.name] = event + if (event.name === 'forward.request_duration_ms') pushSeries('latencyForward', event.durationMs) + if (event.name === 'notify.duration_ms') pushSeries('latencyNotify', event.durationMs) + } + + const onLog = (event: ProxyLogEvent): void => { + logs.push(event) + if (logs.length > 160) { + logs.splice(0, logs.length - 160) + } + } + + const runtimeOptions: ProxyRuntimeOptions = { + ...(typeof logLevel === 'number' ? { logLevel } : {}), + metricsHooks: { + onCounter, + onGauge, + onTiming, + }, + onLog, + } + + const render = (): void => { + const uptimeSec = Math.max(0, Math.floor((Date.now() - startedAt) / 1000)) + const h = Math.floor(uptimeSec / 3600) + const m = Math.floor((uptimeSec % 3600) / 60) + const s = uptimeSec % 60 + const uptime = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` + + const lines: string[] = [] + lines.push('\x1b[2J\x1b[H') + lines.push('\x1b[38;5;117mNotify URL Proxy // Reactive TUI\x1b[0m') + lines.push(`Uptime: ${uptime} | Press Ctrl+C to exit`) + lines.push('') + + lines.push('METRICS') + lines.push( + ` forward.requests_total: ${counters['forward.requests_total'] ?? 0} poll.retry_total: ${counters['poll.retry_total'] ?? 0}`, + ) + lines.push( + ` notify.success_total : ${counters['notify.success_total'] ?? 0} poll.in_flight : ${gauges['poll.in_flight'] ?? 0}`, + ) + lines.push( + ` forward p50-ish last: ${Math.round(timings['forward.request_duration_ms']?.avgMs ?? 0)}ms notify avg: ${Math.round(timings['notify.duration_ms']?.avgMs ?? 0)}ms`, + ) + lines.push('') + + lines.push('GRAPHS') + lines.push(` Forward Throughput ${sparkline(series.forward)}`) + lines.push(` Poll Retries ${sparkline(series.pollRetry)}`) + lines.push(` Notify Success ${sparkline(series.notifyOk)}`) + lines.push(` In-Flight Polls ${sparkline(series.inflight)}`) + lines.push(` Forward Latency ${sparkline(series.latencyForward)}`) + lines.push(` Notify Latency ${sparkline(series.latencyNotify)}`) + lines.push('') + + lines.push('LIVE LOGS') + for (const log of logs.slice(-18)) { + const time = new Date(log.at).toLocaleTimeString() + const level = log.level.padEnd(6, ' ') + lines.push(` ${time} ${level} ${log.message}`) + } + + process.stdout.write(lines.join('\n')) + } + + const start = (): void => { + if (process.stdout.isTTY) { + process.stdout.write('\x1b[?25l') + } + timer = setInterval(render, 180) + render() + } + + const stop = (): void => { + if (timer) { + clearInterval(timer) + timer = null + } + if (process.stdout.isTTY) { + process.stdout.write('\x1b[?25h\n') + } + } + + return { runtimeOptions, start, stop } +} + const { values } = parseArgs({ options: { notifyUrl: { type: 'string' }, target: { type: 'string' }, port: { type: 'string' }, + forwardTimeoutMs: { type: 'string' }, pollIntervalMs: { type: 'string' }, pollMaxIntervalMs: { type: 'string' }, pollBackoffFactor: { type: 'string' }, + pollRequestTimeoutMs: { type: 'string' }, maxPollAttempts: { type: 'string' }, maxInFlightPolls: { type: 'string' }, notifyOnTerminalError: { type: 'boolean' }, 'notify-on-terminal-error': { type: 'boolean' }, + notifyTimeoutMs: { type: 'string' }, + notifyMaxAttempts: { type: 'string' }, + notifyIntervalMs: { type: 'string' }, + notifyMaxIntervalMs: { type: 'string' }, + notifyBackoffFactor: { type: 'string' }, + ui: { type: 'boolean' }, logLevel: { type: 'string', short: 'l' }, 'log-level': { type: 'string' }, help: { type: 'boolean', short: 'h' }, @@ -123,17 +293,25 @@ if (values.help) { console.log(`Usage: notify-url-proxy [options] Options: - --notifyUrl URL to send notifications to (http://localhost allowed, otherwise https) - --target Transloadit endpoint base URL - --port Local listen port - --pollIntervalMs Base poll interval in milliseconds - --pollMaxIntervalMs Max poll backoff interval in milliseconds - --pollBackoffFactor Poll backoff factor (>= 1) - --maxPollAttempts Max number of poll attempts - --maxInFlightPolls Max number of active assembly pollers - --notifyOnTerminalError Send notify payload when terminal error is reached - -l, --log-level Log level (0-8 or emerg/alert/crit/err/warn/notice/info/debug/trace) - -h, --help Show this help + --notifyUrl URL to send notifications to (http://localhost allowed, otherwise https) + --target Transloadit endpoint base URL + --port Local listen port + --forwardTimeoutMs Forward request timeout in milliseconds + --pollIntervalMs Base poll retry interval in milliseconds + --pollMaxIntervalMs Max poll retry interval in milliseconds + --pollBackoffFactor Poll retry backoff factor (>= 1) + --pollRequestTimeoutMs Per poll request timeout in milliseconds + --maxPollAttempts Max number of poll attempts + --maxInFlightPolls Max number of active assembly pollers + --notifyOnTerminalError Send notify payload when terminal error is reached + --notifyTimeoutMs Per notify request timeout in milliseconds + --notifyMaxAttempts Max number of notify attempts + --notifyIntervalMs Base notify retry interval in milliseconds + --notifyMaxIntervalMs Max notify retry interval in milliseconds + --notifyBackoffFactor Notify retry backoff factor (>= 1) + --ui Enable reactive terminal dashboard (TUI) + -l, --log-level Log level (0-8 or emerg/alert/crit/err/warn/notice/info/debug/trace) + -h, --help Show this help Environment fallback: TRANSLOADIT_SECRET, TRANSLOADIT_NOTIFY_URL, TRANSLOADIT_LOG_LEVEL @@ -154,6 +332,9 @@ if (values.target) { if (values.port) { settings.port = parsePositiveIntOption('port', values.port, 65_535) } +if (values.forwardTimeoutMs) { + settings.forwardTimeoutMs = parsePositiveIntOption('forwardTimeoutMs', values.forwardTimeoutMs) +} if (values.pollIntervalMs) { settings.pollIntervalMs = parsePositiveIntOption('pollIntervalMs', values.pollIntervalMs) } @@ -167,6 +348,12 @@ if (values.pollBackoffFactor) { 1, ) } +if (values.pollRequestTimeoutMs) { + settings.pollRequestTimeoutMs = parsePositiveIntOption( + 'pollRequestTimeoutMs', + values.pollRequestTimeoutMs, + ) +} if (values.maxPollAttempts) { settings.maxPollAttempts = parsePositiveIntOption('maxPollAttempts', values.maxPollAttempts) } @@ -180,17 +367,51 @@ if (notifyOnTerminalError) { settings.notifyOnTerminalError = true } +if (values.notifyTimeoutMs) { + settings.notifyTimeoutMs = parsePositiveIntOption('notifyTimeoutMs', values.notifyTimeoutMs) +} +if (values.notifyMaxAttempts) { + settings.notifyMaxAttempts = parsePositiveIntOption('notifyMaxAttempts', values.notifyMaxAttempts) +} +if (values.notifyIntervalMs) { + settings.notifyIntervalMs = parsePositiveIntOption('notifyIntervalMs', values.notifyIntervalMs) +} +if (values.notifyMaxIntervalMs) { + settings.notifyMaxIntervalMs = parsePositiveIntOption( + 'notifyMaxIntervalMs', + values.notifyMaxIntervalMs, + ) +} +if (values.notifyBackoffFactor) { + settings.notifyBackoffFactor = parsePositiveFloatOption( + 'notifyBackoffFactor', + values.notifyBackoffFactor, + 1, + ) +} + const rawLogLevel = values['log-level'] ?? values.logLevel ?? process.env.TRANSLOADIT_LOG_LEVEL const logLevel = rawLogLevel ? parseLogLevelOption(rawLogLevel) : undefined -const loggerOptions = typeof logLevel === 'number' ? { logLevel } : {} + +const tui = values.ui === true ? createTuiMode(logLevel) : null +const runtimeOptions: ProxyRuntimeOptions = tui + ? tui.runtimeOptions + : { + ...(typeof logLevel === 'number' ? { logLevel } : {}), + } const notifyUrlRaw = values.notifyUrl ?? process.env.TRANSLOADIT_NOTIFY_URL const notifyUrl = notifyUrlRaw ? parseNotifyUrlOption(notifyUrlRaw) : undefined -const proxy = new TransloaditNotifyUrlProxy(secret, notifyUrl, loggerOptions) +const proxy = new TransloaditNotifyUrlProxy(secret, notifyUrl, runtimeOptions) proxy.run(settings) +if (tui) { + tui.start() +} + const close = () => { + tui?.stop() proxy.close() process.exit(0) } diff --git a/docs/prompts/2026-03-04-modernize.md b/docs/prompts/2026-03-04-modernize.md index ad7dd1f..d4e8f4f 100644 --- a/docs/prompts/2026-03-04-modernize.md +++ b/docs/prompts/2026-03-04-modernize.md @@ -30,3 +30,17 @@ Verification plan: 2. `yarn check` 3. `yarn test:real` (when env/secrets are present) 4. push branch, confirm GitHub Actions run status + +## Phase 2 (Requested Follow-up) + +Scope requested: +- [x] 2. Stream upstream responses instead of buffering them fully. +- [x] 3. Add per-request timeout policy with distinct forward/poll/notify error codes. +- [x] 7. Add lightweight metrics hooks (counters/timers/gauges) for integration. +- [x] 9. Add proxy/network behavior tests: + - [x] large body response + - [x] redirect passthrough + - [x] multiple `set-cookie` passthrough + - [x] upstream timeout/failure path +- [x] 10. Add chaos retry tests for flaky polling + flaky notify. +- [x] Build reactive `--ui` mode with live logs + metrics graphs (TUI). diff --git a/src/index.ts b/src/index.ts index ee3363c..53fedf9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,8 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' import { Readable } from 'node:stream' +import { pipeline } from 'node:stream/promises' +import type { ReadableStream as NodeReadableStream } from 'node:stream/web' + import { SevLogger } from '@transloadit/sev-logger' import { signParamsSync } from '@transloadit/utils/node' import { @@ -28,39 +31,120 @@ const HOP_BY_HOP_HEADERS = new Set([ 'upgrade', ]) +const MAX_CAPTURED_RESPONSE_BYTES = 512 * 1024 + +export type ProxyErrorCode = + | 'FORWARD_TIMEOUT' + | 'FORWARD_UPSTREAM_ERROR' + | 'POLL_TIMEOUT' + | 'NOTIFY_TIMEOUT' + export interface ProxySettings { target: string port: number + forwardTimeoutMs: number pollIntervalMs: number pollMaxIntervalMs: number pollBackoffFactor: number + pollRequestTimeoutMs: number maxPollAttempts: number maxInFlightPolls: number notifyOnTerminalError: boolean + notifyTimeoutMs: number + notifyMaxAttempts: number + notifyIntervalMs: number + notifyMaxIntervalMs: number + notifyBackoffFactor: number +} + +export interface CounterMetricEvent { + kind: 'counter' + name: string + at: string + delta: number + total: number + tags?: Record +} + +export interface GaugeMetricEvent { + kind: 'gauge' + name: string + at: string + value: number +} + +export interface TimingMetricEvent { + kind: 'timing' + name: string + at: string + durationMs: number + count: number + minMs: number + maxMs: number + avgMs: number + tags?: Record +} + +export interface ProxyMetricsHooks { + onCounter?: (event: CounterMetricEvent) => void + onGauge?: (event: GaugeMetricEvent) => void + onTiming?: (event: TimingMetricEvent) => void +} + +export interface ProxyLogEvent { + at: string + level: 'debug' | 'info' | 'notice' | 'warn' | 'err' + message: string } -export interface ProxyLoggerOptions { +export interface ProxyRuntimeOptions { logger?: SevLogger logLevel?: number + metricsHooks?: ProxyMetricsHooks + onLog?: (event: ProxyLogEvent) => void } type KnownAssemblyState = (typeof assemblyStatusOkCodeSchema.options)[number] export type AssemblyResponse = AssemblyStatus +interface TimingAggregate { + count: number + totalMs: number + minMs: number + maxMs: number + lastMs: number +} + const DEFAULT_SETTINGS: ProxySettings = { target: 'https://api2.transloadit.com', port: 8888, + forwardTimeoutMs: 15_000, pollIntervalMs: 2_000, pollMaxIntervalMs: 30_000, pollBackoffFactor: 2, + pollRequestTimeoutMs: 15_000, maxPollAttempts: 10, maxInFlightPolls: 4, notifyOnTerminalError: false, + notifyTimeoutMs: 15_000, + notifyMaxAttempts: 3, + notifyIntervalMs: 500, + notifyMaxIntervalMs: 5_000, + notifyBackoffFactor: 2, } const DEFAULT_LOG_LEVEL = SevLogger.LEVEL.INFO +class ProxyTimeoutError extends Error { + readonly code: ProxyErrorCode + + constructor(code: ProxyErrorCode, message: string) { + super(message) + this.code = code + } +} + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } @@ -86,6 +170,14 @@ function isAbortLikeError(error: unknown): boolean { return error instanceof Error && error.name === 'AbortError' } +function getErrorCode(error: unknown, fallback: ProxyErrorCode): ProxyErrorCode { + if (error instanceof ProxyTimeoutError) { + return error.code + } + + return fallback +} + function getHeaderValues(name: string, headers: Headers): string[] { const normalized = name.toLowerCase() if (normalized !== 'set-cookie') { @@ -101,6 +193,51 @@ function getHeaderValues(name: string, headers: Headers): string[] { return fallback ? [fallback] : [] } +function isJsonResponse(contentType: string | null): boolean { + if (!contentType) { + return false + } + + return /application\/json|\+json/i.test(contentType) +} + +function createTimeoutSignal( + parentSignal: AbortSignal | null | undefined, + timeoutMs: number, + timeoutError: ProxyTimeoutError, +): { signal: AbortSignal; didTimeout: () => boolean; cleanup: () => void } { + const controller = new AbortController() + let timedOut = false + + const onParentAbort = () => { + controller.abort(parentSignal?.reason) + } + + if (parentSignal) { + if (parentSignal.aborted) { + onParentAbort() + } else { + parentSignal.addEventListener('abort', onParentAbort, { once: true }) + } + } + + const timer = setTimeout(() => { + timedOut = true + controller.abort(timeoutError) + }, timeoutMs) + + return { + signal: controller.signal, + didTimeout: () => timedOut, + cleanup: () => { + clearTimeout(timer) + if (parentSignal) { + parentSignal.removeEventListener('abort', onParentAbort) + } + }, + } +} + export function extractAssemblyUrl(body: string): string | null { try { const payload = JSON.parse(body) as unknown @@ -143,6 +280,8 @@ export default class TransloaditNotifyUrlProxy { private readonly secret: string private readonly notifyUrl: string private readonly logger: SevLogger + private readonly metricsHooks: ProxyMetricsHooks | undefined + private readonly onLog: ((event: ProxyLogEvent) => void) | undefined private readonly defaults: ProxySettings private settings: ProxySettings @@ -151,25 +290,31 @@ export default class TransloaditNotifyUrlProxy { private readonly pollControllers = new Map() private activePollCount = 0 + private readonly counters = new Map() + private readonly gauges = new Map() + private readonly timings = new Map() + constructor( secret: string, notifyUrl = 'http://127.0.0.1:3000/transloadit', - loggerOptions: ProxyLoggerOptions = {}, + runtimeOptions: ProxyRuntimeOptions = {}, ) { this.secret = secret || '' this.notifyUrl = notifyUrl + this.metricsHooks = runtimeOptions.metricsHooks + this.onLog = runtimeOptions.onLog this.defaults = { ...DEFAULT_SETTINGS } this.settings = { ...DEFAULT_SETTINGS } this.logger = - loggerOptions.logger ?? + runtimeOptions.logger ?? new SevLogger({ breadcrumbs: ['notify-url-proxy'], - level: loggerOptions.logLevel ?? DEFAULT_LOG_LEVEL, + level: runtimeOptions.logLevel ?? DEFAULT_LOG_LEVEL, }) - if (loggerOptions.logger && typeof loggerOptions.logLevel === 'number') { - this.logger.update({ level: loggerOptions.logLevel }) + if (runtimeOptions.logger && typeof runtimeOptions.logLevel === 'number') { + this.logger.update({ level: runtimeOptions.logLevel }) } } @@ -180,7 +325,19 @@ export default class TransloaditNotifyUrlProxy { this.isClosing = false this.settings = { ...this.defaults, ...opts } - this.createServer() + + this.setGauge('poll.in_flight', 0) + this.setGauge('poll.pending', 0) + + this.server = createServer((request, response) => { + void this.handleForward(request, response) + }) + + this.server.listen(this.settings.port) + this.log( + 'notice', + `Listening on http://localhost:${this.settings.port}, forwarding to ${this.settings.target}, notifying ${this.notifyUrl}`, + ) } close(): void { @@ -197,21 +354,96 @@ export default class TransloaditNotifyUrlProxy { this.pendingAssemblyUrls.clear() this.activePolls.clear() this.activePollCount = 0 + + this.setGauge('poll.in_flight', 0) + this.setGauge('poll.pending', 0) } - private createServer(): void { - this.server = createServer((request, response) => { - void this.handleForward(request, response) + private log(level: ProxyLogEvent['level'], message: string): void { + if (level === 'debug') { + this.logger.debug(message) + } else if (level === 'info') { + this.logger.info(message) + } else if (level === 'notice') { + this.logger.notice(message) + } else if (level === 'warn') { + this.logger.warn(message) + } else { + this.logger.err(message) + } + + this.onLog?.({ + at: new Date().toISOString(), + level, + message, }) + } - this.server.listen(this.settings.port) + private incrementCounter(name: string, delta = 1, tags?: Record): void { + const total = (this.counters.get(name) ?? 0) + delta + this.counters.set(name, total) + + this.metricsHooks?.onCounter?.({ + kind: 'counter', + name, + at: new Date().toISOString(), + delta, + total, + ...(tags ? { tags } : {}), + }) + } - this.logger.notice( - `Listening on http://localhost:${this.settings.port}, forwarding to ${this.settings.target}, notifying ${this.notifyUrl}`, - ) + private setGauge(name: string, value: number): void { + this.gauges.set(name, value) + + this.metricsHooks?.onGauge?.({ + kind: 'gauge', + name, + at: new Date().toISOString(), + value, + }) + } + + private observeTiming(name: string, durationMs: number, tags?: Record): void { + const existing = this.timings.get(name) + if (!existing) { + this.timings.set(name, { + count: 1, + totalMs: durationMs, + minMs: durationMs, + maxMs: durationMs, + lastMs: durationMs, + }) + } else { + existing.count += 1 + existing.totalMs += durationMs + existing.minMs = Math.min(existing.minMs, durationMs) + existing.maxMs = Math.max(existing.maxMs, durationMs) + existing.lastMs = durationMs + } + + const stats = this.timings.get(name) + if (!stats) { + return + } + + this.metricsHooks?.onTiming?.({ + kind: 'timing', + name, + at: new Date().toISOString(), + durationMs, + count: stats.count, + minMs: stats.minMs, + maxMs: stats.maxMs, + avgMs: stats.totalMs / stats.count, + ...(tags ? { tags } : {}), + }) } private async handleForward(request: IncomingMessage, response: ServerResponse): Promise { + const requestStartedAt = Date.now() + this.incrementCounter('forward.requests_total') + const proxyController = new AbortController() request.on('aborted', () => { proxyController.abort(new Error('Client aborted request')) @@ -220,37 +452,44 @@ export default class TransloaditNotifyUrlProxy { try { const targetUrl = this.resolveTargetUrl(request.url) const requestBody = supportsBody(request.method) - ? (Readable.toWeb(request) as ReadableStream) + ? (Readable.toWeb(request) as unknown as ReadableStream) : undefined + const fetchInit: RequestInit = { method: request.method ?? 'GET', headers: this.createForwardHeaders(request), redirect: 'manual', signal: proxyController.signal, } + if (requestBody) { fetchInit.body = requestBody ;(fetchInit as RequestInit & { duplex: 'half' }).duplex = 'half' } - const upstreamResponse = await fetch(targetUrl, fetchInit) + const upstreamResponse = await this.fetchWithTimeout( + targetUrl, + fetchInit, + this.settings.forwardTimeoutMs, + 'FORWARD_TIMEOUT', + ) - const body = Buffer.from(await upstreamResponse.arrayBuffer()) - this.writeForwardedResponse(response, upstreamResponse, body) - this.maybePollAssemblyFromBody(body) + await this.pipeForwardResponse(response, upstreamResponse) + + this.incrementCounter('forward.requests_ok') + this.observeTiming('forward.request_duration_ms', Date.now() - requestStartedAt) } catch (error) { if (isAbortLikeError(error)) { return } - if (!response.headersSent) { - response.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' }) - } - if (!response.writableEnded) { - response.end('Proxy error') - } + const code = getErrorCode(error, 'FORWARD_UPSTREAM_ERROR') + const statusCode = code === 'FORWARD_TIMEOUT' ? 504 : 502 - this.logger.err(`Proxy error: ${toErrorMessage(error)}`) + this.incrementCounter('forward.requests_error', 1, { code }) + this.observeTiming('forward.request_duration_ms', Date.now() - requestStartedAt, { code }) + this.writeErrorResponse(response, statusCode, code, toErrorMessage(error)) + this.log('err', `Forward request failed with ${code}: ${toErrorMessage(error)}`) } } @@ -294,11 +533,10 @@ export default class TransloaditNotifyUrlProxy { return headers } - private writeForwardedResponse( + private async pipeForwardResponse( response: ServerResponse, upstreamResponse: Response, - body: Buffer, - ): void { + ): Promise { response.statusCode = upstreamResponse.status response.statusMessage = upstreamResponse.statusText @@ -315,7 +553,67 @@ export default class TransloaditNotifyUrlProxy { response.setHeader('set-cookie', setCookies) } - response.end(body) + if (!upstreamResponse.body) { + response.end() + return + } + + const shouldCapture = isJsonResponse(upstreamResponse.headers.get('content-type')) + + const upstreamBodyNode = Readable.fromWeb( + upstreamResponse.body as unknown as NodeReadableStream, + ) + const capturedChunks: Buffer[] = [] + let capturedBytes = 0 + + if (shouldCapture) { + upstreamBodyNode.on('data', (chunk: Buffer | string | Uint8Array) => { + if (capturedBytes >= MAX_CAPTURED_RESPONSE_BYTES) { + return + } + + const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + const remaining = MAX_CAPTURED_RESPONSE_BYTES - capturedBytes + const toCapture = + chunkBuffer.length <= remaining ? chunkBuffer : chunkBuffer.subarray(0, remaining) + + capturedChunks.push(Buffer.from(toCapture)) + capturedBytes += toCapture.length + }) + } + + await pipeline(upstreamBodyNode, response) + + if (shouldCapture && capturedBytes > 0) { + const body = Buffer.concat(capturedChunks, capturedBytes) + this.maybePollAssemblyFromBody(body) + } + } + + private writeErrorResponse( + response: ServerResponse, + statusCode: number, + code: ProxyErrorCode, + message: string, + ): void { + if (response.headersSent) { + if (!response.writableEnded) { + response.end() + } + return + } + + response.writeHead(statusCode, { + 'content-type': 'application/json; charset=utf-8', + 'x-notify-proxy-error-code': code, + }) + + response.end( + JSON.stringify({ + error: code, + message, + }), + ) } private maybePollAssemblyFromBody(body: Buffer): void { @@ -333,12 +631,16 @@ export default class TransloaditNotifyUrlProxy { } if (this.pendingAssemblyUrls.has(assemblyUrl) || this.activePolls.has(assemblyUrl)) { - this.logger.debug(`Skipping duplicate poll registration for ${assemblyUrl}`) + this.incrementCounter('poll.dedupe_skipped_total') + this.log('debug', `Skipping duplicate poll registration for ${assemblyUrl}`) return } this.pendingAssemblyUrls.add(assemblyUrl) - this.logger.info(`Queued poll for ${assemblyUrl}`) + this.setGauge('poll.pending', this.pendingAssemblyUrls.size) + this.incrementCounter('poll.enqueued_total') + this.log('info', `Queued poll for ${assemblyUrl}`) + this.drainPollQueue() } @@ -354,10 +656,12 @@ export default class TransloaditNotifyUrlProxy { } this.pendingAssemblyUrls.delete(next) + this.setGauge('poll.pending', this.pendingAssemblyUrls.size) const controller = new AbortController() this.pollControllers.set(next, controller) this.activePollCount += 1 + this.setGauge('poll.in_flight', this.activePollCount) const pollPromise = this.pollAssembly(next, controller.signal).finally(() => { if (this.activePolls.get(next) !== pollPromise) { @@ -367,6 +671,7 @@ export default class TransloaditNotifyUrlProxy { this.activePolls.delete(next) this.pollControllers.delete(next) this.activePollCount = Math.max(0, this.activePollCount - 1) + this.setGauge('poll.in_flight', this.activePollCount) if (!this.isClosing) { this.drainPollQueue() @@ -379,6 +684,9 @@ export default class TransloaditNotifyUrlProxy { private async pollAssembly(assemblyUrl: string, signal: AbortSignal): Promise { const retries = Math.max(this.settings.maxPollAttempts - 1, 0) + const pollStartedAt = Date.now() + + this.incrementCounter('poll.started_total') try { const response = await pRetry(() => this.checkAssembly(assemblyUrl, signal), { @@ -393,32 +701,48 @@ export default class TransloaditNotifyUrlProxy { return } - this.logger.warn( + this.incrementCounter('poll.retry_total') + this.log( + 'warn', `Attempt ${retryContext.attemptNumber}/${this.settings.maxPollAttempts} failed for ${assemblyUrl}: ${retryContext.error.message}`, ) }, }) - await this.notify(response, signal) + await this.notifyWithRetry(response, signal) + + this.incrementCounter('poll.completed_total') + this.observeTiming('poll.duration_ms', Date.now() - pollStartedAt) } catch (error) { if (error instanceof AbortError) { - this.logger.notice(error.message) + this.incrementCounter('poll.aborted_total') + this.log('notice', error.message) return } if (signal.aborted || this.isClosing || isAbortLikeError(error)) { - this.logger.debug(`Polling cancelled for ${assemblyUrl}`) + this.incrementCounter('poll.cancelled_total') + this.log('debug', `Polling cancelled for ${assemblyUrl}`) return } - this.logger.err( - `No attempts left, giving up on checking assemblyUrl: ${assemblyUrl} (${toErrorMessage(error)})`, - ) + const code = getErrorCode(error, 'POLL_TIMEOUT') + this.incrementCounter('poll.failed_total', 1, { code }) + this.observeTiming('poll.duration_ms', Date.now() - pollStartedAt, { code }) + this.log('err', `No attempts left for ${assemblyUrl}: ${toErrorMessage(error)}`) } } private async checkAssembly(assemblyUrl: string, signal: AbortSignal): Promise { - const response = await fetch(assemblyUrl, { signal }) + this.incrementCounter('poll.fetch_attempt_total') + + const response = await this.fetchWithTimeout( + assemblyUrl, + { signal }, + this.settings.pollRequestTimeoutMs, + 'POLL_TIMEOUT', + ) + if (!response.ok) { throw new Error(`Assembly poll returned HTTP ${response.status}`) } @@ -427,8 +751,11 @@ export default class TransloaditNotifyUrlProxy { if (isAssemblyTerminalError(assembly)) { const errorCode = getError(assembly) ?? 'UNKNOWN_ERROR' + this.incrementCounter('poll.terminal_error_total', 1, { errorCode }) + if (this.settings.notifyOnTerminalError) { - this.logger.notice( + this.log( + 'notice', `${assemblyUrl} reached terminal error state ${errorCode}; notifying because notifyOnTerminalError=true.`, ) return assembly @@ -438,7 +765,8 @@ export default class TransloaditNotifyUrlProxy { } if (isAssemblyTerminalOk(assembly)) { - this.logger.info(`${assemblyUrl} reached terminal state ${getOk(assembly)}.`) + this.incrementCounter('poll.terminal_ok_total', 1, { state: getOk(assembly) ?? 'UNKNOWN' }) + this.log('info', `${assemblyUrl} reached terminal state ${getOk(assembly)}.`) return assembly } @@ -456,26 +784,109 @@ export default class TransloaditNotifyUrlProxy { throw new Error(`${assemblyUrl} returned a non-terminal assembly state.`) } - private async notify(response: AssemblyResponse, signal: AbortSignal): Promise { + private async notifyWithRetry(response: AssemblyResponse, signal: AbortSignal): Promise { + const retries = Math.max(this.settings.notifyMaxAttempts - 1, 0) + + await pRetry(() => this.notifyOnce(response, signal), { + retries, + minTimeout: this.settings.notifyIntervalMs, + maxTimeout: this.settings.notifyMaxIntervalMs, + factor: this.settings.notifyBackoffFactor, + randomize: true, + signal, + onFailedAttempt: (retryContext: RetryContext) => { + if (signal.aborted || isAbortLikeError(retryContext.error)) { + return + } + + if (retryContext.retriesLeft <= 0) { + return + } + + this.incrementCounter('notify.retry_total') + this.log( + 'warn', + `Notify retry ${retryContext.attemptNumber}/${this.settings.notifyMaxAttempts} failed: ${retryContext.error.message}`, + ) + }, + shouldRetry: (retryContext: RetryContext) => { + if (signal.aborted || isAbortLikeError(retryContext.error)) { + return false + } + + return true + }, + }) + } + + private async notifyOnce(response: AssemblyResponse, signal: AbortSignal): Promise { + const notifyStartedAt = Date.now() + this.incrementCounter('notify.attempt_total') + const transloadit = JSON.stringify(response) const signature = getSignature(this.secret, transloadit) - const notifyResponse = await fetch(this.notifyUrl, { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', + const notifyResponse = await this.fetchWithTimeout( + this.notifyUrl, + { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', + }, + body: new URLSearchParams({ + transloadit, + signature, + }), + signal, }, - body: new URLSearchParams({ - transloadit, - signature, - }), - signal, - }) + this.settings.notifyTimeoutMs, + 'NOTIFY_TIMEOUT', + ) if (!notifyResponse.ok) { + this.incrementCounter('notify.failed_total', 1, { code: `HTTP_${notifyResponse.status}` }) + this.observeTiming('notify.duration_ms', Date.now() - notifyStartedAt, { + code: `HTTP_${notifyResponse.status}`, + }) throw new Error(`Notify URL returned HTTP ${notifyResponse.status}`) } - this.logger.notice(`Notify payload sent to ${this.notifyUrl}`) + this.incrementCounter('notify.success_total') + this.observeTiming('notify.duration_ms', Date.now() - notifyStartedAt) + this.log('notice', `Notify payload sent to ${this.notifyUrl}`) + } + + private async fetchWithTimeout( + url: string, + init: RequestInit, + timeoutMs: number, + timeoutCode: ProxyErrorCode, + ): Promise { + const timeoutSignal = createTimeoutSignal( + init.signal, + timeoutMs, + new ProxyTimeoutError(timeoutCode, `${timeoutCode} after ${timeoutMs}ms`), + ) + + const fetchInit: RequestInit = { + ...init, + signal: timeoutSignal.signal, + } + + try { + return await fetch(url, fetchInit) + } catch (error) { + if (timeoutSignal.didTimeout()) { + throw new ProxyTimeoutError(timeoutCode, `${timeoutCode} after ${timeoutMs}ms`) + } + + if (timeoutSignal.signal.reason instanceof ProxyTimeoutError) { + throw timeoutSignal.signal.reason + } + + throw error + } finally { + timeoutSignal.cleanup() + } } } diff --git a/test/chaos.test.ts b/test/chaos.test.ts new file mode 100644 index 0000000..4614d63 --- /dev/null +++ b/test/chaos.test.ts @@ -0,0 +1,274 @@ +import { once } from 'node:events' +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { setTimeout as delay } from 'node:timers/promises' + +import { describe, expect, it } from 'vitest' + +import TransloaditNotifyUrlProxy, { type CounterMetricEvent } from '../src/index.ts' + +async function listen(server: Server): Promise { + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') { + throw new Error('Could not resolve server address') + } + return address.port +} + +async function closeServer(server: Server): Promise { + await new Promise((resolve) => { + server.close(() => resolve()) + }) +} + +async function getFreePort(): Promise { + const server = createServer() + const port = await listen(server) + await closeServer(server) + return port +} + +async function readBody(request: IncomingMessage): Promise { + const chunks: Buffer[] = [] + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + + return Buffer.concat(chunks).toString('utf-8') +} + +function json(response: ServerResponse, statusCode: number, payload: unknown): void { + response.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' }) + response.end(JSON.stringify(payload)) +} + +async function waitFor( + fn: () => boolean, + timeoutMs: number, + intervalMs = 10, + message = 'Timed out waiting for condition', +): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (fn()) { + return + } + await delay(intervalMs) + } + throw new Error(message) +} + +describe('proxy chaos retries', () => { + it('handles flaky polling upstream and still notifies', async () => { + const counters: Record = {} + const onCounter = (event: CounterMetricEvent): void => { + counters[event.name] = event.total + } + + let upstreamPort = 0 + let pollCount = 0 + let notifyCount = 0 + let resolveNotify: (() => void) | null = null + const notifyReceived = new Promise((resolve) => { + resolveNotify = resolve + }) + + const notifyServer = createServer(async (request, response) => { + if (request.method !== 'POST' || request.url !== '/transloadit') { + response.writeHead(404) + response.end() + return + } + + notifyCount += 1 + await readBody(request) + response.writeHead(200) + response.end('ok') + resolveNotify?.() + }) + + const upstreamServer = createServer((request, response) => { + if (request.method === 'POST' && request.url === '/assemblies') { + json(response, 200, { + assembly_url: `http://127.0.0.1:${upstreamPort}/assembly/flaky`, + }) + return + } + + if (request.method === 'GET' && request.url === '/assembly/flaky') { + pollCount += 1 + if (pollCount <= 2) { + json(response, 500, { error: 'TEMP_ERROR' }) + return + } + if (pollCount <= 4) { + json(response, 200, { ok: 'ASSEMBLY_EXECUTING' }) + return + } + + json(response, 200, { ok: 'ASSEMBLY_COMPLETED', assembly_id: 'flaky' }) + return + } + + response.writeHead(404) + response.end() + }) + + const notifyPort = await listen(notifyServer) + upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy( + 'secret', + `http://127.0.0.1:${notifyPort}/transloadit`, + { + logLevel: 0, + metricsHooks: { onCounter }, + }, + ) + proxy.run({ + target: `http://127.0.0.1:${upstreamPort}`, + port: proxyPort, + pollIntervalMs: 10, + pollMaxIntervalMs: 50, + maxPollAttempts: 8, + pollRequestTimeoutMs: 300, + }) + + try { + const createResponse = await fetch(`http://127.0.0.1:${proxyPort}/assemblies`, { + method: 'POST', + body: new URLSearchParams({ params: '{}' }), + }) + expect(createResponse.status).toBe(200) + + await Promise.race([ + notifyReceived, + delay(5_000).then(() => { + throw new Error('Timed out waiting for flaky polling notify') + }), + ]) + await waitFor( + () => (counters['notify.success_total'] ?? 0) >= 1, + 1_000, + 10, + 'Timed out waiting for notify.success_total update', + ) + + expect(notifyCount).toBe(1) + expect(pollCount).toBeGreaterThanOrEqual(5) + expect(counters['poll.retry_total'] ?? 0).toBeGreaterThanOrEqual(3) + expect(counters['notify.success_total'] ?? 0).toBe(1) + } finally { + proxy.close() + await closeServer(notifyServer) + await closeServer(upstreamServer) + } + }, 12_000) + + it('retries flaky notify endpoint until success', async () => { + const counters: Record = {} + const onCounter = (event: CounterMetricEvent): void => { + counters[event.name] = event.total + } + + let upstreamPort = 0 + let notifyAttempts = 0 + let resolveNotify: (() => void) | null = null + const notifyDone = new Promise((resolve) => { + resolveNotify = resolve + }) + + const notifyServer = createServer(async (request, response) => { + if (request.method !== 'POST' || request.url !== '/transloadit') { + response.writeHead(404) + response.end() + return + } + + notifyAttempts += 1 + await readBody(request) + + if (notifyAttempts < 3) { + response.writeHead(500) + response.end('retry me') + return + } + + response.writeHead(200) + response.end('ok') + resolveNotify?.() + }) + + const upstreamServer = createServer((request, response) => { + if (request.method === 'POST' && request.url === '/assemblies') { + json(response, 200, { + assembly_url: `http://127.0.0.1:${upstreamPort}/assembly/notify-flaky`, + }) + return + } + + if (request.method === 'GET' && request.url === '/assembly/notify-flaky') { + json(response, 200, { ok: 'ASSEMBLY_COMPLETED', assembly_id: 'notify-flaky' }) + return + } + + response.writeHead(404) + response.end() + }) + + const notifyPort = await listen(notifyServer) + upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy( + 'secret', + `http://127.0.0.1:${notifyPort}/transloadit`, + { + logLevel: 0, + metricsHooks: { onCounter }, + }, + ) + proxy.run({ + target: `http://127.0.0.1:${upstreamPort}`, + port: proxyPort, + pollIntervalMs: 5, + pollMaxIntervalMs: 20, + notifyIntervalMs: 10, + notifyMaxIntervalMs: 40, + notifyBackoffFactor: 2, + notifyMaxAttempts: 5, + notifyTimeoutMs: 300, + }) + + try { + const createResponse = await fetch(`http://127.0.0.1:${proxyPort}/assemblies`, { + method: 'POST', + body: new URLSearchParams({ params: '{}' }), + }) + expect(createResponse.status).toBe(200) + + await Promise.race([ + notifyDone, + delay(5_000).then(() => { + throw new Error('Timed out waiting for flaky notify success') + }), + ]) + await waitFor( + () => (counters['notify.success_total'] ?? 0) >= 1, + 1_000, + 10, + 'Timed out waiting for notify.success_total update', + ) + + expect(notifyAttempts).toBe(3) + expect(counters['notify.retry_total'] ?? 0).toBeGreaterThanOrEqual(2) + expect(counters['notify.success_total'] ?? 0).toBe(1) + } finally { + proxy.close() + await closeServer(notifyServer) + await closeServer(upstreamServer) + } + }, 12_000) +}) diff --git a/test/network.test.ts b/test/network.test.ts new file mode 100644 index 0000000..99ee870 --- /dev/null +++ b/test/network.test.ts @@ -0,0 +1,196 @@ +import { once } from 'node:events' +import { createServer, type Server, type ServerResponse } from 'node:http' +import { setTimeout as delay } from 'node:timers/promises' + +import { describe, expect, it } from 'vitest' + +import TransloaditNotifyUrlProxy from '../src/index.ts' + +async function listen(server: Server): Promise { + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') { + throw new Error('Could not resolve server address') + } + return address.port +} + +async function closeServer(server: Server): Promise { + await new Promise((resolve) => { + server.close(() => resolve()) + }) +} + +async function getFreePort(): Promise { + const server = createServer() + const port = await listen(server) + await closeServer(server) + return port +} + +function json(response: ServerResponse, statusCode: number, payload: unknown): void { + response.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' }) + response.end(JSON.stringify(payload)) +} + +describe('proxy network behavior', () => { + it('streams large upstream response bodies', async () => { + const upstreamServer = createServer(async (request, response) => { + if (request.method !== 'GET' || request.url !== '/large') { + response.writeHead(404) + response.end() + return + } + + response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }) + for (let i = 0; i < 256; i += 1) { + response.write(`chunk-${i.toString().padStart(3, '0')}-`) + await delay(1) + } + response.end('done') + }) + + const upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy('secret', undefined, { logLevel: 0 }) + proxy.run({ target: `http://127.0.0.1:${upstreamPort}`, port: proxyPort }) + + try { + const response = await fetch(`http://127.0.0.1:${proxyPort}/large`) + const body = await response.text() + + expect(response.status).toBe(200) + expect(body.startsWith('chunk-000-')).toBe(true) + expect(body.endsWith('done')).toBe(true) + expect(body.length).toBeGreaterThan(2_000) + } finally { + proxy.close() + await closeServer(upstreamServer) + } + }, 10_000) + + it('passes redirects through without following them', async () => { + const upstreamServer = createServer((request, response) => { + if (request.url === '/redirect') { + response.writeHead(302, { location: '/final-destination' }) + response.end() + return + } + + response.writeHead(404) + response.end() + }) + + const upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy('secret', undefined, { logLevel: 0 }) + proxy.run({ target: `http://127.0.0.1:${upstreamPort}`, port: proxyPort }) + + try { + const response = await fetch(`http://127.0.0.1:${proxyPort}/redirect`, { redirect: 'manual' }) + expect(response.status).toBe(302) + expect(response.headers.get('location')).toBe('/final-destination') + } finally { + proxy.close() + await closeServer(upstreamServer) + } + }) + + it('passes through multiple set-cookie headers', async () => { + const upstreamServer = createServer((request, response) => { + if (request.url === '/cookies') { + response.writeHead(200, { + 'set-cookie': ['a=1; Path=/', 'b=2; Path=/'], + 'content-type': 'text/plain; charset=utf-8', + }) + response.end('ok') + return + } + + response.writeHead(404) + response.end() + }) + + const upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy('secret', undefined, { logLevel: 0 }) + proxy.run({ target: `http://127.0.0.1:${upstreamPort}`, port: proxyPort }) + + try { + const response = await fetch(`http://127.0.0.1:${proxyPort}/cookies`) + expect(response.status).toBe(200) + + const headersWithSetCookie = response.headers as Headers & { getSetCookie?: () => string[] } + const cookies = + typeof headersWithSetCookie.getSetCookie === 'function' + ? headersWithSetCookie.getSetCookie() + : [] + + expect(cookies).toEqual(['a=1; Path=/', 'b=2; Path=/']) + } finally { + proxy.close() + await closeServer(upstreamServer) + } + }) + + it('returns timeout code when upstream exceeds forward timeout', async () => { + const upstreamServer = createServer(async (request, response) => { + if (request.url === '/slow') { + await delay(200) + json(response, 200, { ok: true }) + return + } + + response.writeHead(404) + response.end() + }) + + const upstreamPort = await listen(upstreamServer) + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy('secret', undefined, { logLevel: 0 }) + proxy.run({ + target: `http://127.0.0.1:${upstreamPort}`, + port: proxyPort, + forwardTimeoutMs: 30, + }) + + try { + const response = await fetch(`http://127.0.0.1:${proxyPort}/slow`) + const payload = (await response.json()) as { error?: string } + + expect(response.status).toBe(504) + expect(response.headers.get('x-notify-proxy-error-code')).toBe('FORWARD_TIMEOUT') + expect(payload.error).toBe('FORWARD_TIMEOUT') + } finally { + proxy.close() + await closeServer(upstreamServer) + } + }) + + it('returns upstream-error code when target cannot be reached', async () => { + const proxyPort = await getFreePort() + + const proxy = new TransloaditNotifyUrlProxy('secret', undefined, { logLevel: 0 }) + proxy.run({ + target: 'http://127.0.0.1:1', + port: proxyPort, + forwardTimeoutMs: 200, + }) + + try { + const response = await fetch(`http://127.0.0.1:${proxyPort}/unreachable`) + const payload = (await response.json()) as { error?: string } + + expect(response.status).toBe(502) + expect(response.headers.get('x-notify-proxy-error-code')).toBe('FORWARD_UPSTREAM_ERROR') + expect(payload.error).toBe('FORWARD_UPSTREAM_ERROR') + } finally { + proxy.close() + } + }) +}) From d19bc3f7327a387884614aca5b95c6e0d45a745e Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 4 Mar 2026 10:42:56 +0100 Subject: [PATCH 11/11] feat: load .env quietly in cli --- bin/notify-url-proxy.ts | 4 +++- package.json | 1 + yarn.lock | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bin/notify-url-proxy.ts b/bin/notify-url-proxy.ts index 0f385fd..9b68ba6 100755 --- a/bin/notify-url-proxy.ts +++ b/bin/notify-url-proxy.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node import { parseArgs } from 'node:util' - import { SevLogger } from '@transloadit/sev-logger' +import { config as loadDotEnv } from 'dotenv' import TransloaditNotifyUrlProxy, { type CounterMetricEvent, type GaugeMetricEvent, @@ -12,6 +12,8 @@ import TransloaditNotifyUrlProxy, { type TimingMetricEvent, } from '../src/index.ts' +loadDotEnv({ quiet: true }) + const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']) const LOG_LEVEL_BY_NAME = { diff --git a/package.json b/package.json index d3ac1ec..6ff096b 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@transloadit/sev-logger": "^0.1.9", "@transloadit/utils": "^4.3.0", "@transloadit/zod": "^4.3.0", + "dotenv": "^17.3.1", "p-retry": "^7.1.1" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 0bacaf5..4a91b23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2594,7 +2594,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^17.2.3": +"dotenv@npm:^17.2.3, dotenv@npm:^17.3.1": version: 17.3.1 resolution: "dotenv@npm:17.3.1" checksum: 10c0/c78e0c2d5a549c751e544cc60e2b95e7cb67e0c551f42e094d161c6b297aa44b630a3c2dcacf5569e529a6c2a6b84e2ab9be8d37b299d425df5a18b81ce4a35f @@ -4317,6 +4317,7 @@ __metadata: "@transloadit/utils": "npm:^4.3.0" "@transloadit/zod": "npm:^4.3.0" "@types/node": "npm:^25.3.3" + dotenv: "npm:^17.3.1" p-retry: "npm:^7.1.1" transloadit: "npm:^4.7.4" typescript: "npm:^5.9.3"