diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 2729574..dc5f6cf 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -4,7 +4,7 @@ on: pull_request: push: branches: [main] - tags: ["v*", "window-multicast-bridge-v*"] + tags: ["v*", "window-multicast-bridge-v*", "mabr-route-proxy-v*"] workflow_dispatch: # Node is pinned to an exact release, never the bare range '24' (the reasoning @@ -82,6 +82,13 @@ jobs: npm test npm run test:dist + - name: Build and test the ROUTE mABR proxy + working-directory: packages/mabr-route-proxy + run: | + npm install --include=dev --no-audit --no-fund + npm run build + npm test + - name: Pack release artifacts run: | mkdir npm-packages @@ -92,7 +99,9 @@ jobs: # npm-packages/ against its tag, and the bridge's version is not that tag. mkdir npm-packages-bridge npm pack ./packages/window-multicast-bridge --pack-destination npm-packages-bridge - ls -l npm-packages npm-packages-bridge + mkdir npm-packages-mabr + npm pack ./packages/mabr-route-proxy --pack-destination npm-packages-mabr + ls -l npm-packages npm-packages-bridge npm-packages-mabr # Asserted on the packed tarballs, never on the repo tree: what ships is # the tarball, and `files` / .npmignore decide which of the two you are @@ -100,7 +109,7 @@ jobs: - name: Verify tarball contents run: | set -euo pipefail - for tgz in npm-packages/*.tgz npm-packages-bridge/*.tgz; do + for tgz in npm-packages/*.tgz npm-packages-bridge/*.tgz npm-packages-mabr/*.tgz; do echo "=== $tgz ===" # Capture the member list once rather than piping `tar` into a # matcher: `grep -q` exits at the first match, which SIGPIPEs tar, @@ -249,6 +258,13 @@ jobs: if-no-files-found: error retention-days: 7 + - uses: actions/upload-artifact@v4 + with: + name: npm-packages-mabr + path: npm-packages-mabr/*.tgz + if-no-files-found: error + retention-days: 7 + # Repo-surface privacy gate. This repository is PUBLIC; the tarball gate in # `build` is structurally incapable of seeing this surface, because both # packages declare `files: ["dist"]`, so docs/, src/*.test.ts and .github/ @@ -459,3 +475,37 @@ jobs: - name: Publish @blockcast/window-multicast-bridge run: npm publish ./npm-packages-bridge/blockcast-window-multicast-bridge-*.tgz --access public --provenance --loglevel silly + + # The ROUTE adapter has its own version stream. Keep its tag and artifact + # separate from the existing v* gateway package release assertion. + publish-mabr: + needs: build + if: startsWith(github.ref, 'refs/tags/mabr-route-proxy-v') + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Update npm + run: | + npm install --global npm@11 + npm --version + - uses: actions/download-artifact@v4 + with: + name: npm-packages-mabr + path: npm-packages-mabr + - name: Assert the tag matches the packed version + run: | + set -euo pipefail + tag="${GITHUB_REF_NAME#mabr-route-proxy-v}" + tgz="$(echo npm-packages-mabr/blockcast-mabr-route-proxy-*.tgz)" + version="$(tar -xOzf "$tgz" package/package.json | node -pe 'JSON.parse(require("fs").readFileSync(0)).version')" + [ "$version" = "$tag" ] || { + echo "::error::$tgz is $version, tag is $tag" + exit 1 + } + - name: Publish @blockcast/mabr-route-proxy + run: npm publish npm-packages-mabr/blockcast-mabr-route-proxy-*.tgz --access public --provenance --loglevel silly diff --git a/packages/mabr-route-proxy/package.json b/packages/mabr-route-proxy/package.json index 86f6e9d..052ea24 100644 --- a/packages/mabr-route-proxy/package.json +++ b/packages/mabr-route-proxy/package.json @@ -10,6 +10,12 @@ "scripts": { "build": "tsc -b", "typecheck": "tsc --noEmit", "test": "node --experimental-strip-types --test 'src/**/*.test.ts'", "test:e2e": "npm run build && node tests/route-wasm-shaka-e2e.mjs", "clean": "rm -rf dist tsconfig.tsbuildinfo" }, "keywords": ["mabr", "route", "cmaf", "dash", "hls", "shaka"], "license": "MIT", + "publishConfig": { "access": "public" }, + "repository": { + "type": "git", + "url": "https://github.com/Blockcast/beacon.git", + "directory": "packages/mabr-route-proxy" + }, "dependencies": {}, "devDependencies": { "typescript": "^5.7.0" } } diff --git a/packages/mabr-route-proxy/src/route.test.ts b/packages/mabr-route-proxy/src/route.test.ts index da1af6c..7ac4057 100644 --- a/packages/mabr-route-proxy/src/route.test.ts +++ b/packages/mabr-route-proxy/src/route.test.ts @@ -424,10 +424,12 @@ test('rejects a sourceless AMT endpoint instead of emitting a DRIAD join', async test('discovers AMT service bytes through a delivery-service hostname', async () => { const originalFetch = globalThis.fetch; const urls: string[] = []; + const fetchOptions: RequestInit[] = []; let config: Record | undefined; - globalThis.fetch = (async (input: RequestInfo | URL) => { + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); urls.push(url); + fetchOptions.push(init ?? {}); if (url === 'https://cdn.onprem-live-multicast.public-st.cdn.blockcast.net/slt/') { return new Response(''); } @@ -454,6 +456,10 @@ test('discovers AMT service bytes through a delivery-service hostname', async () 'https://cdn.onprem-live-multicast.public-st.cdn.blockcast.net/slt/', 'https://cdn.onprem-live-multicast.public-st.cdn.blockcast.net/slt/7', ]); + assert.deepEqual(fetchOptions, [ + { credentials: 'omit', mode: 'cors' }, + { credentials: 'omit', mode: 'cors' }, + ]); assert.deepEqual(config, { origin: 'amt', group: '232.0.1.3', port: 1024, consume: 'bytes', relay: 'amt-relay.example.test', relayPort: 2268, diff --git a/packages/mabr-route-proxy/src/route.ts b/packages/mabr-route-proxy/src/route.ts index ca1c506..dd000f1 100644 --- a/packages/mabr-route-proxy/src/route.ts +++ b/packages/mabr-route-proxy/src/route.ts @@ -231,7 +231,10 @@ async function discoverSlsEndpoint(options: RouteMabrOptions): Promise => { - const response = await globalThis.fetch(url); + // The DS is public HTTP discovery, not a TO control-plane endpoint. Keep + // browser credentials out of every LLS request even when the page has + // cookies for another authority. + const response = await globalThis.fetch(url, { credentials: 'omit', mode: 'cors' }); if (!response.ok) throw new Error(`HTTP LLS fetch ${url} failed with ${response.status}`); return response.text(); };