commit bb8fa8415b0a26e6e34ba2dc72969b7706935bd5 parent fe61cbdbcc10001bada09e796e583b8c86e70525 Author: triesap <tyson@radroots.org> Date: Tue, 25 Aug 2026 08:16:05 +0000 security: seal generated SDK boundaries - verify exact generated source and credential-free inventories - bind package license bytes to the selected Lib producer - make the complete generated-package check non-mutating - document the standalone boundary and regeneration workflow Diffstat:
27 files changed, 285 insertions(+), 28 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md @@ -57,7 +57,10 @@ its subtree. complete diff, and run freshness checks. Generated output never dictates a native source model or creates a second source authority. - Keep package manifests, the pnpm lockfile, provenance, exports, generated - source, and consumer-facing package READMEs synchronized. + source, producer-revision-bound package licenses, and consumer-facing package + READMEs synchronized. Repository-root license files remain repository-owned + and are not a substitute for the selected producer's generated-package + license bytes. - Do not reintroduce retired prototype evidence, outcomes, receipts, event models, runtime contracts, or compatibility aliases. Services-hardening generated changes must expose the approved four coverage states and three @@ -72,13 +75,17 @@ its subtree. `cargo extbuild run -- ...`. - `pnpm run contracts:check` validates the exact historical inventory and the absence of forbidden public roots without requiring a lib checkout. +- `pnpm run boundaries:check` validates the historical API baselines, exact + generated-source inventory, forbidden surfaces, and credential exclusions + without requiring a lib checkout. - `pnpm run supply-chain:check` validates the exact pnpm toolchain and lock, immutable public Lib source lock, package repository identities, workspace- only internal edges, and byte-identical package license artifacts. - `pnpm run test:tools` runs standalone tool and boundary tests. - `pnpm run source:check` and generation/freshness commands require an exact `RADROOTS_LIB_SOURCE_ROOT` matching the checked-in lock. `pnpm run check` is - the full generated-package lane. + the full non-mutating generated-package lane; use `pnpm run generate` + explicitly when intentionally refreshing outputs. - This repository has no local `cargo xtask` package. Do not document or invoke nonexistent SDK-local xtask commands; the artifact adapter invokes the selected producer's governed xtask explicitly. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md @@ -33,6 +33,7 @@ commands are: ```text pnpm run contracts:check +pnpm run boundaries:check pnpm run test:tools pnpm run source:check pnpm run check diff --git a/README b/README @@ -10,10 +10,15 @@ through `tools/radroots_sdk_artifact.mjs` from the exact `radrootslabs/lib` revision recorded in `radroots.lib.source-lock.v1.toml`. Use `pnpm run supply-chain:check` for locked dependency, source, and license -validation, `pnpm run contracts:check` for standalone contract validation, and -`pnpm run check` with an exact `RADROOTS_LIB_SOURCE_ROOT` for the complete -generated-package lane. See `AGENTS.md` and `CONTRIBUTING.md` for repository -rules and command requirements. +validation, `pnpm run boundaries:check` for forbidden-surface, secret, API +baseline, and exact generated-inventory validation, and `pnpm run +contracts:check` for standalone contract validation. Run `pnpm run check` with +an exact `RADROOTS_LIB_SOURCE_ROOT` for the complete producer-backed generated +freshness lane. Verification is non-mutating; `pnpm run generate` is the +explicit write command. Generated package license bytes remain bound to the +selected Lib producer revision, while repository-root licenses remain owned by +this SDK repository. See `AGENTS.md` and `CONTRIBUTING.md` for repository rules +and command requirements. ## Copyright diff --git a/contracts/generated_package_licenses.v1.json b/contracts/generated_package_licenses.v1.json @@ -0,0 +1,15 @@ +{ + "schema": "radroots.sdk.generated-package-licenses.v1", + "source_repository": "https://github.com/radrootslabs/lib", + "source_revision": "3563f3b5a4331eb2cb3f925cafc9de524d844228", + "files": [ + { + "name": "LICENSE-APACHE", + "sha256": "a1cd22441ac900622e68fc6469dcb20ea71894a96f858f62b692e9e9b3451645" + }, + { + "name": "LICENSE-MIT", + "sha256": "abdbcc7cdee9ce504c7e07fcccd37bfd713509d94886308b96d4817a1c02f926" + } + ] +} diff --git a/package.json b/package.json @@ -4,6 +4,7 @@ "packageManager": "pnpm@10.25.0", "scripts": { "supply-chain:check": "node tools/radroots_sdk_supply_chain.mjs", + "boundaries:check": "node tools/radroots_sdk_boundary.mjs", "contracts:check": "node tools/radroots_sdk_contract.mjs", "source:check": "node tools/radroots_sdk_artifact.mjs source-check", "generate:ts": "node tools/radroots_sdk_artifact.mjs write typescript", @@ -13,7 +14,7 @@ "generate": "pnpm run generate:ts && pnpm run generate:wasm && pnpm run generate:swift && pnpm run generate:kotlin", "check:generated": "node tools/radroots_sdk_artifact.mjs check typescript && node tools/radroots_sdk_artifact.mjs check wasm && node tools/radroots_sdk_artifact.mjs check swift && node tools/radroots_sdk_artifact.mjs check kotlin", "test:tools": "node --test tools/*.test.mjs", - "check": "pnpm run supply-chain:check && pnpm run contracts:check && pnpm run generate && pnpm -r build && pnpm -r typecheck && pnpm run test:tools && pnpm run check:generated", + "check": "pnpm run supply-chain:check && pnpm run boundaries:check && pnpm run contracts:check && pnpm run check:generated && pnpm -r build && pnpm -r typecheck && pnpm run test:tools", "build": "pnpm run generate:ts && pnpm run generate:wasm && pnpm -r build", "typecheck": "pnpm -r typecheck" }, diff --git a/packages/core-bindings/LICENSE-APACHE b/packages/core-bindings/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Tyson Lupul +Copyright 2025 Tyson Lupul Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/core-bindings/LICENSE-MIT b/packages/core-bindings/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Tyson Lupul +Copyright (c) 2025 Tyson Lupul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/event-bindings/LICENSE-APACHE b/packages/event-bindings/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Tyson Lupul +Copyright 2025 Tyson Lupul Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/event-bindings/LICENSE-MIT b/packages/event-bindings/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Tyson Lupul +Copyright (c) 2025 Tyson Lupul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/event-codec-wasm/LICENSE-APACHE b/packages/event-codec-wasm/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Tyson Lupul +Copyright 2025 Tyson Lupul Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/event-codec-wasm/LICENSE-MIT b/packages/event-codec-wasm/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Tyson Lupul +Copyright (c) 2025 Tyson Lupul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/identity-bindings/LICENSE-APACHE b/packages/identity-bindings/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Tyson Lupul +Copyright 2025 Tyson Lupul Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/identity-bindings/LICENSE-MIT b/packages/identity-bindings/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Tyson Lupul +Copyright (c) 2025 Tyson Lupul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/replica-schema-bindings/LICENSE-APACHE b/packages/replica-schema-bindings/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Tyson Lupul +Copyright 2025 Tyson Lupul Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/replica-schema-bindings/LICENSE-MIT b/packages/replica-schema-bindings/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Tyson Lupul +Copyright (c) 2025 Tyson Lupul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/replica-store-wasm/LICENSE-APACHE b/packages/replica-store-wasm/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Tyson Lupul +Copyright 2025 Tyson Lupul Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/replica-store-wasm/LICENSE-MIT b/packages/replica-store-wasm/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Tyson Lupul +Copyright (c) 2025 Tyson Lupul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/replica-sync-wasm/LICENSE-APACHE b/packages/replica-sync-wasm/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Tyson Lupul +Copyright 2025 Tyson Lupul Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/replica-sync-wasm/LICENSE-MIT b/packages/replica-sync-wasm/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Tyson Lupul +Copyright (c) 2025 Tyson Lupul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/trade-bindings/LICENSE-APACHE b/packages/trade-bindings/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Tyson Lupul +Copyright 2025 Tyson Lupul Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/trade-bindings/LICENSE-MIT b/packages/trade-bindings/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Tyson Lupul +Copyright (c) 2025 Tyson Lupul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/tools/radroots_sdk_boundary.mjs b/tools/radroots_sdk_boundary.mjs @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + validateProductionSources, + validateTrackedInventory, +} from "./radroots_sdk_boundary_lib.mjs"; +import { validateHistoricalAuthority } from "./radroots_sdk_contract_lib.mjs"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const tracked = execFileSync("git", ["ls-files", "-z"], { + cwd: root, + encoding: "utf8", +}) + .split("\0") + .filter(Boolean); + +validateHistoricalAuthority(root); +validateTrackedInventory(tracked); +validateProductionSources(root, tracked); +process.stdout.write(`SDK boundaries: OK (${tracked.length} tracked files)\n`); diff --git a/tools/radroots_sdk_boundary_inventory.test.mjs b/tools/radroots_sdk_boundary_inventory.test.mjs @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import test from "node:test"; + +import { + GENERATED_FILES, + validateProductionSources, + validateTrackedInventory, +} from "./radroots_sdk_boundary_lib.mjs"; + +test("generated source inventory is exact", () => { + assert.doesNotThrow(() => validateTrackedInventory(GENERATED_FILES)); + assert.throws( + () => validateTrackedInventory(GENERATED_FILES.slice(1)), + /generated source inventory is not exact/, + ); + assert.throws( + () => validateTrackedInventory([...GENERATED_FILES, "generated/extra.ts"]), + /generated source inventory is not exact/, + ); +}); + +test("tracked credential paths fail closed", () => { + assert.throws( + () => validateTrackedInventory([...GENERATED_FILES, "packages/core/.env"]), + /sensitive credential path is tracked/, + ); +}); + +test("production credential material fails closed", () => { + const root = mkdtempSync(join(tmpdir(), "radroots-sdk-boundary-")); + const path = "packages/core/src/index.ts"; + mkdirSync(dirname(join(root, path)), { recursive: true }); + writeFileSync(join(root, path), `export const leaked = "AKIAABCDEFGHIJKLMNOP";\n`); + assert.throws( + () => validateProductionSources(root, [path]), + /production source contains credential material/, + ); +}); diff --git a/tools/radroots_sdk_boundary_lib.mjs b/tools/radroots_sdk_boundary_lib.mjs @@ -0,0 +1,62 @@ +import { lstatSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +export const GENERATED_FILES = Object.freeze([ + "generated/kotlin/source.lock", + "generated/kotlin/uniffi/radroots_sdk/radroots_sdk.kt", + "generated/swift/radroots_sdk.swift", + "generated/swift/radroots_sdkFFI.h", + "generated/swift/radroots_sdkFFI.modulemap", + "generated/swift/source.lock", + "packages/core-bindings/src/generated/types.ts", + "packages/event-bindings/src/generated/constants.ts", + "packages/event-bindings/src/generated/kinds.ts", + "packages/event-bindings/src/generated/types.ts", + "packages/identity-bindings/src/generated/constants.ts", + "packages/replica-schema-bindings/src/generated/types.ts", + "packages/trade-bindings/src/generated/types.ts", +]); + +const SENSITIVE_PATH = /(^|\/)(\.env|id_rsa|id_ed25519|credentials|[^/]+\.(pem|key|p12|pfx|jks|keystore))$/i; +const CREDENTIAL_MATERIAL = /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{36,}|nsec1[023456789acdefghjklmnpqrstuvwxyz]{40,}/; +const TEXT_SOURCE = /\.(?:h|js|json|kt|md|mjs|modulemap|rs|swift|toml|ts)$/; + +function fail(message) { + throw new Error(`boundary_invalid: ${message}`); +} + +function exactInventory(actual, expected, context) { + const sorted = [...actual].sort(); + if (JSON.stringify(sorted) !== JSON.stringify(expected)) { + fail(`${context} is not exact`); + } +} + +export function validateTrackedInventory(paths) { + for (const path of paths) { + if (SENSITIVE_PATH.test(path)) fail("sensitive credential path is tracked"); + } + const generated = paths.filter( + (path) => path.startsWith("generated/") || /\/src\/generated\//.test(path), + ); + exactInventory(generated, GENERATED_FILES, "generated source inventory"); +} + +export function validateProductionSources(root, paths) { + for (const path of paths) { + if ( + !TEXT_SOURCE.test(path) || + !["crates/", "generated/", "packages/"].some((prefix) => path.startsWith(prefix)) + ) { + continue; + } + const fullPath = resolve(root, path); + const metadata = lstatSync(fullPath); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + fail("production source must be a regular non-symlink file"); + } + if (CREDENTIAL_MATERIAL.test(readFileSync(fullPath, "utf8"))) { + fail("production source contains credential material"); + } + } +} diff --git a/tools/radroots_sdk_package_license.test.mjs b/tools/radroots_sdk_package_license.test.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +import { validateGeneratedPackageLicenses } from "./radroots_sdk_package_license_lib.mjs"; + +const sourceRoot = fileURLToPath(new URL("..", import.meta.url)); +const revision = "3563f3b5a4331eb2cb3f925cafc9de524d844228"; + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "radroots-sdk-licenses-")); + const packageDirectory = join(root, "packages/example"); + mkdirSync(join(root, "contracts"), { recursive: true }); + mkdirSync(packageDirectory, { recursive: true }); + copyFileSync( + join(sourceRoot, "contracts/generated_package_licenses.v1.json"), + join(root, "contracts/generated_package_licenses.v1.json"), + ); + for (const name of ["LICENSE-APACHE", "LICENSE-MIT"]) { + copyFileSync(join(sourceRoot, "packages/core-bindings", name), join(packageDirectory, name)); + } + return { root, packageDirectory }; +} + +test("generated package licenses bind the selected producer revision", () => { + const { root, packageDirectory } = fixture(); + assert.doesNotThrow(() => validateGeneratedPackageLicenses(root, [packageDirectory], revision)); + assert.throws( + () => validateGeneratedPackageLicenses(root, [packageDirectory], "0".repeat(40)), + /license authority drifted/, + ); +}); + +test("generated package license drift fails closed", () => { + const { root, packageDirectory } = fixture(); + const path = join(packageDirectory, "LICENSE-MIT"); + writeFileSync(path, `${readFileSync(path, "utf8")}drift\n`); + assert.throws( + () => validateGeneratedPackageLicenses(root, [packageDirectory], revision), + /license bytes drifted/, + ); +}); diff --git a/tools/radroots_sdk_package_license_lib.mjs b/tools/radroots_sdk_package_license_lib.mjs @@ -0,0 +1,56 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const CONTRACT_PATH = "contracts/generated_package_licenses.v1.json"; +const EXPECTED_FILES = Object.freeze(["LICENSE-APACHE", "LICENSE-MIT"]); + +function fail(message) { + throw new Error(`supply_chain_invalid: ${message}`); +} + +function exactKeys(value, expected, context) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + fail(`${context} must be an object`); + } + const actual = Object.keys(value).sort(); + if (JSON.stringify(actual) !== JSON.stringify([...expected].sort())) { + fail(`${context} has invalid keys`); + } +} + +function sha256(path) { + return createHash("sha256").update(readFileSync(path)).digest("hex"); +} + +export function validateGeneratedPackageLicenses(root, packageDirectories, expectedRevision) { + const contract = JSON.parse(readFileSync(join(root, CONTRACT_PATH), "utf8")); + exactKeys( + contract, + ["schema", "source_repository", "source_revision", "files"], + "generated package license contract", + ); + if ( + contract.schema !== "radroots.sdk.generated-package-licenses.v1" || + contract.source_repository !== "https://github.com/radrootslabs/lib" || + contract.source_revision !== expectedRevision + ) { + fail("generated package license authority drifted"); + } + if (!Array.isArray(contract.files) || contract.files.length !== EXPECTED_FILES.length) { + fail("generated package license inventory drifted"); + } + + for (let index = 0; index < EXPECTED_FILES.length; index += 1) { + const file = contract.files[index]; + exactKeys(file, ["name", "sha256"], `generated package license ${index}`); + if (file.name !== EXPECTED_FILES[index] || !/^[0-9a-f]{64}$/.test(file.sha256)) { + fail("generated package license identity drifted"); + } + for (const packageDirectory of packageDirectories) { + if (sha256(join(packageDirectory, file.name)) !== file.sha256) { + fail("generated package license bytes drifted"); + } + } + } +} diff --git a/tools/radroots_sdk_supply_chain.mjs b/tools/radroots_sdk_supply_chain.mjs @@ -2,6 +2,8 @@ import { readFileSync, readdirSync } from "node:fs"; import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { validateGeneratedPackageLicenses } from "./radroots_sdk_package_license_lib.mjs"; + const root = dirname(dirname(fileURLToPath(import.meta.url))); const expectedLicense = "MIT OR Apache-2.0"; const expectedRepository = "git+https://github.com/radrootslabs/sdk.git"; @@ -63,11 +65,6 @@ function verifyManifest(path) { } } } - for (const license of ["LICENSE-MIT", "LICENSE-APACHE"]) { - if (readFileSync(join(path, license)).compare(readFileSync(join(root, license))) !== 0) { - fail("package license bytes differ from the repository authority"); - } - } } const manifest = readJson(join(root, "package.json")); @@ -94,6 +91,8 @@ if (!/^repository = "https:\/\/github\.com\/radrootslabs\/lib"$/m.test(sourceLoc } if (!/^revision = "[0-9a-f]{40}"$/m.test(sourceLock)) fail("Lib revision is not immutable"); if (!/^lockfile_sha256 = "[0-9a-f]{64}"$/m.test(sourceLock)) fail("Cargo lock digest is absent"); +const sourceRevision = sourceLock.match(/^revision = "([0-9a-f]{40})"$/m)?.[1]; +if (!sourceRevision) fail("Lib revision is not immutable"); const packages = packageDirectories(); if ( @@ -103,5 +102,6 @@ if ( fail("package inventory drifted"); } for (const path of packages) verifyManifest(path); +validateGeneratedPackageLicenses(root, packages, sourceRevision); console.log(`supply-chain ok: ${packages.length} packages`);