@danypops/pi-packed 0.19.7 → 0.19.10

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 (94) hide show
  1. package/dist/client.d.ts +109 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +1 -0
  4. package/dist/protocol.d.ts +221 -0
  5. package/dist/protocol.d.ts.map +1 -0
  6. package/dist/protocol.js +1 -0
  7. package/extension/src/{permission.ts → approval/permission.ts} +1 -1
  8. package/extension/src/index.ts +1 -1
  9. package/extension/src/packed.ts +2 -2
  10. package/extension/src/{discover.ts → tabs/discover.ts} +4 -4
  11. package/extension/src/{resource-config.ts → tabs/resource-config.ts} +4 -4
  12. package/extension/src/{security-tui.ts → tabs/security-tui.ts} +3 -3
  13. package/extension/src/tool-output.ts +1 -1
  14. package/extension/src/tools.ts +2 -2
  15. package/extension/src/tui.ts +4 -4
  16. package/package.json +31 -8
  17. package/service/schema/pi-setup-v1.schema.json +70 -0
  18. package/service/setup/danypops-ecosystem.pi-setup.json +15 -0
  19. package/service/src/adoption/advisories.ts +268 -0
  20. package/service/src/adoption/check.ts +872 -0
  21. package/service/src/adoption/commit-freshness.ts +167 -0
  22. package/service/src/adoption/doctor.ts +135 -0
  23. package/service/src/adoption/install-validation.ts +187 -0
  24. package/service/src/adoption/pack.ts +291 -0
  25. package/service/src/adoption/score.ts +466 -0
  26. package/service/src/adoption/smoke-child.ts +113 -0
  27. package/service/src/adoption/smoke.ts +282 -0
  28. package/service/src/cli/cli.ts +926 -0
  29. package/service/src/daemon/cleanup.ts +76 -0
  30. package/service/src/daemon/client.ts +412 -0
  31. package/service/src/daemon/daemon-service.ts +249 -0
  32. package/service/src/daemon/daemon.ts +110 -0
  33. package/service/src/daemon/service.ts +664 -0
  34. package/service/src/daemon/watcher.ts +92 -0
  35. package/service/src/index/build-index.ts +256 -0
  36. package/service/src/packages/catalog.ts +61 -0
  37. package/service/src/packages/db.ts +224 -0
  38. package/service/src/packages/install.ts +60 -0
  39. package/service/src/packages/installed.ts +123 -0
  40. package/service/src/packages/package.ts +141 -0
  41. package/service/src/packages/resources.ts +203 -0
  42. package/service/src/pi/pi-version.ts +171 -0
  43. package/service/src/public/atomic-json.ts +32 -0
  44. package/service/src/public/client.ts +277 -0
  45. package/service/src/public/protocol.ts +169 -0
  46. package/service/src/publish/publish.ts +855 -0
  47. package/service/src/registry/registry.ts +246 -0
  48. package/service/src/security/security.ts +128 -0
  49. package/service/src/self-update/self-update.ts +148 -0
  50. package/service/src/setup/setup.ts +761 -0
  51. package/service/src/shared/atomic-json.ts +33 -0
  52. package/service/src/shared/cache.ts +21 -0
  53. package/service/src/shared/constants.ts +73 -0
  54. package/service/src/shared/log.ts +21 -0
  55. package/service/src/shared/paths.ts +88 -0
  56. package/service/src/shared/state.ts +15 -0
  57. package/service/src/shared/version.ts +46 -0
  58. package/service/test/advisories.test.ts +287 -0
  59. package/service/test/check.test.ts +368 -0
  60. package/service/test/cleanup.test.ts +220 -0
  61. package/service/test/cli.test.ts +1303 -0
  62. package/service/test/core.test.ts +181 -0
  63. package/service/test/daemon-kit-migration.test.ts +181 -0
  64. package/service/test/daemon-service.test.ts +238 -0
  65. package/service/test/db.test.ts +178 -0
  66. package/service/test/doctor.test.ts +234 -0
  67. package/service/test/domain.test.ts +291 -0
  68. package/service/test/fixtures/install-validation/broken-package/extension/index.ts +3 -0
  69. package/service/test/fixtures/install-validation/broken-package/package.json +8 -0
  70. package/service/test/fixtures/install-validation/healthy-package/extension/index.ts +3 -0
  71. package/service/test/fixtures/install-validation/healthy-package/package.json +8 -0
  72. package/service/test/fixtures/install-validation/no-manifest-package/package.json +5 -0
  73. package/service/test/index.test.ts +353 -0
  74. package/service/test/install-validation.test.ts +114 -0
  75. package/service/test/install.test.ts +113 -0
  76. package/service/test/log.test.ts +42 -0
  77. package/service/test/pack-score.test.ts +513 -0
  78. package/service/test/pi-version.test.ts +318 -0
  79. package/service/test/public-boundary.test.ts +54 -0
  80. package/service/test/public-client.test.ts +127 -0
  81. package/service/test/public-consumer.ts +8 -0
  82. package/service/test/publish.test.ts +333 -0
  83. package/service/test/registry-contract.test.ts +148 -0
  84. package/service/test/resources.test.ts +255 -0
  85. package/service/test/security.test.ts +89 -0
  86. package/service/test/self-update.test.ts +257 -0
  87. package/service/test/service.test.ts +555 -0
  88. package/service/test/setup.test.ts +375 -0
  89. package/service/test/smoke.test.ts +118 -0
  90. package/service/test/version.test.ts +37 -0
  91. package/service/tsconfig.consumer.json +13 -0
  92. package/service/tsconfig.public.json +12 -0
  93. /package/extension/src/{reload.ts → approval/reload.ts} +0 -0
  94. /package/extension/src/{discover-model.ts → tabs/discover-model.ts} +0 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "./schema/pi-setup-v1.schema.json",
3
+ "schemaVersion": 1,
4
+ "packages": [
5
+ { "kind": "npm", "scope": "global", "source": "npm:@danypops/pi-packed", "resolved": "0.5.3", "integrity": "sha512-n1qLFkEanvODh7QcoFdODCvuuAuKR9G8rMmF9RR9YTOvgqGYSCYL5XhFnECrODewqF+JrNFN9+SfgCJc5DiyAw==" },
6
+ { "kind": "npm", "scope": "global", "source": "npm:@danypops/pi-papyrus", "resolved": "0.38.0", "integrity": "sha512-M5wV8X6kpKNXCS5kO7XifG+y4i2/6DNe6pYky5fcIbK7CpmAD4NIUVcxQMW2S9/03K5Oe589vKKfGlStvMz6uw==" },
7
+ { "kind": "npm", "scope": "global", "source": "npm:@danypops/pi-lector", "resolved": "0.3.0", "integrity": "sha512-vis+YRDLUyl1joGDP8pck6zG4EHX1nRw6LLkjKwV7CxMZnvBh+FLVNDBv+0s6hKJU6gal5qP0YEOCU24I7VcxA==" },
8
+ { "kind": "npm", "scope": "global", "source": "npm:@danypops/pi-pipes", "resolved": "0.8.1", "integrity": "sha512-Niu0DtG/usPdsQrsUGKhTjNrZR8HxAlc43sMMeLz7mzxC+MKlVFD6wmDgnAXPxyWGQtPKI78ZpzSDrkEobHZCQ==" },
9
+ { "kind": "npm", "scope": "global", "source": "npm:@danypops/pi-tickets", "resolved": "0.5.2", "integrity": "sha512-6ipP4jtcMhQD61CVLWW1PYk6iA2ormUt5t1IGPgUOF4K3yK8xr+M+T3VA0ByXamG4vuE/dZ6b3cLm7qnn3YDOA==" },
10
+ { "kind": "npm", "scope": "global", "source": "npm:@danypops/pi-jittor", "resolved": "0.1.0", "integrity": "sha512-CMyr1tzeop1OYaLJ6ItzZSShzu+0AGWekigFTLZRHG1sI9gVfYVCVYWYjbuEMYuDjtr8/4wEnj9OI5MvrWO9aw==" },
11
+ { "kind": "npm", "scope": "global", "source": "npm:@danypops/pi-web-spider", "resolved": "0.13.0", "integrity": "sha512-n6j/7enB4wUCeQVIIeFDUxeyk/SkV9lxJyQA+KYYQGDSCQd6Wr+ZbtVgM73X5quWPZMCB5akzxO0m3ZU92xNfg==" },
12
+ { "kind": "npm", "scope": "global", "source": "npm:@danypops/enigma", "resolved": "0.22.0", "integrity": "sha512-l/zLpNfaN4MQi5EDTa7IVka8daYk7ScWxDUUqFSUBSoQ2b0AOxtoY5JtdC6JRLcXSNhq9KqXNmJ1VSkf2HAC+w==" }
13
+ ],
14
+ "profiles": {}
15
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * advisories.ts — vulnerability advisory scanning for installed Pi
3
+ * packages. Nothing in Pi or Packed today checks installed dependency
4
+ * trees against known advisories; a third party (exitcode0.net's
5
+ * dep-audit) proved the lockfile-free approach works: batch installed
6
+ * versions per package against npm's own bulk advisory endpoint. Zero
7
+ * code execution, one bounded/timed-out network call for the scan itself.
8
+ */
9
+ import { compare, satisfies, valid } from "semver";
10
+ import { readInstalledPackages, readResolvedVersion } from "../packages/installed.ts";
11
+ import { NPM_REGISTRY_BASE, REGISTRY_FETCH_TIMEOUT_MS } from "../shared/constants.ts";
12
+ import type { Diagnostic } from "./check.ts";
13
+
14
+ const BULK_ADVISORY_PATH = "/-/npm/v1/security/advisories/bulk";
15
+ const MAX_PACKAGES_PER_SCAN = 200;
16
+ const MAX_ADVISORIES_PER_PACKAGE = 25;
17
+ const MAX_FINDINGS = 200;
18
+ const MAX_BULK_RESPONSE_BYTES = 2 * 1024 * 1024;
19
+ const MAX_PACKUMENT_BYTES = 4 * 1024 * 1024;
20
+ const SEMVER_OPTS = { loose: true, includePrerelease: true } as const;
21
+ /** A patch published within this window is weaker assurance than one that
22
+ * has aged without a new report -- not yet enough time for the community
23
+ * to catch a bad or incomplete fix (the "poisoned patch" pattern). This is
24
+ * Packed's own threshold, not copied from a source verified at write time;
25
+ * documented explicitly so it can be revisited. */
26
+ export const PATCHED_VERSION_FRESH_DAYS = 7;
27
+
28
+ export type AdvisorySeverity = "critical" | "high" | "moderate" | "low";
29
+
30
+ export interface RawAdvisory {
31
+ id: number | string;
32
+ url: string;
33
+ title: string;
34
+ severity: AdvisorySeverity;
35
+ vulnerableVersions: string;
36
+ }
37
+
38
+ export interface PatchedVersionAge {
39
+ version: string;
40
+ publishedAt: string;
41
+ ageDays: number;
42
+ /** true when ageDays < PATCHED_VERSION_FRESH_DAYS -- an explicit, separate
43
+ * signal, never folded into severity. */
44
+ fresh: boolean;
45
+ }
46
+
47
+ export interface AdvisoryFinding {
48
+ packageName: string;
49
+ installedVersion: string;
50
+ id: number | string;
51
+ url: string;
52
+ title: string;
53
+ severity: AdvisorySeverity;
54
+ vulnerableVersions: string;
55
+ /** undefined when no published version outside the vulnerable range
56
+ * could be found (registry lookup failed, or truly nothing fixes it
57
+ * yet) -- never guessed. */
58
+ patchedVersionAge?: PatchedVersionAge;
59
+ }
60
+
61
+ export interface AdvisoryReport {
62
+ scanned: number;
63
+ findings: AdvisoryFinding[];
64
+ diagnostics: Diagnostic[];
65
+ truncated: boolean;
66
+ }
67
+
68
+ /** Resolves the real, on-disk installed version for every npm-sourced Pi
69
+ * package (or just one, when named) -- ground truth from node_modules,
70
+ * never a trusted-but-unverified declared pin. Silently omits a package
71
+ * whose version can't be resolved rather than guessing. git:/local
72
+ * sources are omitted (no meaningful npm advisory lookup applies). */
73
+ export function resolveInstalledVersions(piHome: string, only?: string): Record<string, string> {
74
+ const names = readInstalledPackages(piHome)
75
+ .map((pkg) => pkg.name)
76
+ .filter((name) => !only || name === only);
77
+ const out: Record<string, string> = {};
78
+ for (const name of names) {
79
+ const version = readResolvedVersion(piHome, `npm:${name}`);
80
+ if (version) out[name] = version;
81
+ }
82
+ return out;
83
+ }
84
+
85
+ function boundedBodyJson(text: string, maxBytes: number): unknown {
86
+ if (Buffer.byteLength(text, "utf8") > maxBytes) throw new Error("response exceeded bound");
87
+ return JSON.parse(text);
88
+ }
89
+
90
+ /** One bounded, timed-out POST against npm's real bulk advisory endpoint --
91
+ * lockfile-free by design, matching how Pi itself installs packages (no
92
+ * package-lock.json to audit against). Never throws: any failure (network,
93
+ * timeout, malformed response) resolves to {}, so a scan degrades to "no
94
+ * known advisories" rather than crashing or reporting false negatives as
95
+ * if they were verified-clean. */
96
+ export async function fetchBulkAdvisories(
97
+ packages: Record<string, string[]>,
98
+ options: { registryBase?: string; timeoutMs?: number } = {},
99
+ ): Promise<Record<string, RawAdvisory[]>> {
100
+ if (Object.keys(packages).length === 0) return {};
101
+ try {
102
+ const response = await fetch(`${options.registryBase ?? NPM_REGISTRY_BASE}${BULK_ADVISORY_PATH}`, {
103
+ method: "POST",
104
+ headers: { "content-type": "application/json", accept: "application/json" },
105
+ body: JSON.stringify(packages),
106
+ signal: AbortSignal.timeout(options.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS),
107
+ });
108
+ if (!response.ok) return {};
109
+ const text = await response.text();
110
+ const data = boundedBodyJson(text, MAX_BULK_RESPONSE_BYTES) as Record<string, unknown>;
111
+ const out: Record<string, RawAdvisory[]> = {};
112
+ for (const [name, entries] of Object.entries(data)) {
113
+ if (!Array.isArray(entries)) continue;
114
+ out[name] = entries
115
+ .slice(0, MAX_ADVISORIES_PER_PACKAGE)
116
+ .map((raw): RawAdvisory | undefined => {
117
+ const entry = raw as Record<string, unknown>;
118
+ if (typeof entry.id !== "number" && typeof entry.id !== "string") return undefined;
119
+ if (typeof entry.vulnerable_versions !== "string") return undefined;
120
+ const severity: AdvisorySeverity =
121
+ entry.severity === "critical" || entry.severity === "high" || entry.severity === "moderate" || entry.severity === "low"
122
+ ? entry.severity
123
+ : "high";
124
+ return {
125
+ id: entry.id,
126
+ url: typeof entry.url === "string" ? entry.url.slice(0, 2_048) : "",
127
+ title: typeof entry.title === "string" ? entry.title.slice(0, 512) : "unknown advisory",
128
+ severity,
129
+ vulnerableVersions: entry.vulnerable_versions.slice(0, 512),
130
+ };
131
+ })
132
+ .filter((entry): entry is RawAdvisory => entry !== undefined);
133
+ }
134
+ return out;
135
+ } catch {
136
+ return {};
137
+ }
138
+ }
139
+
140
+ /** Fetches only what's needed from the full packument (version list + publish
141
+ * times) to compute a patched-version-age heuristic -- one bounded,
142
+ * best-effort GET per package that actually has an advisory, never per
143
+ * package scanned. Never throws. */
144
+ async function fetchPackageTimes(
145
+ name: string,
146
+ registryBase: string,
147
+ timeoutMs: number,
148
+ ): Promise<{ versions: string[]; time: Record<string, string> } | undefined> {
149
+ try {
150
+ const encoded = encodeURIComponent(name).replace("%2F", "/");
151
+ const response = await fetch(`${registryBase}/${encoded}`, {
152
+ headers: { accept: "application/json" },
153
+ signal: AbortSignal.timeout(timeoutMs),
154
+ });
155
+ if (!response.ok) return undefined;
156
+ const text = await response.text();
157
+ const data = boundedBodyJson(text, MAX_PACKUMENT_BYTES) as { versions?: Record<string, unknown>; time?: Record<string, unknown> };
158
+ if (!data.versions || !data.time) return undefined;
159
+ const time: Record<string, string> = {};
160
+ for (const [version, value] of Object.entries(data.time)) if (typeof value === "string") time[version] = value;
161
+ return { versions: Object.keys(data.versions).filter((v) => valid(v)), time };
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ }
166
+
167
+ /** The smallest published version that is both newer than the installed one
168
+ * and outside the advisory's vulnerable range -- "the fix", derived the same
169
+ * way npm's own arborist re-resolves an avoided range, not from a
170
+ * patched_versions field (the bulk endpoint doesn't return one; only the
171
+ * full legacy audit report does). */
172
+ function findPatchedVersion(installedVersion: string, vulnerableVersions: string, versions: string[]): string | undefined {
173
+ const candidates = versions
174
+ .filter((v) => valid(v) && compare(v, installedVersion) > 0 && !satisfies(v, vulnerableVersions, SEMVER_OPTS))
175
+ .sort(compare);
176
+ return candidates[0];
177
+ }
178
+
179
+ function diagnosticFor(finding: AdvisoryFinding): Diagnostic {
180
+ const severityMap: Record<AdvisorySeverity, Diagnostic["severity"]> = {
181
+ critical: "error",
182
+ high: "error",
183
+ moderate: "warning",
184
+ low: "info",
185
+ };
186
+ const freshNote = finding.patchedVersionAge
187
+ ? finding.patchedVersionAge.fresh
188
+ ? ` (patched in ${finding.patchedVersionAge.version}, published ${finding.patchedVersionAge.ageDays}d ago -- recent enough that the fix itself is a weaker signal)`
189
+ : ` (patched in ${finding.patchedVersionAge.version}, published ${finding.patchedVersionAge.ageDays}d ago)`
190
+ : "";
191
+ return {
192
+ code: "PI_PACKAGE_ADVISORY",
193
+ severity: severityMap[finding.severity],
194
+ path: finding.packageName,
195
+ message: `${finding.title} (${finding.severity}, ${finding.id}) affects installed ${finding.installedVersion}; vulnerable range: ${finding.vulnerableVersions}${freshNote}`,
196
+ ...(finding.patchedVersionAge ? { fix: `upgrade to ${finding.patchedVersionAge.version}` } : {}),
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Scans a bounded set of installed packages (name -> resolved version)
202
+ * against npm's bulk advisory endpoint. No lockfile required, matching how
203
+ * Pi itself installs packages. The patched-version-age heuristic is always
204
+ * a separate, explicit field on each finding -- never silently folded into
205
+ * severity, since a fresh patch and a mature one carry genuinely different
206
+ * trust even at the same advisory severity.
207
+ */
208
+ export async function scanInstalledPackages(
209
+ installed: Record<string, string>,
210
+ options: { registryBase?: string; timeoutMs?: number } = {},
211
+ ): Promise<AdvisoryReport> {
212
+ const registryBase = options.registryBase ?? NPM_REGISTRY_BASE;
213
+ const timeoutMs = options.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS;
214
+ const entries = Object.entries(installed).slice(0, MAX_PACKAGES_PER_SCAN);
215
+ const truncatedByCount = Object.keys(installed).length > entries.length;
216
+ const bulkInput: Record<string, string[]> = {};
217
+ for (const [name, version] of entries) bulkInput[name] = [version];
218
+ const advisoriesByPackage = await fetchBulkAdvisories(bulkInput, { registryBase, timeoutMs });
219
+
220
+ const findings: AdvisoryFinding[] = [];
221
+ let truncated = truncatedByCount;
222
+ for (const [name, version] of entries) {
223
+ const advisories = advisoriesByPackage[name];
224
+ if (!advisories || advisories.length === 0) continue;
225
+ let times: { versions: string[]; time: Record<string, string> } | undefined;
226
+ for (const advisory of advisories) {
227
+ if (findings.length >= MAX_FINDINGS) {
228
+ truncated = true;
229
+ break;
230
+ }
231
+ let patchedVersionAge: PatchedVersionAge | undefined;
232
+ if (times === undefined) times = (await fetchPackageTimes(name, registryBase, timeoutMs)) ?? { versions: [], time: {} };
233
+ const patched = findPatchedVersion(version, advisory.vulnerableVersions, times.versions);
234
+ if (patched && times.time[patched]) {
235
+ const publishedAt = times.time[patched]!;
236
+ const ageDays = Math.max(0, Math.floor((Date.now() - Date.parse(publishedAt)) / (24 * 60 * 60 * 1000)));
237
+ patchedVersionAge = { version: patched, publishedAt, ageDays, fresh: ageDays < PATCHED_VERSION_FRESH_DAYS };
238
+ }
239
+ findings.push({
240
+ packageName: name,
241
+ installedVersion: version,
242
+ id: advisory.id,
243
+ url: advisory.url,
244
+ title: advisory.title,
245
+ severity: advisory.severity,
246
+ vulnerableVersions: advisory.vulnerableVersions,
247
+ patchedVersionAge,
248
+ });
249
+ }
250
+ if (findings.length >= MAX_FINDINGS) break;
251
+ }
252
+
253
+ return { scanned: entries.length, findings, diagnostics: findings.map(diagnosticFor), truncated };
254
+ }
255
+
256
+ const MAX_ADVISORY_OUTPUT = 16 * 1024;
257
+
258
+ /** Same rendering shape as check.ts's formatCheckReport -- one bounded,
259
+ * consistent diagnostic envelope across Packed, not a new ad hoc format. */
260
+ export function formatAdvisoryReport(report: AdvisoryReport, json: boolean): string {
261
+ if (json) return `${JSON.stringify(report)}\n`;
262
+ if (report.diagnostics.length === 0) return `no known advisories for ${report.scanned} scanned package(s)\n`;
263
+ let output = `${report.diagnostics.length} advisory finding(s) across ${report.scanned} scanned package(s)\n`;
264
+ for (const diagnostic of report.diagnostics)
265
+ output += `${diagnostic.severity.toUpperCase()} ${diagnostic.path}: ${diagnostic.message}${diagnostic.fix ? ` Fix: ${diagnostic.fix}` : ""}\n`;
266
+ if (report.truncated) output += "Output truncated by configured bounds.\n";
267
+ return output.slice(0, MAX_ADVISORY_OUTPUT);
268
+ }