diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 49c6bec..3f0740f 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -16,7 +16,11 @@ jobs: # - Dependabot / forks: no CLAUDE_CODE_OAUTH_TOKEN, so the action would fail. # - release-please release PRs: just version bumps + changelog — nothing to # review, and it must never block a release. - if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && !startsWith(github.head_ref, 'release-please--') }} + # - Back-merge PRs (production -> dev, opened by release-please.yml): only + # carry code already reviewed on its way through dev, and the action + # rejects their bot actor ("Workflow initiated by non-human actor"), + # which blocks the merge since claude-review is a required check. + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' && !startsWith(github.head_ref, 'release-please--') && !startsWith(github.head_ref, 'chore/back-merge-') }} runs-on: ubuntu-latest permissions: contents: read diff --git a/package.json b/package.json index c89d462..625fc8d 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,12 @@ }, "dependencies": { "@clack/prompts": "^1.6.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.31.0", "@supabase/supabase-js": "^2.108.2", "bplist-parser": "^0.5.0", "chalk": "^6.0.0", "citty": "^0.2.2", "js-yaml": "^5.2.2", - "node-apk": "^1.2.1", "node-stream-zip": "^1.15.0", "plist": "^5.0.0", "tar": "^7.5.16", @@ -112,12 +111,14 @@ "qs@<6.16.0": "6.16.0", "fast-uri@>=3.0.0 <3.1.8": "3.1.8", "hono@>=4.0.0 <4.13.7": "4.13.7", + "source-map-js@<1.2.2": "1.2.2", "micromatch>picomatch": "^2.3.2", "tinyglobby>picomatch": "^4.0.4" }, "auditConfig": { "ignoreGhsas": [ - "GHSA-frvp-7c67-39w9" + "GHSA-frvp-7c67-39w9", + "GHSA-vfj7-8cjw-p6xm" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07a3d88..78f40e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,7 @@ overrides: qs@<6.16.0: 6.16.0 fast-uri@>=3.0.0 <3.1.8: 3.1.8 hono@>=4.0.0 <4.13.7: 4.13.7 + source-map-js@<1.2.2: 1.2.2 micromatch>picomatch: ^2.3.2 tinyglobby>picomatch: ^4.0.4 @@ -43,8 +44,8 @@ importers: specifier: ^1.6.0 version: 1.8.1 '@modelcontextprotocol/sdk': - specifier: ^1.29.0 - version: 1.30.1(zod@4.6.5) + specifier: ^1.31.0 + version: 1.32.0(zod@4.6.5) '@supabase/supabase-js': specifier: ^2.108.2 version: 2.117.2 @@ -53,16 +54,13 @@ importers: version: 0.5.0 chalk: specifier: ^6.0.0 - version: 6.0.0 + version: 6.0.1 citty: specifier: ^0.2.2 version: 0.2.2 js-yaml: specifier: ^5.2.2 version: 5.4.2 - node-apk: - specifier: ^1.2.1 - version: 1.2.1 node-stream-zip: specifier: ^1.15.0 version: 1.16.0 @@ -84,7 +82,7 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.11.0) + version: 10.0.1(eslint@10.12.0) '@types/chai': specifier: ^5.2.3 version: 5.2.3 @@ -96,28 +94,28 @@ importers: version: 10.0.10 '@types/node': specifier: ^26.0.0 - version: 26.6.3 + version: 26.6.4 '@types/yazl': specifier: ^3.3.1 version: 3.3.1 chai: specifier: ^6.2.2 - version: 6.2.2 + version: 6.3.0 eslint: specifier: ^10.5.0 - version: 10.11.0 + version: 10.12.0 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.11.0) + version: 10.1.8(eslint@10.12.0) eslint-plugin-unicorn: specifier: ^76.0.0 - version: 76.0.0(eslint@10.11.0) + version: 76.0.0(eslint@10.12.0) husky: specifier: ^9.1.7 version: 9.1.7 mocha: specifier: ^12.0.0 - version: 12.0.2 + version: 12.0.3 prettier: specifier: ^3.8.4 version: 3.9.9 @@ -132,7 +130,7 @@ importers: version: 6.0.3 typescript-eslint: specifier: ^8.61.1 - version: 8.70.1(eslint@10.11.0)(typescript@6.0.3) + version: 8.71.0(eslint@10.12.0)(typescript@6.0.3) packages: @@ -388,8 +386,8 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@modelcontextprotocol/sdk@1.30.1': - resolution: {integrity: sha512-H2HxLvC3HDNybePJaLdSrU1hhUK5iQw+WvV1b01myFyI7sdVGe1u/IPTE5D9fGCiJDVtgMV/lmFkQXLmQyIFYA==} + '@modelcontextprotocol/sdk@1.32.0': + resolution: {integrity: sha512-8BviX/hK4Gd2eL1KdTwm0gfl4d3wdoErW0Jd00h6nomUAtk6IJk5vwQJc6kfWtJTzN5OEV/lgIiJKI8fiDGAlA==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -463,69 +461,69 @@ packages: '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} - '@types/node@26.6.3': - resolution: {integrity: sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==} + '@types/node@26.6.4': + resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} '@types/yazl@3.3.1': resolution: {integrity: sha512-DIWfCKpsTp6hE5BDBHV3+fIL/bLUF9Bv13iDrWnMlmhQpH67buNvI291ZauQ1xcccxK3FqQ9honnXpq4R8NMuQ==} - '@typescript-eslint/eslint-plugin@8.70.1': - resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==} + '@typescript-eslint/eslint-plugin@8.71.0': + resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.70.1 + '@typescript-eslint/parser': ^8.71.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.70.1': - resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==} + '@typescript-eslint/parser@8.71.0': + resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==} 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.70.1': - resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==} + '@typescript-eslint/project-service@8.71.0': + resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.70.1': - resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==} + '@typescript-eslint/scope-manager@8.71.0': + resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.70.1': - resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==} + '@typescript-eslint/tsconfig-utils@8.71.0': + resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.70.1': - resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==} + '@typescript-eslint/type-utils@8.71.0': + resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==} 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.70.1': - resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==} + '@typescript-eslint/types@8.71.0': + resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.70.1': - resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==} + '@typescript-eslint/typescript-estree@8.71.0': + resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.70.1': - resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==} + '@typescript-eslint/utils@8.71.0': + resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==} 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.70.1': - resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==} + '@typescript-eslint/visitor-keys@8.71.0': + resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@xmldom/xmldom@0.9.12': @@ -629,12 +627,12 @@ packages: caniuse-lite@1.0.30001810: resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} - chalk@6.0.0: - resolution: {integrity: sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==} + chalk@6.0.1: + resolution: {integrity: sha512-/Ce6KNm3vIbWdMlNna6RVIZ/ICQxnJxCicet5LBKK9ZffBkqzDw0xh9EiKSljdRtiIQ1S1z4YgcscUUGzNCWrA==} engines: {node: '>=22'} change-case@5.4.4: @@ -794,8 +792,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.11.0: - resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==} + eslint@10.12.0: + resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1014,8 +1012,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.10: - resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==} + ignore@7.0.12: + resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} engines: {node: '>= 4'} imurmurhash@0.1.4: @@ -1145,8 +1143,8 @@ packages: lodash.uniqby@4.5.0: resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} - lru-cache@11.5.2: - resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} engines: {node: 20 || >=22} math-intrinsics@1.1.0: @@ -1199,8 +1197,8 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mocha@12.0.2: - resolution: {integrity: sha512-SjAulGHxlMJLCD1bhvJBAiYwYxglydRzA9PDQ5MALq1oLRMMtIhxSQJ0olCFgA7c9YbTdxC0KOV0wl8JEDYVhA==} + mocha@12.0.3: + resolution: {integrity: sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1217,13 +1215,6 @@ packages: nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - node-apk@1.2.1: - resolution: {integrity: sha512-I0TY1x5m1pkFzjYdaGrrAu/Mh9qnnk2/BoMAU6bvBxTTD/oNQyTWbu3LTdONgV2rnLHf23jJ00Y/VV4BzZ6YXQ==} - - node-forge@1.4.0: - resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} - engines: {node: '>= 6.13.0'} - node-releases@2.0.56: resolution: {integrity: sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==} engines: {node: '>=18'} @@ -1424,8 +1415,8 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serialize-javascript@7.1.1: - resolution: {integrity: sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==} + serialize-javascript@7.1.2: + resolution: {integrity: sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==} engines: {node: '>=20.0.0'} serve-static@2.2.1: @@ -1483,8 +1474,8 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} statuses@2.0.2: @@ -1553,8 +1544,8 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - typescript-eslint@8.70.1: - resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==} + typescript-eslint@8.71.0: + resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1739,9 +1730,9 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.11.0)': + '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0)': dependencies: - eslint: 10.11.0 + eslint: 10.12.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1765,11 +1756,11 @@ snapshots: '@eslint/css-tree@4.1.1': dependencies: mdn-data: 2.35.0 - source-map-js: 1.2.1 + source-map-js: 1.2.2 - '@eslint/js@10.0.1(eslint@10.11.0)': + '@eslint/js@10.0.1(eslint@10.12.0)': optionalDependencies: - eslint: 10.11.0 + eslint: 10.12.0 '@eslint/object-schema@3.0.5': {} @@ -1810,7 +1801,7 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@modelcontextprotocol/sdk@1.30.1(zod@4.6.5)': + '@modelcontextprotocol/sdk@1.32.0(zod@4.6.5)': dependencies: '@hono/node-server': 2.1.1(hono@4.13.7) ajv: 8.20.0 @@ -1893,80 +1884,80 @@ snapshots: '@types/mocha@10.0.10': {} - '@types/node@26.6.3': + '@types/node@26.6.4': dependencies: undici-types: 8.9.0 '@types/yazl@3.3.1': dependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3))(eslint@10.12.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.70.1(eslint@10.11.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.70.1 - '@typescript-eslint/type-utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.70.1 - eslint: 10.11.0 - ignore: 7.0.10 + '@typescript-eslint/parser': 8.71.0(eslint@10.12.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/type-utils': 8.71.0(eslint@10.12.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.71.0(eslint@10.12.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.71.0 + eslint: 10.12.0 + ignore: 7.0.12 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.70.1 - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.70.1 + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.71.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.11.0 + eslint: 10.12.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.70.1(typescript@6.0.3)': + '@typescript-eslint/project-service@8.71.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) - '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3) + '@typescript-eslint/types': 8.71.0 debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.70.1': + '@typescript-eslint/scope-manager@8.71.0': dependencies: - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/visitor-keys': 8.70.1 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 - '@typescript-eslint/tsconfig-utils@8.70.1(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.71.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.71.0(eslint@10.12.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3) + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.71.0(eslint@10.12.0)(typescript@6.0.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.11.0 + eslint: 10.12.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.70.1': {} + '@typescript-eslint/types@8.71.0': {} - '@typescript-eslint/typescript-estree@8.70.1(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.71.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.70.1(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/visitor-keys': 8.70.1 + '@typescript-eslint/project-service': 8.71.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3) + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.3 semver: 7.8.5 @@ -1976,20 +1967,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.71.0(eslint@10.12.0)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0) - '@typescript-eslint/scope-manager': 8.70.1 - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3) - eslint: 10.11.0 + '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@6.0.3) + eslint: 10.12.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.70.1': + '@typescript-eslint/visitor-keys@8.71.0': dependencies: - '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/types': 8.71.0 eslint-visitor-keys: 5.0.1 '@xmldom/xmldom@0.9.12': {} @@ -2093,9 +2084,9 @@ snapshots: caniuse-lite@1.0.30001810: {} - chai@6.2.2: {} + chai@6.3.0: {} - chalk@6.0.0: {} + chalk@6.0.1: {} change-case@5.4.4: {} @@ -2224,13 +2215,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.11.0): + eslint-config-prettier@10.1.8(eslint@10.12.0): dependencies: - eslint: 10.11.0 + eslint: 10.12.0 - eslint-plugin-unicorn@76.0.0(eslint@10.11.0): + eslint-plugin-unicorn@76.0.0(eslint@10.12.0): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0) '@eslint/css-tree': 4.1.1 browserslist: 4.29.0 change-case: 5.4.4 @@ -2238,7 +2229,7 @@ snapshots: core-js-compat: 3.50.0 detect-indent: 7.0.2 entities: 8.1.0 - eslint: 10.11.0 + eslint: 10.12.0 find-up-simple: 1.0.1 globals: 17.12.0 identifier-regex: 1.1.0 @@ -2263,9 +2254,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.11.0: + eslint@10.12.0: dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.7.0 @@ -2534,7 +2525,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.10: {} + ignore@7.0.12: {} imurmurhash@0.1.4: {} @@ -2633,7 +2624,7 @@ snapshots: lodash._baseiteratee: 4.7.0 lodash._baseuniq: 4.6.0 - lru-cache@11.5.2: {} + lru-cache@11.5.3: {} math-intrinsics@1.1.0: {} @@ -2672,7 +2663,7 @@ snapshots: dependencies: minipass: 7.1.3 - mocha@12.0.2: + mocha@12.0.3: dependencies: browser-stdout: 1.3.1 chokidar: 5.0.0 @@ -2686,7 +2677,7 @@ snapshots: minimatch: 10.2.3 ms: 2.1.3 picocolors: 1.1.1 - serialize-javascript: 7.1.1 + serialize-javascript: 7.1.2 strip-json-comments: 5.0.3 supports-color: 8.1.1 workerpool: 10.0.3 @@ -2701,12 +2692,6 @@ snapshots: nice-try@1.0.5: {} - node-apk@1.2.1: - dependencies: - node-forge: 1.4.0 - - node-forge@1.4.0: {} - node-releases@2.0.56: {} node-stream-zip@1.16.0: {} @@ -2758,7 +2743,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.5.2 + lru-cache: 11.5.3 minipass: 7.1.3 path-to-regexp@8.4.2: {} @@ -2887,7 +2872,7 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-javascript@7.1.1: {} + serialize-javascript@7.1.2: {} serve-static@2.2.1: dependencies: @@ -2956,7 +2941,7 @@ snapshots: sisteransi@1.0.5: {} - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} statuses@2.0.2: {} @@ -3023,13 +3008,13 @@ snapshots: media-typer: 1.1.1 mime-types: 3.0.2 - typescript-eslint@8.70.1(eslint@10.11.0)(typescript@6.0.3): + typescript-eslint@8.71.0(eslint@10.12.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.70.1(eslint@10.11.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3) - eslint: 10.11.0 + '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3))(eslint@10.12.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.71.0(eslint@10.12.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.71.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.71.0(eslint@10.12.0)(typescript@6.0.3) + eslint: 10.12.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color diff --git a/src/services/metadata-extractor.service.ts b/src/services/metadata-extractor.service.ts index fc7f790..942ca0b 100644 --- a/src/services/metadata-extractor.service.ts +++ b/src/services/metadata-extractor.service.ts @@ -1,15 +1,10 @@ import { parseBuffer } from 'bplist-parser'; -import nodeApk from 'node-apk'; import { readFile, rm } from 'node:fs/promises'; import * as path from 'node:path'; import StreamZip from 'node-stream-zip'; import { parse } from 'plist'; -// node-apk is CJS with no `exports` map; Node's named-export detection for CJS -// (cjs-module-lexer) is version-dependent, so destructure off the default import -// instead — that interop is guaranteed on every Node version. bplist-parser 0.5 -// is a real ESM/CJS dual package with named exports, so it imports directly. -const { Apk } = nodeApk; +import { readManifestPackage } from '../utils/android-manifest.js'; export interface TAppMetadata { appId: string; @@ -52,12 +47,12 @@ export class AndroidMetadataExtractor implements IMetadataExtractor { } async extract(filePath: string): Promise { - const apk = new Apk(filePath); + const zip = new StreamZip.async({ file: filePath }); try { - const manifest = await apk.getManifestInfo(); - return { appId: manifest.package, platform: 'android' }; + const manifest = await zip.entryData('AndroidManifest.xml'); + return { appId: readManifestPackage(manifest), platform: 'android' }; } finally { - apk.close(); + await zip.close().catch(() => {}); } } } diff --git a/src/utils/android-manifest.ts b/src/utils/android-manifest.ts new file mode 100644 index 0000000..0c59a20 --- /dev/null +++ b/src/utils/android-manifest.ts @@ -0,0 +1,109 @@ +// Reads the package name out of an APK's compiled AndroidManifest.xml (Android +// binary XML). This replaced node-apk, whose node-forge dependency carried an +// unpatched advisory for certificate code the CLI never used. Layouts follow +// the ResChunk_header / ResStringPool_header / ResXMLTree_* structs in AOSP's +// libandroidfw ResourceTypes.h; every integer is little-endian. + +const RES_STRING_POOL_TYPE = 0x0001; +const RES_XML_TYPE = 0x0003; +const RES_XML_START_ELEMENT_TYPE = 0x0102; +const UTF8_FLAG = 0x0100; +const TYPE_STRING = 0x03; +const NO_INDEX = 0xffff_ffff; +const CHUNK_HEADER_SIZE = 8; + +/** + * Returns a lookup for the strings in the string pool chunk at `offset`. + * Strings are decoded on demand, so a malformed entry the manifest never + * refers to can't fail the parse. + */ +function stringPool(xml: Buffer, offset: number): (index: number) => string { + const headerSize = xml.readUInt16LE(offset + 2); + const count = xml.readUInt32LE(offset + 8); + const utf8 = (xml.readUInt32LE(offset + 16) & UTF8_FLAG) !== 0; + const stringsStart = offset + xml.readUInt32LE(offset + 20); + + return (index) => { + if (index >= count) { + throw new Error(`String index ${index} is out of range`); + } + let pos = stringsStart + xml.readUInt32LE(offset + headerSize + index * 4); + + if (utf8) { + // Two lengths, each 1 byte or 2 when the high bit is set: the UTF-16 + // length (unused here), then the UTF-8 byte length. + pos += xml[pos] & 0x80 ? 2 : 1; + let length = xml[pos++]; + if (length & 0x80) length = ((length & 0x7f) << 8) | xml[pos++]; + return xml.toString('utf8', pos, pos + length); + } + + // UTF-16 length in code units: 2 bytes, or 4 when the high bit is set. + let length = xml.readUInt16LE(pos); + pos += 2; + if (length & 0x80_00) { + length = ((length & 0x7f_ff) << 16) | xml.readUInt16LE(pos); + pos += 2; + } + return xml.toString('utf16le', pos, pos + length * 2); + }; +} + +/** + * Reads the `package` attribute of the root `` element. + * @param xml - Contents of the APK's AndroidManifest.xml entry + * @returns The application id, e.g. `org.wikipedia` + */ +export function readManifestPackage(xml: Buffer): string { + if (xml.length < CHUNK_HEADER_SIZE || xml.readUInt16LE(0) !== RES_XML_TYPE) { + throw new Error('AndroidManifest.xml is not Android binary XML'); + } + + const end = Math.min(xml.length, xml.readUInt32LE(4)); + let offset = xml.readUInt16LE(2); + let stringAt: ((index: number) => string) | undefined; + + while (offset + CHUNK_HEADER_SIZE <= end) { + const type = xml.readUInt16LE(offset); + const headerSize = xml.readUInt16LE(offset + 2); + const size = xml.readUInt32LE(offset + 4); + if (size < CHUNK_HEADER_SIZE) { + throw new Error(`Malformed binary XML chunk at offset ${offset}`); + } + + if (type === RES_STRING_POOL_TYPE) { + stringAt = stringPool(xml, offset); + } else if (type === RES_XML_START_ELEMENT_TYPE) { + // The first element is the document root. + if (!stringAt) throw new Error('Binary XML has no string pool'); + const ext = offset + headerSize; + if (stringAt(xml.readUInt32LE(ext + 4)) !== 'manifest') { + throw new Error( + 'Root element of AndroidManifest.xml is not ', + ); + } + + const attributeStart = ext + xml.readUInt16LE(ext + 8); + const attributeSize = xml.readUInt16LE(ext + 10); + const attributeCount = xml.readUInt16LE(ext + 12); + for (let i = 0; i < attributeCount; i++) { + const attr = attributeStart + i * attributeSize; + if (stringAt(xml.readUInt32LE(attr + 4)) !== 'package') continue; + + // Use the typed string value when there is one, else the raw string + // the attribute was compiled from. + const raw = xml.readUInt32LE(attr + 8); + const index = + xml[attr + 15] === TYPE_STRING ? xml.readUInt32LE(attr + 16) : raw; + if (index === NO_INDEX) break; + return stringAt(index); + } + + throw new Error(' has no package attribute'); + } + + offset += size; + } + + throw new Error('AndroidManifest.xml has no element'); +} diff --git a/test/unit/android-manifest.test.ts b/test/unit/android-manifest.test.ts new file mode 100644 index 0000000..a37a244 --- /dev/null +++ b/test/unit/android-manifest.test.ts @@ -0,0 +1,186 @@ +import { expect } from 'chai'; + +import { readManifestPackage } from '../../src/utils/android-manifest.js'; + +/** + * Real APKs (test/fixtures/wikipedia.apk and every other APK checked against + * aapt2) compile their manifest with a UTF-16 string pool, so these build + * minimal binary manifests by hand to cover the UTF-8 pool and the edge cases. + */ +const NO_INDEX = 0xff_ff_ff_ff; + +function u16(value: number): Buffer { + const buffer = Buffer.alloc(2); + buffer.writeUInt16LE(value); + return buffer; +} + +function u32s(...values: number[]): Buffer { + const buffer = Buffer.alloc(values.length * 4); + for (const [i, value] of values.entries()) buffer.writeUInt32LE(value, i * 4); + return buffer; +} + +/** A ResChunk_header plus `header` (the rest of the chunk's header) and `body`. */ +function chunk(type: number, header: Buffer, body: Buffer): Buffer { + const headerSize = 8 + header.length; + return Buffer.concat([ + u16(type), + u16(headerSize), + u32s(headerSize + body.length), + header, + body, + ]); +} + +function utf8Length(length: number): Buffer { + return Buffer.from( + length > 0x7f ? [(length >> 8) | 0x80, length & 0xff] : [length], + ); +} + +function stringPool(strings: string[], utf8: boolean): Buffer { + const encoded = strings.map((value) => { + if (utf8) { + const bytes = Buffer.from(value, 'utf8'); + return Buffer.concat([ + utf8Length(value.length), + utf8Length(bytes.length), + bytes, + Buffer.from([0]), + ]); + } + return Buffer.concat([ + u16(value.length), + Buffer.from(value, 'utf16le'), + u16(0), + ]); + }); + + const offsets: number[] = []; + let next = 0; + for (const entry of encoded) { + offsets.push(next); + next += entry.length; + } + const data = Buffer.concat(encoded); + const padded = Buffer.concat([ + data, + Buffer.alloc((4 - (data.length % 4)) % 4), + ]); + + const stringsStart = 28 + strings.length * 4; + const header = u32s(strings.length, 0, utf8 ? 0x01_00 : 0, stringsStart, 0); + return chunk(0x00_01, header, Buffer.concat([u32s(...offsets), padded])); +} + +/** + * A binary AndroidManifest.xml whose root is `` with string-typed + * `attributes`. + */ +function manifest( + attributes: Record, + { tag = 'manifest', utf8 = false } = {}, +): Buffer { + const strings = [tag]; + const index = (value: string): number => { + if (!strings.includes(value)) strings.push(value); + return strings.indexOf(value); + }; + + const tagIndex = index(tag); + const attrs = Object.entries(attributes).map(([name, value]) => { + const valueIndex = index(value); + return Buffer.concat([ + u32s(NO_INDEX, index(name), valueIndex), + u16(8), + Buffer.from([0, 0x03]), // res0, TYPE_STRING + u32s(valueIndex), + ]); + }); + const ext = Buffer.concat([ + u32s(NO_INDEX, tagIndex), + u16(20), // attributeStart + u16(20), // attributeSize + u16(attrs.length), + u16(0), + u16(0), + u16(0), + ]); + const startElement = chunk( + 0x01_02, + u32s(1, NO_INDEX), // lineNumber, comment + Buffer.concat([ext, ...attrs]), + ); + // Android's own parser rejects a document that ends on the start element. + const endElement = chunk( + 0x01_03, + u32s(1, NO_INDEX), + u32s(NO_INDEX, tagIndex), + ); + + return chunk( + 0x00_03, + Buffer.alloc(0), + Buffer.concat([stringPool(strings, utf8), startElement, endElement]), + ); +} + +describe('readManifestPackage', () => { + it('reads the package from a UTF-16 string pool', () => { + expect( + readManifestPackage(manifest({ package: 'com.example.app' })), + ).to.equal('com.example.app'); + }); + + it('reads the package from a UTF-8 string pool', () => { + const xml = manifest({ package: 'com.example.app' }, { utf8: true }); + expect(readManifestPackage(xml)).to.equal('com.example.app'); + }); + + it('reads UTF-8 strings longer than 127 bytes (two-byte lengths)', () => { + const id = `com.example.${'a'.repeat(140)}`; + expect( + readManifestPackage(manifest({ package: id }, { utf8: true })), + ).to.equal(id); + }); + + it('finds the package among other attributes', () => { + const xml = manifest({ + versionCode: '42', + package: 'com.example.app', + versionName: '1.0', + }); + expect(readManifestPackage(xml)).to.equal('com.example.app'); + }); + + it('throws when the root element has no package attribute', () => { + expect(() => + readManifestPackage(manifest({ versionName: '1.0' })), + ).to.throw('no package attribute'); + }); + + it('throws when the root element is not ', () => { + const xml = manifest( + { package: 'com.example.app' }, + { tag: 'application' }, + ); + expect(() => readManifestPackage(xml)).to.throw('not '); + }); + + it('throws on a plain-text manifest', () => { + const xml = Buffer.from(''); + expect(() => readManifestPackage(xml)).to.throw('not Android binary XML'); + }); + + it('throws on a truncated manifest', () => { + const xml = manifest({ package: 'com.example.app' }); + expect(() => readManifestPackage(xml.subarray(0, 40))).to.throw(); + }); + + it('throws on a zero-size chunk instead of looping forever', () => { + const xml = manifest({ package: 'com.example.app' }); + xml.writeUInt32LE(0, 12); // the string pool chunk's size + expect(() => readManifestPackage(xml)).to.throw('Malformed'); + }); +});