code-provenance 0.1.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.
@@ -0,0 +1,192 @@
1
+ import type { ImageRef, ImageResult } from "./types.js";
2
+ import { fetchOciLabels } from "./registry.js";
3
+ import {
4
+ resolveTagToCommit,
5
+ inferRepoFromDockerhub,
6
+ resolveGhcrDigestViaPackages,
7
+ resolveGhcrLatestViaPackages,
8
+ getLatestReleaseCommit,
9
+ getLatestCommit,
10
+ } from "./github.js";
11
+
12
+ const COMMIT_SHA_RE = /^[0-9a-f]{40,}$/;
13
+ const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
14
+
15
+ function isResolvableTag(tag: string): boolean {
16
+ return !!tag && tag !== "latest" && !DIGEST_RE.test(tag);
17
+ }
18
+
19
+ async function inferRepo(
20
+ ref: ImageRef
21
+ ): Promise<[string | null, string | null]> {
22
+ if (ref.registry === "ghcr.io") {
23
+ return [ref.namespace, ref.name];
24
+ }
25
+
26
+ if (ref.registry === "docker.io") {
27
+ const hubResult = await inferRepoFromDockerhub(ref.namespace, ref.name);
28
+ if (hubResult) return hubResult;
29
+ }
30
+
31
+ return [null, null];
32
+ }
33
+
34
+ /**
35
+ * Run the resolution chain for a single image.
36
+ */
37
+ export async function resolveImage(
38
+ service: string,
39
+ ref: ImageRef
40
+ ): Promise<ImageResult> {
41
+ const result: ImageResult = {
42
+ service,
43
+ image: ref.raw,
44
+ registry: ref.registry,
45
+ repo: null,
46
+ tag: ref.tag,
47
+ commit: null,
48
+ commit_url: null,
49
+ status: "repo_not_found",
50
+ resolution_method: null,
51
+ confidence: null,
52
+ steps: [],
53
+ };
54
+
55
+ // Step 1: Check OCI labels
56
+ result.steps.push(`[1/5] Fetching OCI labels from ${ref.registry}/${ref.namespace}/${ref.name}:${ref.tag}`);
57
+ const labels = await fetchOciLabels(ref);
58
+ const source = labels["org.opencontainers.image.source"];
59
+ const revision = labels["org.opencontainers.image.revision"];
60
+ if (source && revision) {
61
+ result.steps.push(`[1/5] Found OCI labels: source=${source}, revision=${revision.slice(0, 12)}`);
62
+ result.repo = source;
63
+ result.commit = revision;
64
+ result.commit_url = `${source}/commit/${revision}`;
65
+ result.status = "resolved";
66
+ result.resolution_method = "oci_labels";
67
+ result.confidence = ref.tag === "latest" ? "approximate" : "exact";
68
+ return result;
69
+ } else {
70
+ result.steps.push("[1/5] No OCI labels found");
71
+ }
72
+
73
+ // Step 2: Infer repo
74
+ result.steps.push(`[2/5] Inferring GitHub repo from ${ref.registry}/${ref.namespace}/${ref.name}`);
75
+ const [owner, repoName] = await inferRepo(ref);
76
+ if (owner && repoName) {
77
+ result.steps.push(`[2/5] Repo inferred: ${owner}/${repoName}`);
78
+ result.repo = `https://github.com/${owner}/${repoName}`;
79
+ } else {
80
+ result.steps.push("[2/5] Could not infer GitHub repo");
81
+ result.status = "repo_not_found";
82
+ return result;
83
+ }
84
+
85
+ // Check if tag is a commit SHA
86
+ if (COMMIT_SHA_RE.test(ref.tag)) {
87
+ result.steps.push("[2/5] Tag is a commit SHA, using directly");
88
+ result.commit = ref.tag;
89
+ result.commit_url = `${result.repo}/commit/${ref.tag}`;
90
+ result.status = "resolved";
91
+ result.resolution_method = "commit_sha_tag";
92
+ result.confidence = "exact";
93
+ return result;
94
+ }
95
+
96
+ // Step 3: Tag-to-commit resolution
97
+ if (isResolvableTag(ref.tag)) {
98
+ result.steps.push(`[3/5] Matching tag "${ref.tag}" against git tags in ${owner}/${repoName}`);
99
+ const tagResult = await resolveTagToCommit(owner, repoName, ref.tag);
100
+ if (tagResult) {
101
+ const [commitSha, isExact] = tagResult;
102
+ if (isExact) {
103
+ result.steps.push(`[3/5] Exact tag match: ${commitSha.slice(0, 12)}`);
104
+ } else {
105
+ result.steps.push(`[3/5] Prefix match (e.g. v2.10 -> v2.10.x): ${commitSha.slice(0, 12)}`);
106
+ }
107
+ result.commit = commitSha;
108
+ result.commit_url = `${result.repo}/commit/${commitSha}`;
109
+ result.status = "resolved";
110
+ result.resolution_method = "tag_match";
111
+ result.confidence = isExact ? "exact" : "approximate";
112
+ return result;
113
+ }
114
+ result.steps.push("[3/5] No matching git tag found");
115
+ result.status = "repo_found_tag_not_matched";
116
+ return result;
117
+ }
118
+
119
+ // Step 4: For GHCR images, try the packages API for digest or :latest
120
+ if (ref.registry === "ghcr.io") {
121
+ let pkgResult: { repo: string; commit: string | null; tags: string[] } | null = null;
122
+ let pkgConfidence: string | null = null;
123
+
124
+ if (DIGEST_RE.test(ref.tag)) {
125
+ result.steps.push(`[4/5] Trying GHCR packages API for digest ${ref.tag.slice(0, 20)}...`);
126
+ pkgResult = await resolveGhcrDigestViaPackages(
127
+ ref.namespace,
128
+ ref.name,
129
+ ref.tag
130
+ );
131
+ pkgConfidence = "exact";
132
+ } else if (ref.tag === "latest" || !ref.tag) {
133
+ result.steps.push("[4/5] Trying GHCR packages API for :latest tag");
134
+ pkgResult = await resolveGhcrLatestViaPackages(ref.namespace, ref.name);
135
+ pkgConfidence = "approximate";
136
+ }
137
+
138
+ if (pkgResult) {
139
+ const repoFull = pkgResult.repo;
140
+ result.repo = `https://github.com/${repoFull}`;
141
+ if (pkgResult.commit) {
142
+ const commit = pkgResult.commit;
143
+ result.steps.push(`[4/5] Packages API: repo=${repoFull}, commit=${commit.slice(0, 12)}`);
144
+ result.commit = commit;
145
+ result.commit_url = `${result.repo}/commit/${result.commit}`;
146
+ result.status = "resolved";
147
+ result.resolution_method = "packages_api";
148
+ result.confidence = pkgConfidence;
149
+ return result;
150
+ }
151
+ result.steps.push("[4/5] Packages API: found package but no resolvable tags");
152
+ const tags = pkgResult.tags ?? [];
153
+ const resolvable = tags.filter((t) => t !== "latest");
154
+ result.status = resolvable.length > 0 ? "repo_found_tag_not_matched" : "no_tag";
155
+ return result;
156
+ }
157
+ }
158
+
159
+ // Step 5: For :latest on any registry, try the latest GitHub release,
160
+ // then fall back to the latest commit on the default branch
161
+ if ((ref.tag === "latest" || !ref.tag) && owner && repoName) {
162
+ result.steps.push(`[5/5] Trying latest GitHub release for ${owner}/${repoName}`);
163
+ const releaseResult = await getLatestReleaseCommit(owner, repoName);
164
+ if (releaseResult) {
165
+ const [commitSha, tagName] = releaseResult;
166
+ result.steps.push(`[5/5] Latest release: tag=${tagName}, commit=${commitSha.slice(0, 12)}`);
167
+ result.commit = commitSha;
168
+ result.commit_url = `${result.repo}/commit/${commitSha}`;
169
+ result.status = "resolved";
170
+ result.resolution_method = "latest_release";
171
+ result.confidence = "approximate";
172
+ return result;
173
+ }
174
+
175
+ // No releases - fall back to latest commit on default branch
176
+ result.steps.push("[5/5] No release found, trying latest commit on default branch");
177
+ const latestSha = await getLatestCommit(owner, repoName);
178
+ if (latestSha) {
179
+ result.steps.push(`[5/5] Latest commit: ${latestSha.slice(0, 12)}`);
180
+ result.commit = latestSha;
181
+ result.commit_url = `${result.repo}/commit/${latestSha}`;
182
+ result.status = "resolved";
183
+ result.resolution_method = "latest_commit";
184
+ result.confidence = "approximate";
185
+ return result;
186
+ }
187
+ }
188
+
189
+ result.steps.push("[5/5] Could not resolve to a commit");
190
+ result.status = "no_tag";
191
+ return result;
192
+ }
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ export interface ImageRef {
2
+ registry: string;
3
+ namespace: string;
4
+ name: string;
5
+ tag: string;
6
+ raw: string;
7
+ }
8
+
9
+ export interface ImageResult {
10
+ service: string;
11
+ image: string;
12
+ registry: string;
13
+ repo: string | null;
14
+ tag: string;
15
+ commit: string | null;
16
+ commit_url: string | null;
17
+ status: string;
18
+ resolution_method: string | null;
19
+ confidence: string | null;
20
+ steps: string[];
21
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "sourceMap": true
13
+ },
14
+ "include": ["src/**/*.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }