ghcr-manager-visualizer 0.0.1-dev.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,399 @@
1
+ import { placeholders } from "./_sql-placeholders.js";
2
+ export class GraphRepository {
3
+ #database;
4
+ constructor(database) {
5
+ this.#database = database;
6
+ }
7
+ listOwners() {
8
+ return this.#database
9
+ .prepare(`
10
+ SELECT DISTINCT owner
11
+ FROM package_scans
12
+ WHERE status = 'completed'
13
+ ORDER BY owner
14
+ `)
15
+ .all();
16
+ }
17
+ listPackages(owner) {
18
+ return this.#database
19
+ .prepare(`
20
+ SELECT DISTINCT package_name AS packageName
21
+ FROM package_scans
22
+ WHERE status = 'completed'
23
+ AND owner = ?
24
+ ORDER BY package_name
25
+ `)
26
+ .all(owner);
27
+ }
28
+ listScans(owner, packageName) {
29
+ return this.#database
30
+ .prepare(`
31
+ SELECT scan_id AS scanId, scan_completed_at AS scanCompletedAt
32
+ FROM package_scans
33
+ WHERE status = 'completed'
34
+ AND owner = ?
35
+ AND package_name = ?
36
+ ORDER BY scan_completed_at DESC, scan_id DESC
37
+ `)
38
+ .all(owner, packageName);
39
+ }
40
+ listTags(owner, packageName, scanId, compareScanId, query, limit) {
41
+ const resolvedScans = this.#resolveScans(owner, packageName, scanId, compareScanId);
42
+ const normalizedLimit = Math.max(1, Math.min(limit, 50));
43
+ const normalizedQuery = query.trim();
44
+ if (normalizedQuery === "") {
45
+ return [];
46
+ }
47
+ const scanInClause = placeholders(resolvedScans.scanIds.length);
48
+ return this.#database
49
+ .prepare(`
50
+ SELECT DISTINCT tag AS tagName
51
+ FROM tags
52
+ WHERE scan_id IN (${scanInClause})
53
+ AND is_digest_tag = 0
54
+ AND tag LIKE ? ESCAPE '\\'
55
+ ORDER BY tag
56
+ LIMIT ?
57
+ `)
58
+ .all(...resolvedScans.scanIds, `%${_escapeLikeValue(normalizedQuery)}%`, normalizedLimit);
59
+ }
60
+ resolveLatestScanId(owner, packageName) {
61
+ const row = this.#database
62
+ .prepare(`
63
+ SELECT scan_id
64
+ FROM v_latest_scan_per_package
65
+ WHERE owner = ?
66
+ AND package_name = ?
67
+ LIMIT 1
68
+ `)
69
+ .get(owner, packageName);
70
+ if (!row) {
71
+ throw new Error(`database does not contain completed package scan for ${owner}/${packageName}`);
72
+ }
73
+ return row.scan_id;
74
+ }
75
+ resolveScanId(owner, packageName, scanId) {
76
+ if (scanId === undefined) {
77
+ return this.resolveLatestScanId(owner, packageName);
78
+ }
79
+ const row = this.#database
80
+ .prepare(`
81
+ SELECT scan_id
82
+ FROM package_scans
83
+ WHERE scan_id = ?
84
+ AND owner = ?
85
+ AND package_name = ?
86
+ AND status = 'completed'
87
+ LIMIT 1
88
+ `)
89
+ .get(scanId, owner, packageName);
90
+ if (!row) {
91
+ throw new Error(`scan ${scanId} is not a completed scan for ${owner}/${packageName}`);
92
+ }
93
+ return row.scan_id;
94
+ }
95
+ resolveManifest(owner, packageName, scanId, compareScanId, args) {
96
+ const resolvedScans = this.#resolveScans(owner, packageName, scanId, compareScanId);
97
+ const digest = args.digest ??
98
+ this.#resolveDigestByTag(resolvedScans.scanIds, args.tag, resolvedScans.scanId, resolvedScans.compareScanId);
99
+ const node = this.#readManifestMap(resolvedScans.scanIds, [digest], true, resolvedScans.newerScanId, resolvedScans.olderScanId).get(digest);
100
+ if (!node) {
101
+ throw new Error(`manifest ${digest} was not found in ${owner}/${packageName}`);
102
+ }
103
+ return {
104
+ owner,
105
+ packageName,
106
+ scanId: resolvedScans.scanId,
107
+ compareScanId: resolvedScans.compareScanId,
108
+ digest: node.digest,
109
+ versionId: node.versionId,
110
+ manifestKind: node.manifestKind,
111
+ tags: node.tags.map((tag) => tag.name)
112
+ };
113
+ }
114
+ getManifest(owner, packageName, scanId, compareScanId, digest) {
115
+ const resolvedScans = this.#resolveScans(owner, packageName, scanId, compareScanId);
116
+ const node = this.#readManifestMap(resolvedScans.scanIds, [digest], true, resolvedScans.newerScanId, resolvedScans.olderScanId).get(digest);
117
+ if (!node) {
118
+ throw new Error(`manifest ${digest} was not found in ${owner}/${packageName}`);
119
+ }
120
+ return node;
121
+ }
122
+ getGraph(owner, packageName, scanId, compareScanId, centerDigest, depth) {
123
+ const resolvedScans = this.#resolveScans(owner, packageName, scanId, compareScanId);
124
+ const normalizedDepth = Math.max(0, depth);
125
+ const visited = new Set([centerDigest]);
126
+ let frontier = new Set([centerDigest]);
127
+ for (let currentDepth = 0; currentDepth < normalizedDepth && frontier.size > 0; currentDepth += 1) {
128
+ const edgeRows = this.#readAdjacentEdges(resolvedScans.scanIds, [...frontier]);
129
+ const nextFrontier = new Set();
130
+ for (const row of edgeRows) {
131
+ if (!visited.has(row.parent_digest)) {
132
+ visited.add(row.parent_digest);
133
+ nextFrontier.add(row.parent_digest);
134
+ }
135
+ if (!visited.has(row.child_digest)) {
136
+ visited.add(row.child_digest);
137
+ nextFrontier.add(row.child_digest);
138
+ }
139
+ }
140
+ frontier = nextFrontier;
141
+ }
142
+ const nodes = [
143
+ ...this.#readManifestMap(resolvedScans.scanIds, [...visited], false, resolvedScans.newerScanId, resolvedScans.olderScanId).values()
144
+ ];
145
+ const edges = this.#readVisibleEdges(resolvedScans.scanIds, [...visited]).map((row) => ({
146
+ id: `${row.parent_digest}|${row.child_digest}|${row.edge_kind}`,
147
+ from: row.parent_digest,
148
+ to: row.child_digest,
149
+ kind: row.edge_kind
150
+ }));
151
+ if (!nodes.some((node) => node.digest === centerDigest)) {
152
+ throw new Error(`manifest ${centerDigest} was not found in ${owner}/${packageName}`);
153
+ }
154
+ return {
155
+ owner,
156
+ packageName,
157
+ scanId: resolvedScans.scanId,
158
+ compareScanId: resolvedScans.compareScanId,
159
+ centerDigest,
160
+ depth: normalizedDepth,
161
+ nodes,
162
+ edges: edges.sort((left, right) => left.id.localeCompare(right.id))
163
+ };
164
+ }
165
+ #resolveScans(owner, packageName, scanId, compareScanId) {
166
+ const resolvedScanId = this.resolveScanId(owner, packageName, scanId);
167
+ if (compareScanId === undefined || compareScanId === resolvedScanId) {
168
+ return {
169
+ scanId: resolvedScanId,
170
+ scanIds: [resolvedScanId],
171
+ newerScanId: resolvedScanId
172
+ };
173
+ }
174
+ const resolvedCompareScanId = this.resolveScanId(owner, packageName, compareScanId);
175
+ const rows = this.#database
176
+ .prepare(`
177
+ SELECT scan_id, scan_completed_at
178
+ FROM package_scans
179
+ WHERE owner = ?
180
+ AND package_name = ?
181
+ AND scan_id IN (?, ?)
182
+ ORDER BY scan_completed_at, scan_id
183
+ `)
184
+ .all(owner, packageName, resolvedScanId, resolvedCompareScanId);
185
+ if (rows.length !== 2) {
186
+ throw new Error(`failed to resolve compare scans for ${owner}/${packageName}`);
187
+ }
188
+ return {
189
+ scanId: resolvedScanId,
190
+ compareScanId: resolvedCompareScanId,
191
+ scanIds: [resolvedScanId, resolvedCompareScanId],
192
+ newerScanId: rows[1].scan_id,
193
+ olderScanId: rows[0].scan_id
194
+ };
195
+ }
196
+ #resolveDigestByTag(scanIds, tag, preferredScanId, fallbackScanId) {
197
+ if (!tag) {
198
+ throw new Error("either digest or tag is required");
199
+ }
200
+ const scanInClause = placeholders(scanIds.length);
201
+ const row = this.#database
202
+ .prepare(`
203
+ SELECT manifest.digest
204
+ FROM tags
205
+ JOIN manifests manifest
206
+ ON manifest.scan_id = tags.scan_id
207
+ AND manifest.version_id = tags.version_id
208
+ WHERE tags.scan_id IN (${scanInClause})
209
+ AND tags.tag = ?
210
+ ORDER BY
211
+ CASE
212
+ WHEN tags.scan_id = ? THEN 0
213
+ WHEN ? IS NOT NULL AND tags.scan_id = ? THEN 1
214
+ ELSE 2
215
+ END
216
+ LIMIT 1
217
+ `)
218
+ .get(...scanIds, tag, preferredScanId, fallbackScanId ?? null, fallbackScanId ?? -1);
219
+ if (!row) {
220
+ throw new Error(`tag ${tag} was not found in selected scan context`);
221
+ }
222
+ return row.digest;
223
+ }
224
+ #readAdjacentEdges(scanIds, digests) {
225
+ const scanInClause = placeholders(scanIds.length);
226
+ const inClause = placeholders(digests.length);
227
+ const sql = `
228
+ SELECT DISTINCT parent_digest, child_digest, edge_kind
229
+ FROM manifest_edges
230
+ WHERE scan_id IN (${scanInClause})
231
+ AND (parent_digest IN (${inClause}) OR child_digest IN (${inClause}))
232
+ ORDER BY parent_digest, child_digest, edge_kind
233
+ `;
234
+ return this.#database.prepare(sql).all(...scanIds, ...digests, ...digests);
235
+ }
236
+ #readVisibleEdges(scanIds, digests) {
237
+ const scanInClause = placeholders(scanIds.length);
238
+ const inClause = placeholders(digests.length);
239
+ const sql = `
240
+ SELECT DISTINCT parent_digest, child_digest, edge_kind
241
+ FROM manifest_edges
242
+ WHERE scan_id IN (${scanInClause})
243
+ AND parent_digest IN (${inClause})
244
+ AND child_digest IN (${inClause})
245
+ ORDER BY parent_digest, child_digest, edge_kind
246
+ `;
247
+ return this.#database.prepare(sql).all(...scanIds, ...digests, ...digests);
248
+ }
249
+ #readManifestMap(scanIds, digests, includePayload, newerScanId, olderScanId) {
250
+ const scanInClause = placeholders(scanIds.length);
251
+ const inClause = placeholders(digests.length);
252
+ const payloadColumn = includePayload ? "payload.raw_json" : "NULL";
253
+ const sql = `
254
+ WITH ranked_platforms AS (
255
+ SELECT
256
+ scan_id,
257
+ child_digest,
258
+ platform_os,
259
+ platform_architecture,
260
+ platform_variant,
261
+ ROW_NUMBER() OVER (
262
+ PARTITION BY scan_id, child_digest
263
+ ORDER BY parent_digest
264
+ ) AS row_number
265
+ FROM manifest_descriptors
266
+ WHERE scan_id IN (${scanInClause})
267
+ AND child_digest IN (${inClause})
268
+ AND (
269
+ platform_os IS NOT NULL
270
+ OR platform_architecture IS NOT NULL
271
+ OR platform_variant IS NOT NULL
272
+ )
273
+ )
274
+ SELECT
275
+ manifest.scan_id,
276
+ manifest.digest,
277
+ manifest.version_id,
278
+ package_version.created_at,
279
+ package_version.updated_at,
280
+ manifest.manifest_kind,
281
+ manifest.media_type,
282
+ platform.platform_os,
283
+ platform.platform_architecture,
284
+ platform.platform_variant,
285
+ manifest.artifact_type,
286
+ manifest.subject_digest,
287
+ ${payloadColumn} AS raw_json,
288
+ tag.tag
289
+ FROM manifests manifest
290
+ JOIN package_versions package_version
291
+ ON package_version.scan_id = manifest.scan_id
292
+ AND package_version.version_id = manifest.version_id
293
+ LEFT JOIN manifest_payloads payload
294
+ ON payload.scan_id = manifest.scan_id
295
+ AND payload.digest = manifest.digest
296
+ LEFT JOIN tags tag
297
+ ON tag.scan_id = manifest.scan_id
298
+ AND tag.version_id = manifest.version_id
299
+ AND tag.is_digest_tag = 0
300
+ LEFT JOIN ranked_platforms platform
301
+ ON platform.scan_id = manifest.scan_id
302
+ AND platform.child_digest = manifest.digest
303
+ AND platform.row_number = 1
304
+ WHERE manifest.scan_id IN (${scanInClause})
305
+ AND manifest.digest IN (${inClause})
306
+ ORDER BY manifest.digest, CASE WHEN manifest.scan_id = ? THEN 0 ELSE 1 END, tag.tag
307
+ `;
308
+ const rows = this.#database
309
+ .prepare(sql)
310
+ .all(...scanIds, ...digests, ...scanIds, ...digests, newerScanId);
311
+ const manifests = new Map();
312
+ const scanMemberships = new Map();
313
+ const tagsByDigest = new Map();
314
+ for (const row of rows) {
315
+ let scanMembership = scanMemberships.get(row.digest);
316
+ if (!scanMembership) {
317
+ scanMembership = new Set();
318
+ scanMemberships.set(row.digest, scanMembership);
319
+ }
320
+ scanMembership.add(row.scan_id);
321
+ let manifest = manifests.get(row.digest);
322
+ if (!manifest || row.scan_id === newerScanId) {
323
+ manifest = {
324
+ id: row.digest,
325
+ digest: row.digest,
326
+ versionId: row.version_id,
327
+ createdAt: row.created_at,
328
+ updatedAt: row.updated_at,
329
+ manifestKind: row.manifest_kind,
330
+ mediaType: row.media_type,
331
+ displayPlatform: _formatPlatform(row.platform_os, row.platform_architecture, row.platform_variant),
332
+ artifactType: row.artifact_type,
333
+ subjectDigest: row.subject_digest,
334
+ tags: [],
335
+ changeStatus: "unchanged",
336
+ rawJson: row.raw_json
337
+ };
338
+ manifests.set(row.digest, manifest);
339
+ }
340
+ if (row.tag) {
341
+ let tags = tagsByDigest.get(row.digest);
342
+ if (!tags) {
343
+ tags = new Map();
344
+ tagsByDigest.set(row.digest, tags);
345
+ }
346
+ let tagScans = tags.get(row.tag);
347
+ if (!tagScans) {
348
+ tagScans = new Set();
349
+ tags.set(row.tag, tagScans);
350
+ }
351
+ tagScans.add(row.scan_id);
352
+ }
353
+ }
354
+ for (const [digest, manifest] of manifests) {
355
+ const tagMap = tagsByDigest.get(digest) ?? new Map();
356
+ manifest.changeStatus = _resolveChangeStatus(scanMemberships.get(digest) ?? new Set(), newerScanId, olderScanId);
357
+ manifest.tags = [...tagMap.entries()]
358
+ .map(([name, tagScans]) => ({
359
+ name,
360
+ changeStatus: _resolveChangeStatus(tagScans, newerScanId, olderScanId)
361
+ }))
362
+ .sort((left, right) => left.name.localeCompare(right.name));
363
+ }
364
+ return manifests;
365
+ }
366
+ }
367
+ function _resolveChangeStatus(scanIds, newerScanId, olderScanId) {
368
+ if (olderScanId === undefined) {
369
+ return "unchanged";
370
+ }
371
+ const hasNewer = scanIds.has(newerScanId);
372
+ const hasOlder = scanIds.has(olderScanId);
373
+ if (hasNewer && hasOlder) {
374
+ return "unchanged";
375
+ }
376
+ return hasNewer ? "added" : "removed";
377
+ }
378
+ function _formatPlatform(os, architecture, variant) {
379
+ const normalizedOs = _normalizePlatformPart(os);
380
+ const normalizedArchitecture = _normalizePlatformPart(architecture);
381
+ const normalizedVariant = _normalizePlatformPart(variant);
382
+ if (!normalizedOs && !normalizedArchitecture && !normalizedVariant) {
383
+ return null;
384
+ }
385
+ const platform = [normalizedOs, normalizedArchitecture].filter((value) => value).join("/");
386
+ if (normalizedVariant) {
387
+ return platform ? `${platform}/${normalizedVariant}` : normalizedVariant;
388
+ }
389
+ return platform || null;
390
+ }
391
+ function _normalizePlatformPart(value) {
392
+ if (!value || value === "unknown") {
393
+ return null;
394
+ }
395
+ return value;
396
+ }
397
+ function _escapeLikeValue(value) {
398
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
399
+ }
@@ -0,0 +1,14 @@
1
+ export interface VisualizerServerOptions {
2
+ databasePath: string;
3
+ host: string;
4
+ port: number;
5
+ }
6
+ export interface VisualizerServerHandle {
7
+ readonly url: string;
8
+ close(): Promise<void>;
9
+ }
10
+ export declare function startVisualizerServer(options: VisualizerServerOptions): Promise<VisualizerServerHandle>;
11
+ export declare function _resolveRuntimePaths(importMetaUrl: string): {
12
+ publicDirectory: string;
13
+ cytoscapePath: string;
14
+ };
@@ -0,0 +1,158 @@
1
+ import Database from "better-sqlite3";
2
+ import { createReadStream, existsSync } from "node:fs";
3
+ import { stat } from "node:fs/promises";
4
+ import { createServer } from "node:http";
5
+ import { extname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { GraphRepository } from "./_graph-repository.js";
8
+ const _mimeTypes = new Map([
9
+ [".css", "text/css; charset=utf-8"],
10
+ [".html", "text/html; charset=utf-8"],
11
+ [".js", "text/javascript; charset=utf-8"],
12
+ [".json", "application/json; charset=utf-8"],
13
+ [".mjs", "text/javascript; charset=utf-8"]
14
+ ]);
15
+ export async function startVisualizerServer(options) {
16
+ const database = new Database(options.databasePath, { readonly: true, fileMustExist: true });
17
+ const repository = new GraphRepository(database);
18
+ const runtimePaths = _resolveRuntimePaths(import.meta.url);
19
+ const server = createServer(async (request, response) => {
20
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
21
+ try {
22
+ if (url.pathname.startsWith("/api/")) {
23
+ _writeJson(response, 200, _handleApi(repository, url));
24
+ return;
25
+ }
26
+ if (url.pathname === "/vendor/cytoscape.js") {
27
+ await _streamFile(response, runtimePaths.cytoscapePath);
28
+ return;
29
+ }
30
+ const requestedPath = url.pathname === "/" ? "/index.html" : url.pathname;
31
+ const staticPath = resolve(runtimePaths.publicDirectory, `.${requestedPath}`);
32
+ if (!staticPath.startsWith(runtimePaths.publicDirectory)) {
33
+ throw Object.assign(new Error("not found"), { code: "ENOENT" });
34
+ }
35
+ await _streamFile(response, staticPath);
36
+ }
37
+ catch (error) {
38
+ if (error.code === "ENOENT") {
39
+ _writeJson(response, 404, { error: "not found" });
40
+ return;
41
+ }
42
+ const message = error instanceof Error ? error.message : "unexpected error";
43
+ const statusCode = message.includes("not found") || message.includes("required") ? 404 : 400;
44
+ _writeJson(response, statusCode, { error: message });
45
+ }
46
+ });
47
+ let url = "";
48
+ await new Promise((resolvePromise) => {
49
+ server.listen(options.port, options.host, () => {
50
+ const address = server.address();
51
+ if (typeof address === "object" && address) {
52
+ url = `http://${options.host}:${address.port}`;
53
+ console.log(`Visualizer listening at ${url}`);
54
+ }
55
+ resolvePromise();
56
+ });
57
+ });
58
+ return {
59
+ url,
60
+ async close() {
61
+ await new Promise((resolvePromise, rejectPromise) => {
62
+ server.close((error) => {
63
+ if (error) {
64
+ rejectPromise(error);
65
+ return;
66
+ }
67
+ resolvePromise();
68
+ });
69
+ });
70
+ database.close();
71
+ }
72
+ };
73
+ }
74
+ export function _resolveRuntimePaths(importMetaUrl) {
75
+ const baseDirectory = resolve(fileURLToPath(new URL("..", importMetaUrl)));
76
+ return {
77
+ publicDirectory: join(baseDirectory, "public"),
78
+ cytoscapePath: join(baseDirectory, "..", "node_modules", "cytoscape", "dist", "cytoscape.esm.min.mjs")
79
+ };
80
+ }
81
+ function _handleApi(repository, url) {
82
+ const segments = url.pathname.split("/").filter(Boolean);
83
+ if (segments.length === 2 && segments[0] === "api" && segments[1] === "owners") {
84
+ return repository.listOwners();
85
+ }
86
+ if (segments.length === 4 && segments[0] === "api" && segments[1] === "owners" && segments[3] === "packages") {
87
+ return repository.listPackages(decodeURIComponent(segments[2]));
88
+ }
89
+ if (segments.length === 5 && segments[0] === "api" && segments[1] === "packages" && segments[4] === "scans") {
90
+ return repository.listScans(decodeURIComponent(segments[2]), decodeURIComponent(segments[3]));
91
+ }
92
+ if (segments.length < 4 || segments[0] !== "api" || segments[1] !== "packages") {
93
+ throw new Error("not found");
94
+ }
95
+ const owner = decodeURIComponent(segments[2]);
96
+ const packageName = decodeURIComponent(segments[3]);
97
+ const scanId = _parseOptionalInteger(url.searchParams.get("scan_id"));
98
+ const compareScanId = _parseOptionalInteger(url.searchParams.get("compare_scan_id"));
99
+ if (segments.length === 5 && segments[4] === "tags") {
100
+ return repository.listTags(owner, packageName, scanId, compareScanId, url.searchParams.get("q") ?? "", _parseOptionalInteger(url.searchParams.get("limit")) ?? 20);
101
+ }
102
+ if (segments.length === 6 && segments[4] === "scans" && segments[5] === "latest") {
103
+ return { scanId: repository.resolveLatestScanId(owner, packageName) };
104
+ }
105
+ if (segments.length === 5 && segments[4] === "manifests" && url.searchParams.has("digest")) {
106
+ return repository.resolveManifest(owner, packageName, scanId, compareScanId, {
107
+ digest: url.searchParams.get("digest") ?? undefined
108
+ });
109
+ }
110
+ if (segments.length === 5 && segments[4] === "manifests" && url.searchParams.has("tag")) {
111
+ return repository.resolveManifest(owner, packageName, scanId, compareScanId, {
112
+ tag: url.searchParams.get("tag") ?? undefined
113
+ });
114
+ }
115
+ if (segments.length === 6 && segments[4] === "manifests") {
116
+ return repository.getManifest(owner, packageName, scanId, compareScanId, decodeURIComponent(segments[5]));
117
+ }
118
+ if (segments.length === 5 && segments[4] === "graph") {
119
+ const centerDigest = url.searchParams.get("center_digest");
120
+ if (!centerDigest) {
121
+ throw new Error("center_digest is required");
122
+ }
123
+ return repository.getGraph(owner, packageName, scanId, compareScanId, centerDigest, _parseOptionalInteger(url.searchParams.get("depth")) ?? 1);
124
+ }
125
+ throw new Error("not found");
126
+ }
127
+ function _parseOptionalInteger(raw) {
128
+ if (raw === null || raw === "") {
129
+ return undefined;
130
+ }
131
+ const value = Number.parseInt(raw, 10);
132
+ if (!Number.isInteger(value)) {
133
+ throw new Error(`invalid integer value: ${raw}`);
134
+ }
135
+ return value;
136
+ }
137
+ async function _streamFile(response, path) {
138
+ if (!existsSync(path)) {
139
+ throw Object.assign(new Error("not found"), { code: "ENOENT" });
140
+ }
141
+ const fileStat = await stat(path);
142
+ if (!fileStat.isFile()) {
143
+ throw Object.assign(new Error("not found"), { code: "ENOENT" });
144
+ }
145
+ response.statusCode = 200;
146
+ response.setHeader("Content-Type", _mimeTypes.get(extname(path)) ?? "application/octet-stream");
147
+ await new Promise((resolvePromise, rejectPromise) => {
148
+ const stream = createReadStream(path);
149
+ stream.on("error", rejectPromise);
150
+ stream.on("end", resolvePromise);
151
+ stream.pipe(response);
152
+ });
153
+ }
154
+ function _writeJson(response, statusCode, body) {
155
+ response.statusCode = statusCode;
156
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
157
+ response.end(JSON.stringify(body));
158
+ }
@@ -0,0 +1 @@
1
+ export declare function placeholders(count: number): string;
@@ -0,0 +1,6 @@
1
+ export function placeholders(count) {
2
+ if (count < 1) {
3
+ throw new Error("placeholder count must be positive");
4
+ }
5
+ return Array.from({ length: count }, () => "?").join(", ");
6
+ }
@@ -0,0 +1,62 @@
1
+ export type GraphEdgeKind = "image-child" | "referrer" | "digest-tag-referrer";
2
+ export type ChangeStatus = "unchanged" | "added" | "removed";
3
+ export interface OwnerOption {
4
+ owner: string;
5
+ }
6
+ export interface PackageOption {
7
+ packageName: string;
8
+ }
9
+ export interface ScanOption {
10
+ scanId: number;
11
+ scanCompletedAt: string;
12
+ }
13
+ export interface TagOption {
14
+ tagName: string;
15
+ }
16
+ export interface GraphTag {
17
+ name: string;
18
+ changeStatus: ChangeStatus;
19
+ }
20
+ export interface GraphNode {
21
+ id: string;
22
+ digest: string;
23
+ versionId: number;
24
+ createdAt: string;
25
+ updatedAt: string;
26
+ manifestKind: string | null;
27
+ mediaType: string;
28
+ displayPlatform: string | null;
29
+ artifactType: string | null;
30
+ subjectDigest: string | null;
31
+ tags: GraphTag[];
32
+ changeStatus: ChangeStatus;
33
+ }
34
+ export interface GraphEdge {
35
+ id: string;
36
+ from: string;
37
+ to: string;
38
+ kind: GraphEdgeKind;
39
+ }
40
+ export interface GraphResponse {
41
+ owner: string;
42
+ packageName: string;
43
+ scanId: number;
44
+ compareScanId?: number;
45
+ centerDigest: string;
46
+ depth: number;
47
+ nodes: GraphNode[];
48
+ edges: GraphEdge[];
49
+ }
50
+ export interface ManifestResolution {
51
+ owner: string;
52
+ packageName: string;
53
+ scanId: number;
54
+ compareScanId?: number;
55
+ digest: string;
56
+ versionId: number;
57
+ manifestKind: string | null;
58
+ tags: string[];
59
+ }
60
+ export interface ManifestDetails extends GraphNode {
61
+ rawJson: string | null;
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { startVisualizerServer } from "./_server.js";
2
+ export type { VisualizerServerHandle } from "./_server.js";
3
+ export interface CliOptions {
4
+ databasePath: string;
5
+ host: string;
6
+ port: number;
7
+ }
8
+ export declare function main(args: string[], startServer?: typeof startVisualizerServer): Promise<void>;
9
+ export declare function parseArgs(args: string[]): CliOptions;
10
+ export declare function resolveDatabasePath(databasePath: string): string;