fix(webauthn): offer RS256 for the PRF-wrapping credential #105
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| # A superseded run on the same PR tells you nothing worth waiting for. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| verify: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # package.json declares engines.node >=24, so prove it on the floor | |
| # (24, Active LTS) as well as on Current, rather than only on whatever | |
| # the dev happens to have. Keep the spread: a single version hides | |
| # version-dependent bugs. The Node 20 runner previously caught a test | |
| # that leaked an event-loop handle, which the newer runner exited past. | |
| node: ["24", "26"] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| cache: npm | |
| - run: npm ci | |
| # Deliberately runs from a cold checkout with no build state. Every | |
| # workspace typechecks against its dependencies' emitted `dist`, so a | |
| # missing build step surfaces here as "cannot find module" in source that | |
| # is perfectly correct. `tsc -b` walks the project references and builds | |
| # them in order; this job is what keeps that wiring honest. | |
| - name: Lint (typecheck, builds project references) | |
| run: npm run lint | |
| - name: Build | |
| run: npm run build | |
| - name: Test | |
| run: npm test | |
| # The MV3 service worker may not call dynamic `import()` (HTML spec; see | |
| # w3c/ServiceWorker#1356), so background.js must stay a single | |
| # self-contained bundle. Rollup silently emits extra chunks if the | |
| # `codeSplitting: false` output option is lost in a future upgrade, and | |
| # the failure would land at runtime in users' browsers rather than here. | |
| - name: Assert MV3 worker is a single self-contained bundle | |
| run: | | |
| bundle=packages/extension/dist/background.js | |
| test -f "$bundle" || { echo "::error::$bundle was not emitted"; exit 1; } | |
| if grep -qE '\bimport\s*\(' "$bundle"; then | |
| echo "::error::$bundle contains a dynamic import(); MV3 service workers cannot load it" | |
| exit 1 | |
| fi | |
| echo "OK: $bundle is a single bundle with no dynamic import()" | |
| # `@openvtc/pnm-core/admin` is operator surface — granting authority at an | |
| # agent, revoking it, destroying contexts. A wallet has no business | |
| # shipping any of it, and the way it would arrive is someone importing it | |
| # from the package root instead of the subpath. The task URIs are the | |
| # tell: they only appear in a bundle that pulled the module in. | |
| - name: Assert the wallet ships no agent-administration surface | |
| run: | | |
| for task in 'acl/grant/0.1' 'acl/revoke/0.1' 'acl/update/0.1' 'contexts/delete/1.0' 'keys/create/0.1' 'keys/sign/0.1' 'policy/upsert/0.2' 'device/wipe/0.1' 'config/patch/0.1' 'vta/did-templates/create/2.0' 'consent/approver-set/1.0' 'keys/import/0.1' 'did-management/did/delete/0.1' 'vta/services/enable/1.0' 'vta/services/disable/1.0' 'vta/credentials/issue/0.1' 'vta/credentials/revoke/0.1'; do | |
| if grep -rlF "$task" packages/extension/dist/; then | |
| echo "::error::the extension bundle contains $task — @openvtc/pnm-core/admin must not be reachable from the wallet (check for a root-barrel import)" | |
| exit 1 | |
| fi | |
| done | |
| echo "OK: no admin task URIs in the extension bundle" | |
| # Build the Chrome Web Store upload artefact from the dist/ that the | |
| # Build step just produced (scripts/package.mjs re-stages it; it does | |
| # not rebuild). Doing this every run means a submission is never the | |
| # first time the packaging path is exercised. | |
| - name: Package Web Store zip | |
| working-directory: packages/extension | |
| run: node scripts/package.mjs | |
| # Two ways a package passes CI and then fails at upload, both silent: | |
| # | |
| # - a `key` field, which the Store's "+ New item" upload rejects | |
| # outright (it issues its own key). `dist/` carries one on purpose so | |
| # local unpacked installs hold a stable ID; only the zip must not. | |
| # - a version that drifted from package.json, which the Store rejects | |
| # as non-increasing against the last accepted upload. | |
| - name: Assert Web Store zip is uploadable | |
| working-directory: packages/extension | |
| run: | | |
| version=$(node -p "require('./package.json').version") | |
| zip="release/vta-wallet-${version}.zip" | |
| test -f "$zip" || { echo "::error::$zip was not produced"; exit 1; } | |
| manifest=$(unzip -p "$zip" manifest.json) | |
| if [ "$(echo "$manifest" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).key ?? ''")" != "" ]; then | |
| echo "::error::$zip manifest contains a 'key' field; the Web Store rejects it on a new item" | |
| exit 1 | |
| fi | |
| got=$(echo "$manifest" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version") | |
| if [ "$got" != "$version" ]; then | |
| echo "::error::packaged manifest version $got != package.json version $version" | |
| exit 1 | |
| fi | |
| if [ "$(echo "$manifest" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).content_scripts ? 'yes' : ''")" = "yes" ]; then | |
| echo "::error::manifest declares content_scripts; the page provider is registered dynamically for granted origins only (src/content-registration.ts). A static match would re-grant blanket host access and double-inject." | |
| exit 1 | |
| fi | |
| # The wallet writes nothing into the browser on a site's behalf, and | |
| # that is the claim docs/web-store-review.md makes to a reviewer. It | |
| # holds for exactly as long as nobody re-adds the permission, so | |
| # assert it rather than trust it: `cookies` is the permission a | |
| # reviewer looks hardest at, and its cost is an extended review. | |
| if [ "$(echo "$manifest" | node -p "(JSON.parse(require('fs').readFileSync(0,'utf8')).permissions ?? []).includes('cookies') ? 'yes' : ''")" = "yes" ]; then | |
| echo "::error::manifest requests the 'cookies' permission; the legacy password-site login that needed it was removed (see README 'The wallet writes nothing into your browser')." | |
| exit 1 | |
| fi | |
| # Checked against dist/, not src/: the built bundle has no comments, | |
| # so prose *about* the removed path (README, code comments) doesn't | |
| # trip it, while a real call site would. | |
| if grep -rl "chrome\.cookies" dist/ >/dev/null 2>&1; then | |
| echo "::error::the built bundle calls chrome.cookies; the wallet must not write to the cookie jar" | |
| grep -rl "chrome\.cookies" dist/ | |
| exit 1 | |
| fi | |
| test -f dist/content.js || { echo "::error::dist/content.js missing; chrome.scripting registers it by path"; exit 1; } | |
| echo "OK: $zip is version $version, no 'key', no static content_scripts, no cookie access" | |
| # One upload only — actions/upload-artifact errors on a duplicate name, | |
| # and the zip is identical across the Node matrix. | |
| - name: Upload Web Store zip | |
| if: matrix.node == '26' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: vta-wallet-extension | |
| path: packages/extension/release/*.zip | |
| if-no-files-found: error |