File size: 3,496 Bytes
518343a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env node
// Builds the in-bundle hash-chained attestation sidecar from the
// per-file MANIFEST.json produced by write-manifest.mjs.
//
// Format (ATTESTATIONS.json):
//   {
//     name, version, gitSha, builtAt, hashAlgorithm: "sha256",
//     manifestSha256,                          // sha256 of MANIFEST.json bytes
//     subjects: ["a11oy-core","a11oy-connection"],
//     chain: [
//       { index, subject, fileCount, totalBytes, subjectSha256,
//         prevHash, entryHash }
//     ],
//     head                                     // entryHash of last entry
//   }
//
// Subject hash:
//   sha256 over the canonical line-stream
//     "<relPath>\t<sha256>\t<size>\n"
//   for every MANIFEST.files entry whose path starts with "<subject>/",
//   sorted by relPath. This is purely a function of MANIFEST.json so the
//   chain is round-trippable.
//
// Entry hash (link in the chain):
//   sha256("<index>\n<subject>\n<subjectSha256>\n<prevHash>\n")
//   prevHash for index=0 is 64 zeros (genesis).
//
// Output is written under <attestDir>/ATTESTATIONS.json so it does NOT
// pollute build/ (verify-manifest.mjs rejects extra files there).

import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";

const buildDir = process.argv[2];
const attestDir = process.argv[3];
if (!buildDir || !attestDir) {
  console.error("usage: write-attestations.mjs <buildDir> <attestDir>");
  process.exit(2);
}

const GENESIS = "0".repeat(64);
const SUBJECTS = [
  "a11oy-core",
  "a11oy-connection",
  "shared/perception-loop",
  "shared/sequence-pipeline",
  "shared/sparse-attention-kit",
];

const manifestPath = join(buildDir, "MANIFEST.json");
const manifestBytes = readFileSync(manifestPath);
const manifest = JSON.parse(manifestBytes.toString("utf8"));

function sha256Hex(input) {
  return createHash("sha256").update(input).digest("hex");
}

function subjectDigest(subject) {
  const prefix = subject + "/";
  const entries = manifest.files
    .filter((f) => f.path.startsWith(prefix))
    .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
  if (entries.length === 0) {
    throw new Error(`no files in MANIFEST.json for subject '${subject}'`);
  }
  const canonical = entries
    .map((f) => `${f.path}\t${f.sha256}\t${f.size}\n`)
    .join("");
  const totalBytes = entries.reduce((n, f) => n + f.size, 0);
  return {
    fileCount: entries.length,
    totalBytes,
    subjectSha256: sha256Hex(canonical),
  };
}

const chain = [];
let prevHash = GENESIS;
for (let i = 0; i < SUBJECTS.length; i++) {
  const subject = SUBJECTS[i];
  const { fileCount, totalBytes, subjectSha256 } = subjectDigest(subject);
  const entryHash = sha256Hex(
    `${i}\n${subject}\n${subjectSha256}\n${prevHash}\n`,
  );
  chain.push({
    index: i,
    subject,
    fileCount,
    totalBytes,
    subjectSha256,
    prevHash,
    entryHash,
  });
  prevHash = entryHash;
}

const attestation = {
  name: "a11oy-attestations",
  version: manifest.version,
  gitSha: manifest.gitSha,
  builtAt: manifest.builtAt,
  hashAlgorithm: "sha256",
  manifestSha256: sha256Hex(manifestBytes),
  subjects: SUBJECTS,
  chain,
  head: prevHash,
};

mkdirSync(attestDir, { recursive: true });
writeFileSync(
  join(attestDir, "ATTESTATIONS.json"),
  JSON.stringify(attestation, null, 2) + "\n",
);

console.log(
  `[a11oy-uds] attestations: ${chain.length} subjects, head=${attestation.head.slice(0, 12)}…`,
);