@volter/twin-catalog 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { admit, defaults, digest, loadIndex, read, requireThat, stable, submissionId, validateIndex, write } from './model.mjs';
5
+
6
+ export function git(root, args) {
7
+ const r = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
8
+ requireThat(r.status === 0, `git ${args[0]}: ${r.stderr}`);
9
+ return r.stdout.trim();
10
+ }
11
+ export function submissions(root) {
12
+ return ['submissions', 'reassessments'].flatMap((kind) => {
13
+ const dir = join(root, kind);
14
+ return existsSync(dir) ? readdirSync(dir).filter((f) => /^[a-f0-9]{64}\.json$/.test(f)).sort().map((f) => {
15
+ const submission = read(join(dir, f));
16
+ requireThat(f === `${submissionId(submission)}.json`, 'submission name/identity mismatch');
17
+ const path = `${kind}/${f}`;
18
+ const commit = git(root, ['log', '--diff-filter=A', '--format=%H', '--', path]).split('\n')[0];
19
+ requireThat(/^[a-f0-9]{40}$/.test(commit), `${path} is not in the committed admission ledger`);
20
+ return { submission, path, reassessment: kind === 'reassessments', commit, at: git(root, ['show', '-s', '--format=%cI', commit]) };
21
+ }) : [];
22
+ });
23
+ }
24
+ export function builtIndex(root) {
25
+ const index = loadIndex(root);
26
+ // Recommendations may refer to admitted submissions not yet present in legacy vendors/.
27
+ const recommendations = index.recommendations, revocations = index.revocations;
28
+ index.recommendations = {}; index.revocations = [];
29
+ for (const item of submissions(root)) admit(index, item.submission, item.commit, item.at, item.reassessment);
30
+ index.recommendations = recommendations; index.revocations = revocations;
31
+ validateIndex(index);
32
+ return index;
33
+ }
34
+ export function build(root, out, evidence = {}) {
35
+ requireThat(!existsSync(out), 'build output must be a new directory');
36
+ const index = builtIndex(root);
37
+ const commit = git(root, ['rev-parse', 'HEAD']);
38
+ const version = `0.2.${git(root, ['rev-list', '--first-parent', '--count', 'HEAD'])}`;
39
+ const references = Object.fromEntries(Object.entries(evidence).map(([id, { envelope, ...receipt }]) => [id, receipt]));
40
+ const identity = digest({ evidence: references, sources: index.sources, vendors: index.vendors, recommendations: index.recommendations, revocations: index.revocations });
41
+ mkdirSync(out, { recursive: true });
42
+ write(join(out, 'sources.json'), index.sources);
43
+ write(join(out, 'recommendations.json'), index.recommendations);
44
+ write(join(out, 'revocations.json'), index.revocations);
45
+ for (const e of index.vendors) write(join(out, 'vendors', `${e.vendor}.json`), e);
46
+ const catalog = { schemaVersion: 1, sourceCommit: commit, digest: identity, versions: index.vendors.flatMap((e) => e.packages.flatMap((p) => p.versions.map((v) => ({ vendor: e.vendor, package: p.name, source: p.source, ...v, evidence: (v.submission ?? v.assessment) ? { submission: v.submission ?? v.assessment, admissionCommit: v.assessmentCommit ?? v.commit, ...(references[v.submission ?? v.assessment] ?? {}) } : null, assessed: Boolean(v.submission ?? v.assessment) })))) };
47
+ write(join(out, 'catalog.json'), catalog);
48
+ for (const [id, receipt] of Object.entries(evidence)) write(join(out, 'evidence', `${id}.json`), receipt.envelope);
49
+ const pkg = { ...read(join(root, 'package.json')), version, catalog: { sourceCommit: commit, digest: identity } };
50
+ delete pkg.devDependencies;
51
+ write(join(out, 'package.json'), pkg);
52
+ for (const path of ['bin', 'lib', 'runner', 'docs', 'README.md', 'policy.json', 'LICENSE']) if (existsSync(join(root, path))) cpSync(join(root, path), join(out, path), { recursive: true });
53
+ return { version, sourceCommit: commit, digest: identity, directory: out };
54
+ }
55
+
56
+ function authorizedMaintainer(pr, policy, mergerPermission) {
57
+ return Array.isArray(policy.reviewBypassUsers) && policy.reviewBypassUsers.includes(pr.merged_by?.login) &&
58
+ ['admin', 'maintain'].includes(mergerPermission);
59
+ }
60
+ function trustedAccountAuthor(pr, policy) {
61
+ return Array.isArray(policy.trustedAccounts) && policy.trustedAccounts.some((account) =>
62
+ Number.isSafeInteger(account?.id) && account.id > 0 && typeof account.login === 'string' &&
63
+ account.login.length > 0 && ['User', 'Bot'].includes(account.type) &&
64
+ account.id === pr.user.id && account.login === pr.user.login && account.type === pr.user.type);
65
+ }
66
+ export function trustedAccountAdmission(pr, policy, mergerPermission) {
67
+ return Boolean(trustedAccountAuthor(pr, policy) && authorizedMaintainer(pr, policy, mergerPermission));
68
+ }
69
+
70
+ export function internalAdmission(pr, source, policy, mergerPermission) {
71
+ const bot = pr.user.type === 'Bot' && (pr.user.login === 'github-actions[bot]' ||
72
+ (Array.isArray(policy.internalBotAuthors) && policy.internalBotAuthors.some((author) =>
73
+ Number.isSafeInteger(author?.id) && author.id > 0 && author.id === pr.user.id && author.login === pr.user.login)));
74
+ return Boolean(source && Array.isArray(policy.internalRepositories) && policy.internalRepositories.includes(source.repository) &&
75
+ authorizedMaintainer(pr, policy, mergerPermission) &&
76
+ pr.head.repo?.full_name === pr.base.repo?.full_name && pr.base.repo?.full_name &&
77
+ (bot || (pr.user.type !== 'Bot' && ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(pr.author_association))));
78
+ }
79
+ export function reviewSatisfied(pr, reviews, checks, internal = false) {
80
+ const latest = new Map();
81
+ for (const r of reviews) {
82
+ if (['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(r.state)) latest.set(r.user.login, r);
83
+ }
84
+ const decisions = [...latest.values()].filter((r) => r.commit_id === pr.head.sha);
85
+ return Boolean(pr.merged_at) && (internal || decisions.some((r) => r.state === 'APPROVED' && r.user.login !== pr.user.login && r.user.type !== 'Bot' && !r.user.login.endsWith('[bot]') && ['MEMBER', 'OWNER', 'COLLABORATOR'].includes(r.author_association))) && !decisions.some((r) => r.state === 'CHANGES_REQUESTED') && checks.some((c) => c.name === 'catalog/readiness' && c.head_sha === pr.head.sha && c.conclusion === 'success' && c.app?.slug === 'github-actions');
86
+ }
87
+ export async function verifyAdmission(root, github, policyClient = github) {
88
+ requireThat(git(root, ['status', '--porcelain']) === '', 'publication requires a clean committed catalog checkout');
89
+ const main = await github.call('git/ref/heads/main');
90
+ requireThat(main.object?.sha === git(root, ['rev-parse', 'HEAD']), 'publication must use current main; retry the latest source commit');
91
+ requireThat(/^\*\s+@/m.test(await github.content('.github/CODEOWNERS', main.object.sha)), 'catalog needs moderator CODEOWNERS over every path');
92
+ const owners = await github.call('codeowners/errors?ref=main');
93
+ requireThat(Array.isArray(owners.errors) && owners.errors.length === 0, 'catalog CODEOWNERS has invalid moderators');
94
+ const protection = await policyClient.call('branches/main/protection');
95
+ requireThat(protection.enforce_admins?.enabled && protection.required_status_checks?.strict && protection.required_status_checks.contexts.includes('catalog/readiness') && protection.required_pull_request_reviews?.dismiss_stale_reviews && protection.required_pull_request_reviews?.require_code_owner_reviews && protection.required_pull_request_reviews?.require_last_push_approval, 'publication requires active current-head readiness and CODEOWNERS branch protection');
96
+ const proofs = [];
97
+ const policy = read(join(root, 'policy.json'));
98
+ const sources = loadIndex(root).sources;
99
+ for (const item of submissions(root)) {
100
+ const pulls = await github.pages(`commits/${item.commit}/pulls`);
101
+ let accepted = false;
102
+ for (const p of pulls.filter((p) => p.merged_at && p.base.ref === 'main')) {
103
+ const pr = await github.call(`pulls/${p.number}`);
104
+ const files = await github.pages(`pulls/${p.number}/files`);
105
+ if (files.length !== 1 || files[0].status !== 'added' || files[0].filename !== item.path) continue;
106
+ const contents = JSON.parse(await github.content(files[0].filename, pr.head.sha));
107
+ if (stable(contents) !== stable(item.submission)) continue;
108
+ const reviews = await github.pages(`pulls/${p.number}/reviews`);
109
+ const checks = await github.call(`commits/${pr.head.sha}/check-runs?check_name=catalog%2Freadiness&filter=latest&per_page=100`);
110
+ const source = sources.find((s) => s.name === item.submission.source);
111
+ let permission;
112
+ if ((trustedAccountAuthor(pr, policy) || (Array.isArray(policy.internalRepositories) && policy.internalRepositories.includes(source?.repository))) &&
113
+ Array.isArray(policy.reviewBypassUsers) && policy.reviewBypassUsers.includes(pr.merged_by?.login)) {
114
+ permission = (await policyClient.call(`collaborators/${encodeURIComponent(pr.merged_by.login)}/permission`)).permission;
115
+ }
116
+ const trusted = trustedAccountAdmission(pr, policy, permission);
117
+ const internal = internalAdmission(pr, source, policy, permission);
118
+ if (reviewSatisfied(pr, reviews, checks.check_runs, trusted || internal)) {
119
+ const check = checks.check_runs.find((c) => c.name === 'catalog/readiness' && c.head_sha === pr.head.sha && c.conclusion === 'success' && c.app?.slug === 'github-actions');
120
+ const parts = (check.external_id ?? '').split(':');
121
+ requireThat([4, 6].includes(parts.length) && parts[0] === String(pr.number) && parts[1] === pr.head.sha && /^[a-f0-9]{40}$/.test(parts[2]) && /^[a-f0-9]{64}$/.test(parts[3]), 'readiness check has no bound evidence receipt');
122
+ let workflow;
123
+ if (parts.length === 6) {
124
+ requireThat(/^[1-9][0-9]*$/.test(parts[4]) && /^[1-9][0-9]*$/.test(parts[5]) && Number.isSafeInteger(Number(parts[4])) && Number.isSafeInteger(Number(parts[5])), 'readiness receipt has an invalid workflow run or attempt');
125
+ workflow = { run: Number(parts[4]), attempt: Number(parts[5]) };
126
+ }
127
+ proofs.push({ ...item, pullRequest: pr.number, head: pr.head.sha, base: parts[2], reportSha256: parts[3], ...(workflow ? { workflow } : {}), check, moderation: { mode: trusted ? 'trusted-account-merge' : internal ? 'internal-maintainer-merge' : 'human-review', mergedBy: pr.merged_by.login, ...(trusted ? { account: { id: pr.user.id, login: pr.user.login, type: pr.user.type } } : {}) } });
128
+ accepted = true; break;
129
+ }
130
+ }
131
+ requireThat(accepted, `${submissionId(item.submission)}: no reviewed current-head admission with successful readiness`);
132
+ }
133
+ return proofs;
134
+ }
135
+
136
+ // Ten minutes rounds up three times the observed delay; see docs/measurements/registry-confirmation.json.
137
+ const confirmationWindowMs = 10 * 60_000;
138
+ export async function publish(directory, registry, fetcher = fetch, execute = spawnSync, confirmation = {}) {
139
+ const pkg = read(join(directory, 'package.json'));
140
+ const url = `${registry.replace(/\/$/, '')}/${encodeURIComponent(pkg.name)}/${pkg.version}`;
141
+ const before = await fetcher(url, { redirect: 'error' });
142
+ if (before.ok) {
143
+ const existing = await before.json();
144
+ requireThat(stable(existing.catalog) === stable(pkg.catalog), 'published version conflicts with this source identity');
145
+ requireThat(existing.dist?.integrity, 'published version has no artifact integrity');
146
+ return { published: false, existing: true, package: pkg.name, version: pkg.version, integrity: existing.dist.integrity, ...pkg.catalog };
147
+ }
148
+ requireThat(before.status === 404, `registry lookup failed: HTTP ${before.status}`);
149
+ const { now = Date.now, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), windowMs = confirmationWindowMs, intervalMs = 15_000 } = confirmation;
150
+ requireThat(Number.isFinite(windowMs) && windowMs > 0 && Number.isFinite(intervalMs) && intervalMs > 0, 'invalid registry confirmation timing');
151
+ const result = execute('npm', ['publish', '--ignore-scripts', '--access', 'public', '--provenance', '--registry', registry], { cwd: directory, encoding: 'utf8' });
152
+ requireThat(result.status === 0, `index publication failed: ${result.stderr}`);
153
+ const deadline = now() + windowMs;
154
+ let response;
155
+ for (;;) {
156
+ response = await fetcher(url, { redirect: 'error' });
157
+ if (response.ok) break;
158
+ requireThat(response.status === 404, `registry confirmation failed: HTTP ${response.status}`);
159
+ const remaining = deadline - now();
160
+ requireThat(remaining > 0, 'npm accepted the upload but registry confirmation is still unavailable; upload is unverified, retry to verify');
161
+ await sleep(Math.min(intervalMs, remaining));
162
+ }
163
+ const published = await response.json();
164
+ requireThat(stable(published.catalog) === stable(pkg.catalog) && published.dist?.integrity, 'published registry identity differs');
165
+ return { published: true, package: pkg.name, version: pkg.version, integrity: published.dist.integrity, ...pkg.catalog };
166
+ }
@@ -0,0 +1,18 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { read, requireThat, stable, validateSources, write } from './model.mjs';
3
+
4
+ /** Prepare a separate registration PR's data. No GitHub or registry action. */
5
+ export function register(sources, source, file) {
6
+ validateSources(sources);
7
+ validateSources([source]);
8
+ const existing = sources.find((s) => s.name === source.name);
9
+ requireThat(!existing || stable(existing) === stable(source), 'source name already has a different registration');
10
+ const next = existing ? sources : validateSources([...sources, source]);
11
+ if (existsSync(file)) {
12
+ const held = stable(read(file));
13
+ requireThat(held === stable(sources) || held === stable(next), 'registration output already has different content');
14
+ }
15
+ write(file, next);
16
+ return { file, source, alreadyRegistered: Boolean(existing),
17
+ next: 'Open a separate pull request changing only sources.json. No publication or PR was performed.' };
18
+ }
@@ -0,0 +1,101 @@
1
+ import { createHash, X509Certificate } from 'node:crypto';
2
+ import { requireThat, packagePattern, versionPattern } from './model.mjs';
3
+
4
+ export async function metadata(name, version, registry, fetcher = fetch, confirmation) {
5
+ requireThat(packagePattern.test(name) && versionPattern.test(version), 'registry lookup requires an exact scoped package version');
6
+ const base = new URL(registry);
7
+ requireThat(base.protocol === 'https:' && !base.username && !base.password, 'registry must be HTTPS without credentials');
8
+ const { now = Date.now, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), windowMs = 600_000, intervalMs = 15_000 } = confirmation ?? {};
9
+ // Publisher confirmation uses the observed registry propagation in docs/measurements/registry-confirmation.json.
10
+ requireThat(!confirmation || (Number.isFinite(windowMs) && windowMs > 0 && Number.isFinite(intervalMs) && intervalMs > 0), 'invalid registry confirmation timing');
11
+ const deadline = now() + windowMs;
12
+ let response;
13
+ for (;;) {
14
+ response = await fetcher(`${registry.replace(/\/$/, '')}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { redirect: 'error' });
15
+ if (response.ok || !confirmation || response.status !== 404) break;
16
+ const remaining = deadline - now();
17
+ requireThat(remaining > 0, 'published package is not yet visible; retry confirmation, never upload this version again');
18
+ await sleep(Math.min(intervalMs, remaining));
19
+ }
20
+ requireThat(response.ok, `registry metadata: HTTP ${response.status}`);
21
+ const doc = await response.json();
22
+ requireThat(doc.name === name && doc.version === version, 'registry returned a different package/version');
23
+ return doc;
24
+ }
25
+ /** Confirm the registry view npm install uses; visibility of /name/version alone is insufficient. */
26
+ export async function installationMetadata(submission, registry, fetcher = fetch, confirmation = {}) {
27
+ const { package: name, version, integrity } = submission;
28
+ requireThat(packagePattern.test(name) && versionPattern.test(version), 'installation lookup requires an exact scoped package version');
29
+ requireThat(typeof integrity === 'string' && /^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity), 'installation lookup requires artifact SHA-512 integrity');
30
+ const base = new URL(registry);
31
+ requireThat(base.protocol === 'https:' && !base.username && !base.password, 'registry must be HTTPS without credentials');
32
+ const { now = Date.now, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), windowMs = 600_000, intervalMs = 15_000 } = confirmation;
33
+ // Reuse the measured bound in docs/measurements/registry-confirmation.json; this is read-only propagation recovery.
34
+ requireThat(Number.isFinite(windowMs) && windowMs > 0 && Number.isFinite(intervalMs) && intervalMs > 0, 'invalid registry confirmation timing');
35
+ const deadline = now() + windowMs;
36
+ for (;;) {
37
+ const response = await fetcher(`${registry.replace(/\/$/, '')}/${encodeURIComponent(name)}`, {
38
+ redirect: 'error', headers: { accept: 'application/vnd.npm.install-v1+json' },
39
+ });
40
+ if (response.ok) {
41
+ const doc = await response.json();
42
+ requireThat(doc?.name === name && doc.versions && typeof doc.versions === 'object' && !Array.isArray(doc.versions), 'registry installation metadata has a different or malformed package identity');
43
+ if (Object.hasOwn(doc.versions, version)) {
44
+ const release = doc.versions[version];
45
+ requireThat(release?.name === name && release.version === version, 'registry installation metadata returned a different package/version');
46
+ requireThat(release.dist?.integrity === integrity, 'registry installation metadata has conflicting artifact integrity');
47
+ return release;
48
+ }
49
+ } else requireThat(response.status === 404, `registry installation metadata: HTTP ${response.status}`);
50
+ const remaining = deadline - now();
51
+ requireThat(remaining > 0, 'published package is not yet visible to npm install; retry confirmation, never upload this version again');
52
+ await sleep(Math.min(intervalMs, remaining));
53
+ }
54
+ }
55
+ export function registryURL(value, registry) {
56
+ const url = new URL(value);
57
+ requireThat(url.origin === new URL(registry).origin && !url.username && !url.password && !url.hash, 'artifact and attestation URLs must remain on the policy registry');
58
+ return url.href;
59
+ }
60
+ export const integrityOf = (bytes) => `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
61
+ export function verifyArtifact(bytes, submission, doc) {
62
+ requireThat(doc.dist?.integrity === submission.integrity && integrityOf(bytes) === submission.integrity, 'artifact integrity mismatch');
63
+ }
64
+ /** Called only after npm audit signatures has verified the installed artifact's attestations. */
65
+ export function attestedIdentity(document, source, bytes) {
66
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
67
+ const sha512 = createHash('sha512').update(bytes).digest('hex');
68
+ const expected = `https://github.com/${source.repository}/.github/workflows/${source.workflow ?? 'release.yml'}@`;
69
+ const attestations = document.attestations ?? [];
70
+ for (const attestation of attestations) {
71
+ const bundle = attestation.bundle;
72
+ const material = bundle?.verificationMaterial;
73
+ const raw = material?.certificate?.rawBytes ?? material?.x509CertificateChain?.certificates?.[0]?.rawBytes;
74
+ if (!raw || !bundle?.dsseEnvelope?.payload) continue;
75
+ const cert = new X509Certificate(Buffer.from(raw, 'base64'));
76
+ const san = /URI:([^,]+)/.exec(cert.subjectAltName ?? '')?.[1];
77
+ const statement = JSON.parse(Buffer.from(bundle.dsseEnvelope.payload, 'base64').toString('utf8'));
78
+ if (!['https://slsa.dev/provenance/v1', 'https://slsa.dev/provenance/v0.2'].includes(statement.predicateType)) continue;
79
+ if (!san?.startsWith(expected) || !statement.subject?.some(({ digest: d }) => d && (d.sha512 === sha512 || d.sha256 === sha256) && (!d.sha512 || d.sha512 === sha512) && (!d.sha256 || d.sha256 === sha256))) continue;
80
+ const dependency = statement.predicate?.buildDefinition?.resolvedDependencies?.find((item) => item.uri?.startsWith(`git+https://github.com/${source.repository}@`));
81
+ const sourceCommit = dependency?.digest?.gitCommit;
82
+ return { signer: san, subjectSha256: sha256, subjectSha512: sha512, predicateType: statement.predicateType, ...(sourceCommit && /^[a-f0-9]{40}$/.test(sourceCommit) ? { sourceCommit } : {}), bundle };
83
+ }
84
+ throw new Error('verified attestation does not bind this tarball to the registered repository and workflow');
85
+ }
86
+
87
+ /** Verify the exact downloaded bundle, so its identity cannot be swapped after npm's audit. */
88
+ export async function verifiedProvenance(document, source, bytes, verifier) {
89
+ const expected = `https://github.com/${source.repository}/.github/workflows/${source.workflow ?? 'release.yml'}@`;
90
+ const regex = `^${expected.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.+$`;
91
+ for (const attestation of document.attestations ?? []) {
92
+ if (!attestation.bundle?.dsseEnvelope?.payload) continue;
93
+ try {
94
+ // npm also supplies a publish receipt signed with its public key. Only repository provenance qualifies here.
95
+ const identity = attestedIdentity({ attestations: [attestation] }, source, bytes);
96
+ await verifier(attestation.bundle, undefined, { certificateIssuer: 'https://token.actions.githubusercontent.com', certificateIdentityURI: regex });
97
+ return identity;
98
+ } catch { /* another subject, signer, predicate or an invalid bundle cannot confer readiness */ }
99
+ }
100
+ throw new Error('no verified registered-workflow provenance for this artifact');
101
+ }
package/lib/runner.mjs ADDED
@@ -0,0 +1,73 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmodSync, mkdtempSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { digest, write, requireThat } from './model.mjs';
7
+
8
+ /** Own a credential-free container workspace; assessment and transport verification share its boundary. */
9
+ export function withContainer(root, input, output, execute, { spawn = spawnSync } = {}) {
10
+ const id = `twin-catalog-${randomUUID()}`;
11
+ const runOwner = process.env.GITHUB_RUN_ID ? `${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT ?? 1}` : id;
12
+ const work = mkdtempSync(join(tmpdir(), 'twin-catalog-input-'));
13
+ // mkdtemp is 0700; the container's unprivileged uid must traverse this data-only input directory.
14
+ chmodSync(work, 0o755);
15
+ write(join(work, 'input.json'), input);
16
+ chmodSync(join(work, 'input.json'), 0o644);
17
+ const imageFile = join(work, 'image-id');
18
+ const logs = [];
19
+ const run = (args, allowFailure = false) => {
20
+ const r = spawn('docker', args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
21
+ logs.push({ command: args[0], status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '', error: r.error?.message });
22
+ if (!allowFailure) requireThat(r.status === 0, `container ${args[0]} failed: ${r.error?.message ?? r.stderr}`);
23
+ return r;
24
+ };
25
+ let volume = false;
26
+ try {
27
+ const tools = input.policy.tools;
28
+ run(['build', '--iidfile', imageFile, '--build-arg', `NODE_IMAGE=${input.policy.nodeImage}`, '--build-arg', `BUN_VERSION=${input.policy.bun}`, '--build-arg', `STANDARD_VERSION=${tools['@volter/twin-standard']}`, '--build-arg', `CORE_VERSION=${tools['@volter/world-core']}`, '--build-arg', `RUNTIME_VERSION=${tools['@volter/world-runtime']}`, '--build-arg', `PLAYWRIGHT_VERSION=${tools.playwright}`, '-f', join(root, 'runner/Dockerfile'), root]);
29
+ const image = readFileSync(imageFile, 'utf8').trim();
30
+ requireThat(/^sha256:[a-f0-9]{64}$/.test(image), 'container image has no immutable identity');
31
+ run(['volume', 'create', '--label', `twin-catalog.owner=${id}`, '--label', `twin-catalog.run=${runOwner}`, id]); volume = true;
32
+ // Docker volumes start root-owned. Only this task's new volume is changed.
33
+ run(['run', '--rm', '--label', `twin-catalog.owner=${id}`, '--label', `twin-catalog.run=${runOwner}`, '--network', 'none', '--user', '0', '--entrypoint', 'chown', '-v', `${id}:/work`, image, '1000:1000', '/work']);
34
+ const common = ['run', '--rm', '--label', `twin-catalog.owner=${id}`, '--label', `twin-catalog.run=${runOwner}`, '--init', '--shm-size', '256m', '--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,nosuid,nodev', '-e', 'HOME=/tmp/home', '-e', 'npm_config_cache=/tmp/npm-cache', '-v', `${id}:/work`, '-v', `${work}:/input:ro`];
35
+ return execute({ image, common, run, owner: id });
36
+ } finally {
37
+ const cleanup = volume ? run(['volume', 'rm', id], true) : null;
38
+ write(`${output}.logs.json`, logs);
39
+ // Retain input identity and diagnostics on failure; no other actor's resources are touched.
40
+ requireThat(!cleanup || cleanup.status === 0, `owned volume teardown could not be verified: ${id}`);
41
+ }
42
+ }
43
+
44
+ /** The host never imports a candidate. Docker supplies the enforced offline evaluation boundary. */
45
+ export function evaluate(root, input, output, options = {}) {
46
+ requireThat(input.policy.requireProvenance === true, 'admission requires verified provenance');
47
+ let phase = 'container-setup';
48
+ let imageIdentity = null;
49
+ try {
50
+ return withContainer(root, input, output, ({ image, common, run, owner }) => {
51
+ imageIdentity = image;
52
+ phase = 'artifact-preparation';
53
+ run([...common, image, '/opt/catalog/runner/prepare.mjs']);
54
+ const prepared = JSON.parse(run([...common, '--network', 'none', image, '-e', 'process.stdout.write(require("fs").readFileSync("/work/prepared.json","utf8"))']).stdout);
55
+ requireThat(prepared.integrity === input.submission.integrity, 'prepared artifact identity changed');
56
+ const lock = run([...common, '--network', 'none', image, '-e', 'process.stdout.write(require("fs").readFileSync("/work/package-lock.json","utf8"))']).stdout;
57
+ requireThat(digest(lock) === prepared.dependencyLockSha256, 'dependency lock identity changed');
58
+ write(`${output}.lock.json`, JSON.parse(lock));
59
+ phase = 'offline-assessment';
60
+ const result = run([...common, '--network', 'none', '--entrypoint', 'bun', image, '/opt/catalog/node_modules/@volter/world-runtime/src/cli.ts', 'run', '/work/world.json', '--root', '/work', '--env-out', '/work/world.env', '--owner', owner, '--', 'bun', '/opt/catalog/runner/evaluate.mjs'], true);
61
+ const reportRead = run([...common, '--network', 'none', image, '-e', 'process.stdout.write(require("fs").readFileSync("/work/report.json","utf8"))'], true);
62
+ const report = reportRead.status === 0 ? JSON.parse(reportRead.stdout) : null;
63
+ requireThat(!report || (report.schemaVersion === 1 && report.package === input.submission.package && report.version === input.submission.version && report.integrity === input.submission.integrity), 'evaluator returned a mismatched report');
64
+ const envelope = { schemaVersion: 1, inputSha256: digest(input), input, image, prepared, dependencyLock: JSON.parse(lock), status: result.status === 0 && report?.ready === true ? 'ready' : 'changes-needed', report, evaluatedAt: new Date().toISOString() };
65
+ write(output, envelope);
66
+ return envelope;
67
+ }, options);
68
+ } catch (error) {
69
+ const envelope = { schemaVersion: 1, inputSha256: digest(input), input, image: imageIdentity, status: 'changes-needed', phase, error: String(error.message ?? error), report: null, evaluatedAt: new Date().toISOString() };
70
+ write(output, envelope);
71
+ return envelope;
72
+ }
73
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@volter/twin-catalog",
3
+ "version": "0.2.2",
4
+ "description": "The moderated twin distribution: approved versions and submission tooling for independent publishers.",
5
+ "type": "module",
6
+ "bin": {
7
+ "twin-catalog": "bin/twin-catalog.mjs"
8
+ },
9
+ "exports": {
10
+ "./browse": {
11
+ "types": "./lib/browse.d.mts",
12
+ "default": "./lib/browse.mjs"
13
+ },
14
+ "./*": "./*"
15
+ },
16
+ "engines": {
17
+ "node": ">=22.3"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/volter-ai/twin-catalog-open.git"
22
+ },
23
+ "author": "Volter (https://github.com/volter-ai)",
24
+ "license": "Apache-2.0",
25
+ "files": [
26
+ "sources.json",
27
+ "vendors",
28
+ "README.md",
29
+ "LICENSE",
30
+ "catalog.json",
31
+ "recommendations.json",
32
+ "revocations.json",
33
+ "policy.json",
34
+ "bin",
35
+ "lib",
36
+ "runner",
37
+ "docs",
38
+ "evidence"
39
+ ],
40
+ "catalog": {
41
+ "sourceCommit": "646cb5db50f24248f177e1fab1ccdeb681e7005b",
42
+ "digest": "1c1ad9dfb2fd6cba0923988b3a1f17c6da892efc3a5ec36f230a91c88a437ff6"
43
+ }
44
+ }
package/policy.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "registry": "https://registry.npmjs.org",
4
+ "tools": {
5
+ "@volter/world-core": "3.0.59",
6
+ "@volter/world-runtime": "3.0.59",
7
+ "@volter/twin-standard": "1.0.51",
8
+ "playwright": "1.63.0"
9
+ },
10
+ "bun": "1.3.11",
11
+ "nodeImage": "node:24-bookworm-slim",
12
+ "requireProvenance": true,
13
+ "reviewBypassUsers": [
14
+ "yueranyuan"
15
+ ],
16
+ "internalBotAuthors": [
17
+ {
18
+ "id": 338186041,
19
+ "login": "volter-twin-catalog-publisher[bot]"
20
+ }
21
+ ],
22
+ "internalRepositories": [
23
+ "volter-ai/twin-packs-open"
24
+ ],
25
+ "trustedAccounts": [
26
+ {
27
+ "id": 2255943,
28
+ "login": "yueranyuan",
29
+ "type": "User"
30
+ },
31
+ {
32
+ "id": 253602084,
33
+ "login": "portable-one",
34
+ "type": "User"
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1,20 @@
1
+ ARG NODE_IMAGE=node:24-bookworm-slim
2
+ FROM ${NODE_IMAGE}
3
+ ARG BUN_VERSION=1.3.11
4
+ ARG STANDARD_VERSION=1.0.37
5
+ ARG CORE_VERSION=3.0.45
6
+ ARG RUNTIME_VERSION=3.0.45
7
+ ARG PLAYWRIGHT_VERSION=1.63.0
8
+ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
9
+ RUN apt-get update && apt-get install -y --no-install-recommends git openssl ca-certificates libfaketime && rm -rf /var/lib/apt/lists/*
10
+ WORKDIR /opt/catalog
11
+ RUN npm install --ignore-scripts --no-audit --no-fund --save-exact \
12
+ @oven/bun-linux-x64@${BUN_VERSION} @volter/twin-standard@${STANDARD_VERSION} \
13
+ @volter/world-core@${CORE_VERSION} @volter/world-runtime@${RUNTIME_VERSION} tar@7.5.7 semver@7.8.5 sigstore@4.1.1 playwright@${PLAYWRIGHT_VERSION}
14
+ ENV PATH="/opt/catalog/node_modules/@oven/bun-linux-x64/bin:/opt/catalog/node_modules/.bin:${PATH}"
15
+ RUN ./node_modules/.bin/playwright install --with-deps chromium
16
+ COPY lib /opt/catalog/lib
17
+ COPY runner /opt/catalog/runner
18
+ USER node
19
+ WORKDIR /work
20
+ ENTRYPOINT ["node"]
@@ -0,0 +1,17 @@
1
+ // Mechanism regression, not a pack or an admission verdict. Invoked only by the offline Actions container.
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { createPack, verifyBrowserTransport } from '@volter/twin-standard';
6
+ import { write } from '../lib/model.mjs';
7
+ assert.ok(process.env.VOLTER_WORLD, 'browser fixture must run through a World');
8
+ const authored = '/work/packs/authoring-fixture';
9
+ await createPack([authored, '--init', 'authoring-fixture', '--package', '@independent/payments']);
10
+ mkdirSync(join(authored, 'src/generated'), { recursive: true });
11
+ write(join(authored, 'src/generated/surface.gen.json'), { version: '1', operations: [], resources: [] });
12
+ write(join(authored, 'journeys/decisions.json'), { decisions: [] });
13
+ await createPack([authored, '--index']);
14
+ assert.equal(JSON.parse(readFileSync(join(authored, 'package.json'), 'utf8')).name, '@independent/payments');
15
+ assert.ok(readFileSync(join(authored, 'src/index.ts'), 'utf8').includes('createAuthoringFixtureTwinServer'));
16
+ const report = await verifyBrowserTransport('/work/packs/browser-fixture');
17
+ write('/work/browser-report.json', { ...report, authoringWithoutPlatformCheckout: true, independentPackageIdentity: '@independent/payments' });
@@ -0,0 +1,12 @@
1
+ // A cancellation may lose the controller; remove only resources explicitly owned by this workflow run.
2
+ import { spawnSync } from 'node:child_process';
3
+ const run = (...args) => spawnSync('docker', args, { encoding: 'utf8' });
4
+ if (!/^[0-9]+-[0-9]+$/.test(process.env.CATALOG_RUN ?? '')) throw new Error('workflow run identity required');
5
+ const containers = run('ps', '-aq', '--filter', `label=twin-catalog.run=${process.env.CATALOG_RUN}`);
6
+ if (containers.status !== 0) throw new Error('cannot inspect owned containers');
7
+ const ids = containers.stdout.trim().split(/\s+/).filter(Boolean);
8
+ if (ids.length && run('rm', '-f', ...ids).status !== 0) throw new Error('owned container teardown failed');
9
+ // Volume ownership is also tagged by the host controller, and no unlabelled resource is eligible.
10
+ const volumes = run('volume', 'ls', '-q', '--filter', `label=twin-catalog.run=${process.env.CATALOG_RUN}`);
11
+ if (volumes.status !== 0) throw new Error('cannot inspect owned volumes');
12
+ for (const id of volumes.stdout.trim().split(/\s+/).filter(Boolean)) if (run('volume', 'rm', id).status !== 0) throw new Error(`owned volume teardown failed: ${id}`);
@@ -0,0 +1,15 @@
1
+ // Runs only inside the offline container and a task-owned World. The released standard owns all pack checks.
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { read, requireThat, write, compareReports } from '../lib/model.mjs';
6
+ requireThat(Boolean(process.env.VOLTER_WORLD), 'evaluation must run inside a World');
7
+ const { submission: s } = read('/input/input.json');
8
+ const requireFromPack = createRequire('/work/package.json');
9
+ const { assessPack } = await import(requireFromPack.resolve('@volter/twin-standard'));
10
+ const assessment = await assessPack(join('/work/packs', s.vendor), { browser: true });
11
+ requireThat(assessment.package === s.package && assessment.version === s.version && assessment.vendor === s.vendor, 'assessment returned a different artifact identity');
12
+ const report = { ...assessment, integrity: s.integrity, prepared: read('/work/prepared.json') };
13
+ report.comparison = compareReports(report, existsSync('/input/baseline.json') ? read('/input/baseline.json') : null);
14
+ write('/work/report.json', report);
15
+ process.exit(report.ready ? 0 : 1);
@@ -0,0 +1,81 @@
1
+ // No credentials enter this container. This phase downloads data; scripts are disabled.
2
+ import { readFileSync, writeFileSync, mkdirSync, cpSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { createRequire } from 'node:module';
6
+ import { x } from 'tar';
7
+ import semver from 'semver';
8
+ import { verify as verifyBundle } from 'sigstore';
9
+ import { read, write, requireThat, digest } from '../lib/model.mjs';
10
+ import { metadata, installationMetadata, registryURL, verifyArtifact, verifiedProvenance } from '../lib/registry.mjs';
11
+
12
+ const input = read('/input/input.json');
13
+ const { submission: s, source, policy } = input;
14
+ const doc = await metadata(s.package, s.version, policy.registry, fetch, {});
15
+ requireThat(doc.dist?.attestations?.url, 'npm provenance required; this version has no attestation');
16
+ const tarResponse = await fetch(registryURL(doc.dist.tarball, policy.registry), { redirect: 'error' });
17
+ requireThat(tarResponse.ok, `tarball: HTTP ${tarResponse.status}`);
18
+ const bytes = Buffer.from(await tarResponse.arrayBuffer());
19
+ verifyArtifact(bytes, s, doc);
20
+ writeFileSync('/work/artifact.tgz', bytes);
21
+ mkdirSync('/work/unpacked', { recursive: true });
22
+ await x({ file: '/work/artifact.tgz', cwd: '/work/unpacked', strict: true, filter: (path, entry) => {
23
+ requireThat(path.startsWith('package/') && !path.split('/').includes('..') && !path.includes('\\') && ['File', 'Directory'].includes(entry.type), `unsafe archive entry: ${path}`);
24
+ requireThat(!path.split('/').includes('node_modules') && !path.endsWith('/.npmrc'), 'artifact carries installation configuration or dependencies');
25
+ return true;
26
+ } });
27
+ const pack = read('/work/unpacked/package/package.json');
28
+ requireThat(pack.name === s.package && pack.version === s.version, 'artifact manifest identity mismatch');
29
+ requireThat(typeof pack.license === 'string' && pack.license.trim() && pack.license !== 'UNLICENSED', 'artifact needs a redistributable license declaration for moderator review');
30
+ const repository = typeof pack.repository === 'string' ? pack.repository : pack.repository?.url;
31
+ requireThat(repository?.replace(/^git\+/, '').replace(/\.git$/, '') === `https://github.com/${source.repository}`, 'artifact repository differs from registered source');
32
+ const facts = read('/work/unpacked/package/generated/pack-facts.json');
33
+ requireThat(Object.keys(facts.packs ?? {}).length === 1 && facts.packs[s.vendor]?.protocol?.declared === '3', 'artifact facts must identify the submitted vendor and Protocol 3');
34
+ for (const group of ['dependencies', 'optionalDependencies', 'peerDependencies', 'devDependencies']) {
35
+ for (const [name, range] of Object.entries(pack[group] ?? {})) requireThat(typeof range === 'string' && semver.validRange(range), `${name}: registry semver dependencies only; git, file, URL and alias dependencies are not admitted`);
36
+ }
37
+ // npm verifies the registry artifact itself, not a local-directory replacement.
38
+ // The standard's client cases and the pack's declared SDK examples need their pinned development clients too.
39
+ // This installs data only; candidate scripts never execute in the networked preparation phase.
40
+ const standardManifest = read('/opt/catalog/node_modules/@volter/twin-standard/package.json');
41
+ const dependencies = { ...standardManifest.devDependencies, ...pack.devDependencies, ...policy.tools, [s.package]: s.version };
42
+ requireThat(!Object.hasOwn(policy.tools, s.package), 'submission collides with evaluator tooling');
43
+ write('/work/package.json', { name: 'catalog-assessment', private: true, dependencies });
44
+ await installationMetadata(s, policy.registry);
45
+ const npm = (args) => {
46
+ const r = spawnSync('npm', [...args, '--registry', policy.registry], { cwd: '/work', encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
47
+ requireThat(r.status === 0, `npm ${args[0]} failed: ${r.stderr || r.stdout}`);
48
+ return r.stdout;
49
+ };
50
+ npm(['install', '--ignore-scripts', '--no-audit', '--no-fund']);
51
+ const lock = read('/work/package-lock.json');
52
+ for (const [path, entry] of Object.entries(lock.packages ?? {})) {
53
+ if (!path) continue;
54
+ requireThat(!entry.link && entry.resolved && entry.integrity, `${path}: dependency is not a locked registry artifact`);
55
+ registryURL(entry.resolved, policy.registry);
56
+ }
57
+ for (const [name, version] of Object.entries(policy.tools)) requireThat(lock.packages?.[`node_modules/${name}`]?.version === version, `${name}: evaluator version differs from policy`);
58
+ requireThat(lock.packages?.[`node_modules/${s.package}`]?.integrity === s.integrity, 'installed artifact differs from submission');
59
+ const candidateRequire = createRequire(join('/work/node_modules', s.package, 'package.json'));
60
+ const evaluatorRequire = createRequire('/work/package.json');
61
+ requireThat(candidateRequire.resolve('@volter/world-core/runtime') === evaluatorRequire.resolve('@volter/world-core/runtime'), 'candidate resolves a different kernel from the policy-pinned assessment; adjust its SDK range');
62
+ const clientSdkLocks = [];
63
+ for (const directory of standardManifest.assessmentClientSdks ?? []) {
64
+ requireThat(typeof directory === 'string' && /^[a-z][a-z0-9-]*$/.test(directory), 'standard client SDK directory must be a package-local name');
65
+ const sdkRoot = join('/work/node_modules/@volter/twin-standard', directory);
66
+ const sdkLock = readFileSync(join(sdkRoot, 'bun.lock'), 'utf8');
67
+ const installed = spawnSync('bun', ['install', '--frozen-lockfile', '--ignore-scripts', '--registry', policy.registry], { cwd: sdkRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
68
+ requireThat(installed.status === 0, `standard client SDK preparation failed: ${installed.stderr}`);
69
+ requireThat(readFileSync(join(sdkRoot, 'bun.lock'), 'utf8') === sdkLock, 'standard client SDK lock changed during frozen installation');
70
+ clientSdkLocks.push({ directory, manifest: read(join(sdkRoot, 'package.json')), lock: sdkLock, sha256: digest(sdkLock) });
71
+ }
72
+ const audit = npm(['audit', 'signatures', '--json']);
73
+ writeFileSync('/work/signatures.json', audit);
74
+ const response = await fetch(registryURL(doc.dist.attestations.url, policy.registry), { redirect: 'error' });
75
+ requireThat(response.ok, `attestation: HTTP ${response.status}`);
76
+ const provenance = await verifiedProvenance(await response.json(), source, bytes, verifyBundle);
77
+ // Keep a vendor-named package directory for the standard's public directory API; preserve the installed artifact.
78
+ mkdirSync('/work/packs', { recursive: true });
79
+ cpSync(join('/work/node_modules', s.package), join('/work/packs', s.vendor), { recursive: true });
80
+ write('/work/prepared.json', { schemaVersion: 1, submission: s, integrity: s.integrity, provenance, dependencyLockSha256: digest(readFileSync('/work/package-lock.json', 'utf8')), clientSdkLocks, tools: policy.tools });
81
+ write('/work/world.json', { id: 'catalog-assessment', network: { egress: [] }, services: [] });