Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4294ddc
test(supply-chain): require deterministic npm lock generator
seonghobae Aug 7, 2026
afb20be
fix(supply-chain): pin npm lock generator metadata
seonghobae Aug 7, 2026
e64c229
ci(supply-chain): prove npm version and lock reproduction
seonghobae Aug 7, 2026
ebbde9b
docs(supply-chain): record npm generator provenance
seonghobae Aug 7, 2026
694ca83
docs(changelog): record npm generator contract
seonghobae Aug 7, 2026
90dc2a0
fix(supply-chain): avoid serializing npm into runtime engines
seonghobae Aug 7, 2026
fecc36b
test(supply-chain): keep npm enforcement out of runtime engines
seonghobae Aug 7, 2026
9eb83c3
docs(supply-chain): separate npm generator from runtime engines
seonghobae Aug 7, 2026
5609b88
ci(supply-chain): publish deterministic lock reproduction evidence
seonghobae Aug 7, 2026
032314d
test(supply-chain): require preserved lock reproduction evidence
seonghobae Aug 7, 2026
0263ad4
test(security): require coordinated PDF.js and Undici baseline
seonghobae Aug 7, 2026
2cbf767
test(security): disable PDF expression evaluation
seonghobae Aug 7, 2026
475de96
fix(security): pin the patched Undici transitive version
seonghobae Aug 7, 2026
deb74ac
fix(security): pin the patched PDF.js release
seonghobae Aug 7, 2026
dc90d5b
fix(security): disable PDF expression evaluation
seonghobae Aug 7, 2026
b23b957
docs(security): record coordinated PDF and HTTP remediation
seonghobae Aug 7, 2026
2e99f72
docs(changelog): record coordinated security remediation
seonghobae Aug 7, 2026
8f50fe4
fix(security): anchor the Undici override to an exact root floor
seonghobae Aug 7, 2026
e2c0c2d
test(security): bind exact root floor and npm package locations
seonghobae Aug 7, 2026
dd93f96
ci(pr783): import exact npm 10.9.8 lock artifact
seonghobae Aug 7, 2026
dd8d1ac
fix(score): align PDF.js boundary with 6.2.108 API
seonghobae Aug 7, 2026
988dc1d
test(score): prove the supported data-only PDF.js boundary
seonghobae Aug 7, 2026
a39e37f
docs(security): record the supported PDF.js 6.2.108 boundary
seonghobae Aug 7, 2026
6b753e6
docs(changelog): describe the supported patched PDF boundary
seonghobae Aug 7, 2026
b773653
ci(pr783): rerun lock import after supported API repair
seonghobae Aug 7, 2026
01cb39e
ci(pr783): publish the verified lock from the bounded importer
seonghobae Aug 7, 2026
ad558ac
ci(pr783): fetch complete lineage for verified lock publication
seonghobae Aug 7, 2026
83865dc
fix(security): import verified npm 10.9.8 lock
github-actions[bot] Aug 7, 2026
e6b48ca
test(ci): preserve canonical npm provenance formatting
seonghobae Aug 7, 2026
3edf173
ci(pr783): diagnose Ruff import ordering
seonghobae Aug 7, 2026
2345219
style(ci): normalize security test imports
seonghobae Aug 7, 2026
dc7e8b4
style(ci): normalize npm provenance test imports
seonghobae Aug 7, 2026
c5ee630
chore(ci): remove completed Ruff diagnostic
seonghobae Aug 7, 2026
f0c9ad1
ci(pr783): finalize exact Ruff formatting
seonghobae Aug 7, 2026
102a89f
chore(ci): remove temporary branch writer
seonghobae Aug 7, 2026
d4887ec
style(test): apply Ruff formatting to security contracts
seonghobae Aug 7, 2026
459abdd
style(test): finish Ruff formatting for npm provenance
seonghobae Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,45 @@
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: develop
EXPECTED_NPM_VERSION: "10.9.8"

jobs:
lock-reproduction:
name: gate / ci / npm-lock-reproduction
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.22.3"
cache: npm
- name: Verify exact npm lockfile generator
run: test "$(npm --version)" = "$EXPECTED_NPM_VERSION"
- name: Reproduce package lock without lifecycle execution
run: npm install --package-lock-only --ignore-scripts --no-audit --no-fund
- name: Preserve the exact generated lock as review evidence
Comment thread
seonghobae marked this conversation as resolved.
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: npm-lock-reproduction-${{ github.event.pull_request.head.sha || github.sha }}
path: package-lock.json
if-no-files-found: error
retention-days: 3
- name: Reject lockfile drift
run: git diff --exit-code -- package-lock.json

verify:
name: ci / build-and-test
needs: lock-reproduction
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.22.3
node-version: "22.22.3"
cache: npm
- name: Verify exact npm lockfile generator
run: test "$(npm --version)" = "$EXPECTED_NPM_VERSION"
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.8.6"
Expand All @@ -51,13 +79,16 @@

rust-check:
name: gate / ci / rust-check
needs: lock-reproduction
runs-on: macos-15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.22.3
node-version: "22.22.3"
cache: npm
- name: Verify exact npm lockfile generator
run: test "$(npm --version)" = "$EXPECTED_NPM_VERSION"
- name: Install stable Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Install node dependencies
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### Changed

- Pinned npm `10.9.8` as the lockfile generator and made primary CI reject a different npm version or any package-lock-only replay diff.

### Fixed

- Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.24.0",
"pdfjs-dist": "6.1.200",
"pdfjs-dist": "6.2.108",
"react": "^19.2.4",
"react-dom": "^19.2.7",
"sonner": "^2.0.7",
Expand Down
42 changes: 42 additions & 0 deletions apps/desktop/src/features/score/pdfjs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getDocument, GlobalWorkerOptions } from "pdfjs-dist";
import { configureScorePdfWorker, loadScorePdf } from "./pdfjs";

vi.mock("pdfjs-dist", () => ({
getDocument: vi.fn(() => ({ promise: Promise.resolve(), destroy: vi.fn() })),
GlobalWorkerOptions: { workerSrc: "" }
}));

vi.mock("pdfjs-dist/build/pdf.worker.min.mjs?url", () => ({
default: "/assets/pdf.worker.min.mjs"
}));

describe("score PDF.js boundary", () => {
beforeEach(() => {
vi.mocked(getDocument).mockClear();
GlobalWorkerOptions.workerSrc = "";
});

it("uses the locally bundled worker asset", () => {
configureScorePdfWorker();

expect(GlobalWorkerOptions.workerSrc).toBe("/assets/pdf.worker.min.mjs");

configureScorePdfWorker();
expect(GlobalWorkerOptions.workerSrc).toBe("/assets/pdf.worker.min.mjs");
});

it("copies validated bytes through the supported data-only API", () => {
const source = new Uint8Array([0x25, 0x50, 0x44, 0x46]);

loadScorePdf(source);

expect(getDocument).toHaveBeenCalledTimes(1);
const parameters = vi.mocked(getDocument).mock.calls[0]?.[0];
expect(parameters).toBeTypeOf("object");
expect(Object.keys(parameters as object)).toEqual(["data"]);
const copiedBytes = (parameters as { data: Uint8Array }).data;
expect(copiedBytes).toEqual(source);
expect(copiedBytes).not.toBe(source);
});
});
5 changes: 5 additions & 0 deletions apps/desktop/src/features/score/pdfjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export function configureScorePdfWorker(): void {
* this helper never fetches arbitrary URLs. The bytes are copied before they
* are handed to pdf.js because pdf.js transfers the underlying buffer to its
* worker, which would otherwise detach the caller's copy and break retries.
*
* PDF.js 6.2.108 no longer exposes the legacy `isEvalSupported` initialization
* option. Security therefore relies on the patched parser release plus this
* narrow data-only, same-origin-worker boundary rather than an ignored and
* falsely reassuring unknown option.
*/
export function loadScorePdf(data: Uint8Array): PDFDocumentLoadingTask {
configureScorePdfWorker();
Expand Down
81 changes: 81 additions & 0 deletions docs/doctoring/high-security-pdf-http-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# High-security PDF and HTTP dependency baseline

## Decision

BandScope treats the PDF parser and its transitive HTTP client as one security-release boundary:

- `pdfjs-dist` is pinned exactly to `6.2.108`;
- `undici` is pinned exactly to `7.29.0` through the root npm override; and
- the complete npm workspace lock is generated only by the repository-pinned npm `10.9.8` workflow and imported unchanged from the workflow artifact.

PDF.js `6.2.108` no longer exposes the legacy `isEvalSupported` member in its public `DocumentInitParameters` contract, and `getDocument` no longer reads that member. BandScope therefore does not cast or pass an unknown option that would be ignored while creating false assurance. The primary remediation is the patched parser release, reinforced by a narrow data-only call, copied caller-owned bytes, and a same-origin bundled worker.

```mermaid
flowchart LR
A[Validated local PDF bytes] --> B[Copied Uint8Array]
B --> D[Data-only DocumentInitParameters]
D --> C[pdfjs-dist 6.2.108]
C --> W[Same-origin bundled worker]
W --> R[Canvas render]
J[jsdom development path] --> U[undici 7.29.0 override]
N[npm 10.9.8] --> L[Exact package-lock artifact]
L --> C
L --> U
```

## Threat boundary

The score viewer accepts only bytes already copied into the app-owned workspace through the native PDF intake boundary. It does not accept a URL, credentials, custom request headers, or a remote worker. This prevents a PDF from selecting an attacker-controlled fetch origin or script asset.

PDF bytes remain untrusted after the native magic-byte, size, and path checks. Parser vulnerabilities, malformed object graphs, embedded actions, and resource-exhaustion paths can still occur inside a syntactically valid PDF. The patched parser, exact dependency lock, copied data-only input, same-origin worker, and existing native intake limits therefore remain mandatory for locally selected files.

Undici is currently a development dependency reached through jsdom, but development and CI parsers process attacker-controlled fixtures, generated HTML, and network-like request bodies. A dev-only label does not make header injection, shared-cache disclosure, retry desynchronization, or cookie-attribute injection acceptable in the trusted build boundary.

## Lockfile provenance

The security manifests are changed before the lock. The exact branch workflow then:

1. verifies Node `22.22.3` and npm `10.9.8`;
2. runs `npm install --package-lock-only --ignore-scripts --no-audit --no-fund`;
3. uploads the generated `package-lock.json` under a head-SHA-bound artifact name; and
4. fails while the generated lock differs from the branch.

The maintainer imports that generated artifact byte-for-byte and reruns the workflow. The second run must produce a clean diff. No tarball URL, SRI, dependency range, `peer` classification, or workspace record is edited by hand.

The lock contract requires the exact public-registry tarball and SHA-512 SRI for both patched packages and requires every existing `node_modules/@esbuild/*` location to retain npm 10.9.8's `peer: true` classification. This distinguishes the intended security graph from unrelated Dependabot generator churn.

## Verification

The merge gate includes:

- exact manifest and lock artifact tests;
- a direct PDF.js wrapper test proving copied bytes, the locally bundled worker, and an exact data-only initialization object;
- TypeScript compilation against the installed PDF.js `DocumentInitParameters` rather than an unsafe cast;
- valid and malformed local score-PDF component tests;
- desktop lint, strict typecheck, complete measured tests, and production build;
- Tauri/Rust checks and native PDF intake regressions;
- `npm audit --workspaces --audit-level=high` with no high finding;
- repository SAST, CodeQL, security scan, secret scan, SBOM, and dependency evidence;
- current-head central coverage and automated review;
- zero unresolved actionable threads and a qualifying independent non-author approval; and
- normal branch protection without administrative bypass.

## Failure, rollback, and incident evidence

On a failed lock replay or parser regression, preserve the exact head SHA, Node/npm versions, generated-lock artifact ID and digest, original and generated lock blob SHA, test output, audit report, and workflow run ID. Do not merge a partially updated graph.

Rollback restores the previous desktop manifest, root override, complete lock, PDF loader, tests, and CHANGELOG entry together. Because the previous graph contains known high findings, rollback is an emergency availability action only and requires an explicit security exception, compensating controls, owner, expiration, and immediate replacement plan.

## References

GitHub. (2026). *PDF.js vulnerable to arbitrary JavaScript execution upon opening a malicious PDF* (GHSA-hq66-cqwq-w95j) [Security advisory]. https://github.com/advisories/GHSA-hq66-cqwq-w95j

Mozilla. (2026). *Document initialization parameters in PDF.js 6.2.108* [Source code]. GitHub. https://github.com/mozilla/pdf.js/blob/v6.2.108/src/display/api.js

Mozilla. (2026). *PDF.js 6.2.108* [Software release]. https://github.com/mozilla/pdf.js/releases/tag/v6.2.108

Node.js contributors. (2026). *Undici 7.29.0* [Software release]. https://github.com/nodejs/undici/releases/tag/v7.29.0

npm, Inc. (2026). *npm ci*. npm Docs. https://docs.npmjs.com/cli/v11/commands/npm-ci/

npm, Inc. (2026). *package-lock.json*. npm Docs. https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json/
77 changes: 77 additions & 0 deletions docs/doctoring/npm-lockfile-generator-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# npm lockfile generator provenance

## Decision

BandScope generates and verifies its root npm workspace lock with exactly npm `10.9.8`. The root manifest records that decision through:

- `packageManager: npm@10.9.8` as package-manager selection metadata; and
- `devEngines.packageManager` with `onFail: error` as npm's source-tree command gate.

The npm version is intentionally not repeated under `engines`. npm serializes `engines` into the root lock package, so adding an npm-only source-tool constraint there creates lock metadata churn unrelated to dependency resolution. `devEngines`, the explicit CI assertion, and the replay gate enforce the generator while the published `engines.node` range remains the runtime compatibility contract.

The primary GitHub Actions workflow uses Node `22.22.3`, verifies the bundled npm version before any installation, runs `npm ci`, then runs a package-lock-only regeneration with scripts, audit, and funding output disabled. Any `package-lock.json` diff fails the exact head.

The Node runtime support decision remains separate. This change does not raise the public `>=22.13 <23` Node range; a coordinated Node-floor migration is tracked independently.

## Why the generator is part of the lock identity

npm documents `package-lock.json` as the location-keyed description of the exact dependency tree. Lockfile version 3 is intended for npm 9 and newer. npm also notes that different package-manager versions may use different installation algorithms and metadata representations. A committed lockfile therefore is not fully reproducible unless the generator version and install-shaping flags are versioned with it.

`npm ci` is the immutable consumption path: it requires a lockfile, rejects manifest/lock dependency disagreement, removes an existing `node_modules`, and does not write the manifest or lock. It does not prove that a future dependency update will regenerate byte-identical metadata. The additional package-lock-only replay closes that gap.

```mermaid
flowchart LR
M[package.json ranges and workspaces] --> G[npm 10.9.8]
C[project npm configuration] --> G
G --> L[package-lock.json v3]
L --> I[npm ci clean install]
I --> R[npm 10.9.8 package-lock-only replay]
R --> D{lock diff?}
D -->|no| A[reproducible exact-head evidence]
D -->|yes| F[fail closed]
```

## Security and operational boundary

- Dependency PRs may change only manifest ranges and the lock records produced by npm `10.9.8`.
- Reviewers must reject unrelated lock metadata that cannot be reproduced by the pinned generator.
- No lock record may be added or removed by hand to satisfy a validator.
- Install-shaping flags that change the tree, such as `legacy-peer-deps` or `install-links`, must be committed in project configuration and used identically by `npm ci` and regeneration.
- Dependency lifecycle scripts remain disabled for the reproduction pass. The normal clean install retains the repository's reviewed execution behavior.
- The exact npm version check occurs before `npm ci`; a different bundled or globally installed npm cannot generate acceptance evidence.
- The lockfile remains the sole npm workspace lock. Nested workspace locks are prohibited.

`packageManager` alone is not the enforcement boundary for npm because Corepack's npm shim is not enabled by default in Node distributions. Enforcement is provided by npm `devEngines`, the explicit CI version assertion, and the lock replay.

## Verification

`services/analysis-engine/tests/test_npm_toolchain_contract.py` verifies the manifest metadata, separation of runtime and generator constraints, exact CI Node/npm identity, replay command and flags, clean lock diff, and lockfile version 3. Repository CI then executes the replay using the hosted toolchain.

A dependency update is mergeable only after:

1. npm `10.9.8` produces the checked-in lock from the updated manifest;
2. a second package-lock-only replay is byte-clean;
3. `npm ci`, lint, strict typecheck, measured tests, production build, Rust/Tauri checks, and security/supply-chain gates succeed on the same head; and
4. current-head review, unresolved-thread, independent-approval, and branch-protection requirements succeed without bypass.

## Incident response and rollback

When replay changes the lock unexpectedly:

1. preserve the exact head SHA, npm and Node versions, command flags, original lock blob SHA, regenerated lock, and CI run ID;
2. determine whether the manifest changed, npm changed, project configuration changed, or the protected lock was generated by a different toolchain;
3. never accept a partial or hand-edited lock;
4. regenerate from a clean checkout using the reviewed npm version and run the replay twice;
5. if rollback is necessary, restore the prior manifest and complete lock together, then rerun the entire exact-head gate.

## References

npm, Inc. (2026). *npm ci*. npm Docs. https://docs.npmjs.com/cli/v11/commands/npm-ci/

npm, Inc. (2026). *npm install*. npm Docs. https://docs.npmjs.com/cli/v10/commands/npm-install/

npm, Inc. (2026). *package-lock.json*. npm Docs. https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json/

npm, Inc. (2026). *package.json*. npm Docs. https://docs.npmjs.com/cli/configuring-npm/package-json/

Node.js contributors. (2026). *Corepack* [Software documentation]. GitHub. https://github.com/nodejs/corepack
35 changes: 18 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading