diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a95a8db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Report a wrong symbol/footprint/component, broken provenance, or validation failure +labels: bug +--- + +**Component ID** + +`openpcb.core....` + +**What is wrong** + +(e.g., footprint pad numbering does not match symbol pins, attribution missing, 3D model size off) + +**Expected** + +What it should be (link to upstream KiCad source if applicable). + +**Output of `bun validate --release --strict`** + +``` +paste here +``` + +**Additional context** diff --git a/.github/ISSUE_TEMPLATE/component_request.md b/.github/ISSUE_TEMPLATE/component_request.md new file mode 100644 index 0000000..25cb171 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/component_request.md @@ -0,0 +1,22 @@ +--- +name: Component request +about: Request a new symbol / footprint / component to be added +labels: enhancement, new-component +--- + +**Component** + +- Manufacturer Part Number (MPN): +- Category: [passive / IC / connector / opto / transistor / diode / ...] +- Datasheet URL: +- Footprint package (IPC-7351B name if known): + +**Source** + +- Available in KiCad libraries? Yes / No +- KiCad symbol library + name: +- KiCad footprint library + name: + +**Why this part** + +Use case, expected popularity, board this would unblock. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..f6d593b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Summary + + + +## Checklist + +- [ ] `bun validate --release --strict` passes +- [ ] `bun test` passes +- [ ] New assets follow the `openpcb.core...` ID convention +- [ ] Provenance fields populated (source, license, attribution, upstreamUrl, upstreamCommit, sourceHash, convertedAt, conversionTool) +- [ ] Symbol pins ↔ footprint pads cross-reference checked +- [ ] 3D model `.glb` committed alongside `.model.json` metadata (or N/A) +- [ ] No new `dist/` artifacts committed (CI handles packing) + +## Notes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ded4cea..444e5f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,11 +37,39 @@ jobs: - name: Validate source run: bun run validate + - name: Restore signing key + if: ${{ env.OPCLIB_SIGNING_KEY != '' }} + env: + OPCLIB_SIGNING_KEY: ${{ secrets.OPCLIB_SIGNING_KEY }} + run: | + install -m 700 -d "$RUNNER_TEMP/keys" + KEY_PATH="$RUNNER_TEMP/keys/opclib-signing.pem" + printf '%s' "$OPCLIB_SIGNING_KEY" > "$KEY_PATH" + chmod 600 "$KEY_PATH" + echo "OPCLIB_SIGN_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV" + - name: Pack release + env: + OPCLIB_SIGNING_KEY: ${{ secrets.OPCLIB_SIGNING_KEY }} + OPCLIB_KEY_ID: ${{ vars.OPCLIB_KEY_ID }} run: | VERSION="${GITHUB_REF_NAME#v}" echo "Packing version ${VERSION}" - bun tools/pack.ts --version="${VERSION}" + SIGN_ARGS="" + if [ -n "${OPCLIB_SIGN_KEY_PATH:-}" ] && [ -n "${OPCLIB_KEY_ID:-}" ]; then + SIGN_ARGS="--sign-key=${OPCLIB_SIGN_KEY_PATH} --key-id=${OPCLIB_KEY_ID}" + echo "Signing enabled (keyId=${OPCLIB_KEY_ID})" + else + echo "Signing disabled (no key/keyId configured)" + fi + bun tools/pack.ts --version="${VERSION}" $SIGN_ARGS + + - name: Generate SHA256SUMS + run: | + VERSION="${GITHUB_REF_NAME#v}" + cd dist + sha256sum "openpcb-core-library-${VERSION}.opclib" > SHA256SUMS + cat SHA256SUMS - name: Verify package run: | @@ -80,12 +108,17 @@ jobs: run: | VERSION="${GITHUB_REF_NAME#v}" ARTIFACT="dist/openpcb-core-library-${VERSION}.opclib" + SUMS="dist/SHA256SUMS" + ASSETS=("$ARTIFACT" "$SUMS") + if [ -f keys/openpcb-core.pub ]; then + ASSETS+=("keys/openpcb-core.pub") + fi if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then - gh release upload "${GITHUB_REF_NAME}" "$ARTIFACT" --clobber + gh release upload "${GITHUB_REF_NAME}" "${ASSETS[@]}" --clobber else gh release create "${GITHUB_REF_NAME}" \ --verify-tag \ --title "${GITHUB_REF_NAME}" \ --notes "OpenPCB Core Library ${VERSION}" \ - "$ARTIFACT" + "${ASSETS[@]}" fi diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6d14aeb..59c6807 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -29,8 +29,8 @@ jobs: - name: Run tests run: bun test - - name: Validate library source - run: bun tools/validate.ts + - name: Validate library source (strict / release) + run: bun tools/validate.ts --release --strict - name: Pack release candidate # Tags `ci-` so artifact filenames don't collide with real releases. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..069a0f8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of Conduct + +The OpenPCB Core Library community follows the [Contributor Covenant 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). + +Report violations to **conduct@openpcb.app**. See the full text at the link above. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9e7afdd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# Contributing to OpenPCB Core Library + +CoreLibrary is the canonical JSON source of the default component library shipped with OpenPCB. Components are KiCad-derived with strict provenance and signed `.opclib` releases. + +## Quick start + +```bash +git clone https://github.com/OpenPCB-app/CoreLibrary.git +cd CoreLibrary +bun install +bun validate # validate the source tree +bun test # run boundary + import + pack tests +bun pack --version 0.0.0-dev +``` + +Requirements: Bun ≥1.3. + +## ID convention + +Every symbol, footprint, component, and 3D model has a dotted ID: + +``` +openpcb.core... +``` + +Examples: + +- `openpcb.core.symbol.passive.resistor_iec` +- `openpcb.core.footprint.passive.R_0603_1608Metric` +- `openpcb.core.component.passive.resistor.r0603` +- `openpcb.core.3d.passive.r0603` + +Slugs are lowercase, words separated by `_`. Use IPC-7351B names for footprints (`R_0603_1608Metric`, not `0603`). + +## Adding a component from KiCad + +```bash +bun tools/import-kicad.ts \ + --symbol-lib path/to/Device.kicad_sym \ + --symbol-name R \ + --footprint-lib path/to/Resistor_SMD.pretty/R_0603_1608Metric.kicad_mod \ + --category passive \ + --slug r0603 +``` + +The importer produces symbol, footprint, and component JSON files with full provenance metadata (`source`, `upstreamUrl`, `upstreamCommit`, `convertedAt`, etc.). Always commit a passing `bun validate --release --strict` before opening a PR. + +## Required provenance fields (strict mode) + +KiCad-derived assets must declare: + +- `source`, `license`, `attribution[]` +- `sourceFormat`, `sourceFileName`, `sourceLibrary`, `sourceItemName` +- `sourceHash` (SHA-256 of upstream) +- `upstreamUrl`, `upstreamCommit` +- `convertedAt`, `conversionTool` + +See existing files under `symbols/`, `footprints/` for reference. + +## 3D models + +Pre-generated `.glb` files are committed alongside `.model.json` metadata; do not skip the GLB. If a STEP is unavailable, leave the entry out — do not ship a hollow placeholder. + +## Tests + +`bun test` covers: + +- Schema validation (Ajv against `schemas/*.json`) +- Cross-references: every component's symbol pins must match its footprint pads +- Uniqueness of IDs and UUIDs +- shared-package boundary (no in-repo duplicates of `@openpcb/*` packages) + +## Release + +Maintainers only — see `README.md` "Releasing" section. Tag pattern is `v..` (aligned with OpenPCB app: `v0.1.0-beta.N` during beta). + +## License + +CoreLibrary contents are licensed under **CC-BY-SA-4.0 with the OpenPCB Library Exception** (mirrors KiCad library licensing). See [LICENSE.md](LICENSE.md), [ATTRIBUTION.md](ATTRIBUTION.md), and [NOTICE.md](NOTICE.md). diff --git a/README.md b/README.md index e6cd9ea..2e25d3a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,21 @@ bun tools/import-kicad.ts # import symbols/footprints from a KiCad library `dist/*.opclib` is generated by `bun pack` and is not committed to the repository. +## Releasing + +1. Bump content/version as needed, run `bun run validate --release`. +2. Tag the release: `git tag v1.2.3 && git push origin v1.2.3`. +3. CI (`.github/workflows/release.yml`) typecheck → test → validate → pack → sign (if `OPCLIB_SIGNING_KEY` secret present) → SHA256SUMS → publish GitHub Release with `.opclib`, `SHA256SUMS`, and `keys/openpcb-core.pub`. + +Signing prerequisites (one-time): + +- Generate an Ed25519 key pair (see `keys/README.md`). +- Commit the `.pub` file to `keys/openpcb-core.pub`. Copy it into OpenPCB's `resources/keys/`. +- Add the PEM-encoded private key as GitHub secret `OPCLIB_SIGNING_KEY`. +- Set repo variable `OPCLIB_KEY_ID` (e.g. `openpcb-core-2026`). + +Without the secret, packs ship unsigned (OpenPCB will warn but still import in dev mode). + ## KiCad provenance This library contains data converted or adapted from the KiCad official diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..025fe65 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +Report security issues — including malformed `.opclib` exploits, signature bypass, or supply-chain concerns in the import pipeline — to **security@openpcb.app**. Do not open a public GitHub issue. + +You can also use [GitHub Security Advisories](https://github.com/OpenPCB-app/CoreLibrary/security/advisories/new) for private disclosure. + +Acknowledgement within 72 hours. Coordinated disclosure typically within 90 days. diff --git a/keys/README.md b/keys/README.md new file mode 100644 index 0000000..e0b2933 --- /dev/null +++ b/keys/README.md @@ -0,0 +1,30 @@ +# Signing Keys + +Ed25519 signing for `.opclib` releases. + +## Layout + +- `openpcb-core.pub` — public verification key, committed; also copied into OpenPCB's `resources/keys/`. +- Private key never committed. Stored as GitHub Actions secret `OPCLIB_SIGNING_KEY` (PEM, pkcs8). The active key id is set via repo variable `OPCLIB_KEY_ID` (e.g. `openpcb-core-2026`). + +## Generate a new key pair + +```bash +node -e ' +const { generateKeyPairSync } = require("node:crypto"); +const { writeFileSync } = require("node:fs"); +const { privateKey, publicKey } = generateKeyPairSync("ed25519"); +writeFileSync("openpcb-core.priv.pem", privateKey.export({ type: "pkcs8", format: "pem" })); +writeFileSync("openpcb-core.pub", publicKey.export({ type: "spki", format: "pem" })); +' +``` + +Commit `openpcb-core.pub`. Upload the `.priv.pem` contents to the `OPCLIB_SIGNING_KEY` GitHub Actions secret. Set `OPCLIB_KEY_ID` repo variable to a stable identifier embedded in every signature (e.g. `openpcb-core-2026`). + +## Rotation + +1. Generate new pair with a new `keyId`. +2. Commit the new `.pub` and copy to `OpenPCB/resources/keys/` (both keys live there simultaneously so verifiers accept old + new during the cutover). +3. Update `OPCLIB_KEY_ID` repo variable and `OPCLIB_SIGNING_KEY` secret. +4. Cut a release; verify signature in OpenPCB. +5. After a deprecation window, remove the old `.pub`. diff --git a/keys/openpcb-core.pub b/keys/openpcb-core.pub new file mode 100644 index 0000000..e75a380 --- /dev/null +++ b/keys/openpcb-core.pub @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEASe5TFAA3VD+EyGUEKfQMQUp1glInlrx3Pdcja9MOZ3Y= +-----END PUBLIC KEY----- diff --git a/package.json b/package.json index 5901420..9cc1674 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openpcb-core-library", - "version": "1.0.0", + "version": "0.1.0-beta.0", "description": "OpenPCB Core Library — readable JSON source for the default component library.", "license": "SEE LICENSE.md", "private": true, @@ -23,7 +23,7 @@ "dependencies": { "@openpcb/kicad-import": "github:OpenPCB-app/shared#kicad-import-v0.1.0", "@openpcb/kicad-parsers": "github:OpenPCB-app/shared#kicad-parsers-v0.1.2", - "@openpcb/opclib-pack": "github:OpenPCB-app/shared#opclib-pack-v0.1.0", + "@openpcb/opclib-pack": "github:OpenPCB-app/shared#opclib-pack-v0.2.0", "@openpcb/rendering-core": "github:OpenPCB-app/shared#rendering-core-v0.1.2", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", diff --git a/tests/shared-package-boundary.test.ts b/tests/shared-package-boundary.test.ts index 20db89c..574d04d 100644 --- a/tests/shared-package-boundary.test.ts +++ b/tests/shared-package-boundary.test.ts @@ -40,7 +40,7 @@ describe("shared package boundary", () => { "github:OpenPCB-app/shared#rendering-core-v0.1.2", ); expect(pkg.dependencies?.["@openpcb/opclib-pack"]).toBe( - "github:OpenPCB-app/shared#opclib-pack-v0.1.0", + "github:OpenPCB-app/shared#opclib-pack-v0.2.0", ); }); }); diff --git a/tools/pack.ts b/tools/pack.ts index 6f9008b..611a793 100644 --- a/tools/pack.ts +++ b/tools/pack.ts @@ -40,12 +40,23 @@ const args = Object.fromEntries( const version = args.version ?? "0.0.0"; const channel = (args.channel ?? "stable") as "stable" | "beta" | "nightly"; const outDir = args.out ? path.resolve(args.out) : path.join(REPO_ROOT, "dist"); +const signKeyPath = + args["sign-key"] && args["sign-key"] !== "true" + ? path.resolve(args["sign-key"]) + : undefined; +const keyId = + args["key-id"] && args["key-id"] !== "true" ? args["key-id"] : undefined; if (!/^[0-9]+\.[0-9]+\.[0-9]+/.test(version)) { console.error(`[pack] invalid --version=${version}`); process.exit(1); } +if (signKeyPath && !keyId) { + console.error("[pack] --sign-key requires --key-id"); + process.exit(1); +} + mkdirSync(outDir, { recursive: true }); function parseJsonBytes(bytes: Uint8Array): T { @@ -56,10 +67,7 @@ function readBytes(absPath: string): Uint8Array { return readFileSync(absPath); } -function assetEntryFor( - absPath: string, - bytes: Uint8Array, -): OpclibAssetEntry { +function assetEntryFor(absPath: string, bytes: Uint8Array): OpclibAssetEntry { const json = parseJsonBytes(bytes); return { id: json.id, @@ -72,9 +80,7 @@ function assetEntryFor( }; } -function packedAssetFor( - absPath: string, -): PackedAsset { +function packedAssetFor(absPath: string): PackedAsset { const bytes = readBytes(absPath); return { entry: assetEntryFor(absPath, bytes), bytes }; } @@ -86,22 +92,29 @@ function packedFootprintFor( const base = assetEntryFor(absPath, bytes); const json = parseJsonBytes<{ mountType?: string; - package?: { code?: string; standard?: string; imperial?: string | null; metric?: string | null }; + package?: { + code?: string; + standard?: string; + imperial?: string | null; + metric?: string | null; + }; models3d?: string[]; }>(bytes); - const packageCode = json.package?.code ?? json.package?.imperial ?? json.package?.metric; + const packageCode = + json.package?.code ?? json.package?.imperial ?? json.package?.metric; return { entry: { ...base, - package: json.package || json.mountType - ? { - code: packageCode ?? undefined, - standard: json.package?.standard, - mountType: json.mountType, - } - : json.mountType - ? { mountType: json.mountType } - : undefined, + package: + json.package || json.mountType + ? { + code: packageCode ?? undefined, + standard: json.package?.standard, + mountType: json.mountType, + } + : json.mountType + ? { mountType: json.mountType } + : undefined, models3d: json.models3d, }, bytes, @@ -109,13 +122,21 @@ function packedFootprintFor( } function packedModel3dFor(absPath: string): PackedModel3d { - const data = parseJsonBytes(readBytes(absPath)); + const data = parseJsonBytes< + OpclibModel3dEntry & { + provenance?: unknown; + offsetMm?: unknown; + rotationDeg?: unknown; + } + >(readBytes(absPath)); const formats: OpclibModel3dEntry["formats"] = {}; const assets: PackedModel3d["assets"] = []; for (const [format, info] of Object.entries(data.formats)) { if (!info) continue; if (format !== "glb" && format !== "step") { - console.error(`[pack] unsupported 3D format ${format} in ${relPath(absPath)}`); + console.error( + `[pack] unsupported 3D format ${format} in ${relPath(absPath)}`, + ); process.exit(1); } const abs = path.join(REPO_ROOT, info.path); @@ -199,6 +220,10 @@ const input: PackOpclibInput = { path.join(REPO_ROOT, "components"), ".component.json", ).map(packedComponentFor), + sign: + signKeyPath && keyId + ? { privateKey: readFileSync(signKeyPath), keyId } + : undefined, }; const { bytes, manifest, packageSha256 } = packOpclib(input); @@ -208,5 +233,8 @@ writeFileSync(outPath, bytes); console.log( `[pack] wrote ${outPath}\n` + ` symbols=${manifest.symbols.length} footprints=${manifest.footprints.length} models3d=${manifest.models3d.length} components=${manifest.components.length}\n` + - ` packageSha256=${packageSha256}`, + ` packageSha256=${packageSha256}` + + (manifest.signature + ? `\n signed: keyId=${manifest.signature.keyId} alg=${manifest.signature.algorithm}` + : "\n (unsigned)"), );