promptkit-os 1.10.1 → 1.11.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.
@@ -9,6 +9,7 @@
9
9
  "use strict";
10
10
 
11
11
  const { spawnSync } = require("node:child_process");
12
+ const crypto = require("node:crypto");
12
13
  const fs = require("node:fs");
13
14
  const path = require("node:path");
14
15
  const os = require("node:os");
@@ -17,6 +18,32 @@ const PKG = require("../package.json");
17
18
  const REPO = "lowqualityloey/promptkit-os";
18
19
  const KIT_DIR = ".promptkit";
19
20
 
21
+ // Pinned SHA-256 (hex) of the GitHub release tarball per courier version.
22
+ // This lives here — not in a sidecar file — because release-npm.yml enforces an
23
+ // npm-pack allowlist of exactly { package.json, README.md, bin/promptkit-os.js };
24
+ // a sidecar would either be excluded from the published package (useless) or
25
+ // require a workflow change to allowlist.
26
+ //
27
+ // UPDATE INSTRUCTION FOR RELEASES: after pushing tag vX.Y.Z (and before the npm
28
+ // publish), download the immutable release tarball, hash it, and add the entry:
29
+ // curl -sL "https://github.com/lowqualityloey/promptkit-os/archive/refs/tags/vX.Y.Z.tar.gz" -o /tmp/release.tar.gz
30
+ // sha256sum /tmp/release.tar.gz
31
+ // then add `"X.Y.Z": "<64-hex-chars>",` below so the courier verifies that exact
32
+ // tarball before extracting or executing anything from it. If no pin exists for
33
+ // the running version the courier warns loudly and continues (pins can only be
34
+ // minted after the tag exists); set PROMPTKIT_REQUIRE_INTEGRITY_PIN=1 to fail
35
+ // closed instead. PROMPTKIT_TARBALL_SHA256 overrides the map (testing / rotation).
36
+ //
37
+ // CAVEAT: these digests pin GitHub's *generated* archive for a tag. That archive
38
+ // is reproducible today (verified by double download when each pin was minted)
39
+ // but is not contractually immutable — a future GitHub-side change to archive
40
+ // generation would change the bytes and make this pin reject an untampered
41
+ // download. On mismatch, confirm the release tag and the release evidence
42
+ // record before treating it as tampering; rotate with PROMPTKIT_TARBALL_SHA256.
43
+ const TARBALL_SHA256_BY_VERSION = {
44
+ "1.10.1": "1e55459914a1c46aa71df8f9cb8ce4c9ff37d1c02d67f70e5607b3bfd4f48d4b",
45
+ };
46
+
20
47
  function die(msg) {
21
48
  process.stderr.write(`[promptkit-os] ${msg}\n`);
22
49
  process.exit(1);
@@ -78,6 +105,30 @@ function projectRootFrom(args) {
78
105
  return path.resolve(positional[0] || process.cwd());
79
106
  }
80
107
 
108
+ function expectedTarballDigest(version) {
109
+ const override = process.env.PROMPTKIT_TARBALL_SHA256;
110
+ if (override !== undefined && override !== "") return override;
111
+ return TARBALL_SHA256_BY_VERSION[version];
112
+ }
113
+
114
+ // Aborts (non-zero exit, no extract, no exec) unless `tarball` hashes to
115
+ // `expectedHex`. Comparison is constant-time so a MITM learns nothing about the
116
+ // pin from timing. Must be called after download() and before extract().
117
+ function verifyTarballIntegrity(tarball, expectedHex, version) {
118
+ const actual = crypto.createHash("sha256").update(tarball).digest();
119
+ const expected = Buffer.from(String(expectedHex), "hex");
120
+ // Buffer.from(hex) never throws on bad input — it truncates — so the length
121
+ // check below is what turns a malformed pin into a mismatch (fail closed).
122
+ if (expected.length !== actual.length || !crypto.timingSafeEqual(actual, expected)) {
123
+ die(
124
+ `tarball integrity mismatch for v${version} ` +
125
+ `(expected sha256 ${expectedHex}, got ${actual.toString("hex")}): ` +
126
+ "refusing to extract or execute — the download may have been tampered with. " +
127
+ "If this version was just released, the pin in TARBALL_SHA256_BY_VERSION needs updating (see above)."
128
+ );
129
+ }
130
+ }
131
+
81
132
  async function download(url) {
82
133
  process.stderr.write(`[promptkit-os] fetching ${url}\n`);
83
134
  const res = await fetch(url, { redirect: "follow" });
@@ -150,10 +201,29 @@ async function main() {
150
201
  fs.mkdirSync(kitDir, { recursive: true });
151
202
 
152
203
  const tarball = await download(url);
204
+ const expectedDigest = expectedTarballDigest(version);
205
+ if (expectedDigest) {
206
+ verifyTarballIntegrity(tarball, expectedDigest, version);
207
+ process.stderr.write(`[promptkit-os] verified release tarball integrity (sha256) for v${version}\n`);
208
+ } else if (process.env.PROMPTKIT_REQUIRE_INTEGRITY_PIN === "1") {
209
+ die(
210
+ `no integrity pin for v${version} — refusing to extract or execute. ` +
211
+ "Add the sha256 of the release tarball to TARBALL_SHA256_BY_VERSION (see above)."
212
+ );
213
+ } else {
214
+ process.stderr.write(
215
+ `[promptkit-os] warning: no integrity pin for v${version}; ` +
216
+ "proceeding unverified — set PROMPTKIT_REQUIRE_INTEGRITY_PIN=1 to fail closed.\n"
217
+ );
218
+ }
153
219
  await extract(tarball, kitDir);
154
220
  process.stderr.write(`[promptkit-os] extracted release v${version} into ${KIT_DIR}/\n`);
155
221
 
156
222
  runInstaller(kitDir, args);
157
223
  }
158
224
 
159
- main().catch((err) => die(err && err.message ? err.message : String(err)));
225
+ if (require.main === module) {
226
+ main().catch((err) => die(err && err.message ? err.message : String(err)));
227
+ } else {
228
+ module.exports = { verifyTarballIntegrity, expectedTarballDigest, TARBALL_SHA256_BY_VERSION };
229
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptkit-os",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "Courier for PromptKit OS: fetches a pinned release tarball and runs the canonical installer. Not a runtime dependency.",
5
5
  "keywords": [
6
6
  "promptkit",