@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.
Files changed (29) hide show
  1. package/README.md +4 -0
  2. package/bin/twin-catalog.mjs +3 -1
  3. package/catalog.json +94 -3
  4. package/content/100cf4069352dee18bcb1f8ef9198c637ce1842d42f66477a6a62702c24f9f13.json +27 -0
  5. package/content/1f197a74b85508b9af3089c0a8807580b479c90f5ad38d1cb0413d7ef4b06861.json +27 -0
  6. package/content/1fa7500b80e0c420b91e602419e1185bb2b2871c338e74eb7d5d20845e167e3c.json +27 -0
  7. package/content/298a3364ab17a8b5c8b07b21d7ff83c9402108bccabc6287ced0431890c7eda9.json +27 -0
  8. package/content/2c123aa480c3d787e21218b005870509fe48379ba534df5a2eed8d2db5956596.json +27 -0
  9. package/content/2f905a18997aea40b709c4d4ea7adce45d4b080cf2216353d09b0cb6a910c3f7.json +27 -0
  10. package/content/32255ffe1d7c2e67218d1a05799128096033136f9d4963b0d55fcb8b71c09e89.json +27 -0
  11. package/content/4edaec8cd2e55c04f66245309742a63c36f102320ef221927ff93d97b3b268b9.json +27 -0
  12. package/content/5eeaa5e7c2a08c8c0818489c40e7b818c114bd74f5c1b8186e0de69f8792049c.json +27 -0
  13. package/content/66285aa1b8bccb8fe943294975108f303bc51ee310c33633d7454afa631e913b.json +27 -0
  14. package/content/6f9f4c4659dc8d86e42363050c0ee444e4f1e05409866bf4ee3b54d12a3c2cc2.json +27 -0
  15. package/content/871d902e2c0d3e14a5a9c90fca585f47f5e0989442abe099ec185b5541c54e14.json +27 -0
  16. package/content/8c35ee3b7e5272a335eccc4e2527b8135eebaeee75c1aab1f874f520fe97de71.json +28 -0
  17. package/content/a37094107a6552d8cb5b49011973a76330b48d392f66a21ec957923c3c0641fd.json +27 -0
  18. package/content/d66776b9b0d4c8b19b09a012f455f5b6697b39fc2205b2bb64e7a3855e734e84.json +27 -0
  19. package/content/fa756dfc21662a70679a660dd88fcb2b4f8bad30f408f1134cd87b0668289fc6.json +27 -0
  20. package/docs/operations.md +11 -0
  21. package/docs/process.md +10 -0
  22. package/evidence/1fa7500b80e0c420b91e602419e1185bb2b2871c338e74eb7d5d20845e167e3c.json +6396 -0
  23. package/lib/browse.d.mts +3 -1
  24. package/lib/browse.mjs +16 -5
  25. package/lib/content.mjs +87 -0
  26. package/lib/publication.mjs +9 -1
  27. package/package.json +5 -4
  28. package/runner/prepare.mjs +6 -2
  29. 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.report.prepared?.provenance?.sourceCommit;
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,
@@ -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
+ }
@@ -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 identity = digest({ evidence: references, sources: index.sources, vendors: index.vendors, recommendations: index.recommendations, revocations: index.revocations });
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.39",
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": "8cc7194bdc4e9bb5c481d862684ebf5f7e75a7ba",
42
- "digest": "b24bd5b8f97b7054833cb45fd84ed20361a08eff401aaf0c04b3fd89fda3d945"
42
+ "sourceCommit": "a4c5e0664985a7f5a173b58cd307b88a7ddd2795",
43
+ "digest": "5103e721dd9632a05ec4d5348d1b9e668ee092b3edf634c0e4b08f08efb76527"
43
44
  }
44
45
  }
@@ -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: [] });
@@ -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",