bootproof 0.3.0 → 0.4.0

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 (70) hide show
  1. package/README.md +840 -152
  2. package/dist/agent-plan.d.ts +44 -0
  3. package/dist/agent-plan.js +826 -0
  4. package/dist/agent-run.d.ts +117 -0
  5. package/dist/agent-run.js +459 -0
  6. package/dist/ai-repair.d.ts +58 -0
  7. package/dist/ai-repair.js +380 -0
  8. package/dist/cli.js +730 -46
  9. package/dist/diagnosis.js +101 -16
  10. package/dist/diff.d.ts +29 -0
  11. package/dist/diff.js +569 -0
  12. package/dist/exec.d.ts +30 -2
  13. package/dist/exec.js +329 -51
  14. package/dist/external-health.d.ts +16 -0
  15. package/dist/external-health.js +214 -0
  16. package/dist/infer.js +238 -39
  17. package/dist/plan.js +2 -0
  18. package/dist/proof.d.ts +78 -2
  19. package/dist/proof.js +265 -12
  20. package/dist/receipt.d.ts +52 -0
  21. package/dist/receipt.js +356 -0
  22. package/dist/redact.d.ts +4 -0
  23. package/dist/redact.js +86 -2
  24. package/dist/registry.d.ts +82 -30
  25. package/dist/registry.js +355 -53
  26. package/dist/remote.js +3 -3
  27. package/dist/repair-playbooks.d.ts +24 -0
  28. package/dist/repair-playbooks.js +593 -0
  29. package/dist/repair-safety.d.ts +130 -0
  30. package/dist/repair-safety.js +766 -0
  31. package/dist/repair.d.ts +43 -11
  32. package/dist/repair.js +716 -7
  33. package/dist/run.d.ts +3 -0
  34. package/dist/run.js +218 -41
  35. package/dist/sbom.d.ts +22 -0
  36. package/dist/sbom.js +99 -0
  37. package/dist/taxonomy.d.ts +8 -3
  38. package/dist/taxonomy.js +404 -8
  39. package/dist/types.d.ts +40 -1
  40. package/docs/AGENT_IN_THE_LOOP.md +171 -0
  41. package/docs/AGENT_RUN_RECEIPTS.md +38 -0
  42. package/docs/CI_ACTION.md +67 -2
  43. package/docs/DETERMINISTIC_REPAIR_SAFETY_MODEL.md +705 -0
  44. package/docs/FAILURE_TAXONOMY.md +28 -1
  45. package/docs/HONESTY_CONTRACT.md +34 -12
  46. package/docs/LAUNCH_PLAYBOOK.md +232 -0
  47. package/docs/REAL_WORLD_FIXTURES.md +105 -0
  48. package/docs/REGISTRY.md +48 -28
  49. package/docs/REPAIR_RECEIPT.md +54 -8
  50. package/docs/agent-loop-gap-analysis.md +188 -0
  51. package/docs/examples/registry-seeds/advertised-port-mismatch.json +28 -0
  52. package/docs/examples/registry-seeds/airbyte-abctl-external-orchestrator.json +36 -0
  53. package/docs/examples/registry-seeds/go-ollama-service.json +36 -0
  54. package/docs/examples/registry-seeds/laravel-vite-sqlite.json +36 -0
  55. package/docs/examples/registry-seeds/monorepo-ambiguous-health.json +29 -0
  56. package/docs/examples/registry-seeds/php-composer.json +33 -0
  57. package/docs/examples/registry-seeds/rails-bundler.json +32 -0
  58. package/docs/examples/registry-seeds/sentry-devenv-direnv.json +41 -0
  59. package/docs/schemas/action-verdict-v1.schema.json +64 -0
  60. package/docs/schemas/agent-plan-v1.schema.json +148 -0
  61. package/docs/schemas/agent-run-receipts-v1.schema.json +192 -0
  62. package/docs/schemas/ai-repair-suggestion-v1.schema.json +70 -0
  63. package/docs/schemas/ci-context-v1.schema.json +63 -0
  64. package/docs/schemas/diff-result-v1.schema.json +66 -0
  65. package/docs/schemas/federated-receipt-v1.schema.json +51 -0
  66. package/docs/schemas/registry-entry-v1.schema.json +95 -0
  67. package/docs/schemas/registry-seed-example-v1.schema.json +102 -0
  68. package/docs/schemas/repair-action-v1.schema.json +136 -0
  69. package/docs/schemas/repair-receipt-v1.schema.json +221 -0
  70. package/package.json +10 -6
@@ -1,38 +1,90 @@
1
- import type { Attestation } from "./types.js";
1
+ import type { Inference, Attestation, FailureClass } from "./types.js";
2
+ import type { RepairReceipt, RepairKind } from "./repair.js";
3
+ import { type SignatureTrustResult } from "./proof.js";
4
+ export type RegistryMode = "local_export" | "federated_public_candidate" | "cloud_upload_candidate";
5
+ export interface RegistrySignature {
6
+ algorithm: "ed25519";
7
+ publicKey: string;
8
+ value: string;
9
+ }
2
10
  export interface RegistryEntry {
3
11
  schema: "bootproof/registry-entry/v1";
4
- tool: string;
5
- repo: {
6
- remote: string | null;
7
- commit: string | null;
8
- dirty: boolean | null;
9
- };
10
- environment: Attestation["environment"];
11
- plan: {
12
- provider: string;
13
- healthUrl: string;
14
- steps: {
15
- kind: string;
16
- command?: string;
17
- }[];
18
- };
19
- result: {
20
- booted: boolean;
21
- healthVerified: boolean;
22
- healthObservation: string | null;
23
- failureClass: string | null;
24
- redactedEvidence: string | null;
12
+ createdAt: string;
13
+ bootproofVersion: string;
14
+ source: "local_cli";
15
+ registryMode: RegistryMode;
16
+ repoFingerprint: string;
17
+ repoHost: string | null;
18
+ repoOwnerHash: string;
19
+ repoNameHash: string;
20
+ publicRepoHint?: string;
21
+ commitHash: string | null;
22
+ branch: string | null;
23
+ os: string;
24
+ arch: string;
25
+ platform: string;
26
+ packageManager: string;
27
+ detectedStack: string[];
28
+ detectedServices: string[];
29
+ selectedCommandHash: string | null;
30
+ selectedCommandRedacted: string | null;
31
+ failureClass: FailureClass | null;
32
+ failureEvidenceFingerprint: string | null;
33
+ evidenceHeadRedacted: string | null;
34
+ evidenceTailRedacted: string | null;
35
+ healthStatus: "healthy" | "unhealthy" | "connection_error" | "not_observed";
36
+ healthUrlPattern: string | null;
37
+ healthRedirectLocationPattern: string | null;
38
+ repairActionType?: RepairKind;
39
+ repairCommandHash?: string;
40
+ repairCommandRedacted?: string;
41
+ beforeFailureClass?: FailureClass;
42
+ afterFailureClass?: FailureClass;
43
+ progressed?: boolean;
44
+ verified: boolean;
45
+ attestationHash: string;
46
+ repairReceiptHash?: string;
47
+ redactionsApplied: string[];
48
+ signature?: RegistrySignature;
49
+ optInRequired: true;
50
+ }
51
+ export interface FederatedReceipt {
52
+ schema: "bootproof/federated-receipt/v1";
53
+ createdAt: string;
54
+ registryEntry: RegistryEntry;
55
+ attestationHash: string;
56
+ repairReceiptHash?: string;
57
+ signature?: RegistrySignature;
58
+ publicRepoDeclaration: true;
59
+ crawlerHint: {
60
+ repoUrl?: string;
61
+ commitHash: string | null;
62
+ branch: string | null;
25
63
  };
26
64
  redactionsApplied: string[];
27
- attestationSha256: string;
28
- attestedAt: string;
29
- signer: {
30
- publicKey: string;
31
- algorithm: "ed25519";
32
- } | null;
33
- signature: string | null;
65
+ noSecretsIncluded: true;
66
+ }
67
+ export interface RegistryBuildOptions {
68
+ registryMode?: RegistryMode;
69
+ inference?: Pick<Inference, "packageManager" | "stack" | "services" | "composeApplicationServices">;
70
+ repairReceipt?: RepairReceipt | null;
71
+ createdAt?: string;
72
+ branch?: string | null;
73
+ sign?: boolean;
74
+ }
75
+ export interface FederatedReceiptBuildOptions {
76
+ createdAt?: string;
77
+ sign?: boolean;
34
78
  }
35
- export declare function buildRegistryEntry(att: Attestation): RegistryEntry;
79
+ export declare function currentGitBranch(repo: string): string | null;
80
+ export declare function buildRegistryEntry(att: Attestation, options?: RegistryBuildOptions): RegistryEntry;
81
+ export declare function buildFederatedReceipt(registryEntry: RegistryEntry, options?: FederatedReceiptBuildOptions): FederatedReceipt;
36
82
  export declare function verifyRegistryEntry(entry: RegistryEntry): boolean;
83
+ export declare function evaluateRegistryEntrySignature(entry: RegistryEntry): SignatureTrustResult;
84
+ export declare function verifyFederatedReceipt(receipt: FederatedReceipt): boolean;
85
+ export declare function validateRegistryEntry(value: unknown): string[];
86
+ export declare function validateFederatedReceipt(value: unknown): string[];
37
87
  export declare function registryEntryPath(repo: string): string;
88
+ export declare function federatedReceiptPath(repo: string, receipt: FederatedReceipt): string;
38
89
  export declare function writeRegistryEntry(repo: string, entry: RegistryEntry): string;
90
+ export declare function writeFederatedReceipt(repo: string, receipt: FederatedReceipt): string;
package/dist/registry.js CHANGED
@@ -1,70 +1,372 @@
1
- // The proposed BootProof registry is federated by design (docs/REGISTRY.md):
2
- // WRITE PATH: developers may commit signed proof to their own repositories.
3
- // FUTURE READ PATH: an index could discover public artifacts and verify every signature.
4
- // No public index is operated today. The CLI never uploads evidence. `attest export` produces a
5
- // redacted, re-signed local entry; sharing it remains a deliberate git/PR action.
1
+ // Registry exports are local artifacts only. They never upload, call a registry service, or
2
+ // claim that a public index exists. See docs/REGISTRY.md.
6
3
  import crypto from "node:crypto";
7
4
  import fs from "node:fs";
8
5
  import path from "node:path";
6
+ import { execFileSync } from "node:child_process";
7
+ import { buildExecutionEnv } from "./exec.js";
9
8
  import { redactText } from "./redact.js";
10
- import { signDetached, verifyDetached } from "./proof.js";
11
- export function buildRegistryEntry(att) {
12
- const redactions = new Set();
13
- let redactedEvidence = null;
14
- if (att.result.failureEvidence) {
15
- const r = redactText(att.result.failureEvidence);
16
- redactedEvidence = r.text;
17
- r.applied.forEach(a => redactions.add(a));
18
- }
19
- const fullHash = crypto.createHash("sha256").update(JSON.stringify(att)).digest("hex");
9
+ import { evaluateDetachedSignature, signDetached, verifyDetached, } from "./proof.js";
10
+ const PUBLIC_REPO_HOSTS = new Set(["github.com", "gitlab.com", "bitbucket.org", "codeberg.org"]);
11
+ const REGISTRY_ENTRY_KEYS = new Set([
12
+ "schema", "createdAt", "bootproofVersion", "source", "registryMode", "repoFingerprint",
13
+ "repoHost", "repoOwnerHash", "repoNameHash", "publicRepoHint", "commitHash", "branch",
14
+ "os", "arch", "platform", "packageManager", "detectedStack", "detectedServices",
15
+ "selectedCommandHash", "selectedCommandRedacted", "failureClass", "failureEvidenceFingerprint",
16
+ "evidenceHeadRedacted", "evidenceTailRedacted", "healthStatus", "healthUrlPattern",
17
+ "healthRedirectLocationPattern", "repairActionType", "repairCommandHash",
18
+ "repairCommandRedacted", "beforeFailureClass", "afterFailureClass", "progressed", "verified",
19
+ "attestationHash", "repairReceiptHash", "redactionsApplied", "signature", "optInRequired",
20
+ ]);
21
+ const FEDERATED_RECEIPT_KEYS = new Set([
22
+ "schema", "createdAt", "registryEntry", "attestationHash", "repairReceiptHash", "signature",
23
+ "publicRepoDeclaration", "crawlerHint", "redactionsApplied", "noSecretsIncluded",
24
+ ]);
25
+ function sha256(value) {
26
+ return crypto.createHash("sha256").update(value).digest("hex");
27
+ }
28
+ function hashObject(value) {
29
+ return sha256(JSON.stringify(value));
30
+ }
31
+ function canonicalWithoutSignature(value) {
32
+ const { signature: _signature, ...body } = value;
33
+ return Buffer.from(JSON.stringify(body));
34
+ }
35
+ function signed(value) {
36
+ const result = signDetached(canonicalWithoutSignature(value));
37
+ return { algorithm: "ed25519", publicKey: result.publicKeyPem, value: result.signature };
38
+ }
39
+ function safeRepoIdentity(att) {
40
+ const fallbackName = path.basename(path.resolve(att.repo.path)) || "repository";
41
+ const fallbackOwner = path.basename(path.dirname(path.resolve(att.repo.path))) || "local";
42
+ const remote = att.repo.remote?.trim() ?? "";
43
+ let candidate = remote;
44
+ const scp = remote.match(/^git@([^:]+):(.+)$/i);
45
+ if (scp)
46
+ candidate = `https://${scp[1]}/${scp[2]}`;
47
+ try {
48
+ const url = new URL(candidate);
49
+ const host = url.hostname.toLowerCase();
50
+ const parts = url.pathname.split("/").filter(Boolean);
51
+ const name = parts.at(-1)?.replace(/\.git$/i, "") ?? "";
52
+ const owner = parts.slice(0, -1).join("/");
53
+ const safeSegments = [...parts.slice(0, -1), name].every(part => /^[A-Za-z0-9_.-]+$/.test(part));
54
+ const safeIdentity = Boolean(host && owner && name && safeSegments) &&
55
+ !url.search &&
56
+ !url.hash;
57
+ const safePublic = url.protocol === "https:" &&
58
+ PUBLIC_REPO_HOSTS.has(host) &&
59
+ !url.username &&
60
+ !url.password &&
61
+ !url.port &&
62
+ safeIdentity;
63
+ if (safeIdentity) {
64
+ return {
65
+ host,
66
+ owner,
67
+ name,
68
+ publicUrl: safePublic ? `https://${host}/${owner}/${name}` : null,
69
+ fingerprintSource: `${host}/${owner}/${name}`,
70
+ };
71
+ }
72
+ }
73
+ catch {
74
+ // Non-public or local remotes are represented only by hashes below.
75
+ }
76
+ return {
77
+ host: null,
78
+ owner: fallbackOwner,
79
+ name: fallbackName,
80
+ publicUrl: null,
81
+ fingerprintSource: remote || path.resolve(att.repo.path),
82
+ };
83
+ }
84
+ function redactRegistryText(input) {
85
+ if (!input)
86
+ return { text: null, applied: [] };
87
+ const base = redactText(input);
88
+ let text = base.text;
89
+ const applied = new Set(base.applied);
90
+ const privateKey = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/g;
91
+ if (privateKey.test(text)) {
92
+ text = text.replace(privateKey, "[redacted-private-key]");
93
+ applied.add("private keys");
94
+ }
95
+ const envAssignment = /(^|[\s;])([A-Za-z_][A-Za-z0-9_]*)=(?:"[^"]*"|'[^']*'|[^\s;]+)/gm;
96
+ if (envAssignment.test(text)) {
97
+ text = text.replace(envAssignment, "$1$2=[redacted]");
98
+ applied.add("environment values");
99
+ }
100
+ const localPath = /\/(?:Users|home)\/[^/\s]+/g;
101
+ if (localPath.test(text) || text.includes("~/")) {
102
+ text = text.replace(localPath, "[home]").replace(/~(?=\/)/g, "[home]");
103
+ applied.add("local user paths");
104
+ }
105
+ return { text, applied: [...applied] };
106
+ }
107
+ function redactUrlPattern(value) {
108
+ if (!value)
109
+ return { text: null, applied: [] };
110
+ try {
111
+ const absolute = new URL(value, "http://bootproof.invalid");
112
+ const relative = !/^[a-z][a-z0-9+.-]*:\/\//i.test(value);
113
+ const dynamicPath = absolute.pathname
114
+ .split("/")
115
+ .map(segment => /^\d+$/.test(segment) || /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(segment) ? ":id" : segment)
116
+ .join("/");
117
+ const port = absolute.port ? ":<port>" : "";
118
+ const text = relative
119
+ ? dynamicPath || "/"
120
+ : `${absolute.protocol}//${absolute.hostname}${port}${dynamicPath || "/"}`;
121
+ const applied = ["URL query and credentials"];
122
+ if (absolute.port)
123
+ applied.push("URL port");
124
+ if (dynamicPath !== absolute.pathname)
125
+ applied.push("dynamic URL path segments");
126
+ return { text, applied };
127
+ }
128
+ catch {
129
+ return redactRegistryText(value);
130
+ }
131
+ }
132
+ function failedProcessEvidence(att) {
133
+ const failed = att.observed.find(step => !step.ok && (step.evidenceHead || step.evidenceTail));
134
+ return {
135
+ head: failed?.evidenceHead ?? null,
136
+ tail: failed?.evidenceTail ?? att.result.failureEvidence,
137
+ };
138
+ }
139
+ function healthStatus(att) {
140
+ const evidence = att.result.healthEvidence;
141
+ if (att.result.healthVerified && evidence?.acceptedAsHealthy)
142
+ return "healthy";
143
+ if (evidence?.connectionError)
144
+ return "connection_error";
145
+ if (evidence || att.result.healthObservation)
146
+ return "unhealthy";
147
+ return "not_observed";
148
+ }
149
+ function repairCommand(receipt) {
150
+ return receipt?.repair?.planDelta
151
+ ?? receipt?.repair?.envDelta
152
+ ?? receipt?.proposedAction.command?.display
153
+ ?? receipt?.proposedAction.instruction
154
+ ?? null;
155
+ }
156
+ function registryRepairKind(receipt) {
157
+ if (receipt.repair)
158
+ return receipt.repair.kind;
159
+ return receipt.actionType === "command" ? "environment" : "plan-step";
160
+ }
161
+ export function currentGitBranch(repo) {
162
+ try {
163
+ const branch = execFileSync("git", ["-C", repo, "symbolic-ref", "--quiet", "--short", "HEAD"], { encoding: "utf8", env: buildExecutionEnv(), stdio: ["ignore", "pipe", "ignore"] }).trim();
164
+ return branch || null;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ export function buildRegistryEntry(att, options = {}) {
171
+ const redactions = new Set(["repository identity hashed"]);
172
+ const identity = safeRepoIdentity(att);
173
+ const selectedCommand = att.plan.steps.find(step => step.kind === "start-app")?.command ?? null;
174
+ const processEvidence = failedProcessEvidence(att);
175
+ const head = redactRegistryText(processEvidence.head);
176
+ const tail = redactRegistryText(processEvidence.tail);
177
+ const command = redactRegistryText(selectedCommand);
178
+ const healthUrl = redactUrlPattern(att.result.healthEvidence?.requestedUrl ?? att.plan.healthUrl);
179
+ const healthRedirect = redactUrlPattern(att.result.healthEvidence?.redirectLocation);
180
+ const receipt = options.repairReceipt ?? null;
181
+ const repair = repairCommand(receipt);
182
+ const repairRedacted = redactRegistryText(repair);
183
+ for (const result of [head, tail, command, healthUrl, healthRedirect, repairRedacted]) {
184
+ result.applied.forEach(item => redactions.add(item));
185
+ }
186
+ const detectedServices = [
187
+ ...(options.inference?.services.map(service => service.kind) ?? []),
188
+ ...(options.inference?.composeApplicationServices.map(service => service.name) ?? []),
189
+ ];
190
+ const platform = att.environment.os.trim().split(/\s+/)[0] || "unknown";
20
191
  const entry = {
21
192
  schema: "bootproof/registry-entry/v1",
22
- tool: att.tool,
23
- repo: { remote: att.repo.remote, commit: att.repo.commit, dirty: att.repo.dirty },
24
- environment: att.environment,
25
- plan: {
26
- provider: att.plan.provider,
27
- healthUrl: att.plan.healthUrl,
28
- steps: att.plan.steps.map(s => {
29
- const r = s.command ? redactText(s.command) : null;
30
- if (r)
31
- r.applied.forEach(a => redactions.add(a));
32
- return { kind: s.kind, command: r?.text };
33
- }),
34
- },
35
- result: {
36
- booted: att.result.booted,
37
- healthVerified: att.result.healthVerified,
38
- healthObservation: att.result.healthObservation,
39
- failureClass: att.result.failureClass,
40
- redactedEvidence,
41
- },
42
- redactionsApplied: [...redactions],
43
- attestationSha256: fullHash,
44
- attestedAt: att.finishedAt,
45
- signer: null,
46
- signature: null,
193
+ createdAt: options.createdAt ?? new Date().toISOString(),
194
+ bootproofVersion: att.tool.replace(/^bootproof@/, ""),
195
+ source: "local_cli",
196
+ registryMode: options.registryMode ?? "local_export",
197
+ repoFingerprint: sha256(`bootproof/repo/v1\0${identity.fingerprintSource}`),
198
+ repoHost: identity.host,
199
+ repoOwnerHash: sha256(`bootproof/repo-owner/v1\0${identity.owner}`),
200
+ repoNameHash: sha256(`bootproof/repo-name/v1\0${identity.name}`),
201
+ ...(identity.publicUrl ? { publicRepoHint: identity.publicUrl } : {}),
202
+ commitHash: att.repo.commit,
203
+ branch: options.branch ?? null,
204
+ os: att.environment.os,
205
+ arch: att.environment.arch,
206
+ platform,
207
+ packageManager: options.inference?.packageManager ?? "unknown",
208
+ detectedStack: [...new Set(options.inference?.stack ?? [])].sort(),
209
+ detectedServices: [...new Set(detectedServices)].sort(),
210
+ selectedCommandHash: selectedCommand ? sha256(selectedCommand) : null,
211
+ selectedCommandRedacted: command.text,
212
+ failureClass: att.result.failureClass,
213
+ failureEvidenceFingerprint: att.result.failureEvidence ? sha256(att.result.failureEvidence) : null,
214
+ evidenceHeadRedacted: head.text,
215
+ evidenceTailRedacted: tail.text,
216
+ healthStatus: healthStatus(att),
217
+ healthUrlPattern: healthUrl.text,
218
+ healthRedirectLocationPattern: healthRedirect.text,
219
+ ...(receipt ? { repairActionType: registryRepairKind(receipt) } : {}),
220
+ ...(repair ? { repairCommandHash: sha256(repair) } : {}),
221
+ ...(repairRedacted.text ? { repairCommandRedacted: repairRedacted.text } : {}),
222
+ ...(receipt ? { beforeFailureClass: receipt.beforeFailureClass } : {}),
223
+ ...(receipt?.afterFailureClass ? { afterFailureClass: receipt.afterFailureClass } : {}),
224
+ ...(receipt ? { progressed: receipt.progressed } : {}),
225
+ verified: att.result.booted === true && att.result.healthVerified === true,
226
+ attestationHash: hashObject(att),
227
+ ...(receipt ? { repairReceiptHash: hashObject(receipt) } : {}),
228
+ redactionsApplied: [...redactions].sort(),
229
+ optInRequired: true,
47
230
  };
48
- const signed = signDetached(canonical(entry));
49
- entry.signature = signed.signature;
50
- entry.signer = { publicKey: signed.publicKeyPem, algorithm: "ed25519" };
231
+ if (options.sign)
232
+ entry.signature = signed(entry);
233
+ assertValidRegistryEntry(entry);
51
234
  return entry;
52
235
  }
53
- function canonical(entry) {
54
- const { signature: _s, signer: _k, ...body } = entry;
55
- return Buffer.from(JSON.stringify(body));
236
+ export function buildFederatedReceipt(registryEntry, options = {}) {
237
+ const receipt = {
238
+ schema: "bootproof/federated-receipt/v1",
239
+ createdAt: options.createdAt ?? registryEntry.createdAt,
240
+ registryEntry,
241
+ attestationHash: registryEntry.attestationHash,
242
+ ...(registryEntry.repairReceiptHash ? { repairReceiptHash: registryEntry.repairReceiptHash } : {}),
243
+ publicRepoDeclaration: true,
244
+ crawlerHint: {
245
+ ...(registryEntry.publicRepoHint ? { repoUrl: registryEntry.publicRepoHint } : {}),
246
+ commitHash: registryEntry.commitHash,
247
+ branch: registryEntry.branch,
248
+ },
249
+ redactionsApplied: [...registryEntry.redactionsApplied],
250
+ noSecretsIncluded: true,
251
+ };
252
+ if (options.sign)
253
+ receipt.signature = signed(receipt);
254
+ assertValidFederatedReceipt(receipt);
255
+ return receipt;
56
256
  }
57
257
  export function verifyRegistryEntry(entry) {
58
- if (!entry.signature || !entry.signer)
59
- return false;
60
- return verifyDetached(canonical(entry), entry.signature, entry.signer.publicKey);
258
+ return Boolean(entry.signature &&
259
+ verifyDetached(canonicalWithoutSignature(entry), entry.signature.value, entry.signature.publicKey));
260
+ }
261
+ export function evaluateRegistryEntrySignature(entry) {
262
+ return evaluateDetachedSignature(canonicalWithoutSignature(entry), entry.signature?.value, entry.signature?.publicKey);
263
+ }
264
+ export function verifyFederatedReceipt(receipt) {
265
+ return Boolean(receipt.signature &&
266
+ verifyDetached(canonicalWithoutSignature(receipt), receipt.signature.value, receipt.signature.publicKey));
267
+ }
268
+ function unknownKeys(value, allowed) {
269
+ return Object.keys(value).filter(key => !allowed.has(key));
270
+ }
271
+ export function validateRegistryEntry(value) {
272
+ if (!value || typeof value !== "object" || Array.isArray(value))
273
+ return ["entry must be an object"];
274
+ const entry = value;
275
+ const errors = unknownKeys(value, REGISTRY_ENTRY_KEYS).map(key => `unsupported field: ${key}`);
276
+ if (entry.schema !== "bootproof/registry-entry/v1")
277
+ errors.push("invalid schema");
278
+ if (entry.source !== "local_cli")
279
+ errors.push("invalid source");
280
+ if (!["local_export", "federated_public_candidate", "cloud_upload_candidate"].includes(String(entry.registryMode))) {
281
+ errors.push("invalid registryMode");
282
+ }
283
+ for (const key of [
284
+ "createdAt", "bootproofVersion", "repoFingerprint", "repoOwnerHash", "repoNameHash",
285
+ "os", "arch", "platform", "packageManager", "attestationHash",
286
+ ]) {
287
+ if (typeof entry[key] !== "string" || !entry[key])
288
+ errors.push(`${key} must be a non-empty string`);
289
+ }
290
+ for (const key of ["detectedStack", "detectedServices", "redactionsApplied"]) {
291
+ if (!Array.isArray(entry[key]) || entry[key]?.some(item => typeof item !== "string")) {
292
+ errors.push(`${key} must be a string array`);
293
+ }
294
+ }
295
+ if (!["healthy", "unhealthy", "connection_error", "not_observed"].includes(String(entry.healthStatus))) {
296
+ errors.push("invalid healthStatus");
297
+ }
298
+ if (typeof entry.verified !== "boolean")
299
+ errors.push("verified must be boolean");
300
+ if (entry.optInRequired !== true)
301
+ errors.push("optInRequired must be true");
302
+ if (entry.signature && (entry.signature.algorithm !== "ed25519" ||
303
+ typeof entry.signature.publicKey !== "string" ||
304
+ typeof entry.signature.value !== "string")) {
305
+ errors.push("invalid signature");
306
+ }
307
+ return errors;
308
+ }
309
+ export function validateFederatedReceipt(value) {
310
+ if (!value || typeof value !== "object" || Array.isArray(value))
311
+ return ["receipt must be an object"];
312
+ const receipt = value;
313
+ const errors = unknownKeys(value, FEDERATED_RECEIPT_KEYS).map(key => `unsupported field: ${key}`);
314
+ if (receipt.schema !== "bootproof/federated-receipt/v1")
315
+ errors.push("invalid schema");
316
+ if (typeof receipt.createdAt !== "string" || !receipt.createdAt)
317
+ errors.push("createdAt must be a non-empty string");
318
+ errors.push(...validateRegistryEntry(receipt.registryEntry).map(error => `registryEntry: ${error}`));
319
+ if (receipt.registryEntry?.registryMode !== "federated_public_candidate") {
320
+ errors.push("registryEntry.registryMode must be federated_public_candidate");
321
+ }
322
+ if (receipt.attestationHash !== receipt.registryEntry?.attestationHash)
323
+ errors.push("attestationHash must match registryEntry");
324
+ if (receipt.repairReceiptHash !== receipt.registryEntry?.repairReceiptHash)
325
+ errors.push("repairReceiptHash must match registryEntry");
326
+ if (receipt.publicRepoDeclaration !== true)
327
+ errors.push("publicRepoDeclaration must be true");
328
+ if (!receipt.crawlerHint || typeof receipt.crawlerHint !== "object")
329
+ errors.push("crawlerHint must be an object");
330
+ if (!Array.isArray(receipt.redactionsApplied) || receipt.redactionsApplied.some(item => typeof item !== "string")) {
331
+ errors.push("redactionsApplied must be a string array");
332
+ }
333
+ if (receipt.noSecretsIncluded !== true)
334
+ errors.push("noSecretsIncluded must be true");
335
+ if (receipt.signature && (receipt.signature.algorithm !== "ed25519" ||
336
+ typeof receipt.signature.publicKey !== "string" ||
337
+ typeof receipt.signature.value !== "string")) {
338
+ errors.push("invalid signature");
339
+ }
340
+ return errors;
341
+ }
342
+ function assertValidRegistryEntry(entry) {
343
+ const errors = validateRegistryEntry(entry);
344
+ if (errors.length)
345
+ throw new Error(`invalid registry entry: ${errors.join("; ")}`);
346
+ }
347
+ function assertValidFederatedReceipt(receipt) {
348
+ const errors = validateFederatedReceipt(receipt);
349
+ if (errors.length)
350
+ throw new Error(`invalid federated receipt: ${errors.join("; ")}`);
61
351
  }
62
352
  export function registryEntryPath(repo) {
63
353
  return path.join(repo, ".bootproof", "registry-entry.json");
64
354
  }
355
+ export function federatedReceiptPath(repo, receipt) {
356
+ const timestamp = receipt.createdAt.replace(/[:.]/g, "-");
357
+ return path.join(repo, ".bootproof", "registry", `${timestamp}-${receipt.attestationHash.slice(0, 12)}.json`);
358
+ }
65
359
  export function writeRegistryEntry(repo, entry) {
66
- const p = registryEntryPath(repo);
67
- fs.mkdirSync(path.dirname(p), { recursive: true });
68
- fs.writeFileSync(p, JSON.stringify(entry, null, 2) + "\n");
69
- return p;
360
+ assertValidRegistryEntry(entry);
361
+ const output = registryEntryPath(repo);
362
+ fs.mkdirSync(path.dirname(output), { recursive: true });
363
+ fs.writeFileSync(output, JSON.stringify(entry, null, 2) + "\n");
364
+ return output;
365
+ }
366
+ export function writeFederatedReceipt(repo, receipt) {
367
+ assertValidFederatedReceipt(receipt);
368
+ const output = federatedReceiptPath(repo, receipt);
369
+ fs.mkdirSync(path.dirname(output), { recursive: true });
370
+ fs.writeFileSync(output, JSON.stringify(receipt, null, 2) + "\n");
371
+ return output;
70
372
  }
package/dist/remote.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
+ import { buildExecutionEnv } from "./exec.js";
4
5
  const REMOTE_PROVIDERS = {
5
6
  "github.com": "github",
6
7
  "gitlab.com": "gitlab",
@@ -73,13 +74,12 @@ export function cloneRemoteTarget(value, cwd) {
73
74
  execFileSync("git", ["-c", "credential.helper=", "clone", "--depth", "1", "--single-branch", "--no-tags", "--", remote.canonicalUrl, repoPath], {
74
75
  encoding: "utf8",
75
76
  stdio: ["ignore", "pipe", "pipe"],
76
- env: {
77
- ...process.env,
77
+ env: buildExecutionEnv({
78
78
  GIT_ASKPASS: "",
79
79
  GIT_CONFIG_GLOBAL: isolatedGitConfig,
80
80
  GIT_CONFIG_NOSYSTEM: "1",
81
81
  GIT_TERMINAL_PROMPT: "0",
82
- },
82
+ }),
83
83
  });
84
84
  fs.rmSync(isolatedGitConfig, { force: true });
85
85
  const marker = {
@@ -0,0 +1,24 @@
1
+ import { type RepairAction } from "./repair-safety.js";
2
+ import type { Attestation, FailureClass } from "./types.js";
3
+ export interface DeterministicRepairFileChange {
4
+ path: string;
5
+ before: string | null;
6
+ after: string;
7
+ }
8
+ export interface DeterministicRepairCandidate {
9
+ id: string;
10
+ failureClass: FailureClass;
11
+ action: RepairAction;
12
+ followUpActions?: RepairAction[];
13
+ fileChanges?: DeterministicRepairFileChange[];
14
+ }
15
+ export interface RepairCandidateOptions {
16
+ repoPath?: string;
17
+ homebrewAvailable?: boolean;
18
+ homebrewPrefix?: string | null;
19
+ homebrewPostgresPackage?: string | null;
20
+ environment?: NodeJS.ProcessEnv;
21
+ }
22
+ export declare function executableAvailableOnPath(executable: string, environment?: NodeJS.ProcessEnv): boolean;
23
+ export declare function deterministicRepairCandidateFor(attestation: Attestation, options?: RepairCandidateOptions): DeterministicRepairCandidate | null;
24
+ export declare function repairProgressed(beforeFailureClass: FailureClass, after: Attestation | null): boolean;