From 44d1dbcb347f324d0db3a6e36479f9339e246c73 Mon Sep 17 00:00:00 2001 From: finalerock44 Date: Thu, 6 Aug 2026 17:30:57 +0100 Subject: [PATCH] chore: release 5.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote from dev to stable: * feat: client-side envelope encryption of app binaries, flow zips and env vars (#94, #101) — opt-in via `--encrypt` / `DCD_ENCRYPT_BINARIES=1` and off by default, so uploads stay byte-identical unless asked for. Per-upload DEK, chunked AES-256-GCM container, X25519 sealed-box DEK wrap; encrypted binaries dedup on the plaintext hash so re-uploads still hit the cache. * feat(artifacts): prefer server-assembled bundle delivery for downloads (#93) — falls back to the inline endpoint on 501, so it degrades cleanly against an API that has not shipped bundles. * feat(device): add Android API level 37 (Android 17) (#107) — the flag enum accepts 37, but the device/API-level pair is validated against the compatibility matrix the *target* API serves, and production still tops out at 36, so 37 is refused client-side until the platform gate flips. * refactor(cloud): remove the enterprise-only --mitmHost / --mitmPath flags (#102). The submitted config payload for runs that never passed them is byte-identical. * fix(deps) / deps: clear every outstanding pnpm audit advisory (#89, #92, #95, #100, #106), bump chalk 5 -> 6, and regenerate the schema types from the current API swagger (#105). No platform prerequisite this time: the envelope decrypt half (dcd api + simulators) is already on production with both env KEK public keys pinned, bundle delivery has a 501 fallback, and API 37 is gated server-side. Carries only the source delta — package.json version, CHANGELOG.md and the release-please manifests stay as release-please left them on production. Release-As: 5.3.0 --- .github/workflows/cli-ci.yml | 15 +- .github/workflows/npm-publish.yml | 4 +- .github/workflows/release-binaries.yml | 4 +- .gitleaks.toml | 29 +- eslint.config.cjs | 13 +- package.json | 19 +- pnpm-lock.yaml | 1715 ++------ src/commands/cloud.ts | 22 +- src/commands/upload.ts | 3 + src/config/environments.ts | 16 + src/config/flags/binary.flags.ts | 5 + src/config/flags/environment.flags.ts | 10 - src/gateways/api-gateway.ts | 142 +- src/mcp/tools/run-cloud-test.ts | 10 +- src/methods.ts | 161 +- src/services/test-submission.service.ts | 70 +- src/types.ts | 8 + src/types/domain/device.types.ts | 1 + src/types/generated/schema.types.ts | 4404 +++++++++++++++++---- src/utils/envelope.ts | 301 ++ test/unit/encrypted-dedup.test.ts | 375 ++ test/unit/envelope.test.ts | 241 ++ test/unit/report-download.service.test.ts | 153 + 23 files changed, 5448 insertions(+), 2273 deletions(-) create mode 100644 src/utils/envelope.ts create mode 100644 test/unit/encrypted-dedup.test.ts create mode 100644 test/unit/envelope.test.ts diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 961eb4a..abee162 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -31,7 +31,16 @@ jobs: run: | curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ | tar -xz gitleaks - ./gitleaks git . --redact --verbose --no-banner + # --log-opts=HEAD scopes the scan to commits reachable from what's + # checked out: the whole history of this branch (or of the PR merge + # commit, i.e. base + PR commits), but NOT unrelated branches. + # `fetch-depth: 0` fetches refs/heads/* — every branch — and gitleaks + # otherwise scans all of them, so an open branch that legitimately + # commits a high-entropy value plus its own .gitleaks.toml allowlist + # would fail every OTHER branch's scan, which is judged against the + # allowlist at its own tip. Each branch is still fully scanned by its + # own PR, and pushes to dev/production scan their full history. + ./gitleaks git . --redact --verbose --no-banner --log-opts=HEAD lint-and-test: runs-on: ubuntu-latest @@ -68,13 +77,13 @@ jobs: /api/swagger.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 with: version: 10 run_install: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '22' cache: 'pnpm' diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 8ff9b19..17873d4 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -27,11 +27,11 @@ jobs: - uses: actions/checkout@v7 # Setup .npmrc file to publish to npm - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 with: run_install: false - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index ba1b227..cb4b85c 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -27,11 +27,11 @@ jobs: - uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 with: run_install: false - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '22.x' cache: 'pnpm' diff --git a/.gitleaks.toml b/.gitleaks.toml index eb5cdaf..555ff26 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,20 +1,33 @@ # Gitleaks configuration — extends the default ruleset. # -# The only allowlisted secrets are the two PUBLIC Supabase anon keys committed -# in src/config/environments.ts. Those JWTs are anon-role keys, designed to be -# embedded in client code and gated by RLS (see the doc comment in that file) — -# they are intentionally not secret. +# The only allowlisted secrets are PUBLIC key material committed in +# src/config/environments.ts: +# - the two Supabase anon keys — anon-role JWTs, designed to be embedded in +# client code and gated by RLS (see the doc comment in that file). +# - the two KEK public keys (prod + dev) for client-side binary envelope +# encryption — base64 of the raw 32-byte X25519 PUBLIC half. They can only +# *encrypt*; the private halves live solely on the API, never in this repo. +# Both are intentionally not secret; gitleaks flags them on entropy alone +# (generic-api-key), not because it recognizes them as credentials. # # They are allowlisted by EXACT VALUE, deliberately not by file path or by the -# whole `jwt` rule: a Supabase service_role key is also a JWT, so a path/rule -# allowlist would let a genuinely sensitive key pasted into the same file slip -# through. Matching exact values keeps that detection intact. +# whole `jwt` / `generic-api-key` rule: a Supabase service_role key is also a +# JWT, and a KEK *private* key is the same shape as its public half, so a +# path/rule allowlist would let a genuinely sensitive key pasted into the same +# file slip through. Matching exact values keeps that detection intact. +# +# When a KEK is rotated, replace the corresponding value below — do not simply +# append, or the retired key stops being distinguishable from a live secret. [extend] useDefault = true [allowlist] -description = "Public Supabase anon keys (safe to commit, gated by RLS)" +description = "Public Supabase anon keys and KEK public keys (safe to commit)" regexes = [ '''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0\.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8''', '''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30\.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ''', + # prod kekPublicKey v1 + '''wtfyWEwK7nJzwI4PD\+9RAW8jxIR1u8kMQq2IhsrVnH4=''', + # dev kekPublicKey v1 + '''RgcToF/OJpcQI9koYvSvtj/WLaebfcN4v5GJoqtr/00=''', ] diff --git a/eslint.config.cjs b/eslint.config.cjs index 7bbdba9..aded632 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -6,11 +6,9 @@ const js = require('@eslint/js'); const tseslint = require('typescript-eslint'); -// These plugins ship as ESM with a `default` export under CJS interop. +// This plugin ships as ESM with a `default` export under CJS interop. const unicornPlugin = require('eslint-plugin-unicorn').default ?? require('eslint-plugin-unicorn'); -const importPlugin = - require('eslint-plugin-import').default ?? require('eslint-plugin-import'); module.exports = tseslint.config( { @@ -23,12 +21,15 @@ module.exports = tseslint.config( js.configs.recommended, ...tseslint.configs.recommended, { - // `unicorn` and `import` are only registered so legacy + // `unicorn` is only registered so legacy // `// eslint-disable-next-line unicorn/...` comments scattered through - // the source resolve. We don't enable any rules from them. + // the source resolve. We don't enable any rules from it. + // `eslint-plugin-import` used to be registered here for the same reason, + // but it was dropped: it dragged in minimatch@3 -> brace-expansion@1.x, + // which has an unpatched DoS advisory (GHSA-mh99-v99m-4gvg, no 1.x + // backport) and failed `pnpm audit`. No rules from it were ever enabled. plugins: { unicorn: unicornPlugin, - import: importPlugin, }, languageOptions: { ecmaVersion: 2022, diff --git a/package.json b/package.json index 75b1838..eb60f45 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "@modelcontextprotocol/sdk": "^1.29.0", "@supabase/supabase-js": "^2.108.2", "bplist-parser": "^0.3.2", - "chalk": "^5.6.2", + "chalk": "^6.0.0", "citty": "^0.2.2", - "js-yaml": "^5.0.0", + "js-yaml": "^5.2.2", "node-apk": "^1.2.1", "node-stream-zip": "^1.15.0", "plist": "^5.0.0", @@ -31,8 +31,7 @@ "chai": "^6.2.2", "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-unicorn": "^69.0.0", + "eslint-plugin-unicorn": "^72.0.0", "husky": "^9.1.7", "mocha": "^11.7.6", "prettier": "^3.8.4", @@ -87,6 +86,7 @@ "overrides": { "js-yaml@<3.14.2": ">=3.14.2", "js-yaml@>=4.0.0 <4.2.0": ">=4.2.0", + "js-yaml@>=5.0.0 <5.2.2": ">=5.2.2", "tar@<7.5.16": ">=7.5.16", "@isaacs/brace-expansion": ">=5.0.1", "fast-xml-parser": ">=5.5.7", @@ -103,13 +103,20 @@ "ajv@<7.0.0": "6.14.0", "diff@>=4.0.0 <6.0.0": "4.0.4", "diff@>=6.0.0": "8.0.3", - "brace-expansion@<1.1.13": "1.1.13", + "brace-expansion@<1.1.16": "1.1.16", "brace-expansion@>=2.0.0 <2.0.3": "2.0.3", - "brace-expansion@>=4.0.0 <5.0.6": "5.0.6", + "brace-expansion@>=3.0.0 <5.0.9": "5.0.9", "ws@>=8.0.0 <8.21.0": "8.21.0", "esbuild@<0.28.1": ">=0.28.1", + "fast-uri@>=3.0.0 <3.1.5": "3.1.5", + "hono@>=4.0.0 <4.12.34": "4.12.34", "micromatch>picomatch": "^2.3.2", "tinyglobby>picomatch": "^4.0.4" + }, + "auditConfig": { + "ignoreGhsas": [ + "GHSA-frvp-7c67-39w9" + ] } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43ccd66..a75f3d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: overrides: js-yaml@<3.14.2: '>=3.14.2' js-yaml@>=4.0.0 <4.2.0: '>=4.2.0' + js-yaml@>=5.0.0 <5.2.2: '>=5.2.2' tar@<7.5.16: '>=7.5.16' '@isaacs/brace-expansion': '>=5.0.1' fast-xml-parser: '>=5.5.7' @@ -23,11 +24,13 @@ overrides: ajv@<7.0.0: 6.14.0 diff@>=4.0.0 <6.0.0: 4.0.4 diff@>=6.0.0: 8.0.3 - brace-expansion@<1.1.13: 1.1.13 + brace-expansion@<1.1.16: 1.1.16 brace-expansion@>=2.0.0 <2.0.3: 2.0.3 - brace-expansion@>=4.0.0 <5.0.6: 5.0.6 + brace-expansion@>=3.0.0 <5.0.9: 5.0.9 ws@>=8.0.0 <8.21.0: 8.21.0 esbuild@<0.28.1: '>=0.28.1' + fast-uri@>=3.0.0 <3.1.5: 3.1.5 + hono@>=4.0.0 <4.12.34: 4.12.34 micromatch>picomatch: ^2.3.2 tinyglobby>picomatch: ^4.0.4 @@ -40,34 +43,34 @@ importers: version: 1.7.0 '@modelcontextprotocol/sdk': specifier: ^1.29.0 - version: 1.29.0(zod@4.4.3) + version: 1.30.0(zod@4.4.3) '@supabase/supabase-js': specifier: ^2.108.2 - version: 2.110.2 + version: 2.111.0 bplist-parser: specifier: ^0.3.2 version: 0.3.2 chalk: - specifier: ^5.6.2 - version: 5.6.2 + specifier: ^6.0.0 + version: 6.0.0 citty: specifier: ^0.2.2 version: 0.2.2 js-yaml: - specifier: ^5.0.0 - version: 5.2.1 + specifier: ^5.2.2 + version: 5.2.2 node-apk: specifier: ^1.2.1 version: 1.2.1 node-stream-zip: specifier: ^1.15.0 - version: 1.15.0 + version: 1.16.0 plist: specifier: ^5.0.0 version: 5.0.0 tar: specifier: ^7.5.16 - version: 7.5.19 + version: 7.5.22 tus-js-client: specifier: ^4.3.1 version: 4.3.1 @@ -80,7 +83,7 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.6.0) + version: 10.0.1(eslint@10.8.0) '@types/chai': specifier: ^5.2.3 version: 5.2.3 @@ -92,7 +95,7 @@ importers: version: 10.0.10 '@types/node': specifier: ^26.0.0 - version: 26.1.1 + version: 26.1.2 '@types/yazl': specifier: ^3.3.1 version: 3.3.1 @@ -101,16 +104,13 @@ importers: version: 6.2.2 eslint: specifier: ^10.5.0 - version: 10.6.0 + version: 10.8.0 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.6.0) - eslint-plugin-import: - specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0) + version: 10.1.8(eslint@10.8.0) eslint-plugin-unicorn: - specifier: ^69.0.0 - version: 69.0.0(eslint@10.6.0) + specifier: ^72.0.0 + version: 72.0.0(eslint@10.8.0) husky: specifier: ^9.1.7 version: 9.1.7 @@ -119,26 +119,22 @@ importers: version: 11.7.6 prettier: specifier: ^3.8.4 - version: 3.9.5 + version: 3.9.6 shx: specifier: ^0.4.0 version: 0.4.0 tsx: specifier: ^4.22.4 - version: 4.23.0 + version: 4.23.1 typescript: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: specifier: ^8.61.1 - version: 8.63.0(eslint@10.6.0)(typescript@6.0.3) + version: 8.65.0(eslint@10.8.0)(typescript@6.0.3) packages: - '@babel/helper-validator-identifier@7.29.7': - resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} - engines: {node: '>=6.9.0'} - '@clack/core@1.4.3': resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} engines: {node: '>= 20.12.0'} @@ -303,8 +299,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -317,14 +313,18 @@ packages: resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.6.0': - resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/css-tree@4.0.5': + resolution: {integrity: sha512-iPmijIAq4hlIJB86PYmY/fcZORHtjphSqICDbwuw32A/JmkhZQ/K/6TjHE03zqf3n5yABpVcbRAMG8Mi9ojy8g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/js@10.0.1': resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -342,11 +342,11 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@hono/node-server@1.19.14': - resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} - engines: {node: '>=18.14.1'} + '@hono/node-server@2.0.12': + resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} + engines: {node: '>=20'} peerDependencies: - hono: ^4 + hono: 4.12.34 '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -376,8 +376,8 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@modelcontextprotocol/sdk@1.29.0': - resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + '@modelcontextprotocol/sdk@1.30.0': + resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -402,34 +402,31 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@supabase/auth-js@2.110.2': - resolution: {integrity: sha512-Qj7a6EDP+AMMQFWqGv+qFa8r6re//dk+qQI5bA0KK+PZmnI3JPu97TDeNt6SMiQ2FkklP79hP2yDFYSnA989OA==} + '@supabase/auth-js@2.111.0': + resolution: {integrity: sha512-hbRLgyQZEX0SDyF4LYXpv94qOIQyFATfpT5SIs2V0SisHnisVRkYgiPQWzv80l4S2mO3qof78rmoH9j5zoFsYQ==} engines: {node: '>=22.0.0'} - '@supabase/functions-js@2.110.2': - resolution: {integrity: sha512-ZjjqrXpxM9/rE+eAtZxiK45EWy9EBoJQ322Q5Y75LccYQNh212neHTgXP/o4MIzmH0LNXT8UzvTZtQOfOzyoeQ==} + '@supabase/functions-js@2.111.0': + resolution: {integrity: sha512-RW/OCsd6MO592zU8ifzP8/f8XzxxIdpb+Up5XaOtE26Fw+3zTp475WX7+GuuktiD1WF8pFUDe6khUPbMp77RCw==} engines: {node: '>=22.0.0'} - '@supabase/phoenix@0.4.4': - resolution: {integrity: sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==} + '@supabase/phoenix@0.4.5': + resolution: {integrity: sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==} - '@supabase/postgrest-js@2.110.2': - resolution: {integrity: sha512-++LBmcIMwCtgO4tISQUmo9+2xkRwHQqS8ZKMCnhXLe9P8k8YQRXuMoh/RiSzQSoev8gqet0W7yOboW0cUxnt0Q==} + '@supabase/postgrest-js@2.111.0': + resolution: {integrity: sha512-pcqeDsnWP0lx9GawduYxNZJHeuTm53O7L0SC8RF8tniV3GWIPY6me6OTdnwzdwNUmNy1dzUVtSyIfE6+OflzPQ==} engines: {node: '>=22.0.0'} - '@supabase/realtime-js@2.110.2': - resolution: {integrity: sha512-z3jTOTPgyn6E3r6dVOOQ10He4yAMB2czjFw7xVdX3s16MHElna5rY1gVaePs0NIo6xvtMYbtmOXlFaFt/ePLpg==} + '@supabase/realtime-js@2.111.0': + resolution: {integrity: sha512-6oRf/vZyRwg8f8GbFSJkrD2w4HAu/yTvyMViHXHS+H5hNJzdXCrUR7cP5oW7daT3YlRnzRPY9LcGSJKZAmfMSg==} engines: {node: '>=22.0.0'} - '@supabase/storage-js@2.110.2': - resolution: {integrity: sha512-EhsRSwSnmQefKJsAxoRUZ0hvHr92ECM8DDGAKR5z0HdoJx4heI60PjHUTruVNZxKX6XeobLGDyLud020Bw1iwg==} + '@supabase/storage-js@2.111.0': + resolution: {integrity: sha512-UEViNmTzVOxE8dqUA81wls+n9xgmlvSFfhfwo6QxrO4kQOytCYyw3ciYFoi4XoD4Jl95NJ3jnndHN5iIudWzqw==} engines: {node: '>=22.0.0'} - '@supabase/supabase-js@2.110.2': - resolution: {integrity: sha512-r9q9w4ZQ6mOjh36aqUNFSisBF611vzpO8JphBESr2Q1SWvmGFQeI7Jq7Y+PaNMZ6Zszz+S2yTlJStCpnaMSnQg==} + '@supabase/supabase-js@2.111.0': + resolution: {integrity: sha512-9q0/AULthQnWeiDh1vGyjoJZbSY04bu6qHcWit70pqEYn5Kv/dkCPY62Ja1123jEnJbB9Vd2pjY7Kvk/lK3peA==} engines: {node: '>=22.0.0'} '@types/chai@5.2.3': @@ -450,75 +447,72 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} - '@types/node@26.1.1': - resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + '@types/node@26.1.2': + resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} '@types/yazl@3.3.1': resolution: {integrity: sha512-DIWfCKpsTp6hE5BDBHV3+fIL/bLUF9Bv13iDrWnMlmhQpH67buNvI291ZauQ1xcccxK3FqQ9honnXpq4R8NMuQ==} - '@typescript-eslint/eslint-plugin@8.63.0': - resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==} + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.63.0 + '@typescript-eslint/parser': ^8.65.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.63.0': - resolution: {integrity: sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==} + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.63.0': - resolution: {integrity: sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==} + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.63.0': - resolution: {integrity: sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==} + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.63.0': - resolution: {integrity: sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==} + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.63.0': - resolution: {integrity: sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==} + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.63.0': - resolution: {integrity: sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==} + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.63.0': - resolution: {integrity: sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==} + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.63.0': - resolution: {integrity: sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==} + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.63.0': - resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@xmldom/xmldom@0.9.10': @@ -534,8 +528,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -572,51 +566,16 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.10.40: - resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} + baseline-browser-mapping@2.11.4: + resolution: {integrity: sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -632,12 +591,9 @@ packages: resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} engines: {node: '>= 5.10.0'} - brace-expansion@1.1.13: - resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -646,8 +602,8 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - browserslist@4.28.4: - resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -670,10 +626,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.9: - resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} - engines: {node: '>= 0.4'} - call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -682,8 +634,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001799: - resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -693,9 +645,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chalk@6.0.0: + resolution: {integrity: sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==} + engines: {node: '>=22'} change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -729,9 +681,6 @@ packages: combine-errors@3.0.3: resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -744,6 +693,10 @@ packages: resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} engines: {node: '>=18'} + convert-hrtime@5.0.0: + resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} + engines: {node: '>=12'} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -770,26 +723,6 @@ packages: custom-error-instance@2.1.1: resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -806,14 +739,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - 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'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -826,10 +751,6 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -840,8 +761,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.381: - resolution: {integrity: sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==} + electron-to-chromium@1.5.396: + resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -856,13 +777,9 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - es-abstract-get@1.0.0: - resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} - engines: {node: '>= 0.4'} - - es-abstract@1.24.2: - resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} - engines: {node: '>= 0.4'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} @@ -876,18 +793,6 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.1: - resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} - engines: {node: '>= 0.4'} - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -910,42 +815,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-import-resolver-node@0.3.10: - resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} - - eslint-module-utils@2.13.0: - resolution: {integrity: sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-unicorn@69.0.0: - resolution: {integrity: sha512-ZN/KtHr9hQ6AOByANSNJpsDbo/+Nn+EyQ6blK4w+dcmS/xpYkqLLfrUc+NA/wOK6vF5uEUvhn8my5B/3sruB9g==} + eslint-plugin-unicorn@72.0.0: + resolution: {integrity: sha512-hqO6ksoOHO+ZhdseTuKRVQbx9U7PRO/cv8qAR1mctwzdVO2hYud8uS9luAhp43RJgziYgHAph8eHyipT8GL0ng==} engines: {node: '>=22'} peerDependencies: eslint: '>=10.4' @@ -962,8 +833,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.6.0: - resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + eslint@10.8.0: + resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1008,8 +879,8 @@ packages: resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} engines: {node: '>=6'} - express-rate-limit@8.5.2: - resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + express-rate-limit@8.6.1: + resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -1037,8 +908,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -1083,12 +954,8 @@ packages: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} @@ -1110,16 +977,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.2.0: - resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} + function-timeout@1.0.2: + resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} + engines: {node: '>=18'} get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} @@ -1137,10 +997,6 @@ packages: resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} engines: {node: '>=6'} - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1154,14 +1010,10 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - globals@17.7.0: - resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + globals@17.8.0: + resolution: {integrity: sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==} engines: {node: '>=18'} - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1169,29 +1021,14 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -1200,8 +1037,8 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - hono@4.12.26: - resolution: {integrity: sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==} + hono@4.12.34: + resolution: {integrity: sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==} engines: {node: '>=16.9.0'} http-errors@2.0.1: @@ -1217,10 +1054,14 @@ packages: resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} engines: {node: '>=20.0.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} + identifier-regex@1.1.0: + resolution: {integrity: sha512-SLX4H/vtcYlYnL7XqnuJKHU7Z8517TgsW9nmQiGOgMCjQ8V/deLYu6bEmbGoXe7WMMhc9+EUGyFFneHja8KabA==} + engines: {node: '>=18'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1240,93 +1081,41 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - interpret@1.4.0: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + ip-address@10.3.1: + resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} engines: {node: '>= 12'} ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-document.all@1.0.0: - resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} - engines: {node: '>= 0.4'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} + is-identifier@1.1.0: + resolution: {integrity: sha512-NhOds0mDx9lJu+1lBRO0xbwFo5nobA7GCk/0e5xjr6+6XugX985+0OyGX35BNrTkPAsdLcIKg02HUQJOK8D8kw==} + engines: {node: '>=18'} is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} @@ -1343,18 +1132,6 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -1363,51 +1140,24 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - 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==} - jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + jose@6.2.5: + resolution: {integrity: sha512-2E5L2yRp03FnwreJLJX8/r7mHiZICCf8kG7fAsTWkSQTDAcc46NIZoQLKy+EJ8sPoJlxyS4OQR5H70LjIZZlIQ==} js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} - js-yaml@5.2.1: - resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} + js-yaml@5.2.2: + resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==} hasBin: true jsesc@3.1.0: @@ -1430,10 +1180,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1476,12 +1222,19 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + make-asynchronous@1.1.0: + resolution: {integrity: sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==} + engines: {node: '>=18'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + mdn-data@2.29.0: + resolution: {integrity: sha512-pVxQFCcaYUEAH853+v7yoI/qzhxXSq1bTb9obMYGYAN1c3Hen+XDCEvr296XhstrwlSTNgOR7mCSD4JPjbJe5A==} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} merge-descriptors@2.0.0: @@ -1508,13 +1261,10 @@ packages: resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} engines: {node: 18 || 20 || >=22} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} - minimatch@3.1.4: - resolution: {integrity: sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==} - minimatch@9.0.7: resolution: {integrity: sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==} engines: {node: '>=16 || 14 >=14.17'} @@ -1551,20 +1301,16 @@ packages: node-apk@1.2.1: resolution: {integrity: sha512-I0TY1x5m1pkFzjYdaGrrAu/Mh9qnnk2/BoMAU6bvBxTTD/oNQyTWbu3LTdONgV2rnLHf23jJ00Y/VV4BzZ6YXQ==} - node-exports-info@1.6.0: - resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} - engines: {node: '>= 0.4'} - node-forge@1.4.0: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - node-releases@2.0.50: - resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} - node-stream-zip@1.15.0: - resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} + node-stream-zip@1.16.0: + resolution: {integrity: sha512-ObaRrRoR8T68wF6suxHd7R4XQNamij6ZQHrwG7Dx1D2zeHcDNLsIOBcWrIwtDm7AsCXBguaPHgXhcjxDa2szrg==} engines: {node: '>=0.12.0'} npm-run-path@2.0.2: @@ -1579,30 +1325,6 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -1614,9 +1336,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} + p-event@6.0.1: + resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} + engines: {node: '>=16.17'} p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} @@ -1630,6 +1352,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -1682,16 +1408,12 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.9.5: - resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} hasBin: true @@ -1709,8 +1431,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} querystringify@2.2.0: @@ -1719,8 +1441,12 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + quote-js-string@0.1.0: + resolution: {integrity: sha512-Y3NoRtprEEZQD8RfxMCfS0ZTqc4e+i18OrXEXAvpM6TfC/3y+0L5rNbZiSnbBBEkDfFzbpd8o+cE8q3/anjMGA==} + engines: {node: '>=22'} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} raw-body@3.0.2: @@ -1735,14 +1461,6 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - regjsparser@0.13.2: resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} hasBin: true @@ -1758,16 +1476,15 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reserved-identifiers@1.2.0: + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} + engines: {node: '>=18'} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true - resolve@2.0.0-next.7: - resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} - engines: {node: '>= 0.4'} - hasBin: true - retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -1783,18 +1500,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.4: - resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} - engines: {node: '>=0.4'} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1802,10 +1507,6 @@ packages: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -1823,18 +1524,6 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} - 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'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -1890,14 +1579,14 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1906,18 +1595,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string.prototype.trim@1.2.11: - resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.10: - resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1926,10 +1603,6 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - strip-eof@1.0.0: resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} engines: {node: '>=0.10.0'} @@ -1942,6 +1615,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + super-regex@1.1.0: + resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1954,10 +1631,14 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tar@7.5.19: - resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} engines: {node: '>=18'} + time-span@5.1.0: + resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} + engines: {node: '>=12'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -1976,14 +1657,11 @@ packages: peerDependencies: typescript: '>=4.8.4' - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.23.0: - resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==} + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -1995,28 +1673,16 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.8: - resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} - engines: {node: '>= 0.4'} - - typescript-eslint@8.63.0: - resolution: {integrity: sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==} + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2027,10 +1693,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -2054,21 +1716,8 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.22: - resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} - engines: {node: '>= 0.4'} + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} @@ -2109,6 +1758,11 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -2138,8 +1792,6 @@ packages: snapshots: - '@babel/helper-validator-identifier@7.29.7': {} - '@clack/core@1.4.3': dependencies: fast-wrap-ansi: 0.2.2 @@ -2230,9 +1882,9 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)': + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0)': dependencies: - eslint: 10.6.0 + eslint: 10.8.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -2241,11 +1893,11 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3(supports-color@8.1.1) - minimatch: 10.2.5 + minimatch: 10.2.6 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.6.0': + '@eslint/config-helpers@0.7.0': dependencies: '@eslint/core': 1.2.1 @@ -2253,9 +1905,14 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.6.0)': + '@eslint/css-tree@4.0.5': + dependencies: + mdn-data: 2.29.0 + source-map-js: 1.2.1 + + '@eslint/js@10.0.1(eslint@10.8.0)': optionalDependencies: - eslint: 10.6.0 + eslint: 10.8.0 '@eslint/object-schema@3.0.5': {} @@ -2264,9 +1921,9 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@hono/node-server@1.19.14(hono@4.12.26)': + '@hono/node-server@2.0.12(hono@4.12.34)': dependencies: - hono: 4.12.26 + hono: 4.12.34 '@humanfs/core@0.19.2': dependencies: @@ -2297,9 +1954,9 @@ snapshots: dependencies: minipass: 7.1.3 - '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + '@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.26) + '@hono/node-server': 2.0.12(hono@4.12.34) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -2308,9 +1965,9 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.1.0 express: 5.2.1 - express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.26 - jose: 6.2.3 + express-rate-limit: 8.6.1(express@5.2.1) + hono: 4.12.34 + jose: 6.2.5 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 @@ -2334,39 +1991,37 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@rtsao/scc@1.1.0': {} - - '@supabase/auth-js@2.110.2': + '@supabase/auth-js@2.111.0': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.110.2': + '@supabase/functions-js@2.111.0': dependencies: tslib: 2.8.1 - '@supabase/phoenix@0.4.4': {} + '@supabase/phoenix@0.4.5': {} - '@supabase/postgrest-js@2.110.2': + '@supabase/postgrest-js@2.111.0': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.110.2': + '@supabase/realtime-js@2.111.0': dependencies: - '@supabase/phoenix': 0.4.4 + '@supabase/phoenix': 0.4.5 tslib: 2.8.1 - '@supabase/storage-js@2.110.2': + '@supabase/storage-js@2.111.0': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/supabase-js@2.110.2': + '@supabase/supabase-js@2.111.0': dependencies: - '@supabase/auth-js': 2.110.2 - '@supabase/functions-js': 2.110.2 - '@supabase/postgrest-js': 2.110.2 - '@supabase/realtime-js': 2.110.2 - '@supabase/storage-js': 2.110.2 + '@supabase/auth-js': 2.111.0 + '@supabase/functions-js': 2.111.0 + '@supabase/postgrest-js': 2.111.0 + '@supabase/realtime-js': 2.111.0 + '@supabase/storage-js': 2.111.0 '@types/chai@5.2.3': dependencies: @@ -2383,27 +2038,25 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} - '@types/mocha@10.0.10': {} - '@types/node@26.1.1': + '@types/node@26.1.2': dependencies: undici-types: 8.3.0 '@types/yazl@3.3.1': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 - '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@6.0.3))(eslint@10.8.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/type-utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.63.0 - eslint: 10.6.0 + '@typescript-eslint/parser': 8.65.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 + eslint: 10.8.0 ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -2411,56 +2064,56 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.63.0 + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.6.0 + eslint: 10.8.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.63.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) - '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.63.0': + '@typescript-eslint/scope-manager@8.65.0': dependencies: - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/visitor-keys': 8.63.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 - '@typescript-eslint/tsconfig-utils@8.63.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.65.0(eslint@10.8.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0)(typescript@6.0.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.6.0 + eslint: 10.8.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.63.0': {} + '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.63.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.63.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/visitor-keys': 8.63.0 + '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.3 semver: 7.8.5 @@ -2470,20 +2123,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.65.0(eslint@10.8.0)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - eslint: 10.6.0 + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + eslint: 10.8.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.63.0': + '@typescript-eslint/visitor-keys@8.65.0': dependencies: - '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 '@xmldom/xmldom@0.9.10': {} @@ -2493,11 +2146,11 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn@8.17.0: {} + acorn@8.18.0: {} ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: @@ -2513,7 +2166,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.5 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -2529,81 +2182,23 @@ snapshots: argparse@2.0.1: {} - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 + assertion-error@2.0.1: {} - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-shim-unscopables: 1.1.0 + balanced-match@4.0.4: {} - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 + baseline-browser-mapping@2.11.4: {} - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 + big-integer@1.6.52: {} - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - assertion-error@2.0.1: {} - - async-function@1.0.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - balanced-match@1.0.2: {} - - balanced-match@4.0.4: {} - - baseline-browser-mapping@2.10.40: {} - - big-integer@1.6.52: {} - - body-parser@2.3.0: + body-parser@2.3.0: dependencies: bytes: 3.1.2 content-type: 2.0.0 debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.2 + qs: 6.15.3 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -2613,12 +2208,7 @@ snapshots: dependencies: big-integer: 1.6.52 - brace-expansion@1.1.13: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@5.0.6: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -2628,13 +2218,13 @@ snapshots: browser-stdout@1.3.1: {} - browserslist@4.28.4: + browserslist@4.28.7: dependencies: - baseline-browser-mapping: 2.10.40 - caniuse-lite: 1.0.30001799 - electron-to-chromium: 1.5.381 - node-releases: 2.0.50 - update-browserslist-db: 1.2.3(browserslist@4.28.4) + baseline-browser-mapping: 2.11.4 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.396 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.7) buffer-crc32@1.0.0: {} @@ -2649,13 +2239,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2663,7 +2246,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001799: {} + caniuse-lite@1.0.30001806: {} chai@6.2.2: {} @@ -2672,7 +2255,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.2: {} + chalk@6.0.0: {} change-case@5.4.4: {} @@ -2703,21 +2286,21 @@ snapshots: custom-error-instance: 2.1.1 lodash.uniqby: 4.5.0 - concat-map@0.0.1: {} - content-disposition@1.1.0: {} content-type@1.0.5: {} content-type@2.0.0: {} + convert-hrtime@5.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} core-js-compat@3.49.0: dependencies: - browserslist: 4.28.4 + browserslist: 4.28.7 cors@2.8.6: dependencies: @@ -2740,28 +2323,6 @@ snapshots: custom-error-instance@2.1.1: {} - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@3.2.7: - dependencies: - ms: 2.1.3 - debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -2772,28 +2333,12 @@ snapshots: deep-is@0.1.4: {} - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - depd@2.0.0: {} detect-indent@7.0.2: {} diff@8.0.3: {} - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2804,7 +2349,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.381: {} + electron-to-chromium@1.5.396: {} emoji-regex@8.0.0: {} @@ -2816,69 +2361,7 @@ snapshots: dependencies: once: 1.4.0 - es-abstract-get@1.0.0: - dependencies: - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - is-callable: 1.2.7 - object-inspect: 1.13.4 - - es-abstract@1.24.2: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.1 - function.prototype.name: 1.2.0 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.4 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.11 - string.prototype.trimend: 1.0.10 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.8 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.22 + entities@4.5.0: {} es-define-property@1.0.1: {} @@ -2888,25 +2371,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.4 - - es-to-primitive@1.3.1: - dependencies: - es-abstract-get: 1.0.0 - es-errors: 1.3.0 - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -2942,76 +2406,33 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.6.0): + eslint-config-prettier@10.1.8(eslint@10.8.0): dependencies: - eslint: 10.6.0 - - eslint-import-resolver-node@0.3.10: - dependencies: - debug: 3.2.7 - is-core-module: 2.16.2 - resolve: 2.0.0-next.7 - transitivePeerDependencies: - - supports-color + eslint: 10.8.0 - eslint-module-utils@2.13.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0): + eslint-plugin-unicorn@72.0.0(eslint@10.8.0): dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - eslint: 10.6.0 - eslint-import-resolver-node: 0.3.10 - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 10.6.0 - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0) - hasown: 2.0.4 - is-core-module: 2.16.2 - is-glob: 4.0.3 - minimatch: 3.1.4 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.10 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-unicorn@69.0.0(eslint@10.6.0): - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) - browserslist: 4.28.4 + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0) + '@eslint/css-tree': 4.0.5 + browserslist: 4.28.7 change-case: 5.4.4 ci-info: 4.4.0 core-js-compat: 3.49.0 detect-indent: 7.0.2 - eslint: 10.6.0 + entities: 4.5.0 + eslint: 10.8.0 find-up-simple: 1.0.1 - globals: 17.7.0 + globals: 17.8.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 - jsesc: 3.1.0 + is-identifier: 1.1.0 pluralize: 8.0.0 + quote-js-string: 0.1.0 regjsparser: 0.13.2 + reserved-identifiers: 1.2.0 semver: 7.8.5 strip-indent: 4.1.1 + yaml: 2.9.0 eslint-scope@9.1.2: dependencies: @@ -3024,12 +2445,12 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.6.0: + eslint@10.8.0: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.6.0 + '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.8 @@ -3053,7 +2474,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: @@ -3061,8 +2482,8 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 esquery@1.7.0: @@ -3095,10 +2516,13 @@ snapshots: signal-exit: 3.0.7 strip-eof: 1.0.0 - express-rate-limit@8.5.2(express@5.2.1): + express-rate-limit@8.6.1(express@5.2.1): dependencies: + debug: 4.4.3(supports-color@8.1.1) express: 5.2.1 - ip-address: 10.2.0 + ip-address: 10.3.1 + transitivePeerDependencies: + - supports-color express@5.2.1: dependencies: @@ -3122,8 +2546,8 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.2 - range-parser: 1.2.1 + qs: 6.15.3 + range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 @@ -3153,7 +2577,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.2: {} + fast-uri@3.1.5: {} fast-wrap-ansi@0.2.2: dependencies: @@ -3195,16 +2619,12 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.3 keyv: 4.5.4 flat@5.0.2: {} - flatted@3.4.2: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 + flatted@3.4.3: {} foreground-child@3.3.1: dependencies: @@ -3220,21 +2640,7 @@ snapshots: function-bind@1.1.2: {} - function.prototype.name@1.2.0: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - es-define-property: 1.0.1 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - hasown: 2.0.4 - is-callable: 1.2.7 - is-document.all: 1.0.0 - - functions-have-names@1.2.3: {} - - generator-function@2.0.1: {} + function-timeout@1.0.2: {} get-caller-file@2.0.5: {} @@ -3260,12 +2666,6 @@ snapshots: dependencies: pump: 3.0.4 - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3283,42 +2683,23 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - globals@17.7.0: {} - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 + globals@17.8.0: {} gopd@1.2.0: {} graceful-fs@4.2.11: {} - has-bigints@1.1.0: {} - has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - has-symbols@1.1.0: {} - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - hasown@2.0.4: dependencies: function-bind: 1.1.2 he@1.2.0: {} - hono@4.12.26: {} + hono@4.12.34: {} http-errors@2.0.1: dependencies: @@ -3332,10 +2713,14 @@ snapshots: iceberg-js@0.8.1: {} - iconv-lite@0.7.2: + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 + identifier-regex@1.1.0: + dependencies: + reserved-identifiers: 1.2.0 + ignore@5.3.2: {} ignore@7.0.6: {} @@ -3346,94 +2731,32 @@ snapshots: inherits@2.0.4: {} - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.4 - side-channel: 1.1.1 - interpret@1.4.0: {} - ip-address@10.2.0: {} + ip-address@10.3.1: {} ipaddr.js@1.9.1: {} - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-builtin-module@5.0.0: dependencies: builtin-modules: 5.3.0 - is-callable@1.2.7: {} - is-core-module@2.16.2: dependencies: hasown: 2.0.4 - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-document.all@1.0.0: - dependencies: - call-bound: 1.0.4 - is-extglob@2.1.1: {} - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - is-fullwidth-code-point@3.0.0: {} - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: + is-identifier@1.1.0: dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 + identifier-regex: 1.1.0 + super-regex: 1.1.0 is-number@7.0.0: {} @@ -3443,53 +2766,12 @@ snapshots: is-promise@4.0.0: {} - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - is-stream@1.1.0: {} is-stream@2.0.1: {} - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.22 - is-unicode-supported@0.1.0: {} - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - isarray@2.0.5: {} - isexe@2.0.0: {} jackspeak@3.4.3: @@ -3498,11 +2780,11 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jose@6.2.3: {} + jose@6.2.5: {} js-base64@3.7.8: {} - js-yaml@5.2.1: + js-yaml@5.2.2: dependencies: argparse: 2.0.1 @@ -3518,10 +2800,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json5@1.0.2: - dependencies: - minimist: 1.2.8 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3568,9 +2846,17 @@ snapshots: lru-cache@10.4.3: {} + make-asynchronous@1.1.0: + dependencies: + p-event: 6.0.1 + type-fest: 4.41.0 + web-worker: 1.5.0 + math-intrinsics@1.1.0: {} - media-typer@1.1.0: {} + mdn-data@2.29.0: {} + + media-typer@1.1.1: {} merge-descriptors@2.0.0: {} @@ -3589,19 +2875,15 @@ snapshots: minimatch@10.2.3: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.9 - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.6 - - minimatch@3.1.4: - dependencies: - brace-expansion: 1.1.13 + brace-expansion: 5.0.9 minimatch@9.0.7: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.9 minimist@1.2.8: {} @@ -3622,7 +2904,7 @@ snapshots: glob: 10.5.0 he: 1.2.0 is-path-inside: 3.0.3 - js-yaml: 5.2.1 + js-yaml: 5.2.2 log-symbols: 4.1.0 minimatch: 9.0.7 ms: 2.1.3 @@ -3647,18 +2929,11 @@ snapshots: dependencies: node-forge: 1.4.0 - node-exports-info@1.6.0: - dependencies: - array.prototype.flatmap: 1.3.3 - es-errors: 1.3.0 - object.entries: 1.1.9 - semver: 6.3.1 - node-forge@1.4.0: {} - node-releases@2.0.50: {} + node-releases@2.0.51: {} - node-stream-zip@1.15.0: {} + node-stream-zip@1.16.0: {} npm-run-path@2.0.2: dependencies: @@ -3668,44 +2943,6 @@ snapshots: object-inspect@1.13.4: {} - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.entries@1.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -3723,11 +2960,9 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - own-keys@1.0.1: + p-event@6.0.1: dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 + p-timeout: 6.1.4 p-finally@1.0.0: {} @@ -3739,6 +2974,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-timeout@6.1.4: {} + package-json-from-dist@1.0.1: {} parseurl@1.3.3: {} @@ -3773,11 +3010,9 @@ snapshots: pluralize@8.0.0: {} - possible-typed-array-names@1.1.0: {} - prelude-ls@1.2.1: {} - prettier@3.9.5: {} + prettier@3.9.6: {} proper-lockfile@4.1.2: dependencies: @@ -3797,21 +3032,24 @@ snapshots: punycode@2.3.1: {} - qs@6.15.2: + qs@6.15.3: dependencies: + es-define-property: 1.0.1 side-channel: 1.1.1 querystringify@2.2.0: {} queue-microtask@1.2.3: {} - range-parser@1.2.1: {} + quote-js-string@0.1.0: {} + + range-parser@1.3.0: {} raw-body@3.0.2: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 unpipe: 1.0.0 readdirp@4.1.2: {} @@ -3820,26 +3058,6 @@ snapshots: dependencies: resolve: 1.22.12 - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - regjsparser@0.13.2: dependencies: jsesc: 3.1.0 @@ -3850,19 +3068,12 @@ snapshots: requires-port@1.0.0: {} - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 + reserved-identifiers@1.2.0: {} - resolve@2.0.0-next.7: + resolve@1.22.12: dependencies: es-errors: 1.3.0 is-core-module: 2.16.2 - node-exports-info: 1.6.0 - object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -3884,31 +3095,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-array-concat@1.1.4: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - safer-buffer@2.1.2: {} semver@5.7.2: {} - semver@6.3.1: {} - semver@7.8.5: {} send@1.2.1: @@ -3922,7 +3112,7 @@ snapshots: mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 - range-parser: 1.2.1 + range-parser: 1.3.0 statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -3938,28 +3128,6 @@ snapshots: transitivePeerDependencies: - supports-color - 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.3.0 - gopd: 1.2.0 - 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 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - setprototypeof@1.2.0: {} shebang-command@1.2.0: @@ -4020,12 +3188,9 @@ snapshots: sisteransi@1.0.5: {} - statuses@2.0.2: {} + source-map-js@1.2.1: {} - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 + statuses@2.0.2: {} string-width@4.2.3: dependencies: @@ -4039,30 +3204,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 - string.prototype.trim@1.2.11: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - has-property-descriptors: 1.0.2 - safe-regex-test: 1.1.0 - - string.prototype.trimend@1.0.10: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4071,14 +3212,18 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-bom@3.0.0: {} - strip-eof@1.0.0: {} strip-indent@4.1.1: {} strip-json-comments@3.1.1: {} + super-regex@1.1.0: + dependencies: + function-timeout: 1.0.2 + make-asynchronous: 1.1.0 + time-span: 5.1.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -4089,7 +3234,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tar@7.5.19: + tar@7.5.22: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -4097,6 +3242,10 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + time-span@5.1.0: + dependencies: + convert-hrtime: 5.0.0 + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) @@ -4112,16 +3261,9 @@ snapshots: dependencies: typescript: 6.0.3 - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - tslib@2.8.1: {} - tsx@4.23.0: + tsx@4.23.1: dependencies: esbuild: 0.28.1 optionalDependencies: @@ -4141,72 +3283,34 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@4.41.0: {} + type-is@2.1.0: dependencies: content-type: 2.0.0 - media-typer: 1.1.0 + media-typer: 1.1.1 mime-types: 3.0.2 - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: + typescript-eslint@8.65.0(eslint@10.8.0)(typescript@6.0.3): dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.8: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typescript-eslint@8.63.0(eslint@10.6.0)(typescript@6.0.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - eslint: 10.6.0 + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@6.0.3))(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.0)(typescript@6.0.3) + eslint: 10.8.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color typescript@6.0.3: {} - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - undici-types@8.3.0: {} unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.4): + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: - browserslist: 4.28.4 + browserslist: 4.28.7 escalade: 3.2.0 picocolors: 1.1.1 @@ -4221,46 +3325,7 @@ snapshots: vary@1.1.2: {} - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.2.0 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.22 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.22: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 + web-worker@1.5.0: {} which@1.3.1: dependencies: @@ -4294,6 +3359,8 @@ snapshots: yallist@5.0.0: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs-unparser@2.0.0: diff --git a/src/commands/cloud.ts b/src/commands/cloud.ts index bedb952..632a2b2 100644 --- a/src/commands/cloud.ts +++ b/src/commands/cloud.ts @@ -40,6 +40,7 @@ import { matrixIsIos, parseDeviceMatrix, } from '../utils/device-matrix.js'; +import { isEncryptionEnabled } from '../utils/envelope.js'; import { detectCiContext, isCI } from '../utils/ci.js'; import { CliError, @@ -194,6 +195,12 @@ export const cloudCommand = defineCommand({ let flows = args.flows as string | undefined; const googlePlay = Boolean(args['google-play']); const ignoreShaCheck = Boolean(args['ignore-sha-check']); + // Single opt-in for client-side envelope encryption of every sensitive + // artifact — the binary (#1138), the flow zip (#1151), and env vars + // (#1152). Flag wins; otherwise DCD_ENCRYPT / DCD_ENCRYPT_BINARIES. + const encrypt = isEncryptionEnabled( + args['encrypt'] ? true : undefined, + ); const includeTags = coerceArray( collectRepeatedFlag(rawArgs, ['--include-tags']), ); @@ -230,8 +237,6 @@ export const cloudCommand = defineCommand({ collectRepeatedFlag(rawArgs, ['--metadata', '-m']), false, ); - const mitmHost = args.mitmHost as string | undefined; - const mitmPath = args.mitmPath as string | undefined; const moropoApiKey = args['moropo-v1-api-key'] as string | undefined; const name = args.name as string | undefined; const orientation = validateEnum( @@ -279,10 +284,6 @@ export const cloudCommand = defineCommand({ ); } - if (mitmPath && !mitmHost) { - throw new CliError('--mitmPath requires --mitmHost to be set'); - } - if (jsonFileName && !jsonFileFlag) { throw new CliError('--json-file-name requires --json-file'); } @@ -415,7 +416,7 @@ export const cloudCommand = defineCommand({ if (runnerType === 'm1') { out( ui.info( - 'runnerType m1 is experimental and currently supports Android (Pixel 7, API Level 34) only.', + 'runnerType m1 is experimental and currently supports Android only (all devices, API level 34-36).', ), ); } @@ -423,7 +424,7 @@ export const cloudCommand = defineCommand({ if (runnerType === 'gpu1') { out( ui.info( - 'runnerType gpu1 is Android-only (all devices, API Level 34 or 35), available to all users.', + 'runnerType gpu1 is Android-only (all devices, API level 34+), available to all users.', ), ); } @@ -762,6 +763,7 @@ export const cloudCommand = defineCommand({ auth, apiUrl, debug, + encrypt, filePath: finalAppFile, ignoreShaCheck, log: !json, @@ -791,6 +793,7 @@ export const cloudCommand = defineCommand({ androidApiLevel, androidDevice, androidNoSnapshot, + apiUrl, appBinaryId: finalBinaryId, cliVersion, commonRoot, @@ -798,6 +801,7 @@ export const cloudCommand = defineCommand({ debug, deviceLocale, deviceMatrix, + encrypt, env, executionPlan, flowFile, @@ -807,8 +811,6 @@ export const cloudCommand = defineCommand({ logger: (m: string) => out(m), maestroVersion: resolvedMaestroVersion, metadata: mergedMetadata, - mitmHost, - mitmPath, name, orientation, raw: [], diff --git a/src/commands/upload.ts b/src/commands/upload.ts index cdb01eb..f56cb83 100644 --- a/src/commands/upload.ts +++ b/src/commands/upload.ts @@ -21,6 +21,7 @@ export const uploadCommand = defineCommand({ ...apiFlags, 'app-url': binaryFlags['app-url'], 'ignore-sha-check': binaryFlags['ignore-sha-check'], + encrypt: binaryFlags.encrypt, debug: outputFlags.debug, json: outputFlags.json, appFile: { @@ -42,6 +43,7 @@ export const uploadCommand = defineCommand({ const apiUrl = resolveApiUrl(args['api-url'] as string | undefined); const appUrl = args['app-url'] as string | undefined; const ignoreShaCheck = Boolean(args['ignore-sha-check']); + const encryptBinary = Boolean(args['encrypt']); const debug = Boolean(args.debug); const positional = args.appFile as string | undefined; @@ -88,6 +90,7 @@ export const uploadCommand = defineCommand({ auth, apiUrl, debug, + encrypt: encryptBinary, filePath: resolvedFile, ignoreShaCheck, log: !json, diff --git a/src/config/environments.ts b/src/config/environments.ts index 9dd7011..ccf06e5 100644 --- a/src/config/environments.ts +++ b/src/config/environments.ts @@ -19,6 +19,20 @@ export interface DcdEnvironment { projectRef: string; anonKey: string; }; + /** + * Pinned KEK public key for client-side binary envelope encryption (dcd#1138). + * `key` is base64 of the raw 32-byte X25519 public key; `version` selects which + * KEK the platform API unwraps with. **Public — safe to embed** (like the anon + * key above): it can only *encrypt*; the private half lives solely on the API. + * + * Left `null` until the KEK is generated and provisioned. Generate a keypair + * with, e.g.: + * node -e 'const c=require("crypto");const{publicKey,privateKey}=c.generateKeyPairSync("x25519");const pub=publicKey.export({type:"spki",format:"der"}).subarray(12);const priv=privateKey.export({type:"pkcs8",format:"der"}).subarray(16);console.log("public :",pub.toString("base64"));console.log("private:",priv.toString("base64"))' + * Pin `public` here; set `private` as `BINARY_KEK_PRIVATE_KEYS={"":""}` + * on the API service and keep one offline escrow copy. Until then, encryption + * can be exercised via the `DCD_BINARY_KEK_PUBLIC` env override (see envelope.ts). + */ + kekPublicKey: { version: number; key: string } | null; } export const ENVIRONMENTS: Record = { @@ -31,6 +45,7 @@ export const ENVIRONMENTS: Record = { anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8', }, + kekPublicKey: { version: 1, key: 'wtfyWEwK7nJzwI4PD+9RAW8jxIR1u8kMQq2IhsrVnH4=' }, }, dev: { apiUrl: 'https://api.dev.devicecloud.dev', @@ -41,6 +56,7 @@ export const ENVIRONMENTS: Record = { anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ', }, + kekPublicKey: { version: 1, key: 'RgcToF/OJpcQI9koYvSvtj/WLaebfcN4v5GJoqtr/00=' }, }, }; diff --git a/src/config/flags/binary.flags.ts b/src/config/flags/binary.flags.ts index c855790..32dde91 100644 --- a/src/config/flags/binary.flags.ts +++ b/src/config/flags/binary.flags.ts @@ -24,4 +24,9 @@ export const binaryFlags = { description: 'Ignore the sha hash check and upload the binary regardless of whether it already exists (not recommended)', }, + encrypt: { + type: 'boolean', + description: + 'Encrypt the app binary, flow zip, and env vars before upload (client-side envelope encryption; each gets its own key). The binary is still deduplicated across runs, so an unchanged app is not re-uploaded. Can also be enabled with DCD_ENCRYPT=1.', + }, } as const satisfies ArgsDef; diff --git a/src/config/flags/environment.flags.ts b/src/config/flags/environment.flags.ts index e15c3b3..f20ba7d 100644 --- a/src/config/flags/environment.flags.ts +++ b/src/config/flags/environment.flags.ts @@ -17,16 +17,6 @@ export const environmentFlags = { description: 'Arbitrary key-value metadata to include with your test run (format: key=value, may be repeated)', }, - mitmHost: { - type: 'string', - description: - 'used for mitmproxy support, enterprise only, contact support if interested', - }, - mitmPath: { - type: 'string', - description: - 'used for mitmproxy support, enterprise only, contact support if interested', - }, 'moropo-v1-api-key': { type: 'string', description: 'API key for Moropo v1 integration', diff --git a/src/gateways/api-gateway.ts b/src/gateways/api-gateway.ts index 48eeb18..2c3dfd9 100644 --- a/src/gateways/api-gateway.ts +++ b/src/gateways/api-gateway.ts @@ -179,14 +179,110 @@ export const ApiGateway = { ); }, + /** + * Prefer server-assembled bundle delivery. The API returns a signed manifest + * plus a URL to a delivery service that streams the ZIP straight from + * storage, so large downloads don't flow through the API itself. The client + * just relays the signed `{ manifest, sig }` to that URL — no auth header, + * because the manifest is already signed. + * + * Returns true once the bundle has been streamed to disk. Returns false when + * the bundle path is unavailable or anything about it doesn't pan out — a + * `501` (deployment doesn't offer it), a non-OK manifest response, a body + * that isn't a manifest, or a delivery-service error — so the caller can fall + * back to the inline download endpoint. Definitive errors (e.g. not found) + * surface through that inline path instead. + */ + async tryBundleDownload( + baseUrl: string, + auth: AuthContext, + manifestEndpoint: string, + destinationPath: string, + operation: string, + ): Promise { + let manifestRes: Response; + try { + manifestRes = await fetch(`${baseUrl}${manifestEndpoint}`, { + headers: { ...auth.headers }, + method: 'GET', + }); + } catch { + return false; + } + + // 501 => this deployment has no bundle delivery; any other non-OK => let + // the inline path re-request and surface the real error. + if (!manifestRes.ok) { + return false; + } + + let bundle: { + bundleUrl?: string; + manifest?: string; + sig?: string; + }; + try { + bundle = (await manifestRes.json()) as typeof bundle; + } catch { + return false; + } + if (!bundle?.bundleUrl || !bundle?.manifest || !bundle?.sig) { + return false; + } + + let zipRes: Response; + try { + zipRes = await fetch(bundle.bundleUrl, { + body: JSON.stringify({ manifest: bundle.manifest, sig: bundle.sig }), + // No auth header: the manifest is signed and is the access token. + headers: { 'content-type': 'application/json' }, + method: 'POST', + }); + } catch { + return false; + } + if (!zipRes.ok) { + return false; + } + + // A mid-stream failure (dropped/truncated connection) or a null body on an + // otherwise-OK response throws here — fall back to the inline path rather + // than let it escape past the caller's fallback. The inline path re-opens + // the destination with flags: 'w', truncating any partial file left behind. + try { + await this.streamResponseToFile(zipRes, destinationPath, operation); + } catch { + return false; + } + return true; + }, + + /** + * Look for an already-uploaded binary to skip re-uploading. + * + * Two lookup keys, because encryption changes what is stable (dcd#1168). + * Unencrypted uploads pass `sha` (the hash of exactly what gets stored). + * Encrypted uploads pass `shaPlain` plus `encrypted: true`: their ciphertext is + * freshly keyed on every upload, so its hash never matches, and the plaintext + * hash is the only stable key. `sha` is deliberately not sent in that case — + * see the caller in methods.ts for why sending it would be unsafe. + * + * `encrypted` in the response reports whether the *matched* binary is stored + * encrypted, so the caller can verify the invariant it asked for instead of + * trusting the server to have applied the right predicate. + */ async checkForExistingUpload( baseUrl: string, auth: AuthContext, - sha: string, + lookup: { encrypted?: boolean; sha?: string; shaPlain?: string } | string, ) { + // Historically this took a bare sha string; keep that shape working. + const body = + typeof lookup === 'string' ? { sha: lookup } : { ...lookup }; + try { const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, { - body: JSON.stringify({ sha }), + body: JSON.stringify(body), headers: { 'content-type': 'application/json', ...auth.headers, @@ -199,7 +295,9 @@ export const ApiGateway = { } return await parseJsonResponse< - paths['/uploads/checkForExistingUpload']['post']['responses']['201']['content']['application/json'] + paths['/uploads/checkForExistingUpload']['post']['responses']['201']['content']['application/json'] & { + encrypted?: boolean; + } >(res, 'Failed to check for existing upload'); } catch (error) { // Handle network-level errors (DNS, connection refused, timeout, etc.) @@ -219,6 +317,19 @@ export const ApiGateway = { results: 'ALL' | 'FAILED', artifactsPath: string = './artifacts.zip', ) { + // Prefer bundle delivery; fall back to the inline download below. + if ( + await this.tryBundleDownload( + baseUrl, + auth, + `/results/${uploadId}/artifacts-bundle?results=${results}`, + artifactsPath, + 'Failed to download artifacts', + ) + ) { + return; + } + try { const res = await fetch(`${baseUrl}/results/${uploadId}/download`, { body: JSON.stringify({ results }), @@ -251,9 +362,15 @@ export const ApiGateway = { metadata: TAppMetadata; path: string; sha?: string; + /** + * Hash of the PLAINTEXT, sent only for encrypted uploads (dcd#1168). Stored + * as `binaries.sha_plain` so later encrypted uploads of the same input can + * dedup; `sha` remains the ciphertext hash. + */ + shaPlain?: string; supabaseSuccess: boolean; }) { - const { baseUrl, auth, id, metadata, path, sha, supabaseSuccess, backblazeSuccess, bytes } = config; + const { baseUrl, auth, id, metadata, path, sha, shaPlain, supabaseSuccess, backblazeSuccess, bytes } = config; try { const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, { body: JSON.stringify({ @@ -263,6 +380,7 @@ export const ApiGateway = { metadata, path, // This is tempPath for TUS uploads ...(sha ? { sha } : {}), + ...(shaPlain ? { shaPlain } : {}), supabaseSuccess, }), headers: { @@ -677,6 +795,22 @@ export const ApiGateway = { const finalReportPath = reportPath || path.resolve(process.cwd(), defaultFilename); const url = `${baseUrl}${endpoint}`; + // The HTML report is a ZIP bundle; prefer bundle delivery when available. + // (junit is a single small file and allure has its own endpoint — both stay + // on the inline path.) + if ( + reportType === 'html' && + (await this.tryBundleDownload( + baseUrl, + auth, + `/results/${uploadId}/report-bundle`, + finalReportPath, + errorPrefix, + )) + ) { + return; + } + try { // Make the download request const res = await fetch(url, { diff --git a/src/mcp/tools/run-cloud-test.ts b/src/mcp/tools/run-cloud-test.ts index 1ba76f3..2cb7072 100644 --- a/src/mcp/tools/run-cloud-test.ts +++ b/src/mcp/tools/run-cloud-test.ts @@ -12,6 +12,7 @@ import { VersionService } from '../../services/version.service.js'; import { uploadBinary, uploadFlowZip, verifyAppZip } from '../../methods.js'; import { getCliVersion } from '../../utils/cli.js'; import { fetchCompatibilityData } from '../../utils/compatibility.js'; +import { isEncryptionEnabled } from '../../utils/envelope.js'; import { getConsoleUrl } from '../../utils/styling.js'; import { getContext, logStderr } from '../context.js'; import { jsonResult, runTool } from '../helpers.js'; @@ -33,7 +34,7 @@ const sleep = (ms: number) => * `waitTimeoutSeconds`. * * Mirrors the `dcd cloud` command's submission path but headless: no Expo URL - * download, mitm, GitHub metadata, or JSON-file output. Use the CLI for those. + * download, GitHub metadata, or JSON-file output. Use the CLI for those. */ export function registerRunCloudTest(server: McpServer): void { server.registerTool( @@ -173,6 +174,10 @@ export function registerRunCloudTest(server: McpServer): void { }); } + // Client-side envelope encryption (binary/flow/env). No MCP flag, so + // it's env-driven: DCD_ENCRYPT / DCD_ENCRYPT_BINARIES. + const encrypt = isEncryptionEnabled(); + // Resolve the binary: existing id, or upload the local file. let appBinaryId = args.appBinaryId; if (!appBinaryId) { @@ -190,6 +195,7 @@ export function registerRunCloudTest(server: McpServer): void { appBinaryId = await uploadBinary({ auth, apiUrl, + encrypt, filePath: args.appFile, ignoreShaCheck: Boolean(args.ignoreShaCheck), log: false, @@ -199,10 +205,12 @@ export function registerRunCloudTest(server: McpServer): void { const { continueOnFailure = true } = executionPlan.sequence ?? {}; const testSubmissionService = new TestSubmissionService(); const { buffer, fields } = await testSubmissionService.buildTestPayload({ + apiUrl, appBinaryId, cliVersion, commonRoot, continueOnFailure, + encrypt, executionPlan, flowFile, env: args.env ?? [], diff --git a/src/methods.ts b/src/methods.ts index ebc7a0f..e3401f8 100644 --- a/src/methods.ts +++ b/src/methods.ts @@ -20,6 +20,14 @@ import { SupabaseGateway } from './gateways/supabase-gateway.js'; import { MetadataExtractorService } from './services/metadata-extractor.service.js'; import { TAppMetadata } from './types.js'; import type { AuthContext } from './types/domain/auth.types.js'; +import { + type BinaryEnvelope, + encryptFileToPath, + generateDek, + isEncryptionEnabled, + resolveKekPublicKey, + wrapDek, +} from './utils/envelope.js'; import { colors, formatId } from './utils/styling.js'; const mimeTypeLookupByExtension: Record = { @@ -94,7 +102,7 @@ export const compressFilesFromRelativePath = async ( }; export const verifyAppZip = async (zipPath: string) => { - // eslint-disable-next-line import/namespace, new-cap + // eslint-disable-next-line new-cap const zip = await new StreamZip.async({ file: zipPath, storeEntries: true, @@ -129,6 +137,12 @@ interface UploadBinaryConfig { auth: AuthContext; apiUrl: string; debug?: boolean; + /** + * Encrypt the binary before upload (client-side envelope encryption, #1138). + * Defaults to `DCD_ENCRYPT` / `DCD_ENCRYPT_BINARIES` when unset (see + * {@link isEncryptionEnabled}). + */ + encrypt?: boolean; filePath: string; ignoreShaCheck?: boolean; log?: boolean; @@ -136,6 +150,7 @@ interface UploadBinaryConfig { export const uploadBinary = async (config: UploadBinaryConfig) => { const { filePath, apiUrl, auth, ignoreShaCheck = false, log = true, debug = false } = config; + const encrypt = isEncryptionEnabled(config.encrypt); if (log) { ux.action.start(colors.bold('Checking and uploading binary'), colors.dim('Initializing'), { stdout: true, @@ -151,17 +166,28 @@ export const uploadBinary = async (config: UploadBinaryConfig) => { const startTime = Date.now(); let source: UploadSource | undefined; + let encCleanupDir: string | undefined; + let enc: BinaryEnvelope | undefined; try { // Prepare file for upload source = await prepareFileForUpload(filePath, debug, startTime); - // Calculate SHA hash - const sha = await calculateFileHash(source, debug, log); + // Hash the PLAINTEXT first, before any encryption (dcd#1168). Encryption + // uses a fresh random DEK per upload, so the ciphertext hash differs every + // time and cannot dedup — the plaintext hash is the only stable key. Doing it + // in this order also means a dedup hit skips the encryption work entirely, + // not just the upload. + const shaPlain = await calculateFileHash(source, debug, log); - // Check for existing upload with same SHA - if (!ignoreShaCheck && sha) { - const { exists, binaryId } = await checkExistingUpload(apiUrl, auth, sha, debug); + // Check for an existing upload before spending anything on encryption. + if (!ignoreShaCheck && shaPlain) { + const { exists, binaryId } = await checkExistingUpload( + apiUrl, + auth, + encrypt ? { encrypted: true, shaPlain } : { sha: shaPlain }, + debug, + ); if (exists && binaryId) { if (log) { @@ -175,8 +201,35 @@ export const uploadBinary = async (config: UploadBinaryConfig) => { } } + // Encrypt after the dedup check, so the SHA sent at finalise, and both + // uploaders, operate on ciphertext (binaries.sha = ciphertext hash, #1138). + if (encrypt) { + const encrypted = await encryptUploadSource(source, apiUrl, debug); + enc = encrypted.enc; + encCleanupDir = encrypted.cleanupDir; + if (log) { + ux.info(colors.dim(`Encrypting binary before upload (KEK v${enc.kek})`)); + } + } + + // Re-hash once encrypted: what lands in storage is the ciphertext, and every + // downstream verifySha hashes the bytes it actually holds (no DEK required). + const sha = encrypt ? await calculateFileHash(source, debug, false) : shaPlain; + // Perform the upload - const uploadId = await performUpload({ auth, apiUrl, debug, filePath, sha, source, startTime }); + const uploadId = await performUpload({ + auth, + apiUrl, + debug, + enc, + filePath, + sha, + // Only encrypted uploads record a plaintext hash; it is what makes the + // next encrypted run of this binary dedupable. + shaPlain: encrypt ? shaPlain : undefined, + source, + startTime, + }); if (log) { ux.action.stop(colors.success('\n✓ Binary uploaded with ID: ') + formatId(uploadId)); @@ -205,9 +258,54 @@ export const uploadBinary = async (config: UploadBinaryConfig) => { if (source?.cleanupDir) { await rm(source.cleanupDir, { recursive: true, force: true }).catch(() => {}); } + if (encCleanupDir) { + await rm(encCleanupDir, { recursive: true, force: true }).catch(() => {}); + } } }; +/** + * Encrypt the prepared upload source in place (dcd#1138): generate a per-upload + * DEK, stream-encrypt `source.diskPath` into a temp ciphertext file, wrap the + * DEK with the environment's pinned KEK public key, and repoint `source` at the + * ciphertext so the SHA, dedup check, and both uploaders operate on ciphertext. + * Returns the envelope metadata (for `binaries.metadata.enc`) plus the temp dir + * to clean up. Throws if no KEK is available for the environment. + */ +async function encryptUploadSource( + source: UploadSource, + apiUrl: string, + debug: boolean, +): Promise<{ enc: BinaryEnvelope; cleanupDir: string }> { + const kek = resolveKekPublicKey(apiUrl); + if (!kek) { + throw new Error( + 'Binary encryption was requested but no KEK public key is configured for this environment. ' + + 'Set DCD_BINARY_KEK_PUBLIC=: or pin one in src/config/environments.ts.', + ); + } + + const dek = generateDek(); + const enc = wrapDek(dek, kek); + const cleanupDir = await mkdtemp(path.join(os.tmpdir(), 'dcd-enc-')); + const cipherPath = path.join(cleanupDir, 'binary.enc'); + + if (debug) { + console.log(`[DEBUG] Encrypting binary with KEK v${kek.version} -> ${cipherPath}`); + } + + await encryptFileToPath(source.diskPath, cipherPath, dek, kek.version); + const { size } = await stat(cipherPath); + source.diskPath = cipherPath; + source.size = size; + + if (debug) { + console.log(`[DEBUG] Ciphertext size: ${(size / 1024 / 1024).toFixed(2)} MB`); + } + + return { enc, cleanupDir }; +} + /** * Disk-backed description of the binary to upload. Every upload path streams * from `diskPath` instead of materializing the file in memory — a 1.5 GB iOS @@ -352,30 +450,43 @@ async function calculateFileHash( } /** - * Checks if an upload with the same SHA already exists + * Checks whether a matching binary has already been uploaded. + * + * `lookup` is `{ sha }` for a plaintext upload, or `{ shaPlain, encrypted: true }` + * for an encrypted one (dcd#1168) — see {@link ApiGateway.checkForExistingUpload}. + * + * When asking as an encrypting client, a hit is only honoured if the server + * confirms the matched binary is itself encrypted. Any binary is a *plausible* + * match on plaintext hash, including a previously-uploaded plaintext copy of the + * same app, and reusing that would hand back an unencrypted binary while the user + * had asked for encryption. The server applies the same predicate; this is the + * client refusing to depend on that, so an older or misbehaving deployment + * degrades into a redundant upload rather than a silent loss of encryption. + * * @param apiUrl API base URL * @param auth AuthContext carrying request headers - * @param sha SHA-256 hash to check + * @param lookup Dedup key — plaintext hash for encrypted uploads, else the sha * @param debug Whether debug logging is enabled * @returns Promise resolving to object with exists flag and optional binaryId */ async function checkExistingUpload( apiUrl: string, auth: AuthContext, - sha: string, + lookup: { encrypted?: boolean; sha?: string; shaPlain?: string }, debug: boolean, ): Promise<{ binaryId?: string; exists: boolean }> { try { if (debug) { console.log('[DEBUG] Checking for existing upload with matching SHA...'); + console.log(`[DEBUG] Lookup: ${JSON.stringify(lookup)}`); console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/checkForExistingUpload`); } const shaCheckStartTime = Date.now(); - const { appBinaryId, exists } = await ApiGateway.checkForExistingUpload( + const { appBinaryId, encrypted, exists } = await ApiGateway.checkForExistingUpload( apiUrl, auth, - sha as string, + lookup, ); if (debug) { @@ -386,6 +497,16 @@ async function checkExistingUpload( } } + if (exists && lookup.encrypted && encrypted !== true) { + if (debug) { + console.log( + '[DEBUG] Ignoring dedup hit: encryption was requested but the matched binary is not encrypted', + ); + } + + return { exists: false }; + } + return { binaryId: appBinaryId, exists }; } catch (error) { // Invalid credentials will fail every subsequent request — surface now @@ -416,8 +537,15 @@ interface PerformUploadConfig { auth: AuthContext; apiUrl: string; debug: boolean; + /** Envelope metadata when the binary was encrypted (#1138); undefined otherwise. */ + enc?: BinaryEnvelope; filePath: string; sha: string | undefined; + /** + * Hash of the plaintext, set only for encrypted uploads (#1168). Persisted as + * `binaries.sha_plain` so the next encrypted upload of this binary can dedup. + */ + shaPlain?: string; source: UploadSource; startTime: number; } @@ -694,13 +822,17 @@ function validateUploadResults( * @returns Promise resolving to upload ID */ async function performUpload(config: PerformUploadConfig): Promise { - const { filePath, apiUrl, auth, source, sha, debug, startTime } = config; + const { filePath, apiUrl, auth, enc, source, sha, shaPlain, debug, startTime } = config; // Request upload URL and paths const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl, auth, filePath, source.size, debug); - // Extract app metadata + // Extract app metadata from the original (plaintext) file. Attach the + // envelope so it lands on binaries.metadata.enc (#1138). const metadata = await extractBinaryMetadata(filePath, debug); + if (enc) { + metadata.enc = enc; + } const env = inferEnvFromApiUrl(apiUrl); @@ -753,6 +885,7 @@ async function performUpload(config: PerformUploadConfig): Promise { path: tempPath, // sha is undefined when hash calculation failed — omit it explicitly ...(sha ? { sha } : {}), + ...(shaPlain ? { shaPlain } : {}), supabaseSuccess: supabaseResult.success, }); diff --git a/src/services/test-submission.service.ts b/src/services/test-submission.service.ts index bbff215..dc04b24 100644 --- a/src/services/test-submission.service.ts +++ b/src/services/test-submission.service.ts @@ -3,6 +3,12 @@ import * as path from 'node:path'; import { compressFilesFromRelativePath } from '../methods.js'; import { DeviceMatrixConfig } from '../types/domain/device.types.js'; +import { + type BinaryEnvelope, + encryptEnv, + encryptFlowBuffer, + resolveKekPublicKey, +} from '../utils/envelope.js'; import { toPortableRelativePath } from '../utils/paths.js'; import { IExecutionPlan } from './execution-plan.service.js'; @@ -10,6 +16,7 @@ export interface TestSubmissionConfig { androidApiLevel?: string; androidDevice?: string; androidNoSnapshot?: boolean; + apiUrl?: string; appBinaryId: string; cliVersion: string; commonRoot: string; @@ -18,6 +25,12 @@ export interface TestSubmissionConfig { deviceLocale?: string; deviceMatrix?: DeviceMatrixConfig[]; disableAnimations?: boolean; + /** + * Encrypt the flow zip and env vars before upload (#1151/#1152), each with its + * own per-upload DEK wrapped under the environment KEK. Requires `apiUrl` to + * resolve the pinned KEK public key. + */ + encrypt?: boolean; env?: string[]; executionPlan: IExecutionPlan; flowFile: string; @@ -28,8 +41,6 @@ export interface TestSubmissionConfig { maestroChromeOnboarding?: boolean; maestroVersion: string; metadata?: string[]; - mitmHost?: string; - mitmPath?: string; name?: string; orientation?: string; raw?: unknown; @@ -60,7 +71,9 @@ export class TestSubmissionService { config: TestSubmissionConfig, ): Promise<{ buffer: Buffer; fields: Record; sha: string }> { const { + apiUrl, appBinaryId, + encrypt = false, flowFile, executionPlan, commonRoot, @@ -78,8 +91,6 @@ export class TestSubmissionService { maestroVersion, deviceLocale, orientation, - mitmHost, - mitmPath, retry, continueOnFailure = true, report, @@ -144,7 +155,7 @@ export class TestSubmissionService { this.logDebug(debug, logger, `[DEBUG] Compressing files from path: ${flowFile}`); - const buffer = await compressFilesFromRelativePath( + const plaintextZip = await compressFilesFromRelativePath( flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml') ? path.dirname(flowFile) : flowFile, @@ -158,9 +169,45 @@ export class TestSubmissionService { commonRoot, ); - this.logDebug(debug, logger, `[DEBUG] Compressed file size: ${buffer.length} bytes`); + this.logDebug(debug, logger, `[DEBUG] Compressed file size: ${plaintextZip.length} bytes`); + + // Client-side envelope encryption (#1151 flow zip, #1152 env vars). Each + // gets its own per-upload DEK wrapped under the environment KEK; the flow + // zip becomes a DCDE container and `sha` is the CIPHERTEXT hash (uploads.sha + // stays a ciphertext hash, mirroring binaries.sha). `enc` rides `fields` + // as a JSON string like every other field, so both the JSON submitFlowTest + // body and the legacy multipart form carry it identically. + let buffer = plaintextZip; + let envObjectToSend: Record = envObject; + let flowEnc: BinaryEnvelope | undefined; + if (encrypt) { + if (!apiUrl) { + throw new Error('Encryption requires apiUrl to resolve the KEK public key'); + } + const kek = resolveKekPublicKey(apiUrl); + if (!kek) { + throw new Error( + 'Encryption was requested but no KEK public key is configured for this environment. ' + + 'Set DCD_BINARY_KEK_PUBLIC=: or pin one in src/config/environments.ts.', + ); + } + const flow = encryptFlowBuffer(plaintextZip, kek); + buffer = flow.ciphertext; + flowEnc = flow.enc; + this.logDebug( + debug, + logger, + `[DEBUG] Encrypting flow zip before upload (KEK v${kek.version}); ciphertext ${buffer.length} bytes`, + ); + // Only encrypt when there are env vars; an empty map has no secret to + // protect and stays a plaintext `{}` (no `enc` marker, passes through). + if (Object.keys(envObject).length > 0) { + envObjectToSend = { enc: encryptEnv(envObject, kek) }; + this.logDebug(debug, logger, `[DEBUG] Encrypting ${Object.keys(envObject).length} env var(s)`); + } + } - // Calculate SHA-256 hash of the flow ZIP + // SHA-256 of what actually gets uploaded (ciphertext when encrypted). const sha = createHash('sha256').update(buffer).digest('hex'); this.logDebug(debug, logger, `[DEBUG] Flow ZIP SHA-256: ${sha}`); @@ -182,7 +229,12 @@ export class TestSubmissionService { fields.sequentialFlows = JSON.stringify( this.normalizePaths(sequentialFlows, commonRoot), ); - fields.env = JSON.stringify(envObject); + fields.env = JSON.stringify(envObjectToSend); + // Flow-zip envelope for uploads.metadata.enc (#1151). JSON string so both + // submission paths carry it like every other field; the API parses it. + if (flowEnc) { + fields.enc = JSON.stringify(flowEnc); + } // Note: googlePlay is now included in configPayload below instead of as a separate field // to work around a FormData parsing issue in the API @@ -227,8 +279,6 @@ export class TestSubmissionService { deviceLocale, googlePlay, maestroVersion, - mitmHost, - mitmPath, orientation, raw: JSON.stringify(raw), report, diff --git a/src/types.ts b/src/types.ts index ff898d3..69f4a35 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,12 @@ +import type { BinaryEnvelope } from './utils/envelope.js'; + export type TAppMetadata = { appId: string; platform: 'android' | 'ios'; + /** + * Present when the binary was client-side envelope-encrypted before upload + * (dcd#1138). Stored on `binaries.metadata.enc`; the platform reads it to + * release the DEK and decrypt. Absent = legacy plaintext upload. + */ + enc?: BinaryEnvelope; }; diff --git a/src/types/domain/device.types.ts b/src/types/domain/device.types.ts index 45e2632..4f1ce7d 100644 --- a/src/types/domain/device.types.ts +++ b/src/types/domain/device.types.ts @@ -35,6 +35,7 @@ export enum EAndroidApiLevels { 'thirtyFive' = '35', 'thirtyFour' = '34', 'thirtyOne' = '31', + 'thirtySeven' = '37', 'thirtySix' = '36', 'thirtyThree' = '33', 'thirtyTwo' = '32', diff --git a/src/types/generated/schema.types.ts b/src/types/generated/schema.types.ts index f1bcbc0..574e604 100644 --- a/src/types/generated/schema.types.ts +++ b/src/types/generated/schema.types.ts @@ -70,6 +70,22 @@ export interface paths { patch?: never; trace?: never; }; + "/uploads/getFlowUploadUrl": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["UploadsController_getFlowUploadUrl"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/uploads/flow": { parameters: { query?: never; @@ -86,6 +102,55 @@ export interface paths { patch?: never; trace?: never; }; + "/uploads/submitFlowTest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Client-direct flow submission (JSON). The flow zip has already been + * uploaded straight to storage via getFlowUploadUrl, so nothing is buffered + * in API memory here — we just create the uploads row from the stored + * reference and run the shared submission flow. + */ + post: operations["UploadsController_submitFlowTest"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/uploads/estimateMatrix": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Dry-run cost + cell-count estimate for a (possibly device-matrix) + * submission. Runs the exact same resolve → validate → fan-out → price core as + * the submit path (so the quote equals the charge) but never persists and + * never touches credits. Lets the CLI print the cell count and estimated cost, + * and surface validation errors, before uploading the flow ZIP. + * + * The dollar estimate is exact for non-Google-Play cells; a Google Play + * column's price is path-dependent until #1100 unifies the parallel and + * sequential Play tiering. + */ + post: operations["UploadsController_estimateMatrix"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/uploads/retryTest": { parameters: { query?: never; @@ -166,23 +231,23 @@ export interface paths { patch?: never; trace?: never; }; - "/results/{uploadId}": { + "/org/paddle-webhook": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["ResultsController_getResults"]; + get?: never; put?: never; - post?: never; + post: operations["OrgController_handlePaddleWebhook"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/results/{uploadId}/download": { + "/org/update-name": { parameters: { query?: never; header?: never; @@ -191,14 +256,14 @@ export interface paths { }; get?: never; put?: never; - post: operations["ResultsController_getTestRunArtifacts"]; + post: operations["OrgController_updateOrgName"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/results/notify/{uploadId}": { + "/org/invite-team-member": { parameters: { query?: never; header?: never; @@ -207,114 +272,110 @@ export interface paths { }; get?: never; put?: never; - post: operations["ResultsController_notifyTestRunComplete"]; + post: operations["OrgController_inviteTeamMember"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/results/{uploadId}/report": { + "/org/accept-invite": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["ResultsController_downloadReport"]; + get?: never; put?: never; - post?: never; + post: operations["OrgController_acceptInvite"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/results/{uploadId}/html-report": { + "/org/revoke-invite": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["ResultsController_downloadHtmlReport"]; + get?: never; put?: never; - post?: never; + post: operations["OrgController_revokeInvite"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/results/{resultId}/html-report-single": { + "/org/change-team-member-role": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["ResultsController_downloadSingleHtmlReport"]; + get?: never; put?: never; - post?: never; + post: operations["OrgController_changeTeamMemberRole"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/results/compatibility/data": { + "/org/remove-team-member": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["ResultsController_getCompatibilityData"]; + get?: never; put?: never; - post?: never; + post: operations["OrgController_removeTeamMember"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/allure/{uploadId}/download": { + "/org/leave-team": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * Download Allure report as HTML - * @description Downloads a single-file Allure report as HTML containing all test results. Report is generated once and stored in Supabase Storage for subsequent downloads. - */ - get: operations["AllureController_downloadAllureReport"]; + get?: never; put?: never; - post?: never; + post: operations["OrgController_leaveTeam"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/webhooks": { + "/org/delete-team": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["WebhooksController_getWebhook"]; + get?: never; put?: never; - post: operations["WebhooksController_setWebhook"]; - delete: operations["WebhooksController_deleteWebhook"]; + post: operations["OrgController_deleteTeam"]; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/webhooks/regenerate-secret": { + "/org/subscriptions": { parameters: { query?: never; header?: never; @@ -323,14 +384,14 @@ export interface paths { }; get?: never; put?: never; - post: operations["WebhooksController_regenerateWebhookSecret"]; + post: operations["OrgController_getAllSubscriptions"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/webhooks/test": { + "/org/update-overage-limit": { parameters: { query?: never; header?: never; @@ -339,14 +400,14 @@ export interface paths { }; get?: never; put?: never; - post: operations["WebhooksController_testWebhook"]; + post: operations["OrgController_updateOverageLimit"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/org/paddle-webhook": { + "/org/usage-history": { parameters: { query?: never; header?: never; @@ -355,14 +416,14 @@ export interface paths { }; get?: never; put?: never; - post: operations["OrgController_handlePaddleWebhook"]; + post: operations["OrgController_getUsageHistory"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/org/update-name": { + "/org/update-gpu-retries": { parameters: { query?: never; header?: never; @@ -371,14 +432,14 @@ export interface paths { }; get?: never; put?: never; - post: operations["OrgController_updateOrgName"]; + post: operations["OrgController_updateGpuRetries"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/org/invite-team-member": { + "/org/update-billing-email": { parameters: { query?: never; header?: never; @@ -387,14 +448,14 @@ export interface paths { }; get?: never; put?: never; - post: operations["OrgController_inviteTeamMember"]; + post: operations["OrgController_updateBillingEmail"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/org/accept-invite": { + "/org/invoices": { parameters: { query?: never; header?: never; @@ -403,14 +464,14 @@ export interface paths { }; get?: never; put?: never; - post: operations["OrgController_acceptInvite"]; + post: operations["OrgController_getInvoices"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/org/subscriptions": { + "/org/invoice-url": { parameters: { query?: never; header?: never; @@ -419,14 +480,14 @@ export interface paths { }; get?: never; put?: never; - post: operations["OrgController_getAllSubscriptions"]; + post: operations["OrgController_getInvoiceUrl"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/org/update-overage-limit": { + "/org/bill-daily-overages": { parameters: { query?: never; header?: never; @@ -435,30 +496,30 @@ export interface paths { }; get?: never; put?: never; - post: operations["OrgController_updateOverageLimit"]; + post: operations["OrgController_billDailyOverages"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/org/usage-history": { + "/results/{uploadId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + get: operations["ResultsController_getResults"]; put?: never; - post: operations["OrgController_getUsageHistory"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/frontend/check-domain-saml": { + "/results/{uploadId}/download": { parameters: { query?: never; header?: never; @@ -467,37 +528,37 @@ export interface paths { }; get?: never; put?: never; - post: operations["FrontendController_checkDomainSaml"]; + post: operations["ResultsController_getTestRunArtifacts"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/frontend/validate-email": { + "/results/{uploadId}/report": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + get: operations["ResultsController_downloadReport"]; put?: never; - post: operations["FrontendController_validateEmail"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/health": { + "/results/{uploadId}/html-report": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["HealthController_health"]; + get: operations["ResultsController_downloadHtmlReport"]; put?: never; post?: never; delete?: never; @@ -506,30 +567,36 @@ export interface paths { patch?: never; trace?: never; }; - "/billing/create-subscription": { + "/results/{resultId}/html-report-single": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + get: operations["ResultsController_downloadSingleHtmlReport"]; put?: never; - post: operations["BillingController_createSubscription"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/stats/marketing": { + "/results/{uploadId}/artifacts-bundle": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["StatsController_getMarketingStats"]; + /** + * CDN-Worker bundle manifests (dcd#1137). These return a signed manifest the + * client POSTs to the Worker, which streams the ZIP from B2 — the artifact + * bytes bypass the API. A `501` means the CDN Worker isn't configured on this + * deployment; the client falls back to the inline download endpoints. + */ + get: operations["ResultsController_getArtifactsBundleManifest"]; put?: never; post?: never; delete?: never; @@ -538,458 +605,3349 @@ export interface paths { patch?: never; trace?: never; }; -} -export type webhooks = Record; -export interface components { - schemas: { - IDBResult: { - binary_upload_id: string; - cost: number | null; - created_at: string; - env: Record; - id: number; - org_id: number; - platform: string; - simulator_name: string; - status: string; - test_file_name: string; - test_upload_id: string; - }; - IGetBinaryUploadUrlArgs: { - /** - * @description Platform for the binary upload (ios or android) - * @enum {string} - */ - platform: "ios" | "android"; - /** @description File size in bytes (optional, for Backblaze upload strategy) */ - fileSize?: number; - /** @description Whether client uses TUS resumable uploads (true for new clients, undefined/false for legacy) */ - useTus?: boolean; + "/results/{uploadId}/report-bundle": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - B2SimpleUpload: { - uploadUrl: string; - authorizationToken: string; + get: operations["ResultsController_getReportBundleManifest"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/results/{resultId}/report-bundle-single": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - B2UploadPartUrl: { - uploadUrl: string; - authorizationToken: string; + get: operations["ResultsController_getSingleReportBundleManifest"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/results/compatibility/data": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - B2LargeUpload: { - fileId: string; - fileName: string; - uploadPartUrls: components["schemas"]["B2UploadPartUrl"][]; + get: operations["ResultsController_getCompatibilityData"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/allure/{uploadId}/download": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - B2UploadStrategy: { - /** @enum {string} */ - strategy: "simple" | "large"; - simple?: components["schemas"]["B2SimpleUpload"]; - large?: components["schemas"]["B2LargeUpload"]; + /** + * Download Allure report as HTML + * @description Downloads a single-file Allure report as HTML containing all test results. Report is generated once and stored in Supabase Storage for subsequent downloads. + */ + get: operations["AllureController_downloadAllureReport"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/webhooks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - IGetBinaryUploadUrlResponse: { - /** @description Temporary upload path in uploads/ folder for TUS upload */ - path: string; - /** @description Temporary upload path (same as path) */ - tempPath: string; - /** @description Final path where file will be moved after upload completes */ - finalPath: string; - /** @description Upload ID */ - id: string; - /** @description Backblaze upload strategy if configured */ + get: operations["WebhooksController_getWebhook"]; + put?: never; + post: operations["WebhooksController_setWebhook"]; + delete: operations["WebhooksController_deleteWebhook"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/webhooks/regenerate-secret": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["WebhooksController_regenerateWebhookSecret"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/webhooks/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["WebhooksController_testWebhook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/slack/oauth/start": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["SlackController_oauthStart"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/slack": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["SlackController_getConnection"]; + put?: never; + post?: never; + delete: operations["SlackController_disconnect"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/slack/channels": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["SlackController_getChannels"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/slack/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["SlackController_setConfig"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/slack/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["SlackController_test"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/github/oauth/start": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["GithubController_oauthStart"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/github": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["GithubController_getConnection"]; + put?: never; + post?: never; + delete: operations["GithubController_disconnect"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/notices": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Active notify notices (deprecation/warn/info/marketing) for the calling + * client. Block notices are never returned here — they are enforced + * server-side at submit time. The consumer applies any `match` gating it alone + * can evaluate (e.g. the selected device version). + */ + get: operations["NoticesController_getNotices"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["ApiKeysController_list"]; + put?: never; + post: operations["ApiKeysController_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api-keys/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["ApiKeysController_revoke"]; + options?: never; + head?: never; + patch: operations["ApiKeysController_update"]; + trace?: never; + }; + "/api-keys/{id}/rotate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["ApiKeysController_rotate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/ip-addresses": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["NetworkController_getIpAddresses"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/frontend/check-domain-saml": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["FrontendController_checkDomainSaml"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/frontend/validate-email": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["FrontendController_validateEmail"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/frontend/binary-download-url": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["FrontendController_getBinaryDownloadUrl"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/frontend/binaries/{binaryId}/download": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["FrontendController_downloadBinary"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/frontend/artifact-download-url": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["FrontendController_getArtifactDownloadUrl"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/frontend/result-detail/{resultId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["FrontendController_getResultDetail"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/frontend/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["FrontendController_ingestLogs"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/frontend/logs/anon": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["FrontendController_ingestLogsAnonymous"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["HealthController_health"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/billing/create-subscription": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["BillingController_createSubscription"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/billing/update-subscription": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["BillingController_updateSubscription"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/stats/marketing": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["StatsController_getMarketingStats"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/flows": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["FlowsController_getFlows"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/flows/runs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["FlowsController_getFlowRuns"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/live": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["LiveController_createSession"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/live/{identifier}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["LiveController_getSession"]; + put?: never; + post?: never; + delete: operations["LiveController_stopSession"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/live/{identifier}/exec": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["LiveController_execTest"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/live/{identifier}/commands/{commandId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["LiveController_getCommandStatus"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/live/{identifier}/keepalive": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["LiveController_keepalive"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/live/{identifier}/install": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["LiveController_installBinary"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me/orgs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["MeController_listOrgs"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me/sessions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** The caller's active auth sessions (for the Settings "Active sessions" panel). */ + get: operations["MeController_listSessions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me/sessions/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Revoke one of the caller's active auth sessions (per-row "Sign out" in the panel). JWT-authed; + * the service scopes the delete to the caller's own id, so you can only kill your own sessions. + * Throttled since it mutates auth state. + */ + delete: operations["MeController_revokeSession"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me/personal-team": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Provision a personal team for the caller — backs the "Create a personal team" button on the + * no-teams empty state (a user who left/was removed from their last org). Throttled. + */ + post: operations["MeController_createPersonalTeam"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me/team": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create an additional, named team for the caller — backs the "+ New team" org-switcher action. + * Always creates (vs /personal-team which is idempotent). Throttled. + */ + post: operations["MeController_createTeam"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me/delete": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Irreversibly delete the caller's own account. JWT-authed; the body must echo the + * account email as a defence-in-depth confirmation (the UI also type-confirms). Throttled + * hard since it's destructive. + */ + post: operations["MeController_deleteAccount"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/cli-login/handoff": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["CliLoginController_handoff"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/cli-login/claim": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["CliLoginController_claim"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/cli/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["CliLogsController_ingestLogs"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email-change/start": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["EmailChangeController_start"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email-change/verify-current": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["EmailChangeController_verifyCurrent"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email-change/verify-new": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["EmailChangeController_verifyNew"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/email-change/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["EmailChangeController_cancel"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + IDBResult: { + binary_upload_id: string; + cost: number | null; + created_at: string; + env: Record; + id: number; + org_id: number; + platform: string; + simulator_name: string; + status: string; + test_file_name: string; + test_upload_id: string; + }; + IGetBinaryUploadUrlArgs: { + /** + * @description Platform for the binary upload (ios or android) + * @enum {string} + */ + platform: "ios" | "android"; + /** @description File size in bytes (optional, for Backblaze upload strategy) */ + fileSize?: number; + /** @description Whether client uses TUS resumable uploads (true for new clients, undefined/false for legacy) */ + useTus?: boolean; + }; + B2SimpleUpload: { + uploadUrl: string; + authorizationToken: string; + }; + B2UploadPartUrl: { + uploadUrl: string; + authorizationToken: string; + }; + B2LargeUpload: { + fileId: string; + fileName: string; + uploadPartUrls: components["schemas"]["B2UploadPartUrl"][]; + }; + B2UploadStrategy: { + /** @enum {string} */ + strategy: "simple" | "large"; + simple?: components["schemas"]["B2SimpleUpload"]; + large?: components["schemas"]["B2LargeUpload"]; + }; + IGetBinaryUploadUrlResponse: { + /** + * @description Temporary upload path in uploads/ folder for TUS upload + * @example uploads/123e4567-e89b-12d3-a456-426614174000/123e4567-e89b-12d3-a456-426614174000.apk + */ + path: string; + /** + * @description Temporary upload path (same as path) + * @example uploads/123e4567-e89b-12d3-a456-426614174000/123e4567-e89b-12d3-a456-426614174000.apk + */ + tempPath: string; + /** + * @description Final path where file will be moved after upload completes + * @example 1/binaries/android/123e4567-e89b-12d3-a456-426614174000.apk + */ + finalPath: string; + /** + * @description Upload ID + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + id: string; + /** @description Backblaze upload strategy if configured */ b2?: components["schemas"]["B2UploadStrategy"]; /** @description Signed upload URL token for legacy clients (deprecated) */ token?: string; }; - ICheckForExistingUploadArgs: { - /** @description SHA-256 hash of the binary file */ - sha: string; + ICheckForExistingUploadArgs: { + /** @description SHA-256 hash of the binary file as uploaded. Required unless `encrypted` is true, in which case `shaPlain` is the lookup key (the ciphertext hash is not yet known at dedup time). */ + sha?: string; + /** @description SHA-256 hash of the PLAINTEXT binary. The lookup key when `encrypted` is true (#1168): encrypted uploads wrap under a fresh random DEK, so the ciphertext hash differs on every upload of identical input and cannot dedup. */ + shaPlain?: string; + /** + * @description Whether the caller intends to upload an encrypted binary. When true the lookup uses `shaPlain` and only ever matches rows that carry an encryption envelope, so an encrypting client can never be handed back a plaintext binary. + * @default false + */ + encrypted: boolean; + }; + ICheckForExistingUploadResponse: { + appBinaryId: string; + exists: boolean; + /** + * @description Whether the matched binary is stored encrypted (#1168). Lets an encrypting + * client assert the invariant it cares about client-side instead of trusting + * the server to have applied the right predicate — so an older or misbehaving + * deployment cannot quietly hand it a plaintext binary. + */ + encrypted?: boolean; + }; + IFinaliseUploadArgs: { + /** @description Unique upload identifier */ + id: string; + /** @description Storage path for the uploaded file */ + path: string; + /** @description File metadata (bundle ID, package name, platform) - required for new clients */ + metadata?: Record; + /** @description SHA-256 hash of the file - required for new clients */ + sha?: string; + /** @description SHA-256 hash of the PLAINTEXT file. Sent only by clients uploading an encrypted binary (#1168), where `sha` is the ciphertext hash; persisted as binaries.sha_plain so later encrypted uploads of the same input can dedup. */ + shaPlain?: string; + /** + * @description Whether the Supabase upload was successful + * @default true + */ + supabaseSuccess: boolean; + /** + * @description Whether the Backblaze upload was successful + * @default false + */ + backblazeSuccess: boolean; + /** @description Whether client uses TUS resumable uploads (true for new clients, undefined/false for legacy) */ + useTus?: boolean; + /** @description File size in bytes */ + bytes?: number; + }; + IFinaliseUploadResponse: Record; + IFinishLargeFileArgs: { + /** + * @description The Backblaze file ID from the large file upload + * @example abc123xyz + */ + fileId: string; + /** + * @description Array of SHA1 hashes for each uploaded part + * @example [ + * "sha1hash1", + * "sha1hash2" + * ] + */ + partSha1Array: string[]; + }; + IFinishLargeFileResponse: { + success: boolean; + result: Record; + }; + IGetFlowUploadUrlArgs: { + /** @description File size in bytes (optional, for Backblaze upload strategy) */ + fileSize?: number; + /** @description Whether client uses TUS resumable uploads (true for new clients, undefined/false for legacy) */ + useTus?: boolean; + }; + ICreateTestUploadArgs: { + testFileNames?: string; + sequentialFlows?: string; + /** @enum {string} */ + androidApiLevel?: "29" | "30" | "31" | "32" | "33" | "34" | "35" | "36" | "37"; + /** @enum {string} */ + androidDevice?: "pixel-6" | "pixel-6-pro" | "pixel-7" | "pixel-7-pro" | "generic-tablet"; + apiKey?: string; + apiUrl?: string; + appBinaryId: string; + appFile?: string; + env: string; + /** @enum {string} */ + iOSVersion?: "16" | "17" | "18" | "26"; + /** @enum {string} */ + iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "ipad-pro-6th-gen"; + platform?: string; + googlePlay?: boolean; + config: string; + name?: string; + /** @enum {string} */ + runnerType?: "m4" | "m1" | "default" | "gpu1" | "cpu1"; + metadata?: string; + workspaceConfig?: string; + flowMetadata?: string; + testFileOverrides?: string; + /** @description JSON array of explicit device configs forming the upload device matrix. Every flow that does not name its own device runs once per entry. Each entry names exactly one validated cell and must match the binary platform — there is no cross-product expansion. iOS: {"iOSDevice":"iphone-16","iOSVersion":"18"}. Android: {"androidDevice":"pixel-7","androidApiLevel":"34","googlePlay":true}. Omit for single-device (legacy) behaviour. */ + deviceMatrix?: string; + /** @description SHA-256 hash of the flow ZIP file */ + sha?: string; + /** @description JSON-encoded envelope { v, kek, wrapped_key } when the flow ZIP was client-side encrypted (#1151). Stored as uploads.metadata.enc; the flow zip carries its own per-upload DEK, distinct from the binary. */ + enc?: string; + /** @description Size of the flow ZIP file in bytes */ + bytes?: number; + /** + * Format: binary + * @description This file must be a zip file + */ + file: string; + }; + ISubmitFlowTestArgs: { + testFileNames?: string; + sequentialFlows?: string; + /** @enum {string} */ + androidApiLevel?: "29" | "30" | "31" | "32" | "33" | "34" | "35" | "36" | "37"; + /** @enum {string} */ + androidDevice?: "pixel-6" | "pixel-6-pro" | "pixel-7" | "pixel-7-pro" | "generic-tablet"; + apiKey?: string; + apiUrl?: string; + appBinaryId: string; + appFile?: string; + env: string; + /** @enum {string} */ + iOSVersion?: "16" | "17" | "18" | "26"; + /** @enum {string} */ + iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "ipad-pro-6th-gen"; + platform?: string; + googlePlay?: boolean; + config: string; + name?: string; + /** @enum {string} */ + runnerType?: "m4" | "m1" | "default" | "gpu1" | "cpu1"; + metadata?: string; + workspaceConfig?: string; + flowMetadata?: string; + testFileOverrides?: string; + /** @description JSON array of explicit device configs forming the upload device matrix. Every flow that does not name its own device runs once per entry. Each entry names exactly one validated cell and must match the binary platform — there is no cross-product expansion. iOS: {"iOSDevice":"iphone-16","iOSVersion":"18"}. Android: {"androidDevice":"pixel-7","androidApiLevel":"34","googlePlay":true}. Omit for single-device (legacy) behaviour. */ + deviceMatrix?: string; + /** @description SHA-256 hash of the flow ZIP file */ + sha?: string; + /** @description JSON-encoded envelope { v, kek, wrapped_key } when the flow ZIP was client-side encrypted (#1151). Stored as uploads.metadata.enc; the flow zip carries its own per-upload DEK, distinct from the binary. */ + enc?: string; + /** @description Size of the flow ZIP file in bytes */ + bytes?: number; + /** @description Flow upload identifier returned by getFlowUploadUrl */ + id: string; + /** @description Storage path where the flow zip was uploaded (tempPath for TUS, finalPath for legacy signed URL) */ + path: string; + /** + * @description Whether the Supabase upload was successful + * @default true + */ + supabaseSuccess: boolean; + /** + * @description Whether the Backblaze upload was successful + * @default false + */ + backblazeSuccess: boolean; + /** @description Whether client uses TUS resumable uploads (true for new clients, undefined/false for legacy) */ + useTus?: boolean; + }; + IFlowTestParams: { + testFileNames?: string; + sequentialFlows?: string; + /** @enum {string} */ + androidApiLevel?: "29" | "30" | "31" | "32" | "33" | "34" | "35" | "36" | "37"; + /** @enum {string} */ + androidDevice?: "pixel-6" | "pixel-6-pro" | "pixel-7" | "pixel-7-pro" | "generic-tablet"; + apiKey?: string; + apiUrl?: string; + appBinaryId: string; + appFile?: string; + env: string; + /** @enum {string} */ + iOSVersion?: "16" | "17" | "18" | "26"; + /** @enum {string} */ + iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "ipad-pro-6th-gen"; + platform?: string; + googlePlay?: boolean; + config: string; + name?: string; + /** @enum {string} */ + runnerType?: "m4" | "m1" | "default" | "gpu1" | "cpu1"; + metadata?: string; + workspaceConfig?: string; + flowMetadata?: string; + testFileOverrides?: string; + /** @description JSON array of explicit device configs forming the upload device matrix. Every flow that does not name its own device runs once per entry. Each entry names exactly one validated cell and must match the binary platform — there is no cross-product expansion. iOS: {"iOSDevice":"iphone-16","iOSVersion":"18"}. Android: {"androidDevice":"pixel-7","androidApiLevel":"34","googlePlay":true}. Omit for single-device (legacy) behaviour. */ + deviceMatrix?: string; + /** @description SHA-256 hash of the flow ZIP file */ + sha?: string; + /** @description JSON-encoded envelope { v, kek, wrapped_key } when the flow ZIP was client-side encrypted (#1151). Stored as uploads.metadata.enc; the flow zip carries its own per-upload DEK, distinct from the binary. */ + enc?: string; + /** @description Size of the flow ZIP file in bytes */ + bytes?: number; + }; + IRetryTestArgs: { + /** @description ID of a specific result to retry. Either resultId or uploadId must be provided, but not both. */ + resultId?: number; + /** @description ID of an upload to retry all failed tests for. Either resultId or uploadId must be provided, but not both. */ + uploadId?: string; + }; + ICancelTestArgs: { + /** @description ID of a specific result to cancel. Either resultId or uploadId must be provided, but not both. */ + resultId?: number; + /** @description ID of an upload to cancel all pending results for. Either resultId or uploadId must be provided, but not both. */ + uploadId?: string; + }; + UpdateOrgNameDto: { + /** + * @description Organization ID + * @example 123 + */ + orgId: number; + /** + * @description Organization name + * @example Acme Corporation + */ + name: string; + }; + InviteTeamMemberDto: { + /** + * @description Email address to invite + * @example teammate@example.com + */ + inviteEmail: string; + /** + * @description Invite acceptance link shown in the email + * @example https://app.devicecloud.dev/login?invite_email=... + */ + link: string; + /** + * @description Organization ID + * @example 1 + */ + orgId: string; + /** @description Organization name shown in the email */ + orgName: string; + }; + AcceptInviteDto: { + /** + * @description Organization ID + * @example 1 + */ + orgId: string; + /** + * @description User email address + * @example user@example.com + */ + email: string; + }; + RevokeInviteDto: { + /** + * @description Organization ID + * @example 1 + */ + orgId: string; + /** + * @description Email of the pending invite to revoke + * @example teammate@example.com + */ + email: string; + }; + ChangeRoleDto: { + /** + * @description Organization ID + * @example 1 + */ + orgId: string; + /** + * @description Email of the team member whose role is being changed + * @example teammate@example.com + */ + email: string; + /** + * @description New role to assign + * @example admin + * @enum {string} + */ + newRole: "admin" | "standard" | "owner"; + }; + RemoveMemberDto: { + /** + * @description Organization ID + * @example 1 + */ + orgId: string; + /** + * @description Email of the team member to remove + * @example teammate@example.com + */ + email: string; + }; + LeaveTeamDto: { + /** + * @description Organization ID to leave + * @example 1 + */ + orgId: string; + }; + DeleteTeamDto: { + /** + * @description Organization ID to delete + * @example 1 + */ + orgId: string; + }; + TResultResponse: { + id: number; + test_file_name: string; + status: string; + retry_of?: number; + fail_reason?: string; + duration_seconds?: number; + simulator_name?: string; + config?: Record; + }; + TFlowSummaryResponse: { + flow_name: string; + file_name: string; + last_run_at: string; + total_runs: number; + passed_runs: number; + failed_runs: number; + pass_rate: number; + avg_duration: number; + daily_data: Record; + tags: string[]; + }; + TFlowRunItem: { + id: number; + status: string; + createdAt: string; + durationSeconds: number | null; + failReason: string | null; + testUploadId: string; + uploadName: string; + }; + HandoffDto: { + /** @description Opaque state token minted by the CLI. */ + state: string; + /** @description base64url(sha256(code_verifier)) — the PKCE S256 challenge. */ + code_challenge: string; + /** @description The browser session's Supabase access token (JWT), used only to verify the user's identity. */ + access_token: string; + /** @description The browser session's refresh token. Sent only for SAML SSO sessions, which cannot be minted a dedicated CLI session server-side. */ + refresh_token?: string; + }; + ClaimDto: { + /** @description Opaque state token minted by the CLI. */ + state: string; + /** @description base64url(random bytes) — the PKCE verifier. */ + code_verifier: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + UploadsController_getBinaryUploadUrl: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IGetBinaryUploadUrlArgs"]; + }; + }; + responses: { + /** @description The url has been successfully created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IGetBinaryUploadUrlResponse"]; + }; + }; + }; + }; + UploadsController_checkForExistingUpload: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ICheckForExistingUploadArgs"]; + }; + }; + responses: { + /** @description The url has been successfully created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ICheckForExistingUploadResponse"]; + }; + }; + }; + }; + UploadsController_finaliseUpload: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IFinaliseUploadArgs"]; + }; + }; + responses: { + /** @description The upload has been completed. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IFinaliseUploadResponse"]; + }; + }; + }; + }; + UploadsController_finishLargeFile: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IFinishLargeFileArgs"]; + }; + }; + responses: { + /** @description The large file upload has been completed. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IFinishLargeFileResponse"]; + }; + }; + }; + }; + UploadsController_getFlowUploadUrl: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IGetFlowUploadUrlArgs"]; + }; + }; + responses: { + /** @description The url has been successfully created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IGetBinaryUploadUrlResponse"]; + }; + }; + }; + }; + UploadsController_createTest: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["ICreateTestUploadArgs"]; + }; + }; + responses: { + /** @description The record has been successfully created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message?: string; + results?: components["schemas"]["IDBResult"][]; + }; + }; + }; + }; + }; + UploadsController_submitFlowTest: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ISubmitFlowTestArgs"]; + }; + }; + responses: { + /** @description The record has been successfully created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message?: string; + results?: components["schemas"]["IDBResult"][]; + }; + }; + }; + }; + }; + UploadsController_estimateMatrix: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IFlowTestParams"]; + }; + }; + responses: { + /** @description Estimated cell count and cost for a device-matrix submission. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + cellCount?: number; + totalCost?: number; + excludedFlows?: string[]; + columns?: { + deviceName?: string; + osVersion?: string; + googlePlay?: boolean; + flowCount?: number; + cost?: number; + }[]; + }; + }; + }; + }; + }; + UploadsController_retryTest: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["IRetryTestArgs"]; + }; + }; + responses: { + /** @description Retry started. Provide resultId to retry a single test (returns the new result id), or uploadId to retry all failed tests in an upload (returns retriedCount). */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message?: string; + id?: number; + success?: boolean; + retriedCount?: number; + }; + }; + }; + }; + }; + UploadsController_cancelTest: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ICancelTestArgs"]; + }; + }; + responses: { + /** @description The record has been successfully cancelled. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message?: string; + success?: boolean; + cancelledCount?: number; + }; + }; + }; + }; + }; + UploadsController_getUploadStatus: { + parameters: { + query?: { + /** @description Upload ID to get status for */ + uploadId?: string; + /** @description Upload name to get status for */ + name?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Upload status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "uploadId": "upload-123", + * "status": "PENDING", + * "tests": [ + * { + * "id": 1, + * "test_file_name": "test-flow.yaml", + * "status": "PENDING" + * } + * ] + * } + */ + "application/json": Record; + }; + }; + }; + }; + UploadsController_listUploads: { + parameters: { + query?: { + /** @description Filter by upload name (supports * wildcard) */ + name?: string; + /** @description Filter uploads created on or after this date (ISO 8601) */ + from?: string; + /** @description Filter uploads created on or before this date (ISO 8601) */ + to?: string; + /** @description Maximum number of uploads to return (default: 20) */ + limit?: number; + /** @description Number of uploads to skip (default: 0) */ + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of flow uploads. Use GET /uploads/status for detailed test results. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "uploads": [ + * { + * "id": "upload-123", + * "name": "Test Upload", + * "created_at": "2024-01-01T00:00:00Z", + * "consoleUrl": "https://console.devicecloud.dev/results/upload-123" + * } + * ], + * "total": 1, + * "limit": 20, + * "offset": 0 + * } + */ + "application/json": { + uploads?: { + id?: string; + name?: string | null; + created_at?: string; + consoleUrl?: string; + }[]; + total?: number; + limit?: number; + offset?: number; + }; + }; + }; + }; + }; + UploadsController_deleteUpload: { + parameters: { + query?: never; + header?: never; + path: { + uploadId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description The upload has been successfully deleted. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + success?: boolean; + message?: string; + }; + }; + }; + }; + }; + OrgController_handlePaddleWebhook: { + parameters: { + query?: never; + header: { + "paddle-signature": string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paddle webhook handler. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + }; + }; + OrgController_updateOrgName: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateOrgNameDto"]; + }; + }; + responses: { + /** @description Organization name updated successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + OrgController_inviteTeamMember: { + parameters: { + query?: never; + header: { + authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["InviteTeamMemberDto"]; + }; + }; + responses: { + /** @description Team member invited successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + OrgController_acceptInvite: { + parameters: { + query?: never; + header: { + authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AcceptInviteDto"]; + }; + }; + responses: { + /** @description Team invite accepted successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + OrgController_revokeInvite: { + parameters: { + query?: never; + header: { + authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RevokeInviteDto"]; + }; + }; + responses: { + /** @description Team invite revoked successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + OrgController_changeTeamMemberRole: { + parameters: { + query?: never; + header: { + authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ChangeRoleDto"]; + }; + }; + responses: { + /** @description Team member role updated successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + OrgController_removeTeamMember: { + parameters: { + query?: never; + header: { + authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RemoveMemberDto"]; + }; + }; + responses: { + /** @description Team member removed successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + OrgController_leaveTeam: { + parameters: { + query?: never; + header: { + authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LeaveTeamDto"]; + }; + }; + responses: { + /** @description Left the team successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + OrgController_deleteTeam: { + parameters: { + query?: never; + header: { + authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteTeamDto"]; + }; + }; + responses: { + /** @description Team deleted successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + OrgController_getAllSubscriptions: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + orgId: string; + }; + }; + }; + responses: { + /** @description All subscription data fetched successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + OrgController_updateOverageLimit: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + orgId: string; + overageLimit: number; + }; + }; + }; + responses: { + /** @description Overage limit updated successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + OrgController_getUsageHistory: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + orgId: string; + /** @enum {string} */ + format?: "json" | "csv"; + /** Format: date-time */ + startDate?: string; + /** Format: date-time */ + endDate?: string; + }; + }; + }; + responses: { + /** @description Usage history fetched successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown[]; + }; + }; + }; + }; + OrgController_updateGpuRetries: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + orgId: number; + gpuRetries: boolean; + }; + }; + }; + responses: { + /** @description GPU retries setting updated successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + OrgController_updateBillingEmail: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + orgId: number; + billingEmail: string; + }; + }; + }; + responses: { + /** @description Billing email updated successfully. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + OrgController_getInvoices: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + orgId: number; + }; + }; + }; + responses: { + /** @description List of Paddle invoices for the organization. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + OrgController_getInvoiceUrl: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + orgId: number; + transactionId: string; + }; + }; + }; + responses: { + /** @description Hosted PDF URL for a single invoice. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + OrgController_billDailyOverages: { + parameters: { + query?: never; + header: { + "x-cron-secret": string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Daily overage billing processed. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + ResultsController_getResults: { + parameters: { + query?: never; + header?: never; + path: { + uploadId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The record has been successfully created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + statusCode?: number; + results?: components["schemas"]["TResultResponse"][]; + }; + }; + }; + }; + }; + ResultsController_getTestRunArtifacts: { + parameters: { + query?: never; + header?: never; + path: { + uploadId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ResultsController_downloadReport: { + parameters: { + query?: never; + header?: never; + path: { + uploadId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Download combined JUNIT test report (report.xml) for the upload */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + }; + }; + ResultsController_downloadHtmlReport: { + parameters: { + query?: never; + header?: never; + path: { + uploadId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Download combined HTML test report with assets (report.zip) for the upload */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + }; + }; + ResultsController_downloadSingleHtmlReport: { + parameters: { + query?: never; + header?: never; + path: { + resultId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Download HTML test report with assets (report.zip) for a single result */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + }; + }; + ResultsController_getArtifactsBundleManifest: { + parameters: { + query: { + results: string; + }; + header?: never; + path: { + uploadId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ResultsController_getReportBundleManifest: { + parameters: { + query?: never; + header?: never; + path: { + uploadId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ResultsController_getSingleReportBundleManifest: { + parameters: { + query?: never; + header?: never; + path: { + resultId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ResultsController_getCompatibilityData: { + parameters: { + query?: never; + header: { + "x-dcd-cli-version": string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Device compatibility lookup data including Maestro versions */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "statusCode": 200, + * "data": { + * "ios": { + * "iphone-14": { + * "name": "iPhone 14", + * "versions": [ + * "16", + * "17", + * "18" + * ], + * "deprecated": false + * }, + * "iphone-15": { + * "name": "iPhone 15", + * "versions": [ + * "17" + * ], + * "deprecated": false + * }, + * "iphone-16": { + * "name": "iPhone 16", + * "versions": [ + * "18", + * "26" + * ], + * "deprecated": false + * }, + * "iphone-16-plus": { + * "name": "iPhone 16 Plus", + * "versions": [ + * "26" + * ], + * "deprecated": false + * }, + * "iphone-16-pro": { + * "name": "iPhone 16 Pro", + * "versions": [ + * "18", + * "26" + * ], + * "deprecated": false + * }, + * "iphone-16-pro-max": { + * "name": "iPhone 16 Pro Max", + * "versions": [ + * "18", + * "26" + * ], + * "deprecated": false + * }, + * "ipad-pro-6th-gen": { + * "name": "iPad Pro (6th gen)", + * "versions": [ + * "18", + * "26" + * ], + * "deprecated": false + * } + * }, + * "android": { + * "pixel-6": { + * "name": "Pixel 6", + * "apiLevels": [ + * "29", + * "30", + * "31", + * "32", + * "33", + * "34", + * "35", + * "36", + * "37" + * ], + * "deprecated": false + * }, + * "pixel-6-pro": { + * "name": "Pixel 6 Pro", + * "apiLevels": [ + * "33", + * "35" + * ], + * "deprecated": false + * }, + * "pixel-7": { + * "name": "Pixel 7", + * "apiLevels": [ + * "33", + * "34", + * "35", + * "36", + * "37" + * ], + * "deprecated": false + * }, + * "pixel-7-pro": { + * "name": "Pixel 7 Pro", + * "apiLevels": [ + * "33", + * "34", + * "35", + * "36", + * "37" + * ], + * "deprecated": false + * }, + * "generic-tablet": { + * "name": "Generic Tablet", + * "apiLevels": [ + * "33" + * ], + * "deprecated": false + * } + * }, + * "androidPlay": { + * "pixel-7": { + * "name": "Pixel 7 (Google Play)", + * "apiLevels": [ + * "34" + * ], + * "deprecated": false + * } + * }, + * "maestro": { + * "supportedVersions": [ + * "2.0.4", + * "2.0.9", + * "2.1.0", + * "2.2.0", + * "2.5.0", + * "2.5.1", + * "2.6.0", + * "2.6.1", + * "2.7.0", + * "2.8.0" + * ], + * "defaultVersion": "2.2.0", + * "latestVersion": "2.8.0" + * } + * } + * } + */ + "application/json": { + statusCode?: number; + data?: { + ios?: Record; + android?: Record; + androidPlay?: Record; + maestro?: { + supportedVersions?: string[]; + defaultVersion?: string; + latestVersion?: string; + }; + }; + }; + }; + }; + }; + }; + AllureController_downloadAllureReport: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The upload ID to generate Allure report for */ + uploadId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Allure report HTML file download */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/html": string; + }; + }; + /** @description Upload not found or no results available */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + WebhooksController_getWebhook: { + parameters: { + query?: { + /** @description Set to true to return full secret instead of masked version */ + show_secret?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current webhook configuration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + webhook_url?: string; + /** @description Full secret (only when show_secret=true) */ + secret_key?: string; + /** @description Masked secret (default) */ + secret_key_masked?: string; + /** Format: date-time */ + created_at?: string; + /** Format: date-time */ + updated_at?: string; + }; + }; + }; + }; + }; + WebhooksController_setWebhook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: uri + * @example https://api.example.com/webhook + */ + url: string; + }; + }; + }; + responses: { + /** @description Webhook URL set successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + WebhooksController_deleteWebhook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Webhook configuration deleted successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + WebhooksController_regenerateWebhookSecret: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Webhook secret regenerated successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + WebhooksController_testWebhook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: uri */ + url?: string; + }; + }; + }; + responses: { + /** @description Test webhook sent successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + SlackController_oauthStart: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - ICheckForExistingUploadResponse: { - appBinaryId: string; - exists: boolean; + requestBody?: never; + responses: { + /** @description Slack authorize URL to redirect the user to */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - IFinaliseUploadArgs: { - /** @description Unique upload identifier */ - id: string; - /** @description Storage path for the uploaded file */ - path: string; - /** @description File metadata (bundle ID, package name, platform) - required for new clients */ - metadata?: Record; - /** @description SHA-256 hash of the file - required for new clients */ - sha?: string; - /** - * @description Whether the Supabase upload was successful - * @default true - */ - supabaseSuccess: boolean; - /** - * @description Whether the Backblaze upload was successful - * @default false - */ - backblazeSuccess: boolean; - /** @description Whether client uses TUS resumable uploads (true for new clients, undefined/false for legacy) */ - useTus?: boolean; - /** @description File size in bytes */ - bytes?: number; + }; + SlackController_getConnection: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - IFinaliseUploadResponse: Record; - IFinishLargeFileArgs: { - /** - * @description The Backblaze file ID from the large file upload - * @example abc123xyz - */ - fileId: string; - /** - * @description Array of SHA1 hashes for each uploaded part - * @example [ - * "sha1hash1", - * "sha1hash2" - * ] - */ - partSha1Array: string[]; + requestBody?: never; + responses: { + /** @description Current Slack connection for the org (or null) */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - IFinishLargeFileResponse: { - success: boolean; - result: Record; + }; + SlackController_disconnect: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - ICreateTestUploadArgs: { - /** - * Format: binary - * @description This file must be a zip file - */ - file: string; - testFileNames?: string; - sequentialFlows?: string; - /** @enum {string} */ - androidApiLevel?: "29" | "30" | "31" | "32" | "33" | "34" | "35" | "36"; - /** @enum {string} */ - androidDevice?: "pixel-6" | "pixel-6-pro" | "pixel-7" | "pixel-7-pro" | "generic-tablet"; - apiKey?: string; - apiUrl?: string; - appBinaryId: string; - appFile?: string; - env: string; - /** @enum {string} */ - iOSVersion?: "16" | "17" | "18" | "26"; - /** @enum {string} */ - iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "ipad-pro-6th-gen"; - platform?: string; - googlePlay: boolean; - config: string; - name?: string; - /** @enum {string} */ - runnerType?: "m4" | "m1" | "default" | "gpu1" | "cpu1"; - metadata?: string; - workspaceConfig?: string; - flowMetadata?: string; - testFileOverrides?: string; - /** @description SHA-256 hash of the flow ZIP file */ - sha?: string; + requestBody?: never; + responses: { + /** @description Slack disconnected and connection removed */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - IRetryTestArgs: { - resultId: number; + }; + SlackController_getChannels: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Channels the bot can see, for the picker */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + SlackController_setConfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Slack channel / preferences updated */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + SlackController_test: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Test message sent to the configured channel */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + GithubController_oauthStart: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description GitHub App install URL to redirect the user to */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + GithubController_getConnection: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current GitHub App connection for the org (or null) */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + GithubController_disconnect: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description GitHub connection removed for the org */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NoticesController_getNotices: { + parameters: { + query: { + surface: string; + platform: string; + }; + header: { + "x-dcd-cli-version": string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active notices for the calling client. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + statusCode?: number; + data?: { + id?: string; + slug?: string | null; + /** @enum {string} */ + level?: "deprecation" | "warn" | "info" | "marketing"; + title?: string; + body?: string; + learnMoreUrl?: string | null; + dismissible?: boolean; + match?: Record | null; + }[]; + }; + }; + }; + }; + }; + ApiKeysController_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List the org API keys (no secrets) */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - ICancelTestArgs: { - /** @description ID of a specific result to cancel. Either resultId or uploadId must be provided, but not both. */ - resultId?: number; - /** @description ID of an upload to cancel all pending results for. Either resultId or uploadId must be provided, but not both. */ - uploadId?: string; + }; + ApiKeysController_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - TResultResponse: { - id: number; - test_file_name: string; - status: string; - retry_of?: number; - fail_reason?: string; - duration_seconds?: number; + requestBody?: never; + responses: { + /** @description Issue a new key; returns the raw key once */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; }; - UpdateOrgNameDto: { - /** - * @description Organization ID - * @example 123 - */ - orgId: number; - /** - * @description Organization name - * @example Acme Corporation - */ - name: string; + }; + ApiKeysController_revoke: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; }; - AcceptInviteDto: { - /** - * @description Organization ID - * @example 1 - */ - orgId: string; - /** - * @description User email address - * @example user@example.com - */ - email: string; + requestBody?: never; + responses: { + /** @description Revoke (soft-delete) a key */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - UploadsController_getBinaryUploadUrl: { + ApiKeysController_update: { parameters: { query?: never; - header: { - "x-app-api-key": string; + header?: never; + path: { + id: number; }; - path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["IGetBinaryUploadUrlArgs"]; + requestBody?: never; + responses: { + /** @description Update a key name/description/expiry */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; }; }; + }; + ApiKeysController_rotate: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; responses: { - /** @description The url has been successfully created. */ + /** @description Rotate a key; returns the new raw key once */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IGetBinaryUploadUrlResponse"]; + "application/json": Record; }; }; }; }; - UploadsController_checkForExistingUpload: { + NetworkController_getIpAddresses: { parameters: { query?: never; - header: { - "x-app-api-key": string; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current DeviceCloud test-runner egress IP addresses. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "updatedAt": "2026-07-06", + * "ipAddresses": [ + * "46.17.215.144", + * "46.17.215.145", + * "83.217.174.249" + * ], + * "ranges": [ + * { + * "cidr": "46.17.215.144/32", + * "platforms": [ + * "android", + * "ios" + * ], + * "type": "egress", + * "description": "Test runner egress" + * } + * ] + * } + */ + "application/json": { + /** @example 2026-07-06 */ + updatedAt?: string; + /** + * @example [ + * "46.17.215.144", + * "46.17.215.145", + * "83.217.174.249" + * ] + */ + ipAddresses?: string[]; + ranges?: { + /** @example 46.17.215.144/32 */ + cidr?: string; + platforms?: ("android" | "ios")[]; + /** @enum {string} */ + type?: "egress"; + description?: string; + }[]; + }; + }; }; + }; + }; + FrontendController_checkDomainSaml: { + parameters: { + query?: never; + header?: never; path?: never; cookie?: never; }; + /** @description Domain to check for SAML configuration */ requestBody: { content: { - "application/json": components["schemas"]["ICheckForExistingUploadArgs"]; + "application/json": { + /** @example example.com */ + domain: string; + }; }; }; responses: { - /** @description The url has been successfully created. */ + /** @description SAML status for the domain */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + forceSaml?: boolean; + }; + }; + }; 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad request - invalid domain or API error */ + 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ICheckForExistingUploadResponse"]; + "application/json": { + error?: string; + }; }; }; }; }; - UploadsController_finaliseUpload: { + FrontendController_validateEmail: { parameters: { query?: never; - header: { - "x-app-api-key": string; - }; + header?: never; path?: never; cookie?: never; }; + /** @description Email address to validate */ requestBody: { content: { - "application/json": components["schemas"]["IFinaliseUploadArgs"]; + "application/json": { + /** @example user@example.com */ + email: string; + }; }; }; responses: { - /** @description The upload has been completed. */ + /** @description Email validation result */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + valid?: boolean; + reason?: string; + }; + }; + }; 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad request - invalid email or API error */ + 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IFinaliseUploadResponse"]; + "application/json": { + error?: string; + }; }; }; }; }; - UploadsController_finishLargeFile: { + FrontendController_getBinaryDownloadUrl: { parameters: { query?: never; header: { - "x-app-api-key": string; + authorization: string; }; path?: never; cookie?: never; }; + /** @description Get a signed download URL for a binary */ requestBody: { content: { - "application/json": components["schemas"]["IFinishLargeFileArgs"]; + "application/json": { + binaryId: string; + orgId: number; + }; }; }; responses: { - /** @description The large file upload has been completed. */ + /** @description Signed download URL for the binary; `dek` (base64) is present when the binary is encrypted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + url?: string; + encrypted?: boolean; + dek?: string; + }; + }; + }; 201: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["IFinishLargeFileResponse"]; + content: { + "application/json": Record; + }; + }; + /** @description Binary not found or not accessible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string; + }; + }; + }; + }; + }; + FrontendController_downloadBinary: { + parameters: { + query: { + orgId: number; + }; + header: { + authorization: string; + }; + path: { + binaryId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Binary bytes (decrypted if encrypted) */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Binary not found or not accessible */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - UploadsController_createTest: { + FrontendController_getArtifactDownloadUrl: { parameters: { query?: never; header: { - "x-app-api-key": string; + authorization: string; }; path?: never; cookie?: never; }; + /** @description Get a signed download URL for a single result artifact */ requestBody: { content: { - "multipart/form-data": components["schemas"]["ICreateTestUploadArgs"]; + "application/json": { + resultId: number; + /** @description Supabase storage path; must be one of the result's recorded files */ + path: string; + /** @description Optional filename override for the saved file */ + download?: string; + }; }; }; responses: { - /** @description The record has been successfully created. */ + /** @description Signed download URL for the artifact (CDN/B2 or Supabase) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + url?: string; + }; + }; + }; 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Artifact not found or not accessible */ + 400: { headers: { [name: string]: unknown; }; content: { "application/json": { - message?: string; - results?: components["schemas"]["IDBResult"][]; + error?: string; }; }; }; }; }; - UploadsController_retryTest: { + FrontendController_getResultDetail: { parameters: { query?: never; header: { - "x-app-api-key": string; + authorization: string; }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["IRetryTestArgs"]; + path: { + resultId: number; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description The record has been successfully created. */ - 201: { + /** @description Bundled result detail: row + result_files + binary + signed media URLs + parsed log in a single round-trip */ + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - message?: string; - results?: components["schemas"]["IDBResult"][]; - }; + content?: never; + }; + /** @description Result not found or caller not authorized */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - UploadsController_cancelTest: { + FrontendController_ingestLogs: { parameters: { query?: never; header: { - "x-app-api-key": string; + authorization: string; + "x-dcd-org": string; }; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["ICancelTestArgs"]; - }; - }; + requestBody?: never; responses: { - /** @description The record has been successfully cancelled. */ - 201: { + /** @description Batch accepted for forwarding to Axiom */ + 202: { headers: { [name: string]: unknown; }; content: { "application/json": { - message?: string; - success?: boolean; - cancelledCount?: number; + accepted?: number; }; }; }; + /** @description Malformed batch payload */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - UploadsController_getUploadStatus: { + FrontendController_ingestLogsAnonymous: { parameters: { - query?: { - /** @description Upload ID to get status for */ - uploadId?: string; - /** @description Upload name to get status for */ - name?: string; - }; - header: { - "x-app-api-key": string; - }; + query?: never; + header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Upload status */ - 200: { + /** @description Batch accepted for forwarding to Axiom */ + 202: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "uploadId": "upload-123", - * "status": "PENDING", - * "tests": [ - * { - * "id": 1, - * "test_file_name": "test-flow.yaml", - * "status": "PENDING" - * } - * ] - * } - */ - "application/json": Record; + "application/json": { + accepted?: number; + }; + }; + }; + /** @description Malformed batch payload */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - UploadsController_listUploads: { + HealthController_health: { parameters: { - query?: { - /** @description Filter by upload name (supports * wildcard) */ - name?: string; - /** @description Filter uploads created on or after this date (ISO 8601) */ - from?: string; - /** @description Filter uploads created on or before this date (ISO 8601) */ - to?: string; - /** @description Maximum number of uploads to return (default: 20) */ - limit?: number; - /** @description Number of uploads to skip (default: 0) */ - offset?: number; - }; - header: { - "x-app-api-key": string; - }; + query?: never; + header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description List of flow uploads. Use GET /uploads/status for detailed test results. */ + /** @description Health check endpoint */ 200: { headers: { [name: string]: unknown; @@ -997,428 +3955,219 @@ export interface operations { content: { /** * @example { - * "uploads": [ - * { - * "id": "upload-123", - * "name": "Test Upload", - * "created_at": "2024-01-01T00:00:00Z", - * "consoleUrl": "https://console.devicecloud.dev/results/upload-123" - * } - * ], - * "total": 1, - * "limit": 20, - * "offset": 0 + * "status": "ok" * } */ "application/json": { - uploads?: { - id?: string; - name?: string | null; - created_at?: string; - consoleUrl?: string; - }[]; - total?: number; - limit?: number; - offset?: number; + /** @example ok */ + status?: string; }; }; }; }; }; - UploadsController_deleteUpload: { + BillingController_createSubscription: { parameters: { query?: never; - header: { - "x-app-api-key": string; - }; - path: { - uploadId: string; - }; + header?: never; + path?: never; cookie?: never; }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; + requestBody: { + content: { + "application/json": { + items: { + price_id?: string; + quantity?: number; + }[]; + customer_email?: string; + custom_data?: { + userId?: string; + }; + billing_details?: { + enable_checkout?: boolean; + }; }; - content?: never; }; - /** @description The upload has been successfully deleted. */ + }; + responses: { + /** @description Subscription created successfully. */ 201: { headers: { [name: string]: unknown; }; content: { - "application/json": { - success?: boolean; - message?: string; - }; + "application/json": Record; }; }; }; }; - ResultsController_getResults: { + BillingController_updateSubscription: { parameters: { query?: never; - header: { - "x-app-api-key": string; - }; - path: { - uploadId: string; - }; + header?: never; + path?: never; cookie?: never; }; - requestBody?: never; - responses: { - /** @description The record has been successfully created. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - statusCode?: number; - results?: components["schemas"]["TResultResponse"][]; - }; + requestBody: { + content: { + "application/json": { + price_id: string; }; }; }; - }; - ResultsController_getTestRunArtifacts: { - parameters: { - query?: never; - header: { - "x-app-api-key": string; - }; - path: { - uploadId: string; - }; - cookie?: never; - }; - requestBody?: never; responses: { + /** @description Subscription updated successfully. */ 201: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": Record; + }; }; }; }; - ResultsController_notifyTestRunComplete: { + StatsController_getMarketingStats: { parameters: { query?: never; - header: { - "x-app-api-key": string; - }; - path: { - uploadId: string; - }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Send results summary email. */ - 201: { + /** @description Public marketing statistics */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": string; + "application/json": { + /** @example 150000 */ + total_count?: number; + }; }; }; }; }; - ResultsController_downloadReport: { + FlowsController_getFlows: { parameters: { - query?: never; - header: { - "x-app-api-key": string; - }; - path: { - uploadId: string; + query?: { + platform?: "android" | "ios"; + appId?: string; + days?: number; + /** @description ISO 8601 date string (e.g. 2026-01-01). Overrides days when provided. */ + startDate?: string; + /** @description ISO 8601 date string (e.g. 2026-01-31). Defaults to now when startDate is set. */ + endDate?: string; + /** @description Comma-separated tag filter. Returns flows that have any of the given tags (e.g. smoke,critical). */ + tags?: string; }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Download combined JUNIT test report (report.xml) for the upload */ + /** @description Aggregated flow statistics for the last N days. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": string; + "application/json": { + statusCode?: number; + flows?: components["schemas"]["TFlowSummaryResponse"][]; + }; }; }; }; }; - ResultsController_downloadHtmlReport: { + FlowsController_getFlowRuns: { parameters: { - query?: never; - header: { - "x-app-api-key": string; - }; - path: { - uploadId: string; + query: { + fileName: string; + platform?: "android" | "ios"; + appId?: string; + limit?: number; + /** @description ISO 8601 date string (e.g. 2026-01-01). */ + startDate?: string; + /** @description ISO 8601 date string (e.g. 2026-01-31). */ + endDate?: string; }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Download combined HTML test report with assets (report.zip) for the upload */ + /** @description Individual run history for a specific flow file. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": string; + "application/json": { + statusCode?: number; + runs?: components["schemas"]["TFlowRunItem"][]; + }; }; }; }; }; - ResultsController_downloadSingleHtmlReport: { + LiveController_createSession: { parameters: { query?: never; - header: { - "x-app-api-key": string; - }; - path: { - resultId: string; - }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Download HTML test report with assets (report.zip) for a single result */ - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": string; + "application/json": Record; }; }; }; }; - ResultsController_getCompatibilityData: { + LiveController_getSession: { parameters: { query?: never; - header: { - "x-app-api-key": string; + header?: never; + path: { + identifier: string; }; - path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Device compatibility lookup data including Maestro versions */ 200: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "statusCode": 200, - * "data": { - * "ios": { - * "iphone-14": { - * "name": "iPhone 14", - * "versions": [ - * "16", - * "17", - * "18" - * ], - * "deprecated": false - * }, - * "iphone-15": { - * "name": "iPhone 15", - * "versions": [ - * "17" - * ], - * "deprecated": false - * }, - * "iphone-16": { - * "name": "iPhone 16", - * "versions": [ - * "18", - * "26" - * ], - * "deprecated": false - * }, - * "iphone-16-plus": { - * "name": "iPhone 16 Plus", - * "versions": [ - * "18", - * "26" - * ], - * "deprecated": false - * }, - * "iphone-16-pro": { - * "name": "iPhone 16 Pro", - * "versions": [ - * "18", - * "26" - * ], - * "deprecated": false - * }, - * "iphone-16-pro-max": { - * "name": "iPhone 16 Pro Max", - * "versions": [ - * "18", - * "26" - * ], - * "deprecated": false - * }, - * "ipad-pro-6th-gen": { - * "name": "iPad Pro (6th gen)", - * "versions": [ - * "18", - * "26" - * ], - * "deprecated": false - * } - * }, - * "android": { - * "pixel-6": { - * "name": "Pixel 6", - * "apiLevels": [ - * "29", - * "30", - * "31", - * "32", - * "33", - * "34", - * "35", - * "36" - * ], - * "deprecated": false - * }, - * "pixel-6-pro": { - * "name": "Pixel 6 Pro", - * "apiLevels": [ - * "33", - * "34", - * "35", - * "36" - * ], - * "deprecated": false - * }, - * "pixel-7": { - * "name": "Pixel 7", - * "apiLevels": [ - * "33", - * "34", - * "35", - * "36" - * ], - * "deprecated": false - * }, - * "pixel-7-pro": { - * "name": "Pixel 7 Pro", - * "apiLevels": [ - * "33", - * "34", - * "35", - * "36" - * ], - * "deprecated": false - * }, - * "generic-tablet": { - * "name": "Generic Tablet", - * "apiLevels": [ - * "33", - * "34", - * "35", - * "36" - * ], - * "deprecated": false - * } - * }, - * "androidPlay": { - * "pixel-6": { - * "name": "Pixel 6 (Google Play)", - * "apiLevels": [ - * "34", - * "35", - * "36" - * ], - * "deprecated": false - * }, - * "pixel-7": { - * "name": "Pixel 7 (Google Play)", - * "apiLevels": [ - * "34", - * "35", - * "36" - * ], - * "deprecated": false - * } - * }, - * "maestro": { - * "supportedVersions": [ - * "1.39.0", - * "1.39.2", - * "1.39.5", - * "1.39.7", - * "1.40.3", - * "1.41.0", - * "2.0.2", - * "2.0.3", - * "2.0.4", - * "2.0.9", - * "2.1.0" - * ], - * "defaultVersion": "1.41.0", - * "latestVersion": "2.1.0" - * } - * } - * } - */ - "application/json": { - statusCode?: number; - data?: { - ios?: Record; - android?: Record; - androidPlay?: Record; - maestro?: { - supportedVersions?: string[]; - defaultVersion?: string; - latestVersion?: string; - }; - }; - }; + "application/json": Record; }; }; }; }; - AllureController_downloadAllureReport: { + LiveController_stopSession: { parameters: { query?: never; - header: { - "x-app-api-key": string; - }; + header?: never; path: { - /** @description The upload ID to generate Allure report for */ - uploadId: string; + identifier: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Allure report HTML file download */ 200: { - headers: { - [name: string]: unknown; - }; - content: { - "text/html": string; - }; - }; - /** @description Upload not found or no results available */ - 404: { headers: { [name: string]: unknown; }; @@ -1426,64 +4175,40 @@ export interface operations { }; }; }; - WebhooksController_getWebhook: { + LiveController_execTest: { parameters: { - query?: { - /** @description Set to true to return full secret instead of masked version */ - show_secret?: boolean; - }; - header: { - "x-app-api-key": string; + query?: never; + header?: never; + path: { + identifier: string; }; - path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Current webhook configuration */ - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": { - webhook_url?: string; - /** @description Full secret (only when show_secret=true) */ - secret_key?: string; - /** @description Masked secret (default) */ - secret_key_masked?: string; - /** Format: date-time */ - created_at?: string; - /** Format: date-time */ - updated_at?: string; - }; + "application/json": Record; }; }; }; }; - WebhooksController_setWebhook: { + LiveController_getCommandStatus: { parameters: { query?: never; - header: { - "x-app-api-key": string; + header?: never; + path: { + identifier: string; + commandId: string; }; - path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": { - /** - * Format: uri - * @example https://api.example.com/webhook - */ - url: string; - }; - }; - }; + requestBody?: never; responses: { - /** @description Webhook URL set successfully */ - 201: { + 200: { headers: { [name: string]: unknown; }; @@ -1493,160 +4218,123 @@ export interface operations { }; }; }; - WebhooksController_deleteWebhook: { + LiveController_keepalive: { parameters: { query?: never; - header: { - "x-app-api-key": string; + header?: never; + path: { + identifier: string; }; - path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Webhook configuration deleted successfully */ - 200: { + 201: { headers: { [name: string]: unknown; }; - content: { - "application/json": Record; - }; + content?: never; }; }; }; - WebhooksController_regenerateWebhookSecret: { + LiveController_installBinary: { parameters: { query?: never; - header: { - "x-app-api-key": string; + header?: never; + path: { + identifier: string; }; - path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Webhook secret regenerated successfully */ 201: { headers: { [name: string]: unknown; }; - content: { - "application/json": Record; - }; + content?: never; }; }; }; - WebhooksController_testWebhook: { + MeController_listOrgs: { parameters: { query?: never; header: { - "x-app-api-key": string; + authorization: string; }; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": { - /** Format: uri */ - url?: string; - }; - }; - }; + requestBody?: never; responses: { - /** @description Test webhook sent successfully */ - 201: { + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": Record; - }; + content?: never; }; }; }; - OrgController_handlePaddleWebhook: { + MeController_listSessions: { parameters: { query?: never; header: { - "paddle-signature": string; + authorization: string; }; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Paddle webhook handler. */ - 201: { + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": string; - }; + content?: never; }; }; }; - OrgController_updateOrgName: { + MeController_revokeSession: { parameters: { query?: never; header: { - "x-app-api-key": string; + authorization: string; }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateOrgNameDto"]; + path: { + id: string; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description Organization name updated successfully. */ - 201: { + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": boolean; - }; + content?: never; }; }; }; - OrgController_inviteTeamMember: { + MeController_createPersonalTeam: { parameters: { query?: never; header: { - "x-app-api-key": string; + authorization: string; }; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": { - inviteEmail: string; - requesterEmail: string; - link: string; - orgId: string; - orgName: string; - }; - }; - }; + requestBody?: never; responses: { - /** @description Team member invited successfully. */ - 201: { + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": boolean; - }; + content?: never; }; }; }; - OrgController_acceptInvite: { + MeController_createTeam: { parameters: { query?: never; header: { @@ -1655,292 +4343,262 @@ export interface operations { path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AcceptInviteDto"]; - }; - }; + requestBody?: never; responses: { - /** @description Team invite accepted successfully. */ - 201: { + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": boolean; - }; + content?: never; }; }; }; - OrgController_getAllSubscriptions: { + MeController_deleteAccount: { parameters: { query?: never; header: { - "x-app-api-key": string; + authorization: string; }; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": { - orgId: string; - }; - }; - }; + requestBody?: never; responses: { - /** @description All subscription data fetched successfully. */ - 201: { + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": Record; - }; + content?: never; }; }; }; - OrgController_updateOverageLimit: { + CliLoginController_handoff: { parameters: { query?: never; - header: { - "x-app-api-key": string; - }; + header?: never; path?: never; cookie?: never; }; requestBody: { content: { - "application/json": { - orgId: string; - overageLimit: number; - }; + "application/json": components["schemas"]["HandoffDto"]; }; }; responses: { - /** @description Overage limit updated successfully. */ - 201: { + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": Record; - }; + content?: never; }; }; }; - OrgController_getUsageHistory: { + CliLoginController_claim: { parameters: { query?: never; - header: { - "x-app-api-key": string; - }; + header?: never; path?: never; cookie?: never; }; requestBody: { content: { - "application/json": { - orgId: string; - /** @enum {string} */ - format?: "json" | "csv"; - /** Format: date-time */ - startDate?: string; - /** Format: date-time */ - endDate?: string; - }; + "application/json": components["schemas"]["ClaimDto"]; }; }; responses: { - /** @description Usage history fetched successfully. */ - 201: { + /** @description Returns the Supabase session on successful claim. */ + 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown[]; - }; + content?: never; }; }; }; - FrontendController_checkDomainSaml: { + CliLogsController_ingestLogs: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** @description Domain to check for SAML configuration */ - requestBody: { - content: { - "application/json": { - /** @example example.com */ - domain: string; - }; - }; - }; + requestBody?: never; responses: { - /** @description SAML status for the domain */ + /** @description Batch accepted for forwarding to Axiom */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": { - forceSaml?: boolean; + accepted?: number; }; }; }; - 201: { + 202: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Bad request - invalid domain or API error */ + /** @description Malformed batch payload */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - error?: string; - }; - }; + content?: never; }; }; }; - FrontendController_validateEmail: { + EmailChangeController_start: { parameters: { query?: never; - header?: never; + header: { + authorization: string; + }; path?: never; cookie?: never; }; - /** @description Email address to validate */ + /** @description New email address to change to */ requestBody: { content: { "application/json": { /** @example user@example.com */ - email: string; + newEmail: string; }; }; }; responses: { - /** @description Email validation result */ + /** @description Verification code sent to the current address */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": { - valid?: boolean; - reason?: string; + ok?: boolean; }; }; }; - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad request - invalid email or API error */ + /** @description Invalid or missing newEmail */ 400: { headers: { [name: string]: unknown; }; content: { "application/json": { - error?: string; + message?: string; }; }; }; }; }; - HealthController_health: { + EmailChangeController_verifyCurrent: { parameters: { query?: never; - header?: never; + header: { + authorization: string; + }; path?: never; cookie?: never; }; - requestBody?: never; + /** @description Code emailed to the current address */ + requestBody: { + content: { + "application/json": { + /** @example 123456 */ + code: string; + }; + }; + }; responses: { - /** @description Health check endpoint */ + /** @description Current address verified; code sent to the new address */ 200: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "status": "ok" - * } - */ "application/json": { - /** @example ok */ - status?: string; + ok?: boolean; + }; + }; + }; + /** @description Invalid or expired code */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message?: string; }; }; }; }; }; - BillingController_createSubscription: { + EmailChangeController_verifyNew: { parameters: { query?: never; - header?: never; + header: { + authorization: string; + }; path?: never; cookie?: never; }; + /** @description Code emailed to the new address */ requestBody: { content: { "application/json": { - items: { - price_id?: string; - quantity?: number; - }[]; - customer_email?: string; - custom_data?: { - orgId?: string; - userId?: string; - }; - billing_details?: { - enable_checkout?: boolean; - }; + /** @example 123456 */ + code: string; }; }; }; responses: { - /** @description Subscription created successfully. */ - 201: { + /** @description Email change applied */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": Record; + "application/json": { + ok?: boolean; + email?: string; + }; + }; + }; + /** @description Invalid or expired code, or current email not verified */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message?: string; + }; }; }; }; }; - StatsController_getMarketingStats: { + EmailChangeController_cancel: { parameters: { query?: never; - header?: never; + header: { + authorization: string; + }; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Public marketing statistics */ + /** @description In-progress email change cancelled (idempotent) */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": { - /** @example 150000 */ - total_count?: number; + ok?: boolean; }; }; }; diff --git a/src/utils/envelope.ts b/src/utils/envelope.ts new file mode 100644 index 0000000..747705b --- /dev/null +++ b/src/utils/envelope.ts @@ -0,0 +1,301 @@ +import { + createCipheriv, + createPublicKey, + diffieHellman, + generateKeyPairSync, + hkdfSync, + randomBytes, +} from 'node:crypto'; +import { open, stat } from 'node:fs/promises'; +import { inferEnvFromApiUrl } from '../config/environments.js'; +import { ENVIRONMENTS } from '../config/environments.js'; + +/** + * Client-side envelope encryption of app binaries before upload (dcd#1138). + * + * The CLI is the *encrypt* half of the contract; the platform (api + + * simulators, in the `dcd` repo) is the *decrypt* half. This module MUST stay + * byte-compatible with `api/src/common/crypto/envelope.ts` — the wire format is + * specified in `dcd/docs/binary-envelope-encryption.md`. + * + * Scheme: a per-upload random 256-bit DEK encrypts the binary (chunked + * AES-256-GCM); the DEK is wrapped with the environment's pinned X25519 KEK + * public key (sealed box). Only the platform API holds the KEK private half, so + * every storage/transport tier sees ciphertext only. + */ + +const MAGIC = Buffer.from('DCDE', 'ascii'); +const CONTAINER_VERSION = 1; +const NONCE_LEN = 12; +const NONCE_PREFIX_LEN = 7; +const DEK_LEN = 32; +export const CHUNK_SIZE = 1024 * 1024; // 1 MiB plaintext segments + +const HKDF_INFO = Buffer.from('dcd-binary-dek-wrap-v1', 'ascii'); +// DER prefix that turns a raw 32-byte X25519 public key into an importable SPKI. +const SPKI_PREFIX = Buffer.from('302a300506032b656e032100', 'hex'); + +/** `binaries.metadata.enc` / `uploads.metadata.enc` shape (binary + flow zip). */ +export interface BinaryEnvelope { + v: number; + kek: number; + wrapped_key: string; +} + +/** + * `results.env.enc` shape (#1152). Same wrapped-DEK fields as a binary, plus the + * env ciphertext carried **inline** (the env map is tiny, so a single DCDE + * segment rides in-column rather than as a separate uploaded blob). + */ +export interface EnvEnvelope extends BinaryEnvelope { + /** base64 of a single-segment DCDE container of `JSON.stringify(env)`. */ + ciphertext: string; +} + +/** + * Whether client-side envelope encryption is on. Explicit `flag` (the + * `--encrypt` CLI flag) wins; otherwise `DCD_ENCRYPT=1` enables it for binary, + * flow, and env, and the legacy `DCD_ENCRYPT_BINARIES=1` is kept as an alias. + * When on, the binary, the flow zip, and the env map are each encrypted with + * their **own** per-upload DEK (all wrapped under the same per-env KEK). + */ +export function isEncryptionEnabled(flag?: boolean): boolean { + if (flag !== undefined) return flag; + return ( + process.env.DCD_ENCRYPT === '1' || process.env.DCD_ENCRYPT_BINARIES === '1' + ); +} + +/** Pinned KEK public key (base64 raw 32-byte X25519) + version, per env. */ +interface KekPublicKey { + version: number; + keyRaw: Buffer; +} + +function x25519PublicFromRaw(raw: Buffer) { + return createPublicKey({ + key: Buffer.concat([SPKI_PREFIX, raw]), + format: 'der', + type: 'spki', + }); +} + +/** + * Resolve the KEK public key for the environment behind `apiUrl`. Order: + * 1. `DCD_BINARY_KEK_PUBLIC` env override (`:`, e.g. `1:AAAA…`) + * — lets the feature be exercised before keys are pinned in the release. + * 2. the pinned `ENVIRONMENTS[env].kekPublicKey`. + * Returns null when no key is available (encryption cannot proceed). + */ +export function resolveKekPublicKey(apiUrl: string): KekPublicKey | null { + const override = process.env.DCD_BINARY_KEK_PUBLIC; + if (override) { + const [versionPart, b64] = override.split(':'); + const version = Number(versionPart); + if (b64 && Number.isInteger(version)) { + const keyRaw = Buffer.from(b64, 'base64'); + if (keyRaw.length === 32) return { version, keyRaw }; + } + throw new Error( + 'DCD_BINARY_KEK_PUBLIC must be ":"', + ); + } + + const env = inferEnvFromApiUrl(apiUrl); + const pinned = ENVIRONMENTS[env].kekPublicKey; + if (!pinned) return null; + const keyRaw = Buffer.from(pinned.key, 'base64'); + if (keyRaw.length !== 32) { + throw new Error(`Pinned KEK public key for ${env} is not a 32-byte key`); + } + return { version: pinned.version, keyRaw }; +} + +/** + * Wrap a DEK for the given KEK public key (X25519 sealed box). Produces base64 of + * `ephPub(32) || iv(12) || ciphertext(32) || tag(16)`. + */ +export function wrapDek(dek: Buffer, kek: KekPublicKey): BinaryEnvelope { + const eph = generateKeyPairSync('x25519'); + const ephPubRaw = eph.publicKey + .export({ type: 'spki', format: 'der' }) + .subarray(SPKI_PREFIX.length); + const shared = diffieHellman({ + privateKey: eph.privateKey, + publicKey: x25519PublicFromRaw(kek.keyRaw), + }); + const key = Buffer.from( + hkdfSync('sha256', shared, Buffer.alloc(0), HKDF_INFO, 32), + ); + const iv = randomBytes(NONCE_LEN); + const cipher = createCipheriv('aes-256-gcm', key, iv); + const ct = Buffer.concat([cipher.update(dek), cipher.final()]); + const wrapped = Buffer.concat([ + ephPubRaw, + iv, + ct, + cipher.getAuthTag(), + ]).toString('base64'); + return { v: CONTAINER_VERSION, kek: kek.version, wrapped_key: wrapped }; +} + +function segmentNonce( + prefix: Buffer, + index: number, + isLast: boolean, +): Buffer { + const nonce = Buffer.alloc(NONCE_LEN); + prefix.copy(nonce, 0, 0, NONCE_PREFIX_LEN); + nonce.writeUInt32BE(index, NONCE_PREFIX_LEN); + nonce.writeUInt8(isLast ? 1 : 0, NONCE_PREFIX_LEN + 4); + return nonce; +} + +/** + * Stream-encrypt `srcPath` into a DCDE container at `destPath` using `dek`, in + * constant memory (one chunk buffered at a time). `kekVersion` is written into + * the header (mirrors the wrapped-key's KEK version). + */ +export async function encryptFileToPath( + srcPath: string, + destPath: string, + dek: Buffer, + kekVersion: number, + chunkSize: number = CHUNK_SIZE, +): Promise { + if (dek.length !== DEK_LEN) { + throw new Error(`DEK must be ${DEK_LEN} bytes`); + } + const { size } = await stat(srcPath); + const noncePrefix = randomBytes(NONCE_PREFIX_LEN); + + const header = Buffer.alloc(17); + MAGIC.copy(header, 0); + header.writeUInt8(CONTAINER_VERSION, 4); + header.writeUInt8(kekVersion, 5); + header.writeUInt32BE(chunkSize, 6); + noncePrefix.copy(header, 10); + + const input = await open(srcPath, 'r'); + const output = await open(destPath, 'w'); + try { + await output.write(header); + const buf = Buffer.alloc(chunkSize); + let index = 0; + let readTotal = 0; + // size 0 → no segments (matches the API's empty-input handling). + while (readTotal < size) { + const { bytesRead } = await input.read(buf, 0, chunkSize, null); + if (bytesRead === 0) break; + readTotal += bytesRead; + const isLast = readTotal >= size; + const cipher = createCipheriv( + 'aes-256-gcm', + dek, + segmentNonce(noncePrefix, index, isLast), + ); + const ct = Buffer.concat([ + cipher.update(buf.subarray(0, bytesRead)), + cipher.final(), + ]); + await output.write(ct); + await output.write(cipher.getAuthTag()); + index += 1; + if (isLast) break; + } + } finally { + await input.close(); + await output.close(); + } +} + +/** + * In-memory twin of {@link encryptFileToPath}: encrypt `plaintext` into a DCDE + * container Buffer using `dek`. Byte-identical wire format (same 17-byte header, + * same per-segment nonce/tag scheme), for payloads already held in memory (flow + * zips, the env map). A payload no larger than `chunkSize` is a single segment. + */ +export function encryptToContainer( + plaintext: Buffer, + dek: Buffer, + kekVersion: number, + chunkSize: number = CHUNK_SIZE, +): Buffer { + if (dek.length !== DEK_LEN) { + throw new Error(`DEK must be ${DEK_LEN} bytes`); + } + const noncePrefix = randomBytes(NONCE_PREFIX_LEN); + + const header = Buffer.alloc(17); + MAGIC.copy(header, 0); + header.writeUInt8(CONTAINER_VERSION, 4); + header.writeUInt8(kekVersion, 5); + header.writeUInt32BE(chunkSize, 6); + noncePrefix.copy(header, 10); + + const parts: Buffer[] = [header]; + // size 0 → no segments (matches encryptFileToPath / the API's empty input). + let index = 0; + let offset = 0; + while (offset < plaintext.length) { + const end = Math.min(offset + chunkSize, plaintext.length); + const isLast = end >= plaintext.length; + const cipher = createCipheriv( + 'aes-256-gcm', + dek, + segmentNonce(noncePrefix, index, isLast), + ); + const ct = Buffer.concat([ + cipher.update(plaintext.subarray(offset, end)), + cipher.final(), + ]); + parts.push(ct, cipher.getAuthTag()); + offset = end; + index += 1; + } + return Buffer.concat(parts); +} + +/** + * Encrypt a flow zip buffer with its own per-upload DEK. Returns the ciphertext + * (a DCDE container) plus the envelope for `uploads.metadata.enc`. `uploads.sha` + * must be recomputed from the returned ciphertext by the caller. + */ +export function encryptFlowBuffer( + buffer: Buffer, + kek: KekPublicKey, +): { ciphertext: Buffer; enc: BinaryEnvelope } { + const dek = generateDek(); + const enc = wrapDek(dek, kek); + const ciphertext = encryptToContainer(buffer, dek, kek.version); + return { ciphertext, enc }; +} + +/** + * Encrypt the `--env KEY=VALUE` map with its own per-submission DEK into the + * `results.env.enc` envelope (#1152). The full map is serialized, encrypted as a + * single-segment container, and carried inline as base64 `ciphertext`. + */ +export function encryptEnv( + env: Record, + kek: KekPublicKey, +): EnvEnvelope { + const dek = generateDek(); + const { wrapped_key } = wrapDek(dek, kek); + const container = encryptToContainer( + Buffer.from(JSON.stringify(env), 'utf8'), + dek, + kek.version, + ); + return { + v: CONTAINER_VERSION, + kek: kek.version, + wrapped_key, + ciphertext: container.toString('base64'), + }; +} + +/** Generate a fresh per-upload DEK. */ +export function generateDek(): Buffer { + return randomBytes(DEK_LEN); +} diff --git a/test/unit/encrypted-dedup.test.ts b/test/unit/encrypted-dedup.test.ts new file mode 100644 index 0000000..47e4c5c --- /dev/null +++ b/test/unit/encrypted-dedup.test.ts @@ -0,0 +1,375 @@ +import { expect } from 'chai'; +import { generateKeyPairSync } from 'node:crypto'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { ApiGateway } from '../../src/gateways/api-gateway.js'; +import { uploadBinary } from '../../src/methods.js'; +import type { AuthContext } from '../../src/types/domain/auth.types.js'; + +/** + * Dedup for client-side encrypted binaries (dcd#1168). + * + * Encryption uses a fresh random DEK per upload, so the ciphertext hash differs + * every time and cannot serve as a dedup key. The CLI therefore hashes the + * PLAINTEXT first, deduplicates on that, and only encrypts on a miss. + * + * The security-critical half is the invariant check: a plaintext row has the same + * plaintext hash as its encrypted twin, so a dedup hit must be rejected unless + * the server confirms the matched binary is itself encrypted. + */ + +const TEST_AUTH: AuthContext = { + mode: 'apiKey', + headers: { 'x-app-api-key': 'test-key' }, +}; + +const API = 'http://localhost:9999'; +const APK = path.join(process.cwd(), 'test/fixtures/wikipedia.apk'); + +const originalFetch = (global as any).fetch; + +type Call = { body: unknown; url: string }; + +/** + * Mock global.fetch with a per-endpoint handler map, recording every call. + * Any endpoint without a handler resolves to a 500 carrying a marker string, so + * a test can assert control reached it. + * @param handlers Map of URL substring to a response factory + * @returns The recorded call list + */ +function mockFetch( + handlers: Record { body: unknown; status: number }>, +): Call[] { + const calls: Call[] = []; + (global as any).fetch = async ( + input: URL | string, + init?: RequestInit, + ): Promise => { + const url = input.toString(); + calls.push({ + body: init?.body ? JSON.parse(String(init.body)) : null, + url, + }); + + const key = Object.keys(handlers).find((k) => url.includes(k)); + const { body, status } = key + ? handlers[key]() + : { body: { message: 'REACHED_UPLOAD_PATH' }, status: 500 }; + + return new Response(JSON.stringify(body), { + headers: { 'content-type': 'application/json' }, + status, + }); + }; + return calls; +} + +/** A throwaway X25519 public key so encryption can run without a pinned KEK. */ +function setTestKek() { + const { publicKey } = generateKeyPairSync('x25519'); + const raw = publicKey.export({ format: 'der', type: 'spki' }).subarray(12); + process.env.DCD_BINARY_KEK_PUBLIC = `1:${raw.toString('base64')}`; +} + +describe('encrypted binary dedup (#1168)', () => { + afterEach(() => { + (global as any).fetch = originalFetch; + delete process.env.DCD_BINARY_KEK_PUBLIC; + }); + + describe('ApiGateway.checkForExistingUpload wire format', () => { + it('sends shaPlain + encrypted (and NOT sha) for an encrypted lookup', async () => { + const calls = mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'enc-binary', encrypted: true, exists: true }, + status: 200, + }), + }); + + const res = await ApiGateway.checkForExistingUpload(API, TEST_AUTH, { + encrypted: true, + shaPlain: 'plain-hash', + }); + + expect(calls[0].body).to.deep.equal({ + encrypted: true, + shaPlain: 'plain-hash', + }); + // The ciphertext hash is not known at dedup time and must not be implied. + expect(calls[0].body).to.not.have.property('sha'); + expect(res.encrypted).to.equal(true); + expect(res.appBinaryId).to.equal('enc-binary'); + }); + + it('sends a bare sha for an unencrypted lookup', async () => { + const calls = mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'plain-binary', encrypted: false, exists: true }, + status: 200, + }), + }); + + await ApiGateway.checkForExistingUpload(API, TEST_AUTH, { + sha: 'cipher-or-plain-hash', + }); + + expect(calls[0].body).to.deep.equal({ sha: 'cipher-or-plain-hash' }); + }); + + it('still accepts a bare string sha (back-compat with the old signature)', async () => { + const calls = mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'b', exists: true }, + status: 200, + }), + }); + + await ApiGateway.checkForExistingUpload(API, TEST_AUTH, 'legacy-sha'); + + expect(calls[0].body).to.deep.equal({ sha: 'legacy-sha' }); + }); + }); + + describe('uploadBinary dedup behaviour', () => { + it('reuses an encrypted match without encrypting or uploading', async () => { + setTestKek(); + const calls = mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'enc-binary', encrypted: true, exists: true }, + status: 200, + }), + }); + + const id = await uploadBinary({ + apiUrl: API, + auth: TEST_AUTH, + encrypt: true, + filePath: APK, + log: false, + }); + + expect(id).to.equal('enc-binary'); + // Nothing beyond the dedup check should have been attempted. + expect(calls).to.have.lengthOf(1); + expect(calls[0].url).to.contain('checkForExistingUpload'); + }); + + it('REJECTS a plaintext match when encryption was requested', async () => { + // The trap: the plaintext row has the same plaintext hash, so the server + // could answer with it. Honouring that hit would return an unencrypted + // binary to a caller who asked for encryption. + setTestKek(); + const calls = mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'plain-binary', encrypted: false, exists: true }, + status: 200, + }), + }); + + let returned: string | undefined; + try { + returned = await uploadBinary({ + apiUrl: API, + auth: TEST_AUTH, + encrypt: true, + filePath: APK, + log: false, + }); + } catch { + // The mocked upload path fails; reaching it at all is the assertion. + } + + // Proceeded past dedup into the upload path rather than silently handing + // back the unencrypted binary. + expect(returned).to.equal(undefined); + expect(calls.some((c) => c.url.includes('getBinaryUploadUrl'))).to.equal( + true, + ); + }); + + it('also rejects a hit when the server omits the encrypted field entirely', async () => { + // An older deployment predating #1168 has no encryption predicate on the + // lookup, so the absence of confirmation must be treated as a miss. + setTestKek(); + const calls = mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'unknown-binary', exists: true }, + status: 200, + }), + }); + + let returned: string | undefined; + try { + returned = await uploadBinary({ + apiUrl: API, + auth: TEST_AUTH, + encrypt: true, + filePath: APK, + log: false, + }); + } catch { + // As above — the mocked upload path fails by design. + } + + expect(returned).to.equal(undefined); + expect(calls.some((c) => c.url.includes('getBinaryUploadUrl'))).to.equal( + true, + ); + }); + + it('dedups on the plaintext hash, so the key is stable across encrypted runs', async () => { + setTestKek(); + const seen: unknown[] = []; + mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'enc-binary', encrypted: true, exists: true }, + status: 200, + }), + }); + + for (let i = 0; i < 2; i++) { + + await uploadBinary({ + apiUrl: API, + auth: TEST_AUTH, + encrypt: true, + filePath: APK, + log: false, + }); + } + + // Reset and capture the lookup keys from two independent invocations. + const calls = mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'enc-binary', encrypted: true, exists: true }, + status: 200, + }), + }); + await uploadBinary({ + apiUrl: API, + auth: TEST_AUTH, + encrypt: true, + filePath: APK, + log: false, + }); + await uploadBinary({ + apiUrl: API, + auth: TEST_AUTH, + encrypt: true, + filePath: APK, + log: false, + }); + for (const c of calls) { + seen.push((c.body as { shaPlain?: string }).shaPlain); + } + + expect(seen).to.have.lengthOf(2); + expect(seen[0]).to.be.a('string'); + // Identical input ⇒ identical lookup key, which is the whole point: the + // ciphertext hash would have differed on every run. + expect(seen[0]).to.equal(seen[1]); + }); + + it('leaves the unencrypted path deduping on the sha exactly as before', async () => { + const calls = mockFetch({ + checkForExistingUpload: () => ({ + body: { appBinaryId: 'plain-binary', encrypted: false, exists: true }, + status: 200, + }), + }); + + const id = await uploadBinary({ + apiUrl: API, + auth: TEST_AUTH, + encrypt: false, + filePath: APK, + log: false, + }); + + expect(id).to.equal('plain-binary'); + expect(calls[0].body).to.have.property('sha'); + expect(calls[0].body).to.not.have.property('encrypted'); + expect(calls[0].body).to.not.have.property('shaPlain'); + }); + + it('honours --ignore-sha-check by skipping the lookup altogether', async () => { + setTestKek(); + const calls = mockFetch({}); + + try { + await uploadBinary({ + apiUrl: API, + auth: TEST_AUTH, + encrypt: true, + filePath: APK, + ignoreShaCheck: true, + log: false, + }); + } catch { + // Upload path is mocked to fail; only the absence of a lookup matters. + } + + expect(calls.some((c) => c.url.includes('checkForExistingUpload'))).to.equal( + false, + ); + }); + }); + + describe('finalise payload', () => { + it('sends sha (ciphertext) alongside shaPlain for an encrypted upload', async () => { + const calls = mockFetch({ + finaliseUpload: () => ({ body: {}, status: 200 }), + }); + + await ApiGateway.finaliseUpload({ + auth: TEST_AUTH, + backblazeSuccess: true, + baseUrl: API, + bytes: 10, + id: 'upload-id', + + metadata: {} as any, + path: 'p', + sha: 'ciphertext-hash', + shaPlain: 'plaintext-hash', + supabaseSuccess: true, + }); + + expect(calls[0].body).to.include({ + sha: 'ciphertext-hash', + shaPlain: 'plaintext-hash', + }); + }); + + it('omits shaPlain for an unencrypted upload', async () => { + const calls = mockFetch({ + finaliseUpload: () => ({ body: {}, status: 200 }), + }); + + await ApiGateway.finaliseUpload({ + auth: TEST_AUTH, + backblazeSuccess: true, + baseUrl: API, + bytes: 10, + id: 'upload-id', + + metadata: {} as any, + path: 'p', + sha: 'plain-hash', + supabaseSuccess: true, + }); + + expect(calls[0].body).to.not.have.property('shaPlain'); + }); + }); +}); + +// Keep the fixture path assumption honest — every uploadBinary case depends on it. +describe('encrypted dedup test fixture', () => { + it('has the wikipedia.apk fixture available', () => { + expect(fs.existsSync(APK), `missing fixture: ${APK}`).to.equal(true); + expect(os.tmpdir()).to.be.a('string'); + }); +}); diff --git a/test/unit/envelope.test.ts b/test/unit/envelope.test.ts new file mode 100644 index 0000000..2c8f1a0 --- /dev/null +++ b/test/unit/envelope.test.ts @@ -0,0 +1,241 @@ +import { expect } from 'chai'; +import { + createDecipheriv, + createPublicKey, + createPrivateKey, + diffieHellman, + generateKeyPairSync, + hkdfSync, + randomBytes, +} from 'node:crypto'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { + encryptEnv, + encryptFileToPath, + encryptFlowBuffer, + generateDek, + resolveKekPublicKey, + wrapDek, +} from '../../src/utils/envelope.js'; + +/** + * Reference implementation of the platform *decrypt* half (as implemented in + * dcd `api/src/common/crypto/envelope.ts` and `simulators/gateways/ + * EnvelopeGateway.ts`). If the CLI's ciphertext round-trips through this, it is + * byte-compatible with the platform. + */ +const SPKI_PREFIX = Buffer.from('302a300506032b656e032100', 'hex'); +const PKCS8_PREFIX = Buffer.from('302e020100300506032b656e04220420', 'hex'); +const HKDF_INFO = Buffer.from('dcd-binary-dek-wrap-v1', 'ascii'); + +function refDecryptContainer(ciphertext: Buffer, dek: Buffer): Buffer { + expect(ciphertext.subarray(0, 4).toString('ascii')).to.equal('DCDE'); + expect(ciphertext.readUInt8(4)).to.equal(1); // container version + const chunkSize = ciphertext.readUInt32BE(6); + const noncePrefix = ciphertext.subarray(10, 17); + const body = ciphertext.subarray(17); + const segBytes = chunkSize + 16; + + const out: Buffer[] = []; + let off = 0; + let index = 0; + while (off < body.length) { + const end = Math.min(off + segBytes, body.length); + const seg = body.subarray(off, end); + const isLast = end >= body.length; + const ct = seg.subarray(0, seg.length - 16); + const tag = seg.subarray(seg.length - 16); + const nonce = Buffer.alloc(12); + noncePrefix.copy(nonce, 0, 0, 7); + nonce.writeUInt32BE(index, 7); + nonce.writeUInt8(isLast ? 1 : 0, 11); + const d = createDecipheriv('aes-256-gcm', dek, nonce); + d.setAuthTag(tag); + out.push(Buffer.concat([d.update(ct), d.final()])); + off = end; + index += 1; + } + return Buffer.concat(out); +} + +function refUnwrapDek(wrappedB64: string, kekPrivRaw: Buffer): Buffer { + const kekPriv = createPrivateKey({ + key: Buffer.concat([PKCS8_PREFIX, kekPrivRaw]), + format: 'der', + type: 'pkcs8', + }); + const blob = Buffer.from(wrappedB64, 'base64'); + const ephPubRaw = blob.subarray(0, 32); + const iv = blob.subarray(32, 44); + const ct = blob.subarray(44, 76); + const tag = blob.subarray(76, 92); + const ephPub = createPublicKey({ + key: Buffer.concat([SPKI_PREFIX, ephPubRaw]), + format: 'der', + type: 'spki', + }); + const shared = diffieHellman({ privateKey: kekPriv, publicKey: ephPub }); + const key = Buffer.from( + hkdfSync('sha256', shared, Buffer.alloc(0), HKDF_INFO, 32), + ); + const d = createDecipheriv('aes-256-gcm', key, iv); + d.setAuthTag(tag); + return Buffer.concat([d.update(ct), d.final()]); +} + +function rawX25519(): { privRaw: Buffer; pubRaw: Buffer } { + const { publicKey, privateKey } = generateKeyPairSync('x25519'); + return { + pubRaw: publicKey + .export({ type: 'spki', format: 'der' }) + .subarray(SPKI_PREFIX.length), + privRaw: privateKey + .export({ type: 'pkcs8', format: 'der' }) + .subarray(PKCS8_PREFIX.length), + }; +} + +describe('binary envelope encryption (#1138)', () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(path.join(os.tmpdir(), 'dcd-enc-test-')); + }); + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + delete process.env.DCD_BINARY_KEK_PUBLIC; + }); + + const sizes: Array<[string, number]> = [ + ['sub-chunk', 500], + ['exactly one chunk', 1024], + ['one chunk + 1 byte', 1025], + ['several chunks + remainder', 4096 + 321], + ]; + + for (const [label, size] of sizes) { + it(`encrypts ${label} payloads into a DCDE container the platform decrypt recovers`, async () => { + const plaintext = randomBytes(size); + const src = path.join(dir, 'plain.bin'); + const dest = path.join(dir, 'cipher.enc'); + await writeFile(src, plaintext); + + const dek = generateDek(); + await encryptFileToPath(src, dest, dek, 1, 1024); // small chunk → multi-segment + + const ciphertext = await readFile(dest); + expect(refDecryptContainer(ciphertext, dek).equals(plaintext)).to.equal( + true, + ); + }); + } + + it('round-trips the wikipedia.apk fixture at the 1 MiB default chunk size', async () => { + const apk = await readFile( + new URL('../fixtures/wikipedia.apk', import.meta.url), + ); + const src = path.join(dir, 'wikipedia.apk'); + const dest = path.join(dir, 'wikipedia.apk.enc'); + await writeFile(src, apk); + + const dek = generateDek(); + await encryptFileToPath(src, dest, dek, 1); + + const ciphertext = await readFile(dest); + // Ciphertext must differ from plaintext and be recoverable byte-for-byte. + expect(ciphertext.subarray(0, 4).toString('ascii')).to.equal('DCDE'); + expect(refDecryptContainer(ciphertext, dek).equals(apk)).to.equal(true); + }); + + it('detects tampering (GCM tag mismatch)', async () => { + const src = path.join(dir, 'p.bin'); + const dest = path.join(dir, 'c.enc'); + await writeFile(src, randomBytes(3000)); + const dek = generateDek(); + await encryptFileToPath(src, dest, dek, 1, 1024); + const ciphertext = await readFile(dest); + ciphertext[25] ^= 0xff; // flip a ciphertext byte + expect(() => refDecryptContainer(ciphertext, dek)).to.throw(); + }); + + it('wraps a DEK that the matching KEK private key unwraps', () => { + const { privRaw, pubRaw } = rawX25519(); + process.env.DCD_BINARY_KEK_PUBLIC = `3:${pubRaw.toString('base64')}`; + const kek = resolveKekPublicKey('https://api.devicecloud.dev'); + expect(kek).to.not.equal(null); + + const dek = generateDek(); + const envelope = wrapDek(dek, kek!); + expect(envelope.v).to.equal(1); + expect(envelope.kek).to.equal(3); + expect(refUnwrapDek(envelope.wrapped_key, privRaw).equals(dek)).to.equal( + true, + ); + }); + + it('resolves the pinned KEK public key for each environment', () => { + const prod = resolveKekPublicKey('https://api.devicecloud.dev'); + expect(prod).to.not.equal(null); + expect(prod!.version).to.equal(1); + expect(prod!.keyRaw.toString('base64')).to.equal( + 'wtfyWEwK7nJzwI4PD+9RAW8jxIR1u8kMQq2IhsrVnH4=', + ); + + const dev = resolveKekPublicKey('https://api.dev.devicecloud.dev'); + expect(dev).to.not.equal(null); + expect(dev!.version).to.equal(1); + expect(dev!.keyRaw.toString('base64')).to.equal( + 'RgcToF/OJpcQI9koYvSvtj/WLaebfcN4v5GJoqtr/00=', + ); + }); +}); + +describe('flow + env envelope encryption (#1151, #1152)', () => { + afterEach(() => { + delete process.env.DCD_BINARY_KEK_PUBLIC; + }); + + it('encryptFlowBuffer produces a container the platform decrypts to the original zip', () => { + const { privRaw, pubRaw } = rawX25519(); + process.env.DCD_BINARY_KEK_PUBLIC = `2:${pubRaw.toString('base64')}`; + const kek = resolveKekPublicKey('https://api.dev.devicecloud.dev')!; + + const zip = randomBytes(5000); + const { ciphertext, enc } = encryptFlowBuffer(zip, kek); + + expect(enc.v).to.equal(1); + expect(enc.kek).to.equal(2); + expect(ciphertext.subarray(0, 4).toString('ascii')).to.equal('DCDE'); + const dek = refUnwrapDek(enc.wrapped_key, privRaw); + expect(refDecryptContainer(ciphertext, dek).equals(zip)).to.equal(true); + }); + + it('encryptEnv produces an inline envelope the platform decrypts back to the map', () => { + const { privRaw, pubRaw } = rawX25519(); + process.env.DCD_BINARY_KEK_PUBLIC = `1:${pubRaw.toString('base64')}`; + const kek = resolveKekPublicKey('https://api.dev.devicecloud.dev')!; + + const env = { API_TOKEN: 'secret', PASSWORD: 'p@ss word=1', EMPTY: '' }; + const enc = encryptEnv(env, kek); + + expect(enc.v).to.equal(1); + expect(enc.kek).to.equal(1); + // API unwraps the DEK from wrapped_key; runner decrypts the inline blob. + const dek = refUnwrapDek(enc.wrapped_key, privRaw); + const plain = refDecryptContainer(Buffer.from(enc.ciphertext, 'base64'), dek); + expect(JSON.parse(plain.toString('utf8'))).to.deep.equal(env); + }); + + it('gives the flow zip and env their own distinct DEKs', () => { + const { pubRaw } = rawX25519(); + process.env.DCD_BINARY_KEK_PUBLIC = `1:${pubRaw.toString('base64')}`; + const kek = resolveKekPublicKey('https://api.dev.devicecloud.dev')!; + + const flow = encryptFlowBuffer(randomBytes(100), kek); + const env = encryptEnv({ A: 'b' }, kek); + // Independent sealed boxes → the wrapped keys must differ. + expect(flow.enc.wrapped_key).to.not.equal(env.wrapped_key); + }); +}); diff --git a/test/unit/report-download.service.test.ts b/test/unit/report-download.service.test.ts index 2ed8566..8c75f13 100644 --- a/test/unit/report-download.service.test.ts +++ b/test/unit/report-download.service.test.ts @@ -303,4 +303,157 @@ describe('ReportDownloadService', () => { expect(warnings.join(' ')).to.match(/failed to download allure/i); }); }); + + // ------------------------------------------------------------------------- + // bundle delivery + // ------------------------------------------------------------------------- + + describe('bundle delivery', () => { + const BASE = { + auth: TEST_AUTH, + apiUrl: 'https://api.example.com', + uploadId: 'run-42', + }; + + let calls: Array<{ + headers: Record; + method: string; + url: string; + }>; + + /** + * Route fetch by URL: a `*-bundle` endpoint returns a signed manifest, and + * the manifest's `bundleUrl` streams the ZIP. Records every call so the + * flow (manifest GET, then bundle POST, no inline call) can be asserted. + */ + function mockBundleFetch( + opts: { manifestStatus?: number; zipBody?: string } = {}, + ) { + const { manifestStatus = 200, zipBody = 'bundle-zip' } = opts; + const encoder = new TextEncoder(); + const stream = (s: string) => + new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(s)); + controller.close(); + }, + }); + + const impl = async ( + input: URL | string, + init?: RequestInit, + ): Promise => { + const url = input.toString(); + calls.push({ + headers: Object.fromEntries( + Object.entries((init?.headers as Record) ?? {}), + ), + method: (init?.method ?? 'GET').toUpperCase(), + url, + }); + + if (url.includes('artifacts-bundle') || url.includes('report-bundle')) { + return new Response( + stream( + JSON.stringify({ + bundleUrl: 'https://cdn.example.com/bundle', + entryCount: 3, + filename: 'artifacts-all.zip', + manifest: '{"version":1}', + sig: 'SIG', + }), + ), + { + headers: { 'content-type': 'application/json' }, + status: manifestStatus, + }, + ); + } + if (url === 'https://cdn.example.com/bundle') { + return new Response(stream(zipBody), { status: 200 }); + } + return new Response(stream('inline-zip'), { status: 200 }); + }; + globalThis.fetch = impl as typeof fetch; + } + + beforeEach(() => { + calls = []; + }); + + it('streams from the bundle URL and skips the inline endpoint', async () => { + mockBundleFetch(); + const outPath = path.join(tempDir, 'bundle.zip'); + + await service.downloadArtifacts({ + ...BASE, + artifactsPath: outPath, + downloadType: 'ALL', + }); + + expect(calls[0]).to.include({ + method: 'GET', + url: 'https://api.example.com/results/run-42/artifacts-bundle?results=ALL', + }); + expect(calls[1]).to.include({ + method: 'POST', + url: 'https://cdn.example.com/bundle', + }); + expect(calls.some((c) => c.url.endsWith('/download'))).to.be.false; + expect(fs.readFileSync(outPath, 'utf8')).to.equal('bundle-zip'); + }); + + it('does not send the auth header to the (pre-signed) bundle URL', async () => { + mockBundleFetch(); + + await service.downloadArtifacts({ + ...BASE, + artifactsPath: path.join(tempDir, 'bundle-noauth.zip'), + downloadType: 'ALL', + }); + + const bundlePost = calls.find( + (c) => c.url === 'https://cdn.example.com/bundle', + ); + expect(bundlePost).to.not.be.undefined; + expect(bundlePost!.headers['x-app-api-key']).to.be.undefined; + }); + + it('falls back to the inline download when unavailable (501)', async () => { + mockBundleFetch({ manifestStatus: 501 }); + const outPath = path.join(tempDir, 'bundle-fallback.zip'); + + await service.downloadArtifacts({ + ...BASE, + artifactsPath: outPath, + downloadType: 'ALL', + }); + + expect( + calls.some((c) => c.url.endsWith('/artifacts-bundle?results=ALL')), + ).to.be.true; + expect( + calls.some((c) => c.method === 'POST' && c.url.endsWith('/download')), + ).to.be.true; + expect(fs.readFileSync(outPath, 'utf8')).to.equal('inline-zip'); + }); + + it('uses bundle delivery for the html report', async () => { + mockBundleFetch(); + + await service.downloadReports({ + ...BASE, + htmlPath: path.join(tempDir, 'report-bundle.zip'), + reportType: 'html', + }); + + expect(calls[0].url).to.equal( + 'https://api.example.com/results/run-42/report-bundle', + ); + expect(calls[1]).to.include({ + method: 'POST', + url: 'https://cdn.example.com/bundle', + }); + }); + }); });