@volter/twin-catalog 0.2.39 → 0.2.41
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.
- package/README.md +4 -0
- package/bin/twin-catalog.mjs +3 -1
- package/catalog.json +94 -3
- package/content/100cf4069352dee18bcb1f8ef9198c637ce1842d42f66477a6a62702c24f9f13.json +27 -0
- package/content/1f197a74b85508b9af3089c0a8807580b479c90f5ad38d1cb0413d7ef4b06861.json +27 -0
- package/content/1fa7500b80e0c420b91e602419e1185bb2b2871c338e74eb7d5d20845e167e3c.json +27 -0
- package/content/298a3364ab17a8b5c8b07b21d7ff83c9402108bccabc6287ced0431890c7eda9.json +27 -0
- package/content/2c123aa480c3d787e21218b005870509fe48379ba534df5a2eed8d2db5956596.json +27 -0
- package/content/2f905a18997aea40b709c4d4ea7adce45d4b080cf2216353d09b0cb6a910c3f7.json +27 -0
- package/content/32255ffe1d7c2e67218d1a05799128096033136f9d4963b0d55fcb8b71c09e89.json +27 -0
- package/content/4edaec8cd2e55c04f66245309742a63c36f102320ef221927ff93d97b3b268b9.json +27 -0
- package/content/5eeaa5e7c2a08c8c0818489c40e7b818c114bd74f5c1b8186e0de69f8792049c.json +27 -0
- package/content/66285aa1b8bccb8fe943294975108f303bc51ee310c33633d7454afa631e913b.json +27 -0
- package/content/6f9f4c4659dc8d86e42363050c0ee444e4f1e05409866bf4ee3b54d12a3c2cc2.json +27 -0
- package/content/871d902e2c0d3e14a5a9c90fca585f47f5e0989442abe099ec185b5541c54e14.json +27 -0
- package/content/8c35ee3b7e5272a335eccc4e2527b8135eebaeee75c1aab1f874f520fe97de71.json +28 -0
- package/content/a37094107a6552d8cb5b49011973a76330b48d392f66a21ec957923c3c0641fd.json +27 -0
- package/content/d66776b9b0d4c8b19b09a012f455f5b6697b39fc2205b2bb64e7a3855e734e84.json +27 -0
- package/content/fa756dfc21662a70679a660dd88fcb2b4f8bad30f408f1134cd87b0668289fc6.json +27 -0
- package/docs/operations.md +11 -0
- package/docs/process.md +10 -0
- package/evidence/1fa7500b80e0c420b91e602419e1185bb2b2871c338e74eb7d5d20845e167e3c.json +6396 -0
- package/lib/browse.d.mts +3 -1
- package/lib/browse.mjs +16 -5
- package/lib/content.mjs +87 -0
- package/lib/publication.mjs +9 -1
- package/package.json +5 -4
- package/runner/prepare.mjs +6 -2
- package/vendors/slack.json +9 -0
package/lib/browse.d.mts
CHANGED
|
@@ -69,7 +69,7 @@ export type Release = {
|
|
|
69
69
|
export type CatalogRelease = Release & {
|
|
70
70
|
packageMetadata: null | {
|
|
71
71
|
description: string | null;
|
|
72
|
-
license: string;
|
|
72
|
+
license: string | null;
|
|
73
73
|
engines: Record<string, string>;
|
|
74
74
|
peerDependencies: Record<string, string>;
|
|
75
75
|
repositoryDirectory: string | null;
|
|
@@ -80,6 +80,8 @@ export type CatalogRelease = Release & {
|
|
|
80
80
|
packageUrl: string;
|
|
81
81
|
issuesUrl: string | null;
|
|
82
82
|
};
|
|
83
|
+
documentation: null | { markdown: string | null; path: string | null; receiptPath: string;
|
|
84
|
+
receiptSha256: string; artifactIntegrity: string };
|
|
83
85
|
revoked: { reason: string } | null;
|
|
84
86
|
selectable: boolean;
|
|
85
87
|
default: boolean;
|
package/lib/browse.mjs
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { digest, loadIndex, read, requireThat, selection, stable, submissionId, validateIndex } from './model.mjs';
|
|
5
|
+
import { contentIdentity, contentReceipt } from './content.mjs';
|
|
5
6
|
|
|
6
7
|
const textOrder = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
7
8
|
const releaseKey = (v) => `${v.package}@${v.version}`;
|
|
8
9
|
|
|
9
|
-
function packageDetails(bound, source, packageName, version) {
|
|
10
|
-
const metadata = bound?.report.prepared?.packageMetadata;
|
|
10
|
+
function packageDetails(bound, source, packageName, version, content) {
|
|
11
|
+
const metadata = content?.packageMetadata ?? bound?.report.prepared?.packageMetadata;
|
|
11
12
|
if (!metadata) return null;
|
|
12
|
-
const commit = bound
|
|
13
|
+
const commit = bound?.report.prepared?.provenance?.sourceCommit;
|
|
13
14
|
const directory = metadata.repositoryDirectory ?? '';
|
|
14
15
|
const parts = typeof directory === 'string' ? directory.split('/').filter(Boolean) : null;
|
|
15
16
|
const validDirectory = parts && parts.every((part) => part !== '.' && part !== '..' && !part.includes('\\'));
|
|
@@ -85,7 +86,8 @@ export function browse(root, { vendor, package: packageName, integrity = null }
|
|
|
85
86
|
requireThat(expected.length === records.size && expected.every((v) => stable(v) === stable(records.get(releaseKey(v)))),
|
|
86
87
|
'catalog records differ from index data');
|
|
87
88
|
const snapshotDigest = (vendors) => digest({ evidence: references, sources: index.sources, vendors,
|
|
88
|
-
recommendations: index.recommendations, revocations: index.revocations
|
|
89
|
+
recommendations: index.recommendations, revocations: index.revocations,
|
|
90
|
+
...(catalog.content ? { content: catalog.content } : {}) });
|
|
89
91
|
// Publication can append newly admitted vendors after the legacy vendors. Directory enumeration is alphabetical;
|
|
90
92
|
// catalog.json retains the publication order. Accept either only when its complete digest matches the snapshot.
|
|
91
93
|
const publicationOrder = new Map([...new Set(catalog.versions.map((v) => v.vendor))].map((name, at) => [name, at]));
|
|
@@ -115,10 +117,19 @@ export function browse(root, { vendor, package: packageName, integrity = null }
|
|
|
115
117
|
versions: p.versions.map((v) => {
|
|
116
118
|
const record = records.get(`${p.name}@${v.version}`);
|
|
117
119
|
const bound = reportFor(root, record);
|
|
120
|
+
const contentReference = catalog.content?.[submissionId(contentIdentity(record))];
|
|
121
|
+
const content = contentReceipt(root, record, contentReference);
|
|
122
|
+
const preparedReadme = bound?.report.prepared?.readme;
|
|
123
|
+
const documentation = content ? { markdown: content.readme?.markdown ?? null,
|
|
124
|
+
path: content.readme?.path ?? null, receiptPath: contentReference.path,
|
|
125
|
+
receiptSha256: contentReference.sha256, artifactIntegrity: v.integrity } :
|
|
126
|
+
preparedReadme ? { ...preparedReadme, receiptPath: record.evidence.reportPath,
|
|
127
|
+
receiptSha256: record.evidence.reportSha256, artifactIntegrity: v.integrity } : null;
|
|
118
128
|
const revoked = index.revocations.find((r) => r.package === p.name && r.version === v.version);
|
|
119
129
|
const selectable = v.status === 'live' && !v.version.includes('-') && !revoked;
|
|
120
130
|
const quick = bound?.report.quick, browser = bound?.report.browser;
|
|
121
|
-
return { ...v, packageMetadata: packageDetails(bound, source, p.name, v.version),
|
|
131
|
+
return { ...v, packageMetadata: packageDetails(bound, source, p.name, v.version, content),
|
|
132
|
+
documentation,
|
|
122
133
|
revoked: revoked ? { reason: revoked.reason } : null, selectable: Boolean(selectable),
|
|
123
134
|
default: choice?.p.name === p.name && choice.v.version === v.version,
|
|
124
135
|
assessment: { recorded: record.assessed, available: Boolean(bound), evidence: record.evidence,
|
package/lib/content.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Artifact documentation is data, independent of assessment and admission evidence.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { digest, read, requireThat, stable, submissionId, validateSubmission, write } from './model.mjs';
|
|
6
|
+
import { artifactBytes, metadata } from './registry.mjs';
|
|
7
|
+
|
|
8
|
+
export const contentIdentity = (v) => ({ schemaVersion: 1, source: v.source, vendor: v.vendor,
|
|
9
|
+
package: v.package, version: v.version, integrity: v.integrity });
|
|
10
|
+
export function contentReceipt(root, v, reference) {
|
|
11
|
+
if (!reference) return null;
|
|
12
|
+
const identity = contentIdentity(v), path = `content/${submissionId(identity)}.json`;
|
|
13
|
+
requireThat(reference.path === path && /^[a-f0-9]{64}$/.test(reference.sha256), 'invalid release content reference');
|
|
14
|
+
const receipt = read(join(root, path));
|
|
15
|
+
requireThat(digest(receipt) === reference.sha256, `${v.package}@${v.version}: content checksum differs`);
|
|
16
|
+
requireThat(receipt.schemaVersion === 1 && stable(receipt.release) === stable(identity), 'release content identity differs');
|
|
17
|
+
requireThat(receipt.packageMetadata && (receipt.readme === null ||
|
|
18
|
+
(typeof receipt.readme?.markdown === 'string' && /^package\/readme(?:\.md|\.markdown|\.txt)?$/i.test(receipt.readme.path))),
|
|
19
|
+
'invalid retained release documentation');
|
|
20
|
+
return receipt;
|
|
21
|
+
}
|
|
22
|
+
export function retainedContent(root, index) {
|
|
23
|
+
const references = {};
|
|
24
|
+
for (const e of index.vendors) for (const p of e.packages) for (const v of p.versions) {
|
|
25
|
+
if (!v.integrity) continue;
|
|
26
|
+
const release = contentIdentity({ vendor: e.vendor, package: p.name, source: p.source, ...v });
|
|
27
|
+
const id = submissionId(release), path = `content/${id}.json`;
|
|
28
|
+
if (!existsSync(join(root, path))) continue;
|
|
29
|
+
const reference = { path, sha256: digest(read(join(root, path))) };
|
|
30
|
+
contentReceipt(root, release, reference);
|
|
31
|
+
references[id] = reference;
|
|
32
|
+
}
|
|
33
|
+
return references;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Read named members to stdout only: no extraction, installation or package execution.
|
|
37
|
+
function archive(bytes, args) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const child = spawn('tar', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
40
|
+
const out = [], errors = [];
|
|
41
|
+
child.stdout.on('data', chunk => out.push(chunk));
|
|
42
|
+
child.stderr.on('data', chunk => errors.push(chunk));
|
|
43
|
+
child.on('error', reject);
|
|
44
|
+
child.stdin.on('error', reject);
|
|
45
|
+
child.on('close', code => code === 0 ? resolve(Buffer.concat(out).toString('utf8')) :
|
|
46
|
+
reject(new Error(`read artifact archive: ${Buffer.concat(errors).toString('utf8')}`)));
|
|
47
|
+
child.stdin.end(bytes);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export async function captureContent(root, index, registry, fetcher = fetch) {
|
|
51
|
+
const releases = [];
|
|
52
|
+
for (const e of index.vendors) for (const p of e.packages) for (const v of p.versions) {
|
|
53
|
+
if (!v.integrity) continue;
|
|
54
|
+
const release = contentIdentity({ vendor: e.vendor, package: p.name, source: p.source, ...v });
|
|
55
|
+
const source = validateSubmission(release, index.sources);
|
|
56
|
+
const id = submissionId(release), path = `content/${id}.json`;
|
|
57
|
+
if (existsSync(join(root, path))) {
|
|
58
|
+
contentReceipt(root, release, { path, sha256: digest(read(join(root, path))) });
|
|
59
|
+
releases.push({ package: p.name, version: v.version, path, existing: true });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const doc = await metadata(p.name, v.version, registry, fetcher);
|
|
63
|
+
const bytes = await artifactBytes(doc, release, registry, fetcher);
|
|
64
|
+
const members = (await archive(bytes, ['-tzf', '-'])).trim().split('\n');
|
|
65
|
+
requireThat(members.filter(name => name === 'package/package.json').length === 1, 'artifact needs one package manifest');
|
|
66
|
+
const pack = JSON.parse(await archive(bytes, ['-xzOf', '-', 'package/package.json']));
|
|
67
|
+
requireThat(pack.name === p.name && pack.version === v.version, 'artifact manifest identity differs');
|
|
68
|
+
const repository = typeof pack.repository === 'string' ? pack.repository : pack.repository?.url;
|
|
69
|
+
requireThat(repository?.replace(/^git\+/, '').replace(/\.git$/, '') === `https://github.com/${source.repository}`,
|
|
70
|
+
'artifact repository differs from registered source');
|
|
71
|
+
const readmes = members.filter(name => /^package\/readme(?:\.md|\.markdown|\.txt)?$/i.test(name));
|
|
72
|
+
requireThat(readmes.length <= 1, 'artifact has ambiguous READMEs');
|
|
73
|
+
const receipt = { schemaVersion: 1, release,
|
|
74
|
+
packageMetadata: {
|
|
75
|
+
description: typeof pack.description === 'string' ? pack.description : null,
|
|
76
|
+
license: typeof pack.license === 'string' ? pack.license : null,
|
|
77
|
+
engines: pack.engines ?? {}, peerDependencies: pack.peerDependencies ?? {},
|
|
78
|
+
repositoryDirectory: typeof pack.repository === 'object' ? pack.repository.directory ?? null : null,
|
|
79
|
+
bugs: typeof pack.bugs === 'string' ? pack.bugs : pack.bugs?.url ?? null,
|
|
80
|
+
},
|
|
81
|
+
readme: readmes.length ? { path: readmes[0], markdown: await archive(bytes, ['-xzOf', '-', readmes[0]]) } : null,
|
|
82
|
+
};
|
|
83
|
+
write(join(root, path), receipt);
|
|
84
|
+
releases.push({ package: p.name, version: v.version, path, existing: false, readme: Boolean(receipt.readme) });
|
|
85
|
+
}
|
|
86
|
+
return { releases, next: 'Commit the captured data with the catalog maintenance change; publication binds it to the snapshot. No assessment was run.' };
|
|
87
|
+
}
|
package/lib/publication.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:f
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { admit, defaults, digest, loadIndex, read, requireThat, stable, submissionId, validateIndex, write } from './model.mjs';
|
|
5
|
+
import { retainedContent } from './content.mjs';
|
|
5
6
|
|
|
6
7
|
export function git(root, args) {
|
|
7
8
|
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
@@ -37,7 +38,9 @@ export function build(root, out, evidence = {}) {
|
|
|
37
38
|
const commit = git(root, ['rev-parse', 'HEAD']);
|
|
38
39
|
const version = `0.2.${git(root, ['rev-list', '--first-parent', '--count', 'HEAD'])}`;
|
|
39
40
|
const references = Object.fromEntries(Object.entries(evidence).map(([id, { envelope, ...receipt }]) => [id, receipt]));
|
|
40
|
-
const
|
|
41
|
+
const content = retainedContent(root, index);
|
|
42
|
+
const contentIdentity = Object.keys(content).length ? { content } : {};
|
|
43
|
+
const identity = digest({ evidence: references, sources: index.sources, vendors: index.vendors, recommendations: index.recommendations, revocations: index.revocations, ...contentIdentity });
|
|
41
44
|
mkdirSync(out, { recursive: true });
|
|
42
45
|
write(join(out, 'sources.json'), index.sources);
|
|
43
46
|
write(join(out, 'recommendations.json'), index.recommendations);
|
|
@@ -45,6 +48,11 @@ export function build(root, out, evidence = {}) {
|
|
|
45
48
|
for (const e of index.vendors) write(join(out, 'vendors', `${e.vendor}.json`), e);
|
|
46
49
|
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
50
|
write(join(out, 'catalog.json'), catalog);
|
|
51
|
+
if (Object.keys(content).length) {
|
|
52
|
+
catalog.content = content;
|
|
53
|
+
write(join(out, 'catalog.json'), catalog);
|
|
54
|
+
for (const { path } of Object.values(content)) write(join(out, path), read(join(root, path)));
|
|
55
|
+
}
|
|
48
56
|
for (const [id, receipt] of Object.entries(evidence)) write(join(out, 'evidence', `${id}.json`), receipt.envelope);
|
|
49
57
|
const pkg = { ...read(join(root, 'package.json')), version, catalog: { sourceCommit: commit, digest: identity } };
|
|
50
58
|
delete pkg.devDependencies;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-catalog",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41",
|
|
4
4
|
"description": "The moderated twin distribution: approved versions and submission tooling for independent publishers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,10 +35,11 @@
|
|
|
35
35
|
"lib",
|
|
36
36
|
"runner",
|
|
37
37
|
"docs",
|
|
38
|
-
"evidence"
|
|
38
|
+
"evidence",
|
|
39
|
+
"content"
|
|
39
40
|
],
|
|
40
41
|
"catalog": {
|
|
41
|
-
"sourceCommit": "
|
|
42
|
-
"digest": "
|
|
42
|
+
"sourceCommit": "a4c5e0664985a7f5a173b58cd307b88a7ddd2795",
|
|
43
|
+
"digest": "5103e721dd9632a05ec4d5348d1b9e668ee092b3edf634c0e4b08f08efb76527"
|
|
43
44
|
}
|
|
44
45
|
}
|
package/runner/prepare.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// No credentials enter this container. This phase downloads data; scripts are disabled.
|
|
2
|
-
import { readFileSync, writeFileSync, mkdirSync, cpSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, cpSync, realpathSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join, relative } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
@@ -103,6 +103,10 @@ for (const [path, entry] of Object.entries(consumerLock.packages ?? {})) {
|
|
|
103
103
|
}
|
|
104
104
|
const consumer = { appDir, cli, artifactIntegrity: s.integrity };
|
|
105
105
|
// Retain listing facts from these exact bytes, never the registry's moving latest metadata.
|
|
106
|
+
const readmeNames = readdirSync('/work/unpacked/package').filter(name => /^readme(?:\.md|\.markdown|\.txt)?$/i.test(name));
|
|
107
|
+
requireThat(readmeNames.length <= 1, 'artifact has ambiguous READMEs');
|
|
108
|
+
const readme = readmeNames.length ? { path: `package/${readmeNames[0]}`,
|
|
109
|
+
markdown: readFileSync(join('/work/unpacked/package', readmeNames[0]), 'utf8') } : null;
|
|
106
110
|
const packageMetadata = {
|
|
107
111
|
description: typeof pack.description === 'string' ? pack.description : null,
|
|
108
112
|
license: pack.license,
|
|
@@ -111,5 +115,5 @@ const packageMetadata = {
|
|
|
111
115
|
repositoryDirectory: typeof pack.repository === 'object' ? pack.repository.directory ?? null : null,
|
|
112
116
|
bugs: typeof pack.bugs === 'string' ? pack.bugs : pack.bugs?.url ?? null,
|
|
113
117
|
};
|
|
114
|
-
write('/work/prepared.json', { schemaVersion: 1, submission: s, integrity: s.integrity, provenance, packageMetadata, dependencyLockSha256: digest(readFileSync('/work/package-lock.json', 'utf8')), clientSdkLocks, clientDependencyChoices, tools: policy.tools, consumer, consumerFixture, consumerDependencyLockSha256: digest(readFileSync(join(appDir, 'package-lock.json'), 'utf8')) });
|
|
118
|
+
write('/work/prepared.json', { schemaVersion: 1, submission: s, integrity: s.integrity, provenance, packageMetadata, readme, dependencyLockSha256: digest(readFileSync('/work/package-lock.json', 'utf8')), clientSdkLocks, clientDependencyChoices, tools: policy.tools, consumer, consumerFixture, consumerDependencyLockSha256: digest(readFileSync(join(appDir, 'package-lock.json'), 'utf8')) });
|
|
115
119
|
write('/work/world.json', { id: 'catalog-assessment', network: { egress: [] }, services: [] });
|
package/vendors/slack.json
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
"name": "@volter/twin-slack",
|
|
6
6
|
"source": "twin-packs-open",
|
|
7
7
|
"versions": [
|
|
8
|
+
{
|
|
9
|
+
"version": "3.0.5",
|
|
10
|
+
"status": "live",
|
|
11
|
+
"integrity": "sha512-P8jzf8Ss3MsXG+bc+uX9QBfYu4R74tmHgeOuSyJ8WjrsfvbJzBGZHQPGV50qUDTHD6vuh17bDqTaTsesK58aZw==",
|
|
12
|
+
"submission": "1fa7500b80e0c420b91e602419e1185bb2b2871c338e74eb7d5d20845e167e3c",
|
|
13
|
+
"commit": "6c08177e4ef0c57c56b5b614f2260ab86d40e065",
|
|
14
|
+
"at": "2026-10-07T17:40:47Z",
|
|
15
|
+
"by": "moderated-merge"
|
|
16
|
+
},
|
|
8
17
|
{
|
|
9
18
|
"version": "3.0.4",
|
|
10
19
|
"status": "live",
|