-
Notifications
You must be signed in to change notification settings - Fork 0
fix(legal): the package declared MIT while shipping the Apache-2.0 text (LEGAL-001) #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| name: license-truth | ||
|
|
||
| # Blocks the class of defect this repo shipped for five months: `package.json` declared MIT | ||
| # while the LICENSE file beside it was the Apache-2.0 text, and the Apache-2.0 NOTICE the | ||
| # repo adopted never reached the published tarball at all. Nothing in the existing gates | ||
| # reads a license FILE, so nothing could see it. | ||
| # | ||
| # Two jobs on purpose: | ||
| # local-truth — offline, reads only files in the repo. Deterministic, safe to require. | ||
| # registry-drift — hits npm and PyPI. Scheduled/manual ONLY, because a required check that | ||
| # depends on a third-party registry being up is a check that goes red for | ||
| # reasons that have nothing to do with the pull request. | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [main] | ||
| schedule: | ||
| # Mondays 08:00 UTC — reconcile every published WAVE artifact against its source repo. | ||
| - cron: "0 8 * * 1" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| local-truth: | ||
| if: github.event_name != 'schedule' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: 22 | ||
| cache: npm | ||
|
|
||
| - run: npm ci --include=dev | ||
|
|
||
| # Declared vs shipped: package.json, README, lockfile, the LICENSE file's actual text, | ||
| # whether LICENSE and NOTICE are in the tarball npm would publish, and strong copyleft | ||
| # in the runtime dependency tree. | ||
| - name: License truth gate | ||
| run: npm run license:check | ||
|
|
||
| - name: License gate unit tests | ||
| run: npx vitest run scripts/ | ||
|
|
||
| registry-drift: | ||
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: 22 | ||
| cache: npm | ||
|
|
||
| - run: npm ci --include=dev | ||
|
|
||
| # Downloads every published WAVE tarball and wheel, reads the LICENSE inside it, and | ||
| # compares it against what the source repo declares today. `--check` makes a drifted | ||
| # artifact fail the run rather than quietly regenerating a ledger nobody reads. | ||
| - name: Reconcile published artifacts against source | ||
| run: npm run license:ledger -- --check | ||
|
|
||
| - name: Upload regenerated ledger | ||
| if: always() | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: license-ledger | ||
| path: LICENSE-LEDGER.md | ||
| if-no-files-found: error | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # WAVE license ledger | ||
|
|
||
| <!-- GENERATED by `npm run license:ledger`. Do not hand-edit; regenerate instead. --> | ||
|
|
||
| Generated: 2026-09-04T01:42:06.068Z | ||
|
|
||
| **Intended license for the open WAVE surface: `Apache-2.0`** — per wave-av/cli@5da8018 ("chore: adopt Apache-2.0 license + add NOTICE"): Standardize the open WAVE protocol/SDK surface on Apache-2.0 (patent grant for adoption). Replaces any prior license; adds NOTICE reserving the WAVE marks. | ||
|
|
||
| ## Published artifacts | ||
|
|
||
| `declared` is the identifier in the published artifact's own metadata. `ships` is the license whose TEXT is in the file inside that artifact. `source declares` is what the manifest on the source repository's default branch says today. All three must agree; any disagreement is drift, and the last pair is the one an artifact cannot self-report. | ||
|
|
||
| | package | registry | version | declared | ships | source declares | NOTICE | verdict | | ||
| | --- | --- | --- | --- | --- | --- | --- | --- | | ||
| | `@wave-av/cli` | npm | 1.0.8 | `MIT` | `MIT` | `MIT` | no | **DRIFT** — source manifest says "MIT" but LICENSE is the Apache-2.0 text | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win Keep the committed ledger synchronized with 🤖 Prompt for AI Agents |
||
| | `@wave-av/sdk` | npm | 2.1.3 | `Apache-2.0` | `Apache-2.0` | `Apache-2.0` | no | consistent | | ||
| | `@wave-av/adk` | npm | 1.0.15 | `Apache-2.0` | `Apache-2.0` | `Apache-2.0` | no | consistent | | ||
| | `@wave-av/mcp-server` | npm | 0.2.0 | `Apache-2.0` | `Apache-2.0` | `Apache-2.0` | no | consistent | | ||
| | `@wave-av/workflow-sdk` | npm | 1.0.6 | `MIT` | `MIT` | `Apache-2.0` | no | **DRIFT** — published as "MIT" but source declares "Apache-2.0"; source manifest says "Apache-2.0" but sdk-typescript/packages/workflow-sdk/LICENSE is the MIT text | | ||
| | `@wave-av/create-app` | npm | 1.0.9 | `MIT` | `MIT` | _unresolved_ | no | **unverified** — artifact self-consistent; source unresolved (UNVERIFIED — no package.json found on any wave-av default branch) | | ||
| | `wave-sdk` | pypi | 2.0.0 | `MIT` | `MIT` | `MIT` | no | **DRIFT** — source manifest says "MIT" but LICENSE is the Apache-2.0 text | | ||
| | `wave-av-sdk` | pypi | 2.0.0 | `MIT` | `MIT` | `Apache-2.0` | no | **DRIFT** — published as "MIT" but source declares "Apache-2.0"; source manifest says "Apache-2.0" but sdk-python/LICENSE is the MIT text | | ||
|
|
||
| | package | source of truth | LICENSE file in that repo | | ||
| | --- | --- | --- | | ||
| | `@wave-av/cli` | wave-av/cli:package.json | `LICENSE` is `Apache-2.0` | | ||
| | `@wave-av/sdk` | wave-av/sdk:package.json | `LICENSE` is `Apache-2.0` | | ||
| | `@wave-av/adk` | wave-av/adk:package.json | `LICENSE` is `Apache-2.0` | | ||
| | `@wave-av/mcp-server` | wave-av/mcp-server:package.json | `LICENSE` is `Apache-2.0` | | ||
| | `@wave-av/workflow-sdk` | wave-av/sdks:sdk-typescript/packages/workflow-sdk/package.json | `sdk-typescript/packages/workflow-sdk/LICENSE` is `MIT` | | ||
| | `@wave-av/create-app` | UNVERIFIED — no package.json found on any wave-av default branch | _unresolved: UNVERIFIED — no package.json found on any wave-av default branch_ | | ||
| | `wave-sdk` | wave-av/sdk-python:pyproject.toml | `LICENSE` is `Apache-2.0` | | ||
| | `wave-av-sdk` | wave-av/sdks:sdk-python/pyproject.toml | `sdk-python/LICENSE` is `MIT` | | ||
|
|
||
| ## This repository | ||
|
|
||
| - package: `@wave-av/cli@1.0.9` | ||
| - `package.json` declares: `Apache-2.0` | ||
| - `LICENSE` file text is: `Apache-2.0` | ||
| - `README.md` License section: `Apache-2.0` | ||
| - `package-lock.json` root: `Apache-2.0` | ||
| - `NOTICE` present in repo: yes | ||
| - offline gate: clean | ||
|
|
||
| ## Dependency licenses | ||
|
|
||
| Strong copyleft (GPL/AGPL/SSPL/EUPL/CC-BY-SA) in a **runtime** dependency fails the gate. Weak, file-level copyleft (MPL/LGPL/EPL/CDDL) is listed here but does not block. | ||
|
|
||
| | scope | total | permissive | weak copyleft | strong copyleft | unknown | | ||
| | --- | --- | --- | --- | --- | --- | | ||
| | runtime | 127 | 127 | 0 | 0 | 0 | | ||
| | dev | 235 | 223 | 12 | 0 | 0 | | ||
|
|
||
| Notable (non-permissive) dependencies: | ||
|
|
||
| | dependency | license | class | | ||
| | --- | --- | --- | | ||
| | `lightningcss@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-android-arm64@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-darwin-arm64@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-darwin-x64@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-freebsd-x64@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-linux-arm-gnueabihf@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-linux-arm64-gnu@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-linux-arm64-musl@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-linux-x64-gnu@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-linux-x64-musl@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-win32-arm64-msvc@1.33.0` | `MPL-2.0` | weak | | ||
| | `lightningcss-win32-x64-msvc@1.33.0` | `MPL-2.0` | weak | | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| /** | ||
| * Dependency-free readers for the two archive formats a registry actually serves: | ||
| * npm's `.tgz` (gzip'd tar) and PyPI's `.whl` (a zip). We parse both in pure Node so the | ||
| * license ledger can inspect PUBLISHED artifacts without shelling out to `tar`/`unzip` | ||
| * (not guaranteed on every runner) and without adding a dependency to a CLI whose whole | ||
| * point in this change is a clean, auditable license surface. | ||
| * | ||
| * Only what the ledger needs is implemented: list entry names, and read one entry's bytes. | ||
| */ | ||
| import { gunzipSync, inflateRawSync } from 'node:zlib'; | ||
|
|
||
| /* ── tar.gz ────────────────────────────────────────────────────────────────── */ | ||
|
|
||
| /** | ||
| * Parse a gzip'd POSIX tar into a Map of path -> Buffer. | ||
| * Handles the ustar `prefix` field and GNU long names (`L` typeflag); skips | ||
| * directories, PAX headers and other metadata entries. | ||
| * @param {Buffer} tgz | ||
| * @returns {Map<string, Buffer>} | ||
| */ | ||
| export function readTarGz(tgz) { | ||
| const buf = gunzipSync(tgz); | ||
| const out = new Map(); | ||
| let offset = 0; | ||
| let longName = null; | ||
|
|
||
| while (offset + 512 <= buf.length) { | ||
| const header = buf.subarray(offset, offset + 512); | ||
| // Two consecutive zero blocks terminate the archive. | ||
| if (header.every((b) => b === 0)) break; | ||
|
|
||
| const name = cstr(header.subarray(0, 100)); | ||
| const size = octal(header.subarray(124, 136)); | ||
| const typeflag = String.fromCharCode(header[156] || 0x30); | ||
| const prefix = cstr(header.subarray(345, 500)); | ||
| const dataStart = offset + 512; | ||
| const dataEnd = dataStart + size; | ||
|
|
||
| if (typeflag === 'L') { | ||
| // GNU long-name: the NEXT header's real name lives in this entry's body. | ||
| longName = cstr(buf.subarray(dataStart, dataEnd)); | ||
| } else if (typeflag === '0' || typeflag === '\0') { | ||
| const full = longName ?? (prefix ? `${prefix}/${name}` : name); | ||
|
Comment on lines
+39
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🌐 Web query:
💡 Result: Yes, npm pack utilizes PAX extended headers to support tar entry paths longer than the standard 100-character limit [1]. Because npm relies on the node-tar library to create tarballs, it automatically uses PAX headers whenever they are required to properly encode metadata, including long file paths [2][3]. When a path exceeds 100 characters, node-tar generates a PAX extended header record (typically using the path keyword) to store the full path, ensuring compatibility with modern tar readers [1]. This mechanism is standard behavior for node-tar unless explicitly disabled [2][4]. Regarding "portable mode" in node-tar: The portable option in node-tar is designed to omit system-specific metadata—such as ctime, atime, device numbers, and inode numbers—from the tarball to ensure consistency across different operating systems [3][4]. It does not disable PAX extended headers. In fact, if you were to use the noPax option—which is distinct from portable mode—it would suppress PAX extended headers, leading to the truncation of long paths and potential misinterpretation of numeric values [2][3]. Therefore, npm pack remains capable of handling long paths even when portable mode is enabled, as it continues to use PAX headers to preserve those paths correctly [1][3]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- scripts/lib/archive.mjs ---'
cat -n scripts/lib/archive.mjs | sed -n '1,90p'
printf '%s\n' '--- scripts/lib/registry.mjs ---'
cat -n scripts/lib/registry.mjs | sed -n '1,75p'
printf '%s\n' '--- package and tar-related references ---'
rg -n --glob 'package.json' --glob 'package-lock.json' --glob '*.mjs' --glob '*.js' 'node-tar|from .tar|readTarGz|inspectNpm|typeflag|PAX|longName' .Repository: wave-av/cli Length of output: 9506 🤖 get_repo_knowledge executed:
Length of output: 666 🏁 Script executed: #!/bin/bash
set -eu
cat -n scripts/lib/archive.mjs | sed -n '1,90p'
cat -n scripts/lib/registry.mjs | sed -n '35,55p'Repository: wave-av/cli Length of output: 5286 Handle PAX ( When 🤖 Prompt for AI Agents |
||
| longName = null; | ||
| out.set(full, buf.subarray(dataStart, dataEnd)); | ||
| } else { | ||
| longName = null; | ||
| } | ||
|
|
||
| offset = dataStart + Math.ceil(size / 512) * 512; | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| /* ── zip (.whl) ────────────────────────────────────────────────────────────── */ | ||
|
|
||
| const EOCD_SIG = 0x06054b50; | ||
| const CD_SIG = 0x02014b50; | ||
| const LFH_SIG = 0x04034b50; | ||
|
|
||
| /** | ||
| * Parse a zip archive into a Map of path -> Buffer. Supports stored (0) and | ||
| * deflate (8) — the only two methods pip/wheel emits. | ||
| * @param {Buffer} zip | ||
| * @returns {Map<string, Buffer>} | ||
| */ | ||
| export function readZip(zip) { | ||
| const eocd = findEocd(zip); | ||
| if (eocd < 0) throw new Error('not a zip archive: no end-of-central-directory record'); | ||
|
|
||
| const entryCount = zip.readUInt16LE(eocd + 10); | ||
| let cd = zip.readUInt32LE(eocd + 16); | ||
| const out = new Map(); | ||
|
|
||
| for (let i = 0; i < entryCount; i++) { | ||
| if (zip.readUInt32LE(cd) !== CD_SIG) throw new Error(`corrupt central directory at entry ${i}`); | ||
| const method = zip.readUInt16LE(cd + 10); | ||
| const compressedSize = zip.readUInt32LE(cd + 20); | ||
| const nameLen = zip.readUInt16LE(cd + 28); | ||
| const extraLen = zip.readUInt16LE(cd + 30); | ||
| const commentLen = zip.readUInt16LE(cd + 32); | ||
|
Comment on lines
+67
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Edge Case: readZip mixes central-directory size with local-header offsets
Was this helpful? React with 👍 / 👎 |
||
| const localOffset = zip.readUInt32LE(cd + 42); | ||
| const name = zip.subarray(cd + 46, cd + 46 + nameLen).toString('utf8'); | ||
|
|
||
| if (!name.endsWith('/')) { | ||
| if (zip.readUInt32LE(localOffset) !== LFH_SIG) { | ||
| throw new Error(`corrupt local header for ${name}`); | ||
| } | ||
| // The local header's own name/extra lengths are authoritative — they may differ | ||
| // from the central directory's extra field. | ||
| const lNameLen = zip.readUInt16LE(localOffset + 26); | ||
| const lExtraLen = zip.readUInt16LE(localOffset + 28); | ||
| const start = localOffset + 30 + lNameLen + lExtraLen; | ||
| const raw = zip.subarray(start, start + compressedSize); | ||
| out.set(name, method === 8 ? inflateRawSync(raw) : Buffer.from(raw)); | ||
| } | ||
|
|
||
| cd += 46 + nameLen + extraLen + commentLen; | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| function findEocd(buf) { | ||
| // The EOCD is at most 22 + 65535 bytes from the end (comment field). | ||
| const min = Math.max(0, buf.length - (22 + 0xffff)); | ||
| for (let i = buf.length - 22; i >= min; i--) { | ||
| if (buf.readUInt32LE(i) === EOCD_SIG) return i; | ||
| } | ||
| return -1; | ||
| } | ||
|
|
||
| /* ── helpers ───────────────────────────────────────────────────────────────── */ | ||
|
|
||
| function cstr(b) { | ||
| const end = b.indexOf(0); | ||
| return b.subarray(0, end === -1 ? b.length : end).toString('utf8'); | ||
| } | ||
|
|
||
| function octal(b) { | ||
| const s = cstr(b).trim(); | ||
| return s ? parseInt(s, 8) || 0 : 0; | ||
| } | ||
|
Comment on lines
+119
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Edge Case: tar octal size field doesn't support GNU base-256 extension
Was this helpful? React with 👍 / 👎 |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Separate event types in the concurrency group.
pushonmain, the weekly schedule on the defaultmainbranch, andworkflow_dispatchonmainall usegithub.ref == 'refs/heads/main'. The current group allows a push to cancel an activeregistry-driftrun before it uploadsLICENSE-LEDGER.md.Include
github.event_namein the group key. Setcancel-in-progressonly forpull_requestruns.🤖 Prompt for AI Agents