ghcr-cleanup-manager 1.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/LICENSE +21 -0
  3. package/README.md +262 -0
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +5 -0
  5. package/dist/cleanup-summary/_cleanup-summary-markdown.js +224 -0
  6. package/dist/cleanup-summary/_cleanup-summary.d.ts +56 -0
  7. package/dist/cleanup-summary/_cleanup-summary.js +59 -0
  8. package/dist/cleanup-summary/index.d.ts +2 -0
  9. package/dist/cleanup-summary/index.js +2 -0
  10. package/dist/cli/_args.d.ts +7 -0
  11. package/dist/cli/_args.js +41 -0
  12. package/dist/cli/_cleanup-command.d.ts +1 -0
  13. package/dist/cli/_cleanup-command.js +142 -0
  14. package/dist/cli/_db-merge-command.d.ts +1 -0
  15. package/dist/cli/_db-merge-command.js +41 -0
  16. package/dist/cli/_github-output.d.ts +10 -0
  17. package/dist/cli/_github-output.js +13 -0
  18. package/dist/cli/_json-output.d.ts +1 -0
  19. package/dist/cli/_json-output.js +11 -0
  20. package/dist/cli/_logger.d.ts +10 -0
  21. package/dist/cli/_logger.js +26 -0
  22. package/dist/cli/_older-than.d.ts +5 -0
  23. package/dist/cli/_older-than.js +42 -0
  24. package/dist/cli/_planner-options.d.ts +20 -0
  25. package/dist/cli/_planner-options.js +83 -0
  26. package/dist/cli/_scan-command.d.ts +1 -0
  27. package/dist/cli/_scan-command.js +39 -0
  28. package/dist/cli/_tag-selector-resolver.d.ts +3 -0
  29. package/dist/cli/_tag-selector-resolver.js +132 -0
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.js +52 -0
  32. package/dist/config/_service-constants.d.ts +3 -0
  33. package/dist/config/_service-constants.js +3 -0
  34. package/dist/config/index.d.ts +9 -0
  35. package/dist/config/index.js +9 -0
  36. package/dist/core/_digest-tag.d.ts +2 -0
  37. package/dist/core/_digest-tag.js +12 -0
  38. package/dist/core/_github-package-owner.d.ts +11 -0
  39. package/dist/core/_github-package-owner.js +45 -0
  40. package/dist/core/_http-error.d.ts +6 -0
  41. package/dist/core/_http-error.js +33 -0
  42. package/dist/core/_types.d.ts +54 -0
  43. package/dist/core/_types.js +8 -0
  44. package/dist/core/index.d.ts +6 -0
  45. package/dist/core/index.js +4 -0
  46. package/dist/db/_cleanup-run-writer.d.ts +10 -0
  47. package/dist/db/_cleanup-run-writer.js +113 -0
  48. package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
  49. package/dist/db/_db-merge-cleanup-copy.js +172 -0
  50. package/dist/db/_db-merge-history.d.ts +2 -0
  51. package/dist/db/_db-merge-history.js +15 -0
  52. package/dist/db/_db-merge-repository.d.ts +8 -0
  53. package/dist/db/_db-merge-repository.js +95 -0
  54. package/dist/db/_db-merge-scan-copy.d.ts +10 -0
  55. package/dist/db/_db-merge-scan-copy.js +70 -0
  56. package/dist/db/_db-merge-types.d.ts +44 -0
  57. package/dist/db/_db-merge-types.js +1 -0
  58. package/dist/db/_github-actions-run-url.d.ts +1 -0
  59. package/dist/db/_github-actions-run-url.js +9 -0
  60. package/dist/db/_manifest-reachability.d.ts +2 -0
  61. package/dist/db/_manifest-reachability.js +158 -0
  62. package/dist/db/_scan-writer.d.ts +20 -0
  63. package/dist/db/_scan-writer.js +184 -0
  64. package/dist/db/_schema.d.ts +2 -0
  65. package/dist/db/_schema.js +19 -0
  66. package/dist/db/_snapshot-repository.d.ts +24 -0
  67. package/dist/db/_snapshot-repository.js +86 -0
  68. package/dist/db/_sql-placeholders.d.ts +2 -0
  69. package/dist/db/_sql-placeholders.js +16 -0
  70. package/dist/db/index.d.ts +10 -0
  71. package/dist/db/index.js +13 -0
  72. package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
  73. package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
  74. package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +7 -0
  75. package/dist/db/planner/_planner-direct-target-root-tag-filters.js +41 -0
  76. package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
  77. package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +189 -0
  78. package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
  79. package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
  80. package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
  81. package/dist/db/planner/_planner-direct-target-roots-tagged.js +117 -0
  82. package/dist/db/planner/_planner-direct-target-roots.d.ts +8 -0
  83. package/dist/db/planner/_planner-direct-target-roots.js +17 -0
  84. package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
  85. package/dist/db/planner/_planner-direct-target-tags.js +54 -0
  86. package/dist/db/planner/_planner-latest-scan.d.ts +10 -0
  87. package/dist/db/planner/_planner-latest-scan.js +20 -0
  88. package/dist/db/planner/_planner-output.d.ts +5 -0
  89. package/dist/db/planner/_planner-output.js +111 -0
  90. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
  91. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +110 -0
  92. package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
  93. package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +188 -0
  94. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
  95. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
  96. package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
  97. package/dist/db/planner/_planner-plan-artifacts.js +109 -0
  98. package/dist/db/planner/_planner-repository.d.ts +44 -0
  99. package/dist/db/planner/_planner-repository.js +105 -0
  100. package/dist/db/planner/_planner-sql.d.ts +12 -0
  101. package/dist/db/planner/_planner-sql.js +46 -0
  102. package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
  103. package/dist/db/planner/_planner-tag-selectors.js +57 -0
  104. package/dist/db/planner/_planner-types.d.ts +144 -0
  105. package/dist/db/planner/_planner-types.js +49 -0
  106. package/dist/db/planner/index.d.ts +3 -0
  107. package/dist/db/planner/index.js +2 -0
  108. package/dist/execute/_http.d.ts +7 -0
  109. package/dist/execute/_http.js +48 -0
  110. package/dist/execute/_manifest-detach.d.ts +4 -0
  111. package/dist/execute/_manifest-detach.js +31 -0
  112. package/dist/execute/_package-version-delete-client.d.ts +4 -0
  113. package/dist/execute/_package-version-delete-client.js +34 -0
  114. package/dist/execute/_package-version-page-client.d.ts +14 -0
  115. package/dist/execute/_package-version-page-client.js +64 -0
  116. package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
  117. package/dist/execute/_package-version-tag-source-client.js +65 -0
  118. package/dist/execute/_plan-executor.d.ts +3 -0
  119. package/dist/execute/_plan-executor.js +69 -0
  120. package/dist/execute/_registry-manifest-client.d.ts +12 -0
  121. package/dist/execute/_registry-manifest-client.js +79 -0
  122. package/dist/execute/_registry-token-client.d.ts +4 -0
  123. package/dist/execute/_registry-token-client.js +37 -0
  124. package/dist/execute/_types.d.ts +34 -0
  125. package/dist/execute/_types.js +1 -0
  126. package/dist/execute/_untag-client.d.ts +2 -0
  127. package/dist/execute/_untag-client.js +30 -0
  128. package/dist/execute/index.d.ts +5 -0
  129. package/dist/execute/index.js +3 -0
  130. package/dist/ingest/github/_manifest-client.d.ts +14 -0
  131. package/dist/ingest/github/_manifest-client.js +108 -0
  132. package/dist/ingest/github/_manifest-ingest.d.ts +3 -0
  133. package/dist/ingest/github/_manifest-ingest.js +90 -0
  134. package/dist/ingest/github/_manifest-kind.d.ts +26 -0
  135. package/dist/ingest/github/_manifest-kind.js +65 -0
  136. package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
  137. package/dist/ingest/github/_package-metadata-load.js +45 -0
  138. package/dist/ingest/github/_package-version-page-load.d.ts +13 -0
  139. package/dist/ingest/github/_package-version-page-load.js +55 -0
  140. package/dist/ingest/github/_packages-client.d.ts +10 -0
  141. package/dist/ingest/github/_packages-client.js +76 -0
  142. package/dist/ingest/github/_parallel-paginated-ingest.d.ts +12 -0
  143. package/dist/ingest/github/_parallel-paginated-ingest.js +49 -0
  144. package/dist/ingest/github/_registry-token-client.d.ts +6 -0
  145. package/dist/ingest/github/_registry-token-client.js +67 -0
  146. package/dist/ingest/github/_shared.d.ts +28 -0
  147. package/dist/ingest/github/_shared.js +70 -0
  148. package/dist/ingest/github/index.d.ts +11 -0
  149. package/dist/ingest/github/index.js +29 -0
  150. package/package.json +78 -0
  151. package/resources/sql/schema/001_schema.sql +222 -0
  152. package/resources/sql/views/001_v_latest_scan_per_package.sql +27 -0
@@ -0,0 +1,158 @@
1
+ export function rebuildManifestReachability(database, scanId) {
2
+ _refreshDigestTagEdges(database, scanId);
3
+ const manifestDigests = _loadManifestDigests(database, scanId);
4
+ const childDigestsByParent = new Map();
5
+ const parentDigestsByChild = new Map();
6
+ const neighborDigestsByDigest = new Map();
7
+ for (const digest of manifestDigests) {
8
+ childDigestsByParent.set(digest, new Set());
9
+ parentDigestsByChild.set(digest, new Set());
10
+ neighborDigestsByDigest.set(digest, new Set());
11
+ }
12
+ for (const manifestEdge of _loadManifestEdges(database, scanId)) {
13
+ childDigestsByParent.get(manifestEdge.parent_digest)?.add(manifestEdge.child_digest);
14
+ parentDigestsByChild.get(manifestEdge.child_digest)?.add(manifestEdge.parent_digest);
15
+ neighborDigestsByDigest.get(manifestEdge.parent_digest)?.add(manifestEdge.child_digest);
16
+ neighborDigestsByDigest.get(manifestEdge.child_digest)?.add(manifestEdge.parent_digest);
17
+ }
18
+ const graphIdsByDigest = _buildGraphIdsByDigest(manifestDigests, neighborDigestsByDigest);
19
+ const remainingChildrenCount = new Map();
20
+ const descendantDistancesByDigest = new Map();
21
+ const readyDigests = [];
22
+ for (const digest of manifestDigests) {
23
+ const childCount = childDigestsByParent.get(digest)?.size ?? 0;
24
+ remainingChildrenCount.set(digest, childCount);
25
+ if (childCount === 0) {
26
+ readyDigests.push(digest);
27
+ }
28
+ }
29
+ while (readyDigests.length > 0) {
30
+ const digest = readyDigests.shift();
31
+ if (!digest) {
32
+ continue;
33
+ }
34
+ const distances = new Map([[digest, 0]]);
35
+ for (const childDigest of childDigestsByParent.get(digest) ?? []) {
36
+ _setMinDistance(distances, childDigest, 1);
37
+ const childDistances = descendantDistancesByDigest.get(childDigest);
38
+ if (!childDistances) {
39
+ throw new Error(`manifest reachability build missing child results for ${childDigest}`);
40
+ }
41
+ for (const [descendantDigest, childDistance] of childDistances) {
42
+ if (descendantDigest === childDigest) {
43
+ continue;
44
+ }
45
+ _setMinDistance(distances, descendantDigest, childDistance + 1);
46
+ }
47
+ }
48
+ descendantDistancesByDigest.set(digest, distances);
49
+ for (const parentDigest of parentDigestsByChild.get(digest) ?? []) {
50
+ const nextCount = (remainingChildrenCount.get(parentDigest) ?? 0) - 1;
51
+ remainingChildrenCount.set(parentDigest, nextCount);
52
+ if (nextCount === 0) {
53
+ readyDigests.push(parentDigest);
54
+ }
55
+ }
56
+ }
57
+ if (descendantDistancesByDigest.size !== manifestDigests.length) {
58
+ throw new Error("manifest reachability build detected a cycle in manifest_edges");
59
+ }
60
+ const insertRow = database.prepare(`
61
+ INSERT OR REPLACE INTO manifest_reachability(
62
+ scan_id,
63
+ ancestor_digest,
64
+ descendant_digest,
65
+ min_distance
66
+ )
67
+ VALUES(?, ?, ?, ?)
68
+ `);
69
+ const insertGraphRow = database.prepare(`
70
+ INSERT OR REPLACE INTO manifest_graphs(
71
+ scan_id,
72
+ digest,
73
+ graph_id
74
+ )
75
+ VALUES(?, ?, ?)
76
+ `);
77
+ const rebuild = database.transaction(() => {
78
+ database.prepare("DELETE FROM manifest_reachability WHERE scan_id = ?").run(scanId);
79
+ database.prepare("DELETE FROM manifest_graphs WHERE scan_id = ?").run(scanId);
80
+ for (const digest of manifestDigests) {
81
+ insertGraphRow.run(scanId, digest, graphIdsByDigest.get(digest));
82
+ for (const [descendantDigest, distance] of descendantDistancesByDigest.get(digest) ?? []) {
83
+ insertRow.run(scanId, digest, descendantDigest, distance);
84
+ }
85
+ }
86
+ });
87
+ rebuild();
88
+ }
89
+ function _buildGraphIdsByDigest(manifestDigests, neighborDigestsByDigest) {
90
+ const graphIdsByDigest = new Map();
91
+ let nextGraphId = 1;
92
+ for (const rootDigest of manifestDigests) {
93
+ if (graphIdsByDigest.has(rootDigest)) {
94
+ continue;
95
+ }
96
+ const pendingDigests = [rootDigest];
97
+ graphIdsByDigest.set(rootDigest, nextGraphId);
98
+ while (pendingDigests.length > 0) {
99
+ const digest = pendingDigests.pop();
100
+ if (!digest) {
101
+ continue;
102
+ }
103
+ for (const neighborDigest of neighborDigestsByDigest.get(digest) ?? []) {
104
+ if (graphIdsByDigest.has(neighborDigest)) {
105
+ continue;
106
+ }
107
+ graphIdsByDigest.set(neighborDigest, nextGraphId);
108
+ pendingDigests.push(neighborDigest);
109
+ }
110
+ }
111
+ nextGraphId += 1;
112
+ }
113
+ return graphIdsByDigest;
114
+ }
115
+ function _refreshDigestTagEdges(database, scanId) {
116
+ database.prepare("DELETE FROM manifest_edges WHERE scan_id = ? AND edge_kind = 'digest-tag-referrer'").run(scanId);
117
+ database
118
+ .prepare(`
119
+ INSERT OR IGNORE INTO manifest_edges(scan_id, parent_digest, child_digest, edge_kind)
120
+ SELECT
121
+ t.scan_id,
122
+ m.digest AS parent_digest,
123
+ 'sha256:' || SUBSTR(t.tag, 8, 64) AS child_digest,
124
+ 'digest-tag-referrer' AS edge_kind
125
+ FROM tags t
126
+ JOIN manifests m
127
+ ON m.scan_id = t.scan_id
128
+ AND m.version_id = t.version_id
129
+ JOIN manifests child_manifest
130
+ ON child_manifest.scan_id = t.scan_id
131
+ AND child_manifest.digest = 'sha256:' || SUBSTR(t.tag, 8, 64)
132
+ WHERE t.scan_id = ?
133
+ AND t.is_digest_tag = 1
134
+ `)
135
+ .run(scanId);
136
+ }
137
+ function _loadManifestDigests(database, scanId) {
138
+ const rows = database
139
+ .prepare("SELECT digest FROM manifests WHERE scan_id = ? ORDER BY digest")
140
+ .all(scanId);
141
+ return rows.map((row) => row.digest);
142
+ }
143
+ function _loadManifestEdges(database, scanId) {
144
+ return database
145
+ .prepare(`
146
+ SELECT DISTINCT parent_digest, child_digest
147
+ FROM manifest_edges
148
+ WHERE scan_id = ?
149
+ ORDER BY parent_digest, child_digest
150
+ `)
151
+ .all(scanId);
152
+ }
153
+ function _setMinDistance(distances, digest, distance) {
154
+ const currentDistance = distances.get(digest);
155
+ if (currentDistance === undefined || distance < currentDistance) {
156
+ distances.set(digest, distance);
157
+ }
158
+ }
@@ -0,0 +1,20 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { ManifestDescriptorRecord, ManifestEdgeRecord, ManifestRecord, PackageVersionRecord, TagRecord } from "../core/index.js";
3
+ export declare class ScanWriter {
4
+ #private;
5
+ constructor(database: Database.Database);
6
+ startScan(owner: string, packageName: string, scanStartedAt: string, packageMetadata: {
7
+ rawJson: string;
8
+ }): void;
9
+ markScanCompleted(scanCompletedAt: string): void;
10
+ markScanFailed(scanCompletedAt: string): void;
11
+ insertPackageVersion(version: PackageVersionRecord): void;
12
+ insertPackageVersionPayload(versionId: number, rawJson: string): void;
13
+ insertTag(tag: TagRecord): void;
14
+ insertManifest(manifest: ManifestRecord): void;
15
+ insertManifestPayload(digest: string, rawJson: string): void;
16
+ insertManifestDescriptor(descriptor: ManifestDescriptorRecord): void;
17
+ insertManifestEdge(edge: ManifestEdgeRecord): void;
18
+ rebuildManifestReachability(): void;
19
+ getActiveScanId(): number;
20
+ }
@@ -0,0 +1,184 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isDigestTag } from "../core/index.js";
3
+ import { resolveGitHubActionsRunUrl } from "./_github-actions-run-url.js";
4
+ import { rebuildManifestReachability } from "./_manifest-reachability.js";
5
+ export class ScanWriter {
6
+ #database;
7
+ #startScanStatement;
8
+ #markScanCompletedStatement;
9
+ #markScanFailedStatement;
10
+ #insertPackageVersionStatement;
11
+ #insertPackageVersionPayloadStatement;
12
+ #insertTagStatement;
13
+ #insertManifestStatement;
14
+ #insertManifestPayloadStatement;
15
+ #insertManifestDescriptorStatement;
16
+ #insertManifestEdgeStatement;
17
+ #activeScanId = null;
18
+ constructor(database) {
19
+ this.#database = database;
20
+ this.#startScanStatement = this.#database.prepare(`
21
+ INSERT INTO package_scans(
22
+ scan_uuid,
23
+ owner,
24
+ package_name,
25
+ package_metadata_json,
26
+ github_actions_run_url,
27
+ scan_started_at,
28
+ scan_completed_at,
29
+ status
30
+ )
31
+ VALUES(?, ?, ?, ?, ?, ?, NULL, 'running')
32
+ `);
33
+ this.#markScanCompletedStatement = this.#database.prepare(`
34
+ UPDATE package_scans
35
+ SET scan_completed_at = ?, status = 'completed'
36
+ WHERE scan_id = ?
37
+ `);
38
+ this.#markScanFailedStatement = this.#database.prepare(`
39
+ UPDATE package_scans
40
+ SET scan_completed_at = ?, status = 'failed'
41
+ WHERE scan_id = ?
42
+ `);
43
+ this.#insertPackageVersionStatement = this.#database.prepare(`
44
+ INSERT OR REPLACE INTO package_versions(scan_id, version_id, created_at, updated_at)
45
+ VALUES(@scanId, @versionId, @createdAt, @updatedAt)
46
+ `);
47
+ this.#insertPackageVersionPayloadStatement = this.#database.prepare(`
48
+ INSERT OR REPLACE INTO package_version_payloads(scan_id, version_id, raw_json)
49
+ VALUES(?, ?, ?)
50
+ `);
51
+ this.#insertTagStatement = this.#database.prepare(`
52
+ INSERT OR REPLACE INTO tags(scan_id, tag, version_id, is_digest_tag)
53
+ VALUES(@scanId, @tag, @versionId, @isDigestTag)
54
+ `);
55
+ this.#insertManifestStatement = this.#database.prepare(`
56
+ INSERT OR REPLACE INTO manifests(
57
+ scan_id,
58
+ version_id,
59
+ digest,
60
+ media_type,
61
+ artifact_type,
62
+ config_media_type,
63
+ subject_digest,
64
+ annotations_json,
65
+ manifest_kind
66
+ )
67
+ VALUES(
68
+ @scanId,
69
+ @versionId,
70
+ @digest,
71
+ @mediaType,
72
+ @artifactType,
73
+ @configMediaType,
74
+ @subjectDigest,
75
+ @annotationsJson,
76
+ @manifestKind
77
+ )
78
+ `);
79
+ this.#insertManifestPayloadStatement = this.#database.prepare(`
80
+ INSERT OR REPLACE INTO manifest_payloads(scan_id, digest, raw_json)
81
+ VALUES(?, ?, ?)
82
+ `);
83
+ this.#insertManifestDescriptorStatement = this.#database.prepare(`
84
+ INSERT OR REPLACE INTO manifest_descriptors(
85
+ scan_id,
86
+ parent_digest,
87
+ child_digest,
88
+ media_type,
89
+ artifact_type,
90
+ platform_os,
91
+ platform_architecture,
92
+ platform_variant
93
+ )
94
+ VALUES(
95
+ @scanId,
96
+ @parentDigest,
97
+ @childDigest,
98
+ @mediaType,
99
+ @artifactType,
100
+ @platformOs,
101
+ @platformArchitecture,
102
+ @platformVariant
103
+ )
104
+ `);
105
+ this.#insertManifestEdgeStatement = this.#database.prepare(`
106
+ INSERT OR IGNORE INTO manifest_edges(scan_id, parent_digest, child_digest, edge_kind)
107
+ VALUES(@scanId, @parentDigest, @childDigest, @edgeKind)
108
+ `);
109
+ }
110
+ startScan(owner, packageName, scanStartedAt, packageMetadata) {
111
+ const result = this.#startScanStatement.run(randomUUID(), owner, packageName, packageMetadata.rawJson, resolveGitHubActionsRunUrl(), scanStartedAt);
112
+ this.#activeScanId = Number(result.lastInsertRowid);
113
+ }
114
+ markScanCompleted(scanCompletedAt) {
115
+ this.#markScanCompletedStatement.run(scanCompletedAt, this.#requireScanId());
116
+ }
117
+ markScanFailed(scanCompletedAt) {
118
+ this.#markScanFailedStatement.run(scanCompletedAt, this.#requireScanId());
119
+ }
120
+ insertPackageVersion(version) {
121
+ this.#insertPackageVersionStatement.run({
122
+ scanId: this.#requireScanId(),
123
+ versionId: version.versionId,
124
+ createdAt: version.createdAt,
125
+ updatedAt: version.updatedAt
126
+ });
127
+ }
128
+ insertPackageVersionPayload(versionId, rawJson) {
129
+ this.#insertPackageVersionPayloadStatement.run(this.#requireScanId(), versionId, rawJson);
130
+ }
131
+ insertTag(tag) {
132
+ this.#insertTagStatement.run({
133
+ scanId: this.#requireScanId(),
134
+ ...tag,
135
+ isDigestTag: isDigestTag(tag.tag) ? 1 : 0
136
+ });
137
+ }
138
+ insertManifest(manifest) {
139
+ this.#insertManifestStatement.run({
140
+ scanId: this.#requireScanId(),
141
+ versionId: manifest.versionId,
142
+ digest: manifest.digest,
143
+ mediaType: manifest.mediaType,
144
+ artifactType: manifest.artifactType ?? null,
145
+ configMediaType: manifest.configMediaType ?? null,
146
+ subjectDigest: manifest.subjectDigest ?? null,
147
+ annotationsJson: manifest.annotations ? JSON.stringify(manifest.annotations) : null,
148
+ manifestKind: manifest.manifestKind ?? null
149
+ });
150
+ }
151
+ insertManifestPayload(digest, rawJson) {
152
+ this.#insertManifestPayloadStatement.run(this.#requireScanId(), digest, rawJson);
153
+ }
154
+ insertManifestDescriptor(descriptor) {
155
+ this.#insertManifestDescriptorStatement.run({
156
+ scanId: this.#requireScanId(),
157
+ parentDigest: descriptor.parentDigest,
158
+ childDigest: descriptor.childDigest,
159
+ mediaType: descriptor.mediaType,
160
+ artifactType: descriptor.artifactType ?? null,
161
+ platformOs: descriptor.platform?.os ?? null,
162
+ platformArchitecture: descriptor.platform?.architecture ?? null,
163
+ platformVariant: descriptor.platform?.variant ?? null
164
+ });
165
+ }
166
+ insertManifestEdge(edge) {
167
+ this.#insertManifestEdgeStatement.run({
168
+ scanId: this.#requireScanId(),
169
+ ...edge
170
+ });
171
+ }
172
+ rebuildManifestReachability() {
173
+ rebuildManifestReachability(this.#database, this.#requireScanId());
174
+ }
175
+ getActiveScanId() {
176
+ return this.#requireScanId();
177
+ }
178
+ #requireScanId() {
179
+ if (this.#activeScanId === null) {
180
+ throw new Error("package not initialized; call startScan(owner, packageName, scanStartedAt, packageMetadata) first");
181
+ }
182
+ return this.#activeScanId;
183
+ }
184
+ }
@@ -0,0 +1,2 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare function initializeSchema(database: Database.Database): void;
@@ -0,0 +1,19 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export function initializeSchema(database) {
4
+ _initializeSqlDirectory(database, "schema");
5
+ _initializeSqlDirectory(database, "views");
6
+ }
7
+ function _initializeSqlDirectory(database, directoryName) {
8
+ const sqlDirectory = join(process.cwd(), "resources", "sql", directoryName);
9
+ if (!existsSync(sqlDirectory)) {
10
+ return;
11
+ }
12
+ const sqlFiles = readdirSync(sqlDirectory)
13
+ .filter((file) => file.endsWith(".sql"))
14
+ .sort();
15
+ for (const sqlFile of sqlFiles) {
16
+ const sql = readFileSync(join(sqlDirectory, sqlFile), "utf8");
17
+ database.exec(sql);
18
+ }
19
+ }
@@ -0,0 +1,24 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare class SnapshotRepository {
3
+ #private;
4
+ constructor(database: Database.Database);
5
+ getPackageMetadata(scanId: number): {
6
+ owner: string;
7
+ packageName: string;
8
+ scanCompletedAt: string;
9
+ };
10
+ countPackageVersions(scanId: number): number;
11
+ countTaggedVersions(scanId: number): number;
12
+ countTags(scanId: number): number;
13
+ countManifests(scanId: number): number;
14
+ countManifestEdges(scanId: number): number;
15
+ listManifestDigests(scanId: number): string[];
16
+ listManifestPayloads(scanId: number): Array<{
17
+ digest: string;
18
+ rawJson: string;
19
+ }>;
20
+ listPackageVersionManifestRefs(scanId: number): Array<{
21
+ versionId: number;
22
+ digest: string;
23
+ }>;
24
+ }
@@ -0,0 +1,86 @@
1
+ export class SnapshotRepository {
2
+ #database;
3
+ constructor(database) {
4
+ this.#database = database;
5
+ }
6
+ getPackageMetadata(scanId) {
7
+ const row = this.#database
8
+ .prepare(`
9
+ SELECT owner, package_name, scan_completed_at
10
+ FROM package_scans
11
+ WHERE scan_id = ?
12
+ `)
13
+ .get(scanId);
14
+ if (!row) {
15
+ throw new Error(`database does not contain package scan for scan_id=${scanId}`);
16
+ }
17
+ if (!row.scan_completed_at) {
18
+ throw new Error(`scan ${scanId} has not completed`);
19
+ }
20
+ return {
21
+ owner: row.owner,
22
+ packageName: row.package_name,
23
+ scanCompletedAt: row.scan_completed_at
24
+ };
25
+ }
26
+ countPackageVersions(scanId) {
27
+ return _count(this.#database, "SELECT COUNT(*) AS total FROM package_versions WHERE scan_id = ?", "total", scanId);
28
+ }
29
+ countTaggedVersions(scanId) {
30
+ return _count(this.#database, "SELECT COUNT(DISTINCT version_id) AS total FROM tags WHERE scan_id = ?", "total", scanId);
31
+ }
32
+ countTags(scanId) {
33
+ return _count(this.#database, "SELECT COUNT(*) AS total FROM tags WHERE scan_id = ?", "total", scanId);
34
+ }
35
+ countManifests(scanId) {
36
+ return _count(this.#database, "SELECT COUNT(*) AS total FROM manifests WHERE scan_id = ?", "total", scanId);
37
+ }
38
+ countManifestEdges(scanId) {
39
+ return _count(this.#database, "SELECT COUNT(*) AS total FROM manifest_edges WHERE scan_id = ?", "total", scanId);
40
+ }
41
+ listManifestDigests(scanId) {
42
+ const rows = this.#database
43
+ .prepare("SELECT digest FROM manifests WHERE scan_id = ? ORDER BY digest")
44
+ .all(scanId);
45
+ return rows.map((row) => row.digest);
46
+ }
47
+ listManifestPayloads(scanId) {
48
+ const rows = this.#database
49
+ .prepare(`
50
+ SELECT digest, raw_json
51
+ FROM manifest_payloads
52
+ WHERE scan_id = ?
53
+ ORDER BY digest
54
+ `)
55
+ .all(scanId);
56
+ return rows.map((row) => ({
57
+ digest: row.digest,
58
+ rawJson: row.raw_json
59
+ }));
60
+ }
61
+ listPackageVersionManifestRefs(scanId) {
62
+ const rows = this.#database
63
+ .prepare(`
64
+ SELECT version_id, raw_json
65
+ FROM package_version_payloads
66
+ WHERE scan_id = ?
67
+ ORDER BY version_id
68
+ `)
69
+ .all(scanId);
70
+ return rows.map((row) => ({
71
+ versionId: row.version_id,
72
+ digest: _parsePackageVersionDigest(row.version_id, row.raw_json)
73
+ }));
74
+ }
75
+ }
76
+ function _count(database, sql, field, ...params) {
77
+ const row = database.prepare(sql).get(...params);
78
+ return row[field];
79
+ }
80
+ function _parsePackageVersionDigest(versionId, rawJson) {
81
+ const payload = JSON.parse(rawJson);
82
+ if (typeof payload.name !== "string" || payload.name.length === 0) {
83
+ throw new Error(`package version payload for version_id=${versionId} did not include digest name`);
84
+ }
85
+ return payload.name;
86
+ }
@@ -0,0 +1,2 @@
1
+ export declare function buildInClausePlaceholders(valueCount: number): string;
2
+ export declare function buildTuplePlaceholders(rowCount: number, columnCount: number): string;
@@ -0,0 +1,16 @@
1
+ export function buildInClausePlaceholders(valueCount) {
2
+ if (valueCount <= 0) {
3
+ throw new Error("valueCount must be greater than 0");
4
+ }
5
+ return Array.from({ length: valueCount }, () => "?").join(", ");
6
+ }
7
+ export function buildTuplePlaceholders(rowCount, columnCount) {
8
+ if (rowCount <= 0) {
9
+ throw new Error("rowCount must be greater than 0");
10
+ }
11
+ if (columnCount <= 0) {
12
+ throw new Error("columnCount must be greater than 0");
13
+ }
14
+ const tuple = `(${Array.from({ length: columnCount }, () => "?").join(", ")})`;
15
+ return Array.from({ length: rowCount }, () => tuple).join(", ");
16
+ }
@@ -0,0 +1,10 @@
1
+ import Database from "better-sqlite3";
2
+ export { ScanWriter } from "./_scan-writer.js";
3
+ export { CleanupRunWriter } from "./_cleanup-run-writer.js";
4
+ export { DbMergeRepository } from "./_db-merge-repository.js";
5
+ export { PlannerRepository } from "./planner/index.js";
6
+ export { SnapshotRepository } from "./_snapshot-repository.js";
7
+ export type { DeletePlan, DeletePlanBlockReasonCode, DeletePlanSelectionMode, DeletePlanSelectionReason, DeletePlanValidationReasonCode, DeletePlanValidationStatus } from "./planner/index.js";
8
+ export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./planner/index.js";
9
+ export type { DbMergeSourceSummary } from "./_db-merge-repository.js";
10
+ export declare function openDatabase(databasePath: string): Database.Database;
@@ -0,0 +1,13 @@
1
+ import Database from "better-sqlite3";
2
+ import { initializeSchema } from "./_schema.js";
3
+ export { ScanWriter } from "./_scan-writer.js";
4
+ export { CleanupRunWriter } from "./_cleanup-run-writer.js";
5
+ export { DbMergeRepository } from "./_db-merge-repository.js";
6
+ export { PlannerRepository } from "./planner/index.js";
7
+ export { SnapshotRepository } from "./_snapshot-repository.js";
8
+ export { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./planner/index.js";
9
+ export function openDatabase(databasePath) {
10
+ const database = new Database(databasePath);
11
+ initializeSchema(database);
12
+ return database;
13
+ }
@@ -0,0 +1,11 @@
1
+ export interface DirectTargetRootOptions {
2
+ deleteTags: string[];
3
+ deleteTagsRequested: boolean;
4
+ deleteOrphanedImages?: boolean;
5
+ excludeTags: string[];
6
+ deleteUntagged: boolean;
7
+ keepNTagged?: number;
8
+ keepNUntagged?: number;
9
+ useRegex?: boolean;
10
+ cutoffTimestamp?: string;
11
+ }
@@ -0,0 +1,7 @@
1
+ import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
2
+ import type { PlannerSql } from "./_planner-sql.js";
3
+ export interface DirectTargetRootTagFilters {
4
+ selectedTagsSql: string;
5
+ selectedParams: Array<number | string>;
6
+ }
7
+ export declare function buildDirectTargetRootTagFilters(sql: PlannerSql, scanId: number, options: DirectTargetRootOptions): DirectTargetRootTagFilters;
@@ -0,0 +1,41 @@
1
+ import { buildTagSelectorPredicate } from "./_planner-tag-selectors.js";
2
+ export function buildDirectTargetRootTagFilters(sql, scanId, options) {
3
+ const selectedTagPredicate = options.deleteTags.length > 0
4
+ ? buildTagSelectorPredicate(sql.database, "t.tag", options.deleteTags, options.useRegex ?? false)
5
+ : undefined;
6
+ const excludedTagPredicate = options.excludeTags.length > 0
7
+ ? buildTagSelectorPredicate(sql.database, "xt.tag", options.excludeTags, options.useRegex ?? false)
8
+ : undefined;
9
+ const selectedTagDigestFlag = options.deleteOrphanedImages ? 1 : 0;
10
+ const selectedTagsSql = selectedTagPredicate
11
+ ? `
12
+ SELECT DISTINCT t.version_id, t.tag
13
+ FROM tags t
14
+ WHERE t.scan_id = ?
15
+ AND t.is_digest_tag = ?
16
+ AND (${selectedTagPredicate.sql})
17
+ ${excludedTagPredicate
18
+ ? `
19
+ AND NOT EXISTS (
20
+ SELECT 1
21
+ FROM tags xt
22
+ WHERE xt.scan_id = t.scan_id
23
+ AND xt.version_id = t.version_id
24
+ AND xt.tag = t.tag
25
+ AND (${excludedTagPredicate.sql})
26
+ )
27
+ `
28
+ : ""}
29
+ `
30
+ : `
31
+ SELECT NULL AS version_id, NULL AS tag
32
+ WHERE 1 = 0
33
+ `;
34
+ const selectedParams = selectedTagPredicate
35
+ ? [scanId, selectedTagDigestFlag, ...selectedTagPredicate.params, ...(excludedTagPredicate?.params ?? [])]
36
+ : [];
37
+ return {
38
+ selectedTagsSql,
39
+ selectedParams
40
+ };
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { DirectTargetRootOptions } from "./_planner-direct-target-root-options.js";
2
+ export interface CombinedDirectTargetRootsQuery {
3
+ query: string;
4
+ baseParams: Array<number | string>;
5
+ tailParams: Array<number | string>;
6
+ }
7
+ export declare function buildCombinedDirectTargetRootsQuery(scanId: number, options: DirectTargetRootOptions, selectedTagsSql: string): CombinedDirectTargetRootsQuery;