From 4bc11c027ef7859c1c28b9cd13636868dbd10cf3 Mon Sep 17 00:00:00 2001 From: CJ_Clippy Date: Thu, 25 Jul 2024 05:53:52 -0800 Subject: [PATCH] capture progress --- .vscode/launch.json | 18 + Makefile | 17 +- README.md | 24 + Tiltfile | 16 +- charts/fp/templates/capture.yaml | 16 + charts/fp/values.yaml | 6 + d.capture.dockerfile | 2 + devbox.json | 24 + devbox.lock | 298 ++++++ packages/capture/README.md | 4 + packages/capture/package.json | 7 + packages/capture/pnpm-lock.yaml | 916 +++++++++++++++++- packages/capture/src/Record.spec.ts | 75 +- packages/capture/src/Record.ts | 208 ++-- packages/capture/src/app.ts | 10 +- packages/capture/src/fixtures/mime.types | 2 + packages/capture/src/fixtures/my.abc | 1 + packages/capture/src/index copy.ts | 77 -- packages/capture/src/index.api.ts | 80 -- packages/capture/src/index.worker.ts | 41 - packages/capture/src/poc-lite.js | 25 + packages/capture/src/poc-s3-alt.ts | 135 +++ packages/capture/src/poc-s3.ts | 122 +++ packages/capture/src/poc.ts | 178 ++++ .../capture/src/{record.js => record.js.old} | 0 packages/capture/src/tasks/record.ts | 35 +- packages/scout/src/cb.spec.ts | 30 +- packages/scout/src/cb.ts | 115 ++- packages/utils/pnpm-lock.yaml | 34 - packages/utils/src/index.ts | 2 +- scripts/k8s-secrets.sh | 4 +- scripts/postgres-drop.sh | 11 +- 32 files changed, 2147 insertions(+), 386 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100644 packages/capture/src/fixtures/mime.types create mode 100644 packages/capture/src/fixtures/my.abc delete mode 100644 packages/capture/src/index copy.ts delete mode 100644 packages/capture/src/index.api.ts delete mode 100644 packages/capture/src/index.worker.ts create mode 100644 packages/capture/src/poc-lite.js create mode 100644 packages/capture/src/poc-s3-alt.ts create mode 100644 packages/capture/src/poc-s3.ts create mode 100644 packages/capture/src/poc.ts rename packages/capture/src/{record.js => record.js.old} (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..08d9336 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "name": "tsx", + "type": "node", + "request": "launch", + + "program": "${file}", + + "runtimeExecutable": "tsx", + + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + + "skipFiles": [ + "/**", + + "${workspaceFolder}/node_modules/**", + ], +} \ No newline at end of file diff --git a/Makefile b/Makefile index 95abe9e..1261353 100644 --- a/Makefile +++ b/Makefile @@ -47,22 +47,7 @@ clean: dotenvx run -f .env.${ENV} -- node ./packages/infra/vultr-delete-orphaned-resources.js deps: - echo "Some of the install methods for these dependencies are not cross-platform compatible. Some of the install methods are not tested. Expect this to fail. Please consult the Makefile for URLs to project sources." - sudo pamac install make entr nvm kubectl docker helm expect - curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - echo "go to https://github.com/txn2/kubefwd/releases/latest to get kubefwd" - echo "go to https://github.com/tilt-dev/ctlptl/releases/latest to get ctlptl" - sudo systemctl enable docker - sudo systemctl start docker - usermod -aG docker cj - newgrp docker - npm i -g pnpm - pnpm install -g @dotenvx/dotenvx - curl -OL 'https://github.com/vmware-tanzu/velero/releases/download/v1.13.2/velero-v1.13.2-linux-amd64.tar.gz' - OS=$(go env GOOS); ARCH=$(go env GOARCH); curl -fsSL -o cmctl https://github.com/cert-manager/cmctl/releases/latest/download/cmctl_${OS}_${ARCH} - chmod +x cmctl - sudo mv cmctl /usr/local/bin + echo "use `devbox install`" # A gitea act runner which runs locally # https://docs.gitea.com/next/usage/actions/overview diff --git a/README.md b/README.md index 921d5f6..6c47cff 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,33 @@ # futureporn.net +[![Built with Devbox](https://www.jetify.com/img/devbox/shield_galaxy.svg)](https://www.jetify.com/devbox/docs/contributor-quickstart/) + Source Code for https://futureporn.net See ./ARCHITECTURE.md for overview +## Getting Started + +I'm working towards a better development experience with devbox and Tilt. This process is in a state of flux and is likely to be broken. + +The main gist is as follows + +1. Install [devbox](https://www.jetify.com/devbox/docs/installing_devbox/) +2. Install development environment & packages using devbox. + + devbox install + +3. Start a local KIND Kubernetes cluster + + make cluster + +4. Start Tilt + + make tilt + +Tilt will manage the KIND cluster, downloading necessary docker containers and building the containers listed in the fp helm chart at ./Charts/fp. Making changes to these charts or the application code will update or re-build the images as necessary. + + ## Metrics Notes Keeping track of metrics we want to scrape using Prometheus diff --git a/Tiltfile b/Tiltfile index dfd0012..9b2fa69 100644 --- a/Tiltfile +++ b/Tiltfile @@ -181,14 +181,20 @@ cmd_button('postgres:drop', argv=['sh', './scripts/postgres-drop.sh'], resource='postgresql-primary', icon_name='delete', - text='DROP futureporn_db' + text='DROP all databases' ) -cmd_button('postgres:backup', - argv=['sh', './scripts/postgres-backup.sh'], +cmd_button('capture-api:create', + argv=['http', '--ignore-stdin', 'POST', 'http://localhost:5003/api/record', "url='https://twitch.tv/ironmouse'", "channel='ironmouse'"], + resource='capture-api', + icon_name='send', + text='Start Recording' +) +cmd_button('postgres:graphile', + argv=['sh', './scripts/postgres-test-graphile.sh'], resource='postgresql-primary', - icon_name='download', - text='backup the database' + icon_name='graph', + text='create graphile test job', ) cmd_button('postgres:graphile', argv=['sh', './scripts/postgres-test-graphile.sh'], diff --git a/charts/fp/templates/capture.yaml b/charts/fp/templates/capture.yaml index f3a020a..8caa5fe 100644 --- a/charts/fp/templates/capture.yaml +++ b/charts/fp/templates/capture.yaml @@ -45,6 +45,22 @@ spec: key: databaseUrl - name: PORT value: "{{ .Values.capture.api.port }}" + - name: S3_ENDPOINT + value: "{{ .Values.s3.endpoint }}" + - name: S3_REGION + value: "{{ .Values.s3.region }}" + - name: S3_BUCKET_NAME + value: "{{ .Values.s3.buckets.usc }}" + - name: S3_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: capture + key: s3AccessKeyId + - name: S3_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: capture + key: s3SecretAccessKey resources: limits: cpu: 1000m diff --git a/charts/fp/values.yaml b/charts/fp/values.yaml index 62725b1..4da01ea 100644 --- a/charts/fp/values.yaml +++ b/charts/fp/values.yaml @@ -1,6 +1,12 @@ environment: development # storageClassName: csi-hostpath-sc # used by minikube storageClassName: standard # used by Kind +s3: + endpoint: https://s3.us-west-000.backblazeb2.com + region: us-west-000 + buckets: + main: fp-dev + usc: fp-usc-dev link2cid: imageName: fp/link2cid next: diff --git a/d.capture.dockerfile b/d.capture.dockerfile index 01f004d..ae038a8 100644 --- a/d.capture.dockerfile +++ b/d.capture.dockerfile @@ -44,7 +44,9 @@ RUN ls -la /prod/capture ## start the app with dumb init to spawn the Node.js runtime process ## with signal support +## The mode @futureporn/capture uses when starting is determined by FUNCTION environment variable. (worker|api) FROM base AS capture +RUN ls -la /usr/local/bin/yt-dlp ENV HOSTNAME=0.0.0.0 NODE_ENV=production COPY --from=build /prod/capture . CMD [ "dumb-init", "node", "dist/index.js" ] \ No newline at end of file diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..64e2308 --- /dev/null +++ b/devbox.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.12.0/.schema/devbox.schema.json", + "packages": [ + "nodejs@20", + "tilt@latest", + "ctlptl@latest", + "kubectl@latest", + "cmctl@latest" + ], + "env": { + "DEVBOX_COREPACK_ENABLED": "true", + "ENV": "development" + }, + "shell": { + "init_hook": [ + "pnpm install" + ], + "scripts": { + "test": [ + "echo \"Error: no test specified\" && exit 1" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..830aa8a --- /dev/null +++ b/devbox.lock @@ -0,0 +1,298 @@ +{ + "lockfile_version": "1", + "packages": { + "cmctl@latest": { + "last_modified": "2024-07-07T07:43:47Z", + "resolved": "github:NixOS/nixpkgs/b60793b86201040d9dee019a05089a9150d08b5b#cmctl", + "source": "devbox-search", + "version": "1.14.7", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/69lc5y36840ccy6d1pzph994psadk4bm-cmctl-1.14.7", + "default": true + } + ], + "store_path": "/nix/store/69lc5y36840ccy6d1pzph994psadk4bm-cmctl-1.14.7" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/d5l61iil1gaax5sipnzg80mb0p1hqk9f-cmctl-1.14.7", + "default": true + } + ], + "store_path": "/nix/store/d5l61iil1gaax5sipnzg80mb0p1hqk9f-cmctl-1.14.7" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/j0r3wavi836mp31l0s7r1c3rjryw2z62-cmctl-1.14.7", + "default": true + } + ], + "store_path": "/nix/store/j0r3wavi836mp31l0s7r1c3rjryw2z62-cmctl-1.14.7" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/lnmy96wjzmjna7z9f0dbqd16nf2x5qbv-cmctl-1.14.7", + "default": true + } + ], + "store_path": "/nix/store/lnmy96wjzmjna7z9f0dbqd16nf2x5qbv-cmctl-1.14.7" + } + } + }, + "ctlptl@latest": { + "last_modified": "2024-07-07T07:43:47Z", + "resolved": "github:NixOS/nixpkgs/b60793b86201040d9dee019a05089a9150d08b5b#ctlptl", + "source": "devbox-search", + "version": "0.8.29", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gvnmvb315zngbg5a0idynlwxcc45gmyd-ctlptl-0.8.29", + "default": true + } + ], + "store_path": "/nix/store/gvnmvb315zngbg5a0idynlwxcc45gmyd-ctlptl-0.8.29" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/vgwbxwaf773mmgcbc1j5h6n5a7g587lf-ctlptl-0.8.29", + "default": true + } + ], + "store_path": "/nix/store/vgwbxwaf773mmgcbc1j5h6n5a7g587lf-ctlptl-0.8.29" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ga4rp2188c6k24162s2n23nfi4846790-ctlptl-0.8.29", + "default": true + } + ], + "store_path": "/nix/store/ga4rp2188c6k24162s2n23nfi4846790-ctlptl-0.8.29" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/wbfjbk575ny949rfyqpm4vai4ap5rbpp-ctlptl-0.8.29", + "default": true + } + ], + "store_path": "/nix/store/wbfjbk575ny949rfyqpm4vai4ap5rbpp-ctlptl-0.8.29" + } + } + }, + "kubectl@latest": { + "last_modified": "2024-07-07T07:43:47Z", + "resolved": "github:NixOS/nixpkgs/b60793b86201040d9dee019a05089a9150d08b5b#kubectl", + "source": "devbox-search", + "version": "1.30.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/i1zidf41bkfzs2l1pq9fi1frymsfgywc-kubectl-1.30.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/dzxnn9mk9plcx3w9862jyd0nxys2yywz-kubectl-1.30.2-man", + "default": true + }, + { + "name": "convert", + "path": "/nix/store/v9ij5fnxxa02jkzpjvkbxw2jc4p9cbld-kubectl-1.30.2-convert" + } + ], + "store_path": "/nix/store/i1zidf41bkfzs2l1pq9fi1frymsfgywc-kubectl-1.30.2" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/k7ql4247qs6ny27m3iz5c9xf5gb248a2-kubectl-1.30.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/wy64r4nn3isydw4nx257h95qy2x2z4mx-kubectl-1.30.2-man", + "default": true + }, + { + "name": "convert", + "path": "/nix/store/ic8za302hvb4kf4zrs55ivr4q2n2lznn-kubectl-1.30.2-convert" + } + ], + "store_path": "/nix/store/k7ql4247qs6ny27m3iz5c9xf5gb248a2-kubectl-1.30.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/v029n959l5b289br0cq591b04yc48516-kubectl-1.30.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/0dvcxn7gsi2ycy9blb7pcy506w4xp2vi-kubectl-1.30.2-man", + "default": true + }, + { + "name": "convert", + "path": "/nix/store/2nfq4ivwa4a7jwc0183f2wpl1jxbn754-kubectl-1.30.2-convert" + } + ], + "store_path": "/nix/store/v029n959l5b289br0cq591b04yc48516-kubectl-1.30.2" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/3vkf0406s1i6l89hk5wrakh4bbn0p1p2-kubectl-1.30.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/3wbvgkkka1knkxvyr4c8qbpr448smw8i-kubectl-1.30.2-man", + "default": true + }, + { + "name": "convert", + "path": "/nix/store/h5zxz8db6wligwhw5gnwk4gbc8j1ixik-kubectl-1.30.2-convert" + } + ], + "store_path": "/nix/store/3vkf0406s1i6l89hk5wrakh4bbn0p1p2-kubectl-1.30.2" + } + } + }, + "nodejs@20": { + "last_modified": "2024-07-07T07:43:47Z", + "plugin_version": "0.0.2", + "resolved": "github:NixOS/nixpkgs/b60793b86201040d9dee019a05089a9150d08b5b#nodejs_20", + "source": "devbox-search", + "version": "20.14.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/sqnbldm1fjw88v23yq4v6531y4m7v2fh-nodejs-20.14.0", + "default": true + }, + { + "name": "libv8", + "path": "/nix/store/1i0rb2axkrxvsq5pz8s2q07ard2p36a1-nodejs-20.14.0-libv8" + } + ], + "store_path": "/nix/store/sqnbldm1fjw88v23yq4v6531y4m7v2fh-nodejs-20.14.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/r1nwmlbsn67f5rhayr7jjjdmiflxpk92-nodejs-20.14.0", + "default": true + }, + { + "name": "libv8", + "path": "/nix/store/5ii3xkbd3iv0xvqqvjg3agsm0dinidgm-nodejs-20.14.0-libv8" + } + ], + "store_path": "/nix/store/r1nwmlbsn67f5rhayr7jjjdmiflxpk92-nodejs-20.14.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/wzgnws4r1c98vzj5q6gq4drz2jfq7d5q-nodejs-20.14.0", + "default": true + }, + { + "name": "libv8", + "path": "/nix/store/gc2gnkc8hvkh51ab3a29fvgzy2qsqb2s-nodejs-20.14.0-libv8" + } + ], + "store_path": "/nix/store/wzgnws4r1c98vzj5q6gq4drz2jfq7d5q-nodejs-20.14.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ilkfhnqz4xczrliqjva8770x2svbfznd-nodejs-20.14.0", + "default": true + }, + { + "name": "libv8", + "path": "/nix/store/2qaf68dzimr8as4bgli0xmsn11c0ah2j-nodejs-20.14.0-libv8" + } + ], + "store_path": "/nix/store/ilkfhnqz4xczrliqjva8770x2svbfznd-nodejs-20.14.0" + } + } + }, + "tilt@latest": { + "last_modified": "2024-07-15T21:47:20Z", + "resolved": "github:NixOS/nixpkgs/b2c1f10bfbb3f617ea8e8669ac13f3f56ceb2ea2#tilt", + "source": "devbox-search", + "version": "0.33.17", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/l19qinywsx7y86xp8vgwr3bgnbi0rfcj-tilt-0.33.17", + "default": true + } + ], + "store_path": "/nix/store/l19qinywsx7y86xp8vgwr3bgnbi0rfcj-tilt-0.33.17" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/f6swxifmvnxjxifxyw4k4aiyxh0dgfyq-tilt-0.33.17", + "default": true + } + ], + "store_path": "/nix/store/f6swxifmvnxjxifxyw4k4aiyxh0dgfyq-tilt-0.33.17" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/d59rlsabcqxax6bgw6d30zhmflw65ch0-tilt-0.33.17", + "default": true + } + ], + "store_path": "/nix/store/d59rlsabcqxax6bgw6d30zhmflw65ch0-tilt-0.33.17" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/qfv96sjcsslynqbilwj823x8nxvgj5cv-tilt-0.33.17", + "default": true + } + ], + "store_path": "/nix/store/qfv96sjcsslynqbilwj823x8nxvgj5cv-tilt-0.33.17" + } + } + } + } +} diff --git a/packages/capture/README.md b/packages/capture/README.md index 74f9b56..8d9fd89 100644 --- a/packages/capture/README.md +++ b/packages/capture/README.md @@ -15,6 +15,10 @@ Worker container runs a Graphile Worker which listens for specific tasks related ## Misc dev notes +### idea for taking snapshots of stream in progress + +https://ofarukcaki.medium.com/producing-real-time-video-with-node-js-and-ffmpeg-a59ac27461a1 + ### youtube-dl end of stream output The end-of-stream output from yt-dlp when recording CB looks like this diff --git a/packages/capture/package.json b/packages/capture/package.json index 5e3df01..60aa4d3 100644 --- a/packages/capture/package.json +++ b/packages/capture/package.json @@ -21,6 +21,7 @@ "@futureporn/utils": "workspace:^", "@paralleldrive/cuid2": "^2.2.2", "@types/chai": "^4.3.16", + "@types/fluent-ffmpeg": "^2.1.24", "@types/mocha": "^10.0.7", "@types/node": "^20.14.11", "diskusage": "^1.2.0", @@ -46,8 +47,12 @@ "youtube-dl-wrap": "github:insanity54/youtube-dl-wrap" }, "devDependencies": { + "@smithy/util-stream": "^3.1.2", "@types/sinon": "^17.0.3", "@types/sinon-chai": "^3.2.12", + "aws-sdk": "^2.1663.0", + "aws-sdk-client-mock": "^4.0.1", + "aws-sdk-mock": "^6.0.4", "chai": "^4.4.1", "cheerio": "1.0.0-rc.12", "mocha": "^10.7.0", @@ -55,6 +60,8 @@ "node-abort-controller": "^3.1.1", "node-fetch": "^3.3.2", "nodemon": "^2.0.22", + "pretty-bytes": "^6.1.1", + "s3": "link:aws-sdk/clients/s3", "sinon": "^15.2.0", "sinon-chai": "^3.7.0", "sinon-test": "^3.1.6", diff --git a/packages/capture/pnpm-lock.yaml b/packages/capture/pnpm-lock.yaml index fb62aa7..819b26d 100644 --- a/packages/capture/pnpm-lock.yaml +++ b/packages/capture/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@types/chai': specifier: ^4.3.16 version: 4.3.16 + '@types/fluent-ffmpeg': + specifier: ^2.1.24 + version: 2.1.24 '@types/mocha': specifier: ^10.0.7 version: 10.0.7 @@ -99,12 +102,24 @@ importers: specifier: github:insanity54/youtube-dl-wrap version: https://codeload.github.com/insanity54/youtube-dl-wrap/tar.gz/b47e9388063b4ef516624112ef59b7d11f7775d9 devDependencies: + '@smithy/util-stream': + specifier: ^3.1.2 + version: 3.1.2 '@types/sinon': specifier: ^17.0.3 version: 17.0.3 '@types/sinon-chai': specifier: ^3.2.12 version: 3.2.12 + aws-sdk: + specifier: ^2.1663.0 + version: 2.1663.0 + aws-sdk-client-mock: + specifier: ^4.0.1 + version: 4.0.1 + aws-sdk-mock: + specifier: ^6.0.4 + version: 6.0.4 chai: specifier: ^4.4.1 version: 4.4.1 @@ -126,6 +141,12 @@ importers: nodemon: specifier: ^2.0.22 version: 2.0.22 + pretty-bytes: + specifier: ^6.1.1 + version: 6.1.1 + s3: + specifier: link:aws-sdk/clients/s3 + version: link:aws-sdk/clients/s3 sinon: specifier: ^15.2.0 version: 15.2.0 @@ -826,6 +847,9 @@ packages: '@smithy/fetch-http-handler@3.2.2': resolution: {integrity: sha512-3LaWlBZObyGrOOd7e5MlacnAKEwFBmAeiW/TOj2eR9475Vnq30uS2510+tnKbxrGjROfNdOhQqGo5j3sqLT6bA==} + '@smithy/fetch-http-handler@3.2.3': + resolution: {integrity: sha512-m4dzQeafWi5KKCCnDwGGHYk9lqcLs9LvlXZRB0J38DMectsEbxdiO/Rx1NaYYMIkath7AnjpR+r0clL+7dwclQ==} + '@smithy/hash-blob-browser@3.1.2': resolution: {integrity: sha512-hAbfqN2UbISltakCC2TP0kx4LqXBttEv2MqSPE98gVuDFMf05lU+TpC41QtqGP3Ff5A3GwZMPfKnEy0VmEUpmg==} @@ -879,6 +903,10 @@ packages: resolution: {integrity: sha512-UiKZm8KHb/JeOPzHZtRUfyaRDO1KPKPpsd7iplhiwVGOeVdkiVJ5bVe7+NhWREMOKomrDIDdSZyglvMothLg0Q==} engines: {node: '>=16.0.0'} + '@smithy/node-http-handler@3.1.4': + resolution: {integrity: sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==} + engines: {node: '>=16.0.0'} + '@smithy/property-provider@3.1.3': resolution: {integrity: sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==} engines: {node: '>=16.0.0'} @@ -887,6 +915,10 @@ packages: resolution: {integrity: sha512-fAA2O4EFyNRyYdFLVIv5xMMeRb+3fRKc/Rt2flh5k831vLvUmNFXcydeg7V3UeEhGURJI4c1asmGJBjvmF6j8Q==} engines: {node: '>=16.0.0'} + '@smithy/protocol-http@4.1.0': + resolution: {integrity: sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==} + engines: {node: '>=16.0.0'} + '@smithy/querystring-builder@3.0.3': resolution: {integrity: sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==} engines: {node: '>=16.0.0'} @@ -965,8 +997,8 @@ packages: resolution: {integrity: sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==} engines: {node: '>=16.0.0'} - '@smithy/util-stream@3.1.1': - resolution: {integrity: sha512-EhRnVvl3AhoHAT2rGQ5o+oSDRM/BUSMPLZZdRJZLcNVUsFAjOs4vHaPdNQivTSzRcFxf5DA4gtO46WWU2zimaw==} + '@smithy/util-stream@3.1.2': + resolution: {integrity: sha512-08zDzB7BqvybHfZKnav5lL1UniFDK6o6nZ3OWp60PKsi/na2LpU6OX8MCtDNVaPBpKpc8EH26fvFhNT6wvMlbw==} engines: {node: '>=16.0.0'} '@smithy/util-uri-escape@3.0.0': @@ -994,6 +1026,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/fluent-ffmpeg@2.1.24': + resolution: {integrity: sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==} + '@types/interpret@1.1.3': resolution: {integrity: sha512-uBaBhj/BhilG58r64mtDb/BEdH51HIQLgP5bmWzc5qCtFMja8dCk/IOJmk36j0lbi9QHwI6sbtUNGuqXdKCAtQ==} @@ -1018,6 +1053,9 @@ packages: '@types/sinon-chai@3.2.12': resolution: {integrity: sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==} + '@types/sinon@10.0.20': + resolution: {integrity: sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg==} + '@types/sinon@17.0.3': resolution: {integrity: sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==} @@ -1087,10 +1125,18 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -1107,9 +1153,24 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + avvio@8.3.2: resolution: {integrity: sha512-st8e519GWHa/azv8S87mcJvZs4WsgTBjOw/Ih1CP6u+8SZvcOeAYNG6JbsIrAUUJJ7JfmrnOkR8ipDS+u9SIRQ==} + aws-sdk-client-mock@4.0.1: + resolution: {integrity: sha512-yD2mmgy73Xce097G5hIpr1k7j50qzvJ49/+6osGZiCyk4m6cwhb+2x7kKFY1gEMwTzaS8+m8fXv9SB29SkRYyQ==} + + aws-sdk-mock@6.0.4: + resolution: {integrity: sha512-xcvjYQ548cOAoHWKLk6VKS6U1KjeJdBY/vFPROk+kktMUzRfVkb9PnUzcZdBitohbRFg2fWj2OBW9xcwKbESmA==} + engines: {node: '>=18.0.0'} + + aws-sdk@2.1663.0: + resolution: {integrity: sha512-zgXHOw0sBhYcw/yC2YKPLEMNTLnglYLB5UzhAYYShFgOng2NvxkrkuqGFFQ9+haMx2zx6t6CgeqQ7nT0TFUf/g==} + engines: {node: '>= 10.0.0'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1139,6 +1200,9 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + buffer@5.6.0: resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} @@ -1155,6 +1219,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1268,6 +1336,18 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -1296,6 +1376,14 @@ packages: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -1350,6 +1438,30 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} @@ -1379,6 +1491,10 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + events@1.1.1: + resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} + engines: {node: '>=0.4.x'} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -1477,6 +1593,9 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + foreground-child@3.2.1: resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} engines: {node: '>=14'} @@ -1497,6 +1616,16 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1504,10 +1633,18 @@ packages: get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + get-tsconfig@4.7.6: resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} @@ -1524,10 +1661,17 @@ packages: engines: {node: '>=12'} deprecated: Glob versions prior to v9 are no longer supported + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + graphile-config@0.0.1-beta.9: resolution: {integrity: sha512-7vNxXZ24OAgXxDKXYi9JtgWPMuNbBL3057Yf32Ux+/rVP4+EePgySCc+NNnn0tORi8qwqVreN8bdWqGIcSwNXg==} engines: {node: '>=16'} @@ -1537,6 +1681,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1545,6 +1692,25 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -1569,6 +1735,9 @@ packages: resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} engines: {node: '>=12.20.0'} + ieee754@1.1.13: + resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1590,6 +1759,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + interpret@3.1.1: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} @@ -1602,16 +1775,43 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1620,10 +1820,22 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1632,6 +1844,14 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1640,16 +1860,41 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1789,6 +2034,9 @@ packages: nise@5.1.9: resolution: {integrity: sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==} + nise@6.0.0: + resolution: {integrity: sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==} + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -1824,6 +2072,18 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -1967,6 +2227,10 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -2024,6 +2288,10 @@ packages: resolution: {integrity: sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==} engines: {node: '>=12'} + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} @@ -2044,10 +2312,18 @@ packages: pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -2084,6 +2360,10 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2132,9 +2412,17 @@ packages: rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + safe-regex2@3.1.0: resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} @@ -2142,6 +2430,9 @@ packages: resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} engines: {node: '>=10'} + sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -2168,6 +2459,14 @@ packages: set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2176,6 +2475,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2205,6 +2508,12 @@ packages: resolution: {integrity: sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==} deprecated: 16.1.1 + sinon@16.1.3: + resolution: {integrity: sha512-mjnWWeyxcAf9nC0bXcPmiDut+oE8HYridTNzBbF98AYVLmWwGRp2ISEpyhYflG1ifILT+eNn3BmKUJPxjXUPlA==} + + sinon@18.0.0: + resolution: {integrity: sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2243,6 +2552,17 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2318,6 +2638,10 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + traverse@0.6.9: + resolution: {integrity: sha512-7bBrcF+/LQzSgFmT0X5YclVqQxtv7TDJ1f8Wj7ibBu/U6BMLeOpUxuZjV7rMc44UtKxlnMFigdhFAIszSX1DMg==} + engines: {node: '>= 0.4'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2363,11 +2687,34 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + + typedarray.prototype.slice@1.0.3: + resolution: {integrity: sha512-8WbVAQAUlENo1q3c3zZYuy5k9VzBQvp8AX9WOtbvyWlLM1v5JaSRmjubLjzHF4JFtptjH/5c/i95yaElvcjC0A==} + engines: {node: '>= 0.4'} + typescript@5.5.3: resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} engines: {node: '>=14.17'} hasBin: true + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} @@ -2381,9 +2728,19 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url@0.10.3: + resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + uuid@8.0.0: + resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -2406,6 +2763,13 @@ packages: whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -2437,6 +2801,14 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -2578,7 +2950,7 @@ snapshots: '@smithy/util-defaults-mode-node': 3.0.11 '@smithy/util-endpoints': 2.0.5 '@smithy/util-retry': 3.0.3 - '@smithy/util-stream': 3.1.1 + '@smithy/util-stream': 3.1.2 '@smithy/util-utf8': 3.0.0 '@smithy/util-waiter': 3.1.2 tslib: 2.6.3 @@ -2744,7 +3116,7 @@ snapshots: '@smithy/protocol-http': 4.0.4 '@smithy/smithy-client': 3.1.9 '@smithy/types': 3.3.0 - '@smithy/util-stream': 3.1.1 + '@smithy/util-stream': 3.1.2 tslib: 2.6.3 '@aws-sdk/credential-provider-ini@3.616.0(@aws-sdk/client-sso-oidc@3.616.0(@aws-sdk/client-sts@3.616.0))(@aws-sdk/client-sts@3.616.0)': @@ -2888,7 +3260,7 @@ snapshots: '@smithy/smithy-client': 3.1.9 '@smithy/types': 3.3.0 '@smithy/util-config-provider': 3.0.0 - '@smithy/util-stream': 3.1.1 + '@smithy/util-stream': 3.1.2 '@smithy/util-utf8': 3.0.0 tslib: 2.6.3 @@ -3363,6 +3735,14 @@ snapshots: '@smithy/util-base64': 3.0.0 tslib: 2.6.3 + '@smithy/fetch-http-handler@3.2.3': + dependencies: + '@smithy/protocol-http': 4.1.0 + '@smithy/querystring-builder': 3.0.3 + '@smithy/types': 3.3.0 + '@smithy/util-base64': 3.0.0 + tslib: 2.6.3 + '@smithy/hash-blob-browser@3.1.2': dependencies: '@smithy/chunked-blob-reader': 3.0.0 @@ -3455,6 +3835,14 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.6.3 + '@smithy/node-http-handler@3.1.4': + dependencies: + '@smithy/abort-controller': 3.1.1 + '@smithy/protocol-http': 4.1.0 + '@smithy/querystring-builder': 3.0.3 + '@smithy/types': 3.3.0 + tslib: 2.6.3 + '@smithy/property-provider@3.1.3': dependencies: '@smithy/types': 3.3.0 @@ -3465,6 +3853,11 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.6.3 + '@smithy/protocol-http@4.1.0': + dependencies: + '@smithy/types': 3.3.0 + tslib: 2.6.3 + '@smithy/querystring-builder@3.0.3': dependencies: '@smithy/types': 3.3.0 @@ -3501,7 +3894,7 @@ snapshots: '@smithy/middleware-stack': 3.0.3 '@smithy/protocol-http': 4.0.4 '@smithy/types': 3.3.0 - '@smithy/util-stream': 3.1.1 + '@smithy/util-stream': 3.1.2 tslib: 2.6.3 '@smithy/types@3.3.0': @@ -3581,10 +3974,10 @@ snapshots: '@smithy/types': 3.3.0 tslib: 2.6.3 - '@smithy/util-stream@3.1.1': + '@smithy/util-stream@3.1.2': dependencies: - '@smithy/fetch-http-handler': 3.2.2 - '@smithy/node-http-handler': 3.1.3 + '@smithy/fetch-http-handler': 3.2.3 + '@smithy/node-http-handler': 3.1.4 '@smithy/types': 3.3.0 '@smithy/util-base64': 3.0.0 '@smithy/util-buffer-from': 3.0.0 @@ -3620,6 +4013,10 @@ snapshots: '@types/estree@1.0.5': {} + '@types/fluent-ffmpeg@2.1.24': + dependencies: + '@types/node': 20.14.11 + '@types/interpret@1.1.3': dependencies: '@types/node': 20.14.11 @@ -3647,6 +4044,10 @@ snapshots: '@types/chai': 4.3.16 '@types/sinon': 17.0.3 + '@types/sinon@10.0.20': + dependencies: + '@types/sinonjs__fake-timers': 8.1.5 + '@types/sinon@17.0.3': dependencies: '@types/sinonjs__fake-timers': 8.1.5 @@ -3701,8 +4102,24 @@ snapshots: argparse@2.0.1: {} + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + array-union@2.1.0: {} + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + asap@2.0.6: {} assertion-error@1.1.0: {} @@ -3713,11 +4130,40 @@ snapshots: atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + avvio@8.3.2: dependencies: '@fastify/error': 3.4.1 fastq: 1.17.1 + aws-sdk-client-mock@4.0.1: + dependencies: + '@types/sinon': 10.0.20 + sinon: 16.1.3 + tslib: 2.6.3 + + aws-sdk-mock@6.0.4: + dependencies: + aws-sdk: 2.1663.0 + sinon: 18.0.0 + traverse: 0.6.9 + + aws-sdk@2.1663.0: + dependencies: + buffer: 4.9.2 + events: 1.1.1 + ieee754: 1.1.13 + jmespath: 0.16.0 + querystring: 0.2.0 + sax: 1.2.1 + url: 0.10.3 + util: 0.12.5 + uuid: 8.0.0 + xml2js: 0.6.2 + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -3743,6 +4189,12 @@ snapshots: browser-stdout@1.3.1: {} + buffer@4.9.2: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + buffer@5.6.0: dependencies: base64-js: 1.5.1 @@ -3760,6 +4212,14 @@ snapshots: cac@6.7.14: {} + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + callsites@3.1.0: {} camelcase@6.3.0: {} @@ -3902,6 +4362,24 @@ snapshots: data-uri-to-buffer@4.0.1: {} + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dateformat@4.6.3: {} debug@3.2.7(supports-color@5.5.0): @@ -3922,6 +4400,18 @@ snapshots: dependencies: type-detect: 4.0.8 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + denque@2.1.0: {} diff@5.2.0: {} @@ -3973,6 +4463,77 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract@1.23.3: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.2 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + es6-promise@4.2.8: {} esbuild@0.21.5: @@ -4036,6 +4597,8 @@ snapshots: event-target-shim@5.0.1: {} + events@1.1.1: {} + events@3.3.0: {} execa@5.1.1: @@ -4173,6 +4736,10 @@ snapshots: fn.name@1.1.0: {} + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + foreground-child@3.2.1: dependencies: cross-spawn: 7.0.3 @@ -4189,12 +4756,37 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + get-caller-file@2.0.5: {} get-func-name@2.0.2: {} + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + get-stream@6.0.1: {} + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + get-tsconfig@4.7.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -4220,6 +4812,11 @@ snapshots: minimatch: 5.1.6 once: 1.4.0 + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -4229,6 +4826,10 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + graphile-config@0.0.1-beta.9: dependencies: '@types/interpret': 1.1.3 @@ -4259,10 +4860,28 @@ snapshots: - supports-color - typescript + has-bigints@1.0.2: {} + has-flag@3.0.0: {} has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + he@1.2.0: {} help-me@5.0.0: {} @@ -4282,6 +4901,8 @@ snapshots: human-signals@3.0.1: {} + ieee754@1.1.13: {} + ieee754@1.2.1: {} ignore-by-default@1.0.1: {} @@ -4300,6 +4921,12 @@ snapshots: inherits@2.0.4: {} + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + interpret@3.1.1: {} ioredis@5.4.1: @@ -4318,32 +4945,100 @@ snapshots: ipaddr.js@1.9.1: {} + is-arguments@1.1.1: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + is-arrayish@0.2.1: {} is-arrayish@0.3.2: {} + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-negative-zero@2.0.3: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-plain-obj@2.1.0: {} + is-regex@1.1.4: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + is-stream@2.0.1: {} is-stream@3.0.0: {} + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + is-unicode-supported@0.1.0: {} + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + isexe@2.0.0: {} jackspeak@3.4.3: @@ -4352,6 +5047,8 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jmespath@0.16.0: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -4494,6 +5191,14 @@ snapshots: just-extend: 6.2.0 path-to-regexp: 6.2.2 + nise@6.0.0: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 11.2.2 + '@sinonjs/text-encoding': 0.7.2 + just-extend: 6.2.0 + path-to-regexp: 6.2.2 + node-abort-controller@3.1.1: {} node-domexception@1.0.0: {} @@ -4533,6 +5238,17 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.2: {} + + object-keys@1.1.1: {} + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + obuf@1.1.2: {} on-exit-leak-free@2.1.2: {} @@ -4696,6 +5412,8 @@ snapshots: pirates@4.0.6: {} + possible-typed-array-names@1.0.0: {} + postcss-load-config@6.0.1(tsx@4.16.2): dependencies: lilconfig: 3.1.2 @@ -4726,6 +5444,8 @@ snapshots: postgres@3.4.4: {} + pretty-bytes@6.1.1: {} + process-warning@3.0.0: {} process@0.11.10: {} @@ -4744,8 +5464,12 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + punycode@1.3.2: {} + punycode@2.3.1: {} + querystring@0.2.0: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -4782,6 +5506,13 @@ snapshots: dependencies: redis-errors: 1.2.0 + regexp.prototype.flags@1.5.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -4832,14 +5563,29 @@ snapshots: dependencies: tslib: 2.6.3 + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + safe-regex2@3.1.0: dependencies: ret: 0.4.3 safe-stable-stringify@2.4.3: {} + sax@1.2.1: {} + secure-json-parse@2.7.0: {} semver@5.7.2: {} @@ -4856,12 +5602,35 @@ snapshots: set-cookie-parser@2.6.0: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -4892,6 +5661,24 @@ snapshots: nise: 5.1.9 supports-color: 7.2.0 + sinon@16.1.3: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 10.3.0 + '@sinonjs/samsam': 8.0.0 + diff: 5.2.0 + nise: 5.1.9 + supports-color: 7.2.0 + + sinon@18.0.0: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 11.2.2 + '@sinonjs/samsam': 8.0.0 + diff: 5.2.0 + nise: 6.0.0 + supports-color: 7.2.0 + slash@3.0.0: {} sliced@0.0.5: {} @@ -4932,6 +5719,25 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -5007,6 +5813,12 @@ snapshots: dependencies: punycode: 2.3.1 + traverse@0.6.9: + dependencies: + gopd: 1.0.1 + typedarray.prototype.slice: 1.0.3 + which-typed-array: 1.1.15 + tree-kill@1.2.2: {} triple-beam@1.4.1: {} @@ -5054,8 +5866,56 @@ snapshots: type-detect@4.0.8: {} + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + + typedarray.prototype.slice@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + typed-array-buffer: 1.0.2 + typed-array-byte-offset: 1.0.2 + typescript@5.5.3: {} + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + undefsafe@2.0.5: {} undici-types@5.26.5: {} @@ -5067,8 +5927,23 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + url@0.10.3: + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 + + uuid@8.0.0: {} + uuid@9.0.1: {} web-streams-polyfill@3.3.3: {} @@ -5089,6 +5964,22 @@ snapshots: tr46: 1.0.1 webidl-conversions: 4.0.2 + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 @@ -5133,6 +6024,13 @@ snapshots: wrappy@1.0.2: {} + xml2js@0.6.2: + dependencies: + sax: 1.2.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/packages/capture/src/Record.spec.ts b/packages/capture/src/Record.spec.ts index b81a6cd..0ce4ae7 100644 --- a/packages/capture/src/Record.spec.ts +++ b/packages/capture/src/Record.spec.ts @@ -1,22 +1,67 @@ import Record from "./Record.js" import { expect } from "chai" +import { ChildProcess, spawn } from "child_process" +import { createReadStream, readFileSync, ReadStream } from "fs" +import AWSMock from 'aws-sdk-mock' +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { PutObjectCommand, PutObjectCommandInput, S3Client, CreateMultipartUploadCommand, UploadPartCommand, UploadPartCommandInput } from "@aws-sdk/client-s3" +import { join, dirname } from "path" +import { fileURLToPath } from "url" +import { S3 } from 'aws-sdk'; +import { HeadObjectOutput } from 'aws-sdk/clients/s3'; +import { Readable } from 'stream'; +import { mockClient } from 'aws-sdk-client-mock'; +import { sdkStreamMixin } from '@smithy/util-stream' + + +// "pay no attention to that man behind the curtain" + +// export function getObjectReadStream(s3Client: S3, Bucket: string, Key: string): Readable { +// return s3Client.getObject({ Bucket, Key }).createReadStream(); +// } + +// export async function waitForObjectExists( +// s3Client: S3Client, +// Bucket: string, +// Key: string +// ): Promise { +// return s3Client.waitFor('objectExists', { Bucket, Key }).promise(); +// } +const __dirname = dirname(fileURLToPath(import.meta.url)); + + + +const testStreamDir = '/tmp/record-test-stream.m3u8' describe('Record', function () { - let record: Record - this.beforeEach(function () { - record = new Record({ url: 'https://example.com/my-cool-stream' }) + // mocking @aws-sdk/lib-storage Upload() has some nuances. + // @see https://github.com/m-radzikowski/aws-sdk-client-mock?tab=readme-ov-file#lib-storage-upload + + + it('should accept a {ReadStream} as input', async function () { + const inputStream = createReadStream(join(__dirname, './fixtures/mock-stream0.mp4')) // 192627 bytes + const s3ClientMock = mockClient(S3Client) + s3ClientMock.on(CreateMultipartUploadCommand).resolves({UploadId: '1'}); + s3ClientMock.on(UploadPartCommand).resolves({ETag: '1'}); + const s3Client = new S3Client({ region: 'us-west-000' }) + const record = new Record({ inputStream, s3Client, channel: 'coolguy_69', bucket: 'test' }) + await record.start() + expect(record).to.have.property('counter', 192627) + expect(record).to.have.property('bucket', 'test') }) - describe('start()', function () { - it('should start the recording', async function () { - await record.start() - expect(record).to.have.property('id') - expect(record).to.have.property('url') - }) - }) - describe('stop()', function () { - it('should stop the recording', async function () { - await record.stop() - expect(record).to.have.property('cdnUrl') - }) + + xit('should restart if a EPIPE is encountered', async function () { + // @todo IDK how to implement this. + const inputStream = createReadStream(join(__dirname, './fixtures/mock-stream0.mp4')) + const s3ClientMock = mockClient(S3Client) + s3ClientMock.on(CreateMultipartUploadCommand).resolves({UploadId: '1'}) + s3ClientMock.on(UploadPartCommand).rejectsOnce({cause: 'simulated network interruption'}).resolves({ ETag: '1' }) // this rejection is probably not specific enough to simulate EPIPE + const s3Client = new S3Client({ region: 'us-west-000' }) + const record = new Record({ inputStream, s3Client, channel: 'coolguy_69', bucket: 'test' }) + await record.start() + expect(record).to.have.property('counter', 192627) }) + + }) \ No newline at end of file diff --git a/packages/capture/src/Record.ts b/packages/capture/src/Record.ts index 45b5ab8..a560e49 100644 --- a/packages/capture/src/Record.ts +++ b/packages/capture/src/Record.ts @@ -1,102 +1,184 @@ import { createId } from '@paralleldrive/cuid2' import { spawn } from 'child_process'; import { ua0 } from '@futureporn/scout/ua.js' -import { getTmpFile } from '@futureporn/utils' +import { PassThrough, pipeline, Readable } from 'stream'; +import prettyBytes from 'pretty-bytes'; +import { Upload } from "@aws-sdk/lib-storage"; +import { S3Client } from "@aws-sdk/client-s3"; +import 'dotenv/config' + export interface RecordArgs { - url: string; filename?: string; - channel?: string; + channel: string; + s3Client: S3Client; + bucket: string; date?: string; + inputStream: Readable; +} + +interface MakeS3ClientOptions { + accessKeyId: string; + secretAccessKey: string; + region: string; + endpoint: string +} + +interface getFFmpegDownloadOptions { + url: string; } export default class Record { readonly id: string; - readonly url: string; + private s3Client: S3Client; + private uploadStream: PassThrough; + private ticker?: NodeJS.Timeout; + inputStream: Readable; + counter: number; + bucket: string; + keyName: string; + datestamp: string; filename?: string; - channel?: string; + channel: string; date?: string; - constructor({ url }: RecordArgs) { - if (!url) throw new Error('url passed to Record constructor was undefined.'); + constructor({ inputStream, channel, s3Client, bucket }: RecordArgs) { + if (!inputStream) throw new Error('Record constructor was missing inputStream.'); + if (!bucket) throw new Error('Record constructor was missing bucket.'); + if (!channel) throw new Error('Record constructer was missing channel!'); + if (!s3Client) throw new Error('Record constructer was missing s3Client'); + this.inputStream = inputStream this.id = createId() - this.url = url + this.s3Client = s3Client + this.bucket = bucket + this.channel = channel + this.counter = 0 + this.datestamp = new Date().toISOString() + this.keyName = `${this.datestamp}-${channel}-${createId()}.ts` + this.uploadStream = new PassThrough() } - async start() { - console.log(`@TODO record start with id=${this.id}, url=${this.url}`) + makeProgressTicker() { + this.ticker = setInterval(() => { + console.log(`[progress] ${this.counter} bytes (aggregate) (${prettyBytes(this.counter)}) have passed through the pipeline.`) + }, 1000 * 30) + } - const playlistUrlPromise = new Promise((resolve) => { - const ytdlp = spawn('yt-dlp', [ - '-g', - this.url - ]) - ytdlp.stdout.on('data', (data) => { - resolve(data) - }) + + static makeS3Client({ + accessKeyId, + secretAccessKey, + region, + endpoint + }: MakeS3ClientOptions): S3Client { + const client = new S3Client({ + endpoint, + region, + credentials: { + accessKeyId, + secretAccessKey + } }) - const playlistUrl = await playlistUrlPromise - const filename = getTmpFile(`stream.ts`) + return client + } - const ffmpeg = spawn('ffmpeg', [ + static getFFmpegDownload({ url }: getFFmpegDownloadOptions): Readable { + + + const ffmpegProc = spawn('ffmpeg', [ '-headers', `"User-Agent: ${ua0}"`, - '-i', playlistUrl, + '-i', url, '-c:v', 'copy', '-c:a', 'copy', '-movflags', 'faststart', '-y', '-f', 'mpegts', - filename - ]) - - const ffmpegProcess = spawn('ffmpeg', [ - '-headers', `"User-Agent: ${ua0}"`, - '-i', playlistUrl, - '-c:v', 'copy', - '-c:a', 'copy', - '-movflags', 'faststart', - '-y', - '-f', 'mpegts', - filename + '-loglevel', 'quiet', + 'pipe:1' ], { - stdio: 'inherit' - }); + // ignoring stderr is important because if not, ffmpeg will fill that buffer and node will hang + stdio: ['pipe', 'pipe', 'ignore'] + }) + return ffmpegProc.stdout - const ps = spawn('ps', ['ax']); - const grep = spawn('grep', ['ssh']); - - ps.stdout.on('data', (data) => { - grep.stdin.write(data); - }); - - ps.stderr.on('data', (data) => { - console.error(`ps stderr: ${data}`); - }); - - ps.on('close', (code) => { - if (code !== 0) { - console.log(`ps process exited with code ${code}`); + } + + + async uploadToS3() { + + const target = { + Bucket: this.bucket, + Key: this.keyName, + // We do this to keep TS happy. Body expects a Readable, not a ReadableStream nor a NodeJS.ReadableStream + // Body: new Readable().wrap(this.uploadStream) + Body: this.uploadStream + } + + // greets https://stackoverflow.com/a/70159394/1004931 + try { + const parallelUploads3 = new Upload({ + client: this.s3Client, + partSize: 1024 * 1024 * 5, + queueSize: 1, + leavePartsOnError: false, + params: target, + }); + + + parallelUploads3.on("httpUploadProgress", (progress) => { + console.log(progress) + if (progress?.loaded) { + console.log(`loaded ${progress.loaded} bytes (${prettyBytes(progress.loaded)})`); + } else { + console.log(`httpUploadProgress ${JSON.stringify(progress, null, 2)}`) + } + }); + + await parallelUploads3.done(); + + } catch (e) { + if (e instanceof Error) { + console.error(`while uploading a file to s3, we encountered an error`) + throw new Error(e.message); + } else { + throw new Error(`error of some sort ${JSON.stringify(e, null, 2)}`) } - grep.stdin.end(); - }); + } + } - grep.stdout.on('data', (data) => { - console.log(data.toString()); - }); - grep.stderr.on('data', (data) => { - console.error(`grep stderr: ${data}`); - }); - grep.on('close', (code) => { - if (code !== 0) { - console.log(`grep process exited with code ${code}`); + async start() { + + this.makeProgressTicker() + + // streams setup + this.uploadStream.on('data', (data) => { + this.counter += data.length + }) + + + // stream pipeline setup + pipeline( + this.inputStream, + this.uploadStream, + (err) => { + if (err) { + console.error(`pipeline errored.`) + console.error(err) + } else { + console.log('pipeline succeeded.') + } } - }); + ) + + await this.uploadToS3() + clearInterval(this.ticker) return { id: this.id, - url: this.url + keyName: this.keyName, + channel: this.channel } } diff --git a/packages/capture/src/app.ts b/packages/capture/src/app.ts index 3f8608e..ef402f9 100644 --- a/packages/capture/src/app.ts +++ b/packages/capture/src/app.ts @@ -6,7 +6,8 @@ import graphileWorkerPlugin, { type ExtendedFastifyInstance } from './fastify-gr const version = getPackageVersion('../package.json') interface RecordBodyType { - url: string + url: string; + channel: string; } function build(opts: Record={}, connectionString: string) { @@ -17,9 +18,10 @@ function build(opts: Record={}, connectionString: string) { return { app: '@futureporn/capture', version } }) app.post('/api/record', async function (request: FastifyRequest<{ Body: RecordBodyType }>, reply) { - const { url } = request.body - console.log(`POST /api/record with url=${url}`) - const job = await app.graphile.addJob('record', { url }) + const { url, channel } = request.body + console.log(`POST /api/record with url=${url}, channel=${channel}`) + + const job = await app.graphile.addJob('record', { url, channel }) return job }) return app diff --git a/packages/capture/src/fixtures/mime.types b/packages/capture/src/fixtures/mime.types new file mode 100644 index 0000000..a8dbef1 --- /dev/null +++ b/packages/capture/src/fixtures/mime.types @@ -0,0 +1,2 @@ +application/vnd.apple.mpegurl mp4 +text/x-abc abc \ No newline at end of file diff --git a/packages/capture/src/fixtures/my.abc b/packages/capture/src/fixtures/my.abc new file mode 100644 index 0000000..1f6cac0 --- /dev/null +++ b/packages/capture/src/fixtures/my.abc @@ -0,0 +1 @@ +asdfaslfk;sdf \ No newline at end of file diff --git a/packages/capture/src/index copy.ts b/packages/capture/src/index copy.ts deleted file mode 100644 index 0f8d5a0..0000000 --- a/packages/capture/src/index copy.ts +++ /dev/null @@ -1,77 +0,0 @@ - -'use strict' - -import { build } from './app.js' -import 'dotenv/config' -import { run } from 'graphile-worker' -import { dirname } from 'node:path'; -import { fileURLToPath } from 'url'; -const __dirname = dirname(fileURLToPath(import.meta.url)); - -if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is missing in env'); -if (!process.env.FUNCTION) throw new Error(`FUNCTION env var was missing. FUNCTION env var must be either 'api' or 'worker'.`); -const connectionString = process.env.DATABASE_URL! - - - -async function api() { - if (!process.env.PORT) throw new Error('PORT is missing in env'); - const PORT = parseInt(process.env.PORT!) - - const fastifyOpts = { - logger: { - level: 'info', - transport: { - target: 'pino-pretty' - } - } - } - const server = build(fastifyOpts, connectionString) - - server.listen({ port: PORT }, (err) => { - if (err) { - server.log.error(err) - process.exit(1) - } - }) - -} - -async function worker() { - const concurrency = (process.env?.WORKER_CONCURRENCY) ? parseInt(process.env.WORKER_CONCURRENCY) : 1 - - // Run a worker to execute jobs: - const runner = await run({ - connectionString, - concurrency, - // Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc - noHandleSignals: false, - pollInterval: 1000, - taskDirectory: `${__dirname}/tasks`, - }); - - // Immediately await (or otherwise handle) the resulting promise, to avoid - // "unhandled rejection" errors causing a process crash in the event of - // something going wrong. console.log() - - await runner.promise; - - // If the worker exits (whether through fatal error or otherwise), the above - // promise will resolve/reject. -} - - -async function main() { - if (process.env.FUNCTION === 'api') { - api() - } else if (process.env.FUNCTION === 'worker') { - worker() - } else { - throw new Error('process.env.FUNCTION must be either api or worker. got '+process.env.FUNCTION) - } -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/packages/capture/src/index.api.ts b/packages/capture/src/index.api.ts deleted file mode 100644 index 4924775..0000000 --- a/packages/capture/src/index.api.ts +++ /dev/null @@ -1,80 +0,0 @@ -'use strict' - -import { build } from './app.js' -import 'dotenv/config' -import { run } from 'graphile-worker' -import { dirname } from 'node:path'; -import { fileURLToPath } from 'url'; -const __dirname = dirname(fileURLToPath(import.meta.url)); - -if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is missing in env'); -if (!process.env.FUNCTION) throw new Error(`FUNCTION env var was missing. FUNCTION env var must be either 'api' or 'worker'.`); -const connectionString = process.env.DATABASE_URL! - - - - -async function api() { - if (!process.env.PORT) throw new Error('PORT is missing in env'); - const PORT = parseInt(process.env.PORT!) - - const fastifyOpts = { - logger: { - level: 'info', - transport: { - target: 'pino-pretty' - } - } - } - const server = build(fastifyOpts, connectionString) - - server.listen({ port: PORT }, (err) => { - if (err) { - server.log.error(err) - process.exit(1) - } - }) - -} - - - -async function worker() { - if (!process.env.WORKER_CONCURRENCY) throw new Error('WORKER_CONCURRENCY is missing in env'); - const concurrency = (process.env?.WORKER_CONCURRENCY) ? parseInt(process.env.WORKER_CONCURRENCY) : 1 - - // Run a worker to execute jobs: - const runner = await run({ - connectionString: process.env.DATABASE_URL!, - concurrency, - // Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc - noHandleSignals: false, - pollInterval: 1000, - taskDirectory: `${__dirname}/tasks`, - }); - - // Immediately await (or otherwise handle) the resulting promise, to avoid - // "unhandled rejection" errors causing a process crash in the event of - // something going wrong. console.log() - - await runner.promise; - - // If the worker exits (whether through fatal error or otherwise), the above - // promise will resolve/reject. -} - -async function main() { - if (process.env.FUNCTION === 'worker') { - worker() - } else if (process.env.FUNCTION === 'api') { - api() - } else { - console.error(`FUNCTION environment variable must be 'worker' or 'api', but it was set to ${process.env.FUNCTION}`) - } -} - - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/packages/capture/src/index.worker.ts b/packages/capture/src/index.worker.ts deleted file mode 100644 index e95acb9..0000000 --- a/packages/capture/src/index.worker.ts +++ /dev/null @@ -1,41 +0,0 @@ -'use strict' - -import { run } from 'graphile-worker' -import { dirname } from 'node:path'; -import { fileURLToPath } from 'url'; -const __dirname = dirname(fileURLToPath(import.meta.url)); - -if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is undefined in env'); -const concurrency = (process.env?.WORKER_CONCURRENCY) ? parseInt(process.env.WORKER_CONCURRENCY) : 1 - - - - -async function main() { - // Run a worker to execute jobs: - const runner = await run({ - connectionString: process.env.DATABASE_URL!, - concurrency, - // Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc - noHandleSignals: false, - pollInterval: 1000, - taskDirectory: `${__dirname}/tasks`, - }); - - // Immediately await (or otherwise handle) the resulting promise, to avoid - // "unhandled rejection" errors causing a process crash in the event of - // something going wrong. console.log() - - await runner.promise; - - // If the worker exits (whether through fatal error or otherwise), the above - // promise will resolve/reject. -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); - - - diff --git a/packages/capture/src/poc-lite.js b/packages/capture/src/poc-lite.js new file mode 100644 index 0000000..6bcef30 --- /dev/null +++ b/packages/capture/src/poc-lite.js @@ -0,0 +1,25 @@ +import { createWriteStream } from 'node:fs' +import ffmpeg from 'fluent-ffmpeg' + +// test stream from https://ottverse.com/free-hls-m3u8-test-urls/ +const playlistUrl = 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8' +const fileOutputStream = createWriteStream('/tmp/test-stream.ts') + +ffmpeg() + .input(playlistUrl) + .audioCodec('copy') + .videoCodec('copy') + .addOutputOptions('-movflags faststart') + .output(fileOutputStream) + .format('mpegts') + .on('end', () => { + console.log('Finished'); + }) + .on('error', (err, stdout, stderr) => { + console.error(`there was an error`); + console.error(err); + console.error(stdout); + console.error(stderr); + throw new Error(err.message); + }) + .run(); diff --git a/packages/capture/src/poc-s3-alt.ts b/packages/capture/src/poc-s3-alt.ts new file mode 100644 index 0000000..e4b1a21 --- /dev/null +++ b/packages/capture/src/poc-s3-alt.ts @@ -0,0 +1,135 @@ +import { PassThrough, pipeline, Readable } from "stream"; +import { type Progress, Upload } from "@aws-sdk/lib-storage"; +import { S3Client } from "@aws-sdk/client-s3"; +import { createReadStream, createWriteStream } from 'fs'; +import { createId } from '@paralleldrive/cuid2'; +import prettyBytes from 'pretty-bytes'; +import dotenv from 'dotenv' +dotenv.config({ + path: '../../.env.development' +}) + +if (!process.env.S3_BUCKET_NAME) throw new Error('S3_BUCKET_NAME missing in env'); +if (!process.env.S3_BUCKET_KEY_ID) throw new Error('S3_BUCKET_KEY_ID missing in env'); +if (!process.env.S3_BUCKET_APPLICATION_KEY) throw new Error('S3_BUCKET_APPLICATION_KEY missing in env'); + +async function uploadStream(client: S3Client, stream: NodeJS.ReadableStream, keyName: string) { + // const pass = new PassThrough() + // Create a stream to the S3 bucket. We use this stream to upload the livestream to Backblaze S3 service + console.log(`keyName=${keyName}`) + const target = { + Bucket: process.env.S3_BUCKET_NAME!, + Key: keyName, + Body: new Readable().wrap(stream) + } + console.log(target) + // greets https://stackoverflow.com/a/70159394/1004931 + try { + const parallelUploads3 = new Upload({ + client: client, + partSize: 1024 * 1024 * 5, + // tags: [...], // optional tags + queueSize: 1, // optional concurrency configuration + leavePartsOnError: false, // optional manually handle dropped parts + params: target, + }); + + + parallelUploads3.on("httpUploadProgress", (progress) => { + if (progress?.loaded) { + console.log(`loaded ${progress.loaded} bytes (${prettyBytes(progress.loaded)})`); + } else { + console.log(`httpUploadProgress ${JSON.stringify(progress, null, 2)}`) + } + }); + + const res = await parallelUploads3.done(); + return res + + } catch (e) { + if (e instanceof Error) { + console.error(`while uploading a file to s3, we encountered an error`) + throw new Error(e.message); + } else { + throw new Error(`error of some sort ${JSON.stringify(e, null, 2)}`) + } + } +} + +async function main() { + try { + const client = new S3Client({ + endpoint: 'https://s3.us-west-000.backblazeb2.com', + region: 'us-west-000', + credentials: { + accessKeyId: process.env.S3_BUCKET_KEY_ID!, + secretAccessKey: process.env.S3_BUCKET_APPLICATION_KEY! + } + }); + + + + // let debugCounter = 0 + // let uploadStream = new PassThrough() + // uploadStream.on('data', (data) => { + // debugCounter += data.length + // console.log(`[data] uploadStream. ${debugCounter} aggregated bytes (${prettyBytes(debugCounter)}).`) + // }) + // uploadStream.on('drain', () => { + // console.log('[drain] uploadStream') + // }) + // uploadStream.on('close', () => { + // console.log(`[close] uploadStream closed`) + // }) + // uploadStream.on('error', (err) => { + // console.error('[error] uploadStream had an error as follows') + // console.error(err) + // }) + // uploadStream.on('exit', (code) => { + // console.log(`[exit] uploadStream exited with code ${code}`) + // }) + // uploadStream.on('disconnect', () => { + // console.log('[disconnect] uploadStream disconnected') + // }) + // uploadStream.on('message', (msg) => { + // console.log('[message] uploadStream sent a message as follows') + // console.log(msg) + // }) + + + + const fileStream = createReadStream('/home/cj/Downloads/stream-23894234.ts') + const datestamp = new Date().toISOString() + const keyName = `${datestamp}-stream3-chaturbate-${createId()}.ts` + await uploadStream(client, fileStream, keyName) + + // // we set up a pipeline which has an readable stream (ffmpeg), a transform stream (debug), and a writable stream (s3 Upload) + // pipeline( + // fileStream, + // uploadStream, + // (err) => { + // if (err) { + // console.error(`pipeline errored.`) + // console.error(err) + // } else { + // console.log('pipeline succeeded.') + // } + // } + // ) + + + + } catch (e) { + if (e instanceof Error) { + console.error(`Eyy lookat me, I'm a big nastry try/catch block and I did my jorb!`) + console.error(e) + } else { + console.error('err or some sort') + console.error(e) + } + } + +} +main() + + diff --git a/packages/capture/src/poc-s3.ts b/packages/capture/src/poc-s3.ts new file mode 100644 index 0000000..ea592b2 --- /dev/null +++ b/packages/capture/src/poc-s3.ts @@ -0,0 +1,122 @@ +import { PassThrough, pipeline, Readable } from "stream"; +import { Upload } from "@aws-sdk/lib-storage"; +import { S3Client } from "@aws-sdk/client-s3"; +import { createReadStream } from 'fs'; +import { createId } from '@paralleldrive/cuid2'; +import prettyBytes from 'pretty-bytes'; +import dotenv from 'dotenv' +dotenv.config({ + path: '../../.env.development' +}) + +if (!process.env.S3_BUCKET_NAME) throw new Error('S3_BUCKET_NAME missing in env'); +if (!process.env.S3_BUCKET_KEY_ID) throw new Error('S3_BUCKET_KEY_ID missing in env'); +if (!process.env.S3_BUCKET_APPLICATION_KEY) throw new Error('S3_BUCKET_APPLICATION_KEY missing in env'); + +function makeProgressTicker(counter: number) { + const ticker = setInterval(() => { + console.log(`[progress] ${counter} bytes (aggregate) (${prettyBytes(counter)}) have passed through the pipeline.`) + }, 1000 * 30) + return ticker +} + +function makeS3Client() { + const client = new S3Client({ + endpoint: 'https://s3.us-west-000.backblazeb2.com', + region: 'us-west-000', + credentials: { + accessKeyId: process.env.S3_BUCKET_KEY_ID!, + secretAccessKey: process.env.S3_BUCKET_APPLICATION_KEY! + } + }) + return client +} + + +async function uploadToS3({ client, uploadStream, keyName }: { client: S3Client, uploadStream: NodeJS.ReadableStream, keyName: string }) { + + const target = { + Bucket: process.env.S3_BUCKET_NAME!, + Key: keyName, + Body: new Readable().wrap(uploadStream) + } + + // greets https://stackoverflow.com/a/70159394/1004931 + try { + const parallelUploads3 = new Upload({ + client: client, + partSize: 1024 * 1024 * 5, + queueSize: 1, + leavePartsOnError: false, + params: target, + }); + + + parallelUploads3.on("httpUploadProgress", (progress) => { + console.log(progress) + if (progress?.loaded) { + console.log(`loaded ${progress.loaded} bytes (${prettyBytes(progress.loaded)})`); + } else { + console.log(`httpUploadProgress ${JSON.stringify(progress, null, 2)}`) + } + }); + + await parallelUploads3.done(); + + } catch (e) { + if (e instanceof Error) { + console.error(`while uploading a file to s3, we encountered an error`) + throw new Error(e.message); + } else { + throw new Error(`error of some sort ${JSON.stringify(e, null, 2)}`) + } + } +} + +async function main() { + + let counter = 0 + const client = makeS3Client() + const ticker = makeProgressTicker(counter) + const datestamp = new Date().toISOString() + const keyName = `${datestamp}-stream3-chaturbate-${createId()}.ts` + console.log(`Uploading ${keyName} to S3`) + + /** + * setup the streams which process the data + */ + const ffmpegStream = createReadStream('/home/cj/Downloads/stream-23894234.ts') + const uploadStream = new PassThrough() + + // update the progress ticker data + uploadStream.on('data', (data) => { + counter += data.length + }) + + /** + * we set up a pipeline which has an readable stream (ffmpeg), a transform stream (debug), and a writable stream (s3 Upload) + */ + pipeline( + ffmpegStream, + uploadStream, + (err) => { + if (err) { + console.error(`pipeline errored.`) + console.error(err) + } else { + console.log('pipeline succeeded.') + } + } + ) + + + await uploadToS3({client, uploadStream, keyName }) + clearInterval(ticker) + + +} + + +main() + + diff --git a/packages/capture/src/poc.ts b/packages/capture/src/poc.ts new file mode 100644 index 0000000..a272579 --- /dev/null +++ b/packages/capture/src/poc.ts @@ -0,0 +1,178 @@ +/** + * + * @todo if we have the ffmpeg stream send an end event, does the ffmpegStream close? + * so far, we have observed the end of a CB stream, and the uploadStream is what shows as closed. + * It would be better to have the ffmpeg stream do the closing, amirite? or does it even matter? + * Here's what the console.log shows when the CB stream ended while we were not using { end: true } on the ffmpeg stream + * + * + * [data] uploadStream. 118018880 aggregated bytes (118 MB). + [data] uploadStream. 118067384 aggregated bytes (118 MB). + [data] uploadStream. 118101224 aggregated bytes (118 MB). + [data] uploadStream. 118119648 aggregated bytes (118 MB). + [close] uploadStream closed + pipeline succeeded. + */ + + + +import { getRandomRoom } from '@futureporn/scout/cb.js' +import { ua0 } from "@futureporn/scout/ua.js"; +import { spawn } from "child_process"; +import { PassThrough, pipeline } from "stream"; +import { type Progress, Upload } from "@aws-sdk/lib-storage"; +import { S3Client } from "@aws-sdk/client-s3"; +import { createWriteStream } from 'fs'; +import ffmpeg from 'fluent-ffmpeg' +import { createId } from '@paralleldrive/cuid2'; +import prettyBytes from 'pretty-bytes'; +import dotenv from 'dotenv' +dotenv.config({ + path: '../../.env.development' +}) + +if (!process.env.S3_BUCKET_NAME) throw new Error('S3_BUCKET_NAME missing in env'); +if (!process.env.S3_BUCKET_KEY_ID) throw new Error('S3_BUCKET_KEY_ID missing in env'); +if (!process.env.S3_BUCKET_APPLICATION_KEY) throw new Error('S3_BUCKET_APPLICATION_KEY missing in env'); + +async function main() { + const client = new S3Client({ + endpoint: 'https://s3.us-west-000.backblazeb2.com', + region: 'us-west-000', + credentials: { + accessKeyId: process.env.S3_BUCKET_KEY_ID!, + secretAccessKey: process.env.S3_BUCKET_APPLICATION_KEY! + } + }); + + + const randomRoom = await getRandomRoom() + console.log(`randomRoom=${randomRoom.name}`) + + const playlistUrl: string = await new Promise((resolve, reject) => { + + + // get the m3u8 playlist for the livestream + const ytdlp = spawn('yt-dlp', [ '-g', randomRoom.url ]) + let output = '' + ytdlp.on('error', (err) => { + console.error(err) + }) + ytdlp.once('exit', (code) => { + console.log(`code=${code}`) + if (code !== 0) reject(`yt-dlp exited with code ${code}. stderr as follows ${JSON.stringify(ytdlp.stderr, null, 2)}`); + resolve(output) + }) + ytdlp.stderr.on('data', (data) => { + console.error('stderr data as follows') + console.error(data.toString()) + }) + ytdlp.stdout.on('data', (data) => { + output = data + }) + + }) + console.log(`playlistUrl=${playlistUrl}`) + if (!playlistUrl) throw new Error(`failed to get playlistUrl from yt-dlp -g ${randomRoom.url}`); + + + let debugCounter = 0 + let fileOutputStream = createWriteStream('/home/cj/Downloads/outputfile.ts'); + // let ffmpegLogStream = createWriteStream('/tmp/ffmpeg-log.txt') + let uploadStream = new PassThrough() + uploadStream.on('data', (data) => { + debugCounter += data.length + console.log(`[data] uploadStream. ${debugCounter} aggregated bytes (${prettyBytes(debugCounter)}).`) + }) + uploadStream.on('drain', () => { + console.log('[drain] uploadStream') + }) + uploadStream.on('close', () => { + console.log(`[close] uploadStream closed`) + }) + uploadStream.on('error', (err) => { + console.error('[error] uploadStream had an error as follows') + console.error(err) + }) + uploadStream.on('exit', (code) => { + console.log(`[exit] uploadStream exited with code ${code}`) + }) + uploadStream.on('disconnect', () => { + console.log('[disconnect] uploadStream disconnected') + }) + uploadStream.on('message', (msg) => { + console.log('[message] uploadStream sent a message as follows') + console.log(msg) + }) + + + const datestamp = new Date().toISOString() + + + + const ffmpegProc = spawn('ffmpeg', [ + '-headers', `"User-Agent: ${ua0}"`, + '-i', playlistUrl, + '-c:v', 'copy', + '-c:a', 'copy', + '-movflags', 'faststart', + '-y', + '-f', 'mpegts', + '-loglevel', 'quiet', + 'pipe:1' + ], { + // ignoring stderr is important because if not, ffmpeg will fill that buffer and node will hang + stdio: ['pipe', 'pipe', 'ignore'] + }) + + + // we set up a pipeline which has an readable stream (ffmpeg), a transform stream (debug), and a writable stream (s3 Upload) + pipeline( + ffmpegProc.stdout, + uploadStream, + (err) => { + if (err) { + console.error(`pipeline errored.`) + console.error(err) + } else { + console.log('pipeline succeeded.') + } + } + ) + + // Create a stream to the S3 bucket. We use this stream to upload the livestream to Backblaze S3 service + const keyName = `${datestamp}-${randomRoom.name}-chaturbate-${createId()}.ts` + console.log(`keyName=${keyName}`) + const target = { + Bucket: process.env.S3_BUCKET_NAME, + Key: keyName, + Body: uploadStream + } + // greets https://stackoverflow.com/a/70159394/1004931 + try { + const parallelUploads3 = new Upload({ + client: client, + //tags: [...], // optional tags + queueSize: 4, // optional concurrency configuration + leavePartsOnError: false, // optional manually handle dropped parts + params: target, + }); + + await parallelUploads3.done(); + + } catch (e) { + if (e instanceof Error) { + console.error(`while uploading a file to s3, we encountered an error`) + throw new Error(e.message); + } else { + throw new Error(`error of some sort ${JSON.stringify(e, null, 2)}`) + } + } + + + + +} +main() + + diff --git a/packages/capture/src/record.js b/packages/capture/src/record.js.old similarity index 100% rename from packages/capture/src/record.js rename to packages/capture/src/record.js.old diff --git a/packages/capture/src/tasks/record.ts b/packages/capture/src/tasks/record.ts index cb045fd..9ff9e2e 100644 --- a/packages/capture/src/tasks/record.ts +++ b/packages/capture/src/tasks/record.ts @@ -1,17 +1,38 @@ import { type Helpers } from 'graphile-worker' import Record from '../Record.ts' +import 'dotenv/config' + +if (!process.env.S3_BUCKET_NAME) throw new Error('S3_BUCKET_NAME was undefined in env'); +if (!process.env.S3_ENDPOINT) throw new Error('S3_ENDPOINT was undefined in env'); +if (!process.env.S3_REGION) throw new Error('S3_REGION was undefined in env'); +if (!process.env.S3_ACCESS_KEY_ID) throw new Error('S3_ACCESS_KEY_ID was undefined in env'); +if (!process.env.S3_SECRET_ACCESS_KEY) throw new Error('S3_SECRET_ACCESS_KEY was undefined in env'); + type Payload = { - url: string + url: string; + channel: string; } -export default async function (payload: Payload, helpers: Helpers) { - const { url } = payload; - helpers.logger.info(`'record' task execution begin with url=${url} (@todo implement)`); - const record = new Record({ url: 'https://example.com/stream' }) - record.start() +export default async function (payload: Payload, helpers: Helpers): Promise { + const { url, channel } = payload; + helpers.logger.info(`'record' task execution begin with url=${url}, channel=${channel}`); - return record.id + + const bucket = process.env.S3_BUCKET_NAME! + const endpoint = process.env.S3_ENDPOINT! + const region = process.env.S3_REGION! + const accessKeyId = process.env.S3_ACCESS_KEY_ID! + const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY! + + const s3Client = Record.makeS3Client({ accessKeyId, secretAccessKey, region, endpoint }) + const inputStream = Record.getFFmpegDownload({ url }) + const record = new Record({ inputStream, bucket, s3Client, channel }) + await record.start() + + + + return record.id }; \ No newline at end of file diff --git a/packages/scout/src/cb.spec.ts b/packages/scout/src/cb.spec.ts index 373364f..97f46eb 100644 --- a/packages/scout/src/cb.spec.ts +++ b/packages/scout/src/cb.spec.ts @@ -1,16 +1,26 @@ import { describe } from 'mocha' import { expect } from 'chai'; -import { getInitialRoomDossier } from './cb.js' +import { getInitialRoomDossier, getRandomRoom } from './cb.js' describe('cb', function () { - describe('getInitialRoomDossier', function () { - /** - * this is an integration test that fails in CI due to CB blocking IP ranges - * @todo use a proxy or something - */ - xit('should return json', async function () { - const dossier = await getInitialRoomDossier('https://chaturbate.com/projektmelody') - expect(dossier).to.have.property('wschat_host') - }) + describe('getInitialRoomDossier', function () { + /** + * this is an integration test that fails in CI due to CB blocking IP ranges + * @todo use a proxy or something + */ + xit('should return json', async function () { + const dossier = await getInitialRoomDossier('https://chaturbate.com/projektmelody') + expect(dossier).to.have.property('wschat_host') }) + }) + describe('getRandomRoom', function () { + it('should return a Room object of an online room', async function () { + this.timeout(1000*60*2) + const room = await getRandomRoom() + expect(room).to.have.property('url') + expect(room).to.have.property('name') + expect(room.name).to.match(/[a-z_]/) + expect(room.url).to.match(/https:\/\//) + }) + }) }) \ No newline at end of file diff --git a/packages/scout/src/cb.ts b/packages/scout/src/cb.ts index 32fab69..37d2961 100644 --- a/packages/scout/src/cb.ts +++ b/packages/scout/src/cb.ts @@ -1,4 +1,42 @@ import * as cheerio from 'cheerio' +import fetch from 'node-fetch' + + +export interface ChaturbateModel { + gender: string; + location: string; + current_show: 'public' | 'private'; + username: string; + room_subject: string; + tags: string[]; + is_new: boolean; + num_users: number; + num_followers: number; + country: string; + spoken_languages: string; + display_name: string; + birthday: string; + is_hd: boolean; + age: number; + seconds_online: number; + image_url: string; + image_url_360x270: string; + chat_room_url_revshare: string; + iframe_embed_revshare: string; + chat_room_url: string; + iframe_embed: string; + slug: string; +} + +export interface ChaturbateOnlineModelsResponse { + results: ChaturbateModel[], + count: number +} + +export interface Room { + name: string; + url: string; +} /** * @@ -6,29 +44,62 @@ import * as cheerio from 'cheerio' * @returns {Object} initialRoomDossier */ export async function getInitialRoomDossier(roomUrl: string) { - try { - const res = await fetch(roomUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', - } - }); - const body = await res.text() - const $ = cheerio.load(body); - let rawScript = $('script:contains(window.initialRoomDossier)').html(); - if (!rawScript) { - throw new Error('window.initialRoomDossier is null. This could mean the channel is in password mode'); + try { + const res = await fetch(roomUrl, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', } - let rawDossier = rawScript.slice(rawScript.indexOf('"'), rawScript.lastIndexOf('"') + 1); - let dossier = JSON.parse(JSON.parse(rawDossier)); + }); + const body = await res.text() + const $ = cheerio.load(body); + let rawScript = $('script:contains(window.initialRoomDossier)').html(); + if (!rawScript) { + throw new Error('window.initialRoomDossier is null. This could mean the channel is in password mode'); + } + let rawDossier = rawScript.slice(rawScript.indexOf('"'), rawScript.lastIndexOf('"') + 1); + let dossier = JSON.parse(JSON.parse(rawDossier)); - return dossier; - } catch (error) { - if (error instanceof Error) { - // Handle the error gracefully - console.error(`Error fetching initial room dossier: ${error.message}`); - return null; // Or any other appropriate action you want to take - } else { - console.error('caught an exotic error, uh-oh') - } + return dossier; + } catch (error) { + if (error instanceof Error) { + // Handle the error gracefully + console.error(`Error fetching initial room dossier: ${error.message}`); + return null; // Or any other appropriate action you want to take + } else { + console.error('caught an exotic error, uh-oh') } } +} + + + +export async function getRandomRoom(): Promise { + try { + const res = await fetch('https://chaturbate.com/api/public/affiliates/onlinerooms/?wm=DiPkB&client_ip=request_ip'); + const data = await res.json() as ChaturbateOnlineModelsResponse; + + if (!data || !Array.isArray(data.results) || data.results.length === 0) { + throw new Error('No results found'); + } + + const results = data.results; + const randomIndex = Math.floor(Math.random() * results.length); + + if (!results[randomIndex]) { + throw new Error('No result found at random index'); + } + + const username = results[randomIndex].username; + return { + url: `https://chaturbate.com/${username}`, + name: username + } + } catch (error) { + if (error instanceof Error) { + console.error(`Error in getRandomRoom: ${error.message}`); + } else { + console.error('An unexpected error occurred'); + } + throw error; // Re-throw the error to propagate it further + } +} \ No newline at end of file diff --git a/packages/utils/pnpm-lock.yaml b/packages/utils/pnpm-lock.yaml index bd53e17..b81eca9 100644 --- a/packages/utils/pnpm-lock.yaml +++ b/packages/utils/pnpm-lock.yaml @@ -43,40 +43,6 @@ importers: specifier: ^4.16.2 version: 4.16.2 - ../..: {} - - ../bot: {} - - ../capture: {} - - ../image: {} - - ../infra: {} - - ../mailbox: {} - - ../meal: {} - - ../next: {} - - ../old: {} - - ../scout: {} - - ../storage: {} - - ../strapi: {} - - ../taco: {} - - ../types: {} - - ../uppy: {} - - ../video: {} - - ../worker: {} - packages: '@cspotcode/source-map-support@0.8.1': diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index f0949a7..6005142 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -10,7 +10,7 @@ import { dirname, basename, join, isAbsolute } from 'node:path'; import { fileURLToPath } from 'url'; export const __filename = fileURLToPath(import.meta.url); -export const __dirname = dirname(fileURLToPath(import.meta.url)); +const __dirname = dirname(fileURLToPath(import.meta.url)); export function getPackageVersion(packageJsonPath: string): string { if (!isAbsolute(packageJsonPath)) { diff --git a/scripts/k8s-secrets.sh b/scripts/k8s-secrets.sh index 7709b1e..421d426 100755 --- a/scripts/k8s-secrets.sh +++ b/scripts/k8s-secrets.sh @@ -39,7 +39,9 @@ EOF kubectl --namespace futureporn delete secret capture --ignore-not-found kubectl --namespace futureporn create secret generic capture \ ---from-literal=databaseUrl=${WORKER_DATABASE_URL} +--from-literal=databaseUrl=${WORKER_DATABASE_URL} \ +--from-literal=s3AccessKeyId=${S3_ACCESS_KEY_ID} \ +--from-literal=s3SecretAccessKey=${S3_SECRET_ACCESS_KEY} kubectl --namespace futureporn delete secret mailbox --ignore-not-found kubectl --namespace futureporn create secret generic mailbox \ diff --git a/scripts/postgres-drop.sh b/scripts/postgres-drop.sh index d3e0a95..0f01ba4 100644 --- a/scripts/postgres-drop.sh +++ b/scripts/postgres-drop.sh @@ -1,3 +1,12 @@ +if [ -z $POSTGRES_PASSWORD ]; then + echo "POSTGRES_PASSWORD was missing in env" + exit 5 +fi + ## drop futureporn_db -kubectl -n futureporn exec postgres-primary-0 -- psql -U postgres --command "DROP DATABASE futureporn_db WITH (FORCE);" +kubectl -n futureporn exec postgresql-primary-0 -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres --command "DROP DATABASE futureporn_db WITH (FORCE);" + + +## drop graphile_worker +kubectl -n futureporn exec postgresql-primary-0 -- env PGPASSWORD=${POSTGRES_PASSWORD} psql -U postgres --command "DROP DATABASE graphile_worker WITH (FORCE);"