Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
#!/usr/bin/env bun
/**
* Import-allowlist conformance gate.
*
* Scans every workspace package's `src/` for imports of
* `@suaveplan/crypto-core`, `@suaveplan/crypto-cipher`,
* `@suaveplan/crypto-curve`, or `@suaveplan/crypto-hash` MAC subpaths.
* Fails with `error` severity for any package not on the committed
* allowlist (`scripts/conformance/crypto-core-allowlist.yml`).
*
* Open imports (always allowed):
* - `@suaveplan/crypto-utils` (encoding helpers)
* - `@suaveplan/crypto-hash` for sha256 / sha384 / sha512 / hkdf /
* pbkdf2 / argon2id (pure-hash + KDF uses); only the MAC subpath
* `@suaveplan/crypto-hash/mac` is gated, because HMAC is the
* primitive that should travel through Authority instead.
*
* Exit codes:
* 0 — all packages comply
* 1 — at least one package outside the allowlist imports a gated module
*/
import { readdirSync, readFileSync, statSync } from "node:fs";
import { dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(HERE, "..", "..");
const ALLOWLIST_PATH = join(HERE, "crypto-core-allowlist.yml");
const PACKAGES_ROOT = join(REPO_ROOT, "packages");
const GATED_PATTERNS = [
/@suaveplan\/crypto-core(\/[^"'`]*)?/,
/@suaveplan\/crypto-cipher(\/[^"'`]*)?/,
/@suaveplan\/crypto-curve(\/[^"'`]*)?/,
/@suaveplan\/crypto-hash\/mac(\/[^"'`]*)?/,
];
interface AllowlistDoc {
readonly allowlist: readonly string[];
}
function readAllowlist(path: string): AllowlistDoc {
const text = readFileSync(path, "utf8");
const allowlist: string[] = [];
let inAllowlist = false;
for (const raw of text.split(/\r?\n/)) {
const line = raw.trimEnd();
if (line.startsWith("allowlist:")) {
inAllowlist = true;
continue;
}
if (inAllowlist) {
// Section ends at the next top-level key
if (/^\S/.test(line) && !line.startsWith(" ")) {
inAllowlist = false;
continue;
}
const m = line.match(/^\s*-\s+"([^"]+)"\s*$/);
if (m?.[1]) allowlist.push(m[1]);
}
}
return { allowlist };
}
function findPackageRoots(start: string): string[] {
const out: string[] = [];
function walk(dir: string): void {
for (const entry of readdirSync(dir)) {
if (entry.startsWith(".")) continue;
const full = join(dir, entry);
const st = statSync(full);
if (!st.isDirectory()) continue;
if (entry === "node_modules" || entry === "dist") continue;
const pj = join(full, "package.json");
try {
statSync(pj);
out.push(full);
} catch {
walk(full);
}
}
}
walk(start);
return out;
}
function* walkSrcFiles(packageDir: string): Generator<string> {
const src = join(packageDir, "src");
try {
statSync(src);
} catch {
return;
}
function* recurse(d: string): Generator<string> {
for (const entry of readdirSync(d)) {
const full = join(d, entry);
const st = statSync(full);
if (st.isDirectory()) {
yield* recurse(full);
} else if (
full.endsWith(".ts") &&
!full.endsWith(".test.ts") &&
!full.endsWith(".spec.ts")
) {
yield full;
}
}
}
yield* recurse(src);
}
interface Finding {
readonly packageName: string;
readonly file: string;
readonly line: number;
readonly importPath: string;
}
function packageNameOf(packageDir: string): string {
const pj = JSON.parse(
readFileSync(join(packageDir, "package.json"), "utf8")
) as { name?: string };
return pj.name ?? `(unknown)@${relative(REPO_ROOT, packageDir)}`;
}
function scanFile(file: string, packageName: string): Finding[] {
const findings: Finding[] = [];
const text = readFileSync(file, "utf8");
const lines = text.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = lines[i] ?? "";
if (!line.includes("@suaveplan/crypto-")) continue;
for (const pat of GATED_PATTERNS) {
const m = line.match(pat);
if (m?.[0]) {
findings.push({
packageName,
file: relative(REPO_ROOT, file),
line: i + 1,
importPath: m[0],
});
}
}
}
return findings;
}
export interface AllowlistCheckResult {
readonly violations: readonly Finding[];
}
export function checkAllowlist(): AllowlistCheckResult {
const allow = readAllowlist(ALLOWLIST_PATH);
const allowSet = new Set(allow.allowlist);
const violations: Finding[] = [];
for (const packageDir of findPackageRoots(PACKAGES_ROOT)) {
const name = packageNameOf(packageDir);
if (allowSet.has(name)) continue;
for (const file of walkSrcFiles(packageDir)) {
for (const f of scanFile(file, name)) {
violations.push(f);
}
}
}
return { violations };
}
if (import.meta.main) {
const result = checkAllowlist();
if (result.violations.length === 0) {
console.log("crypto-core allowlist: 0 violations");
process.exit(0);
}
console.error(
`crypto-core allowlist: ${result.violations.length} violation(s)`
);
for (const v of result.violations) {
console.error(
` [error] ${v.packageName} at ${v.file}:${v.line} imports ${v.importPath}`
);
}
process.exit(1);
}

View File

@@ -0,0 +1,77 @@
# crypto-core import allowlist
#
# Packages on this list MAY import directly from any of:
# - @suaveplan/crypto-core (and subpaths)
# - @suaveplan/crypto-cipher (and subpaths)
# - @suaveplan/crypto-curve (and subpaths)
# - @suaveplan/crypto-hash MAC subpaths (hmac and similar)
#
# Every other package MUST go through @suaveplan/crypto's opinionated
# layer (Authority, Packer, Vault). Encoding helpers from
# @suaveplan/crypto-utils and pure-hash uses from @suaveplan/crypto-hash
# (sha256/sha384/sha512) remain open.
#
# Adding a package to this list requires reviewer approval and a
# documented rationale. The allowlist is intentionally short; the
# discipline this gate enforces is the single most important guarantee
# in restore-crypto-keychain-packer.
allowlist:
- "@suaveplan/crypto"
# The opinionated layer itself. Composes primitives into the
# Authority/Packer/Vault APIs and re-exports the primitive surface
# for ergonomic single-package imports.
- "@suaveplan/crypto-cli"
# The keychain CLI. Wraps the opinionated layer for operators.
# Imports are routed through @suaveplan/crypto's barrel only.
- "@suaveplan/crypto-sodium"
# libsodium engine adapter for @suaveplan/crypto-core. Implements
# the SignEngine / AgreeEngine / AeadEngine / AuthBoxEngine /
# SealedBoxEngine interfaces. Engine adapters are by design the
# only direct consumers of the primitive engine types.
- "@suaveplan/crypto-browser"
# WebCrypto engine adapter. Same rationale as crypto-sodium.
- "@suaveplan/bitpacker-crypto"
# SignedBitSchema / EnvelopeSchema / HashChainSchema build on
# primitives directly to provide bit-packed wire format building
# blocks. The opinionated layer (Vault, Packer) is built on top of
# bitpacker-crypto, so it cannot itself depend on the opinionated
# layer without a cycle.
- "@suaveplan/slsa"
# DSSE PAE signature verification. DSSE is governed by an external
# spec (in-toto.io/dsse) without Suaveplan domain tags; the PAE
# bytes ARE the message. Verifying via verifyAuthoritySignature
# would require fabricating a domain tag that the verifier
# contract doesn't have. Builder identity SIGNING (not verifying)
# goes through @suaveplan/crypto's Authority — separate concern.
- "@suaveplan/packer"
# Pre-existing chunked AEAD packer (legacy of restore-crypto). To
# be migrated to @suaveplan/crypto's Packer in a future change;
# allowlisted for now to keep slice 4 reviewable.
- "@suaveplan/drive-plugin-chunks"
# Legacy direct primitive use; same rationale as @suaveplan/packer.
- "@suaveplan/crypto-core"
# crypto-core IS the primitive layer. It composes the
# crypto-cipher / crypto-curve / crypto-hash selector packages
# internally to expose its public API. Self-imports.
- "@suaveplan/streams"
# streams/transforms/crypto consumes crypto-cipher selectors
# directly. Migration to Authority is tracked as a future change.
- "@suaveplan/audit-logging"
# chain-integrity uses HMAC for tamper-evidence. Audit-log MAC is
# a low-level primitive that doesn't fit the Authority model.
# Encoding helpers from @suaveplan/crypto-utils stay open for ALL
# packages. The gate does not flag imports of toHex/fromHex/toBase64/etc.
encoding_helpers_open:
- "@suaveplan/crypto-utils"