promptkit-os 1.11.0 → 2.0.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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ If `.promptkit/` already exists and is not empty, the courier **refuses to overl
28
28
  # git-submodule install
29
29
  git submodule update --remote --merge .promptkit && bash .promptkit/init.sh
30
30
 
31
- # courier install (tarball is pinned per version — replace it)
31
+ # courier install (replace the existing tree; new couriers verify the archive)
32
32
  rm -rf .promptkit
33
33
  npx promptkit-os@latest --balanced
34
34
  ```
@@ -37,7 +37,11 @@ The courier detects which case applies (a submodule install has `.promptkit/.git
37
37
 
38
38
  ## Version pinning
39
39
 
40
- `npx promptkit-os@X.Y.Z` fetches release tag `vX.Y.Z` and never a moving ref, so a given install is reproducible forever.
40
+ `npx promptkit-os@X.Y.Z` fetches the GitHub archive for tag `vX.Y.Z`, not a branch head. New couriers require a valid SHA-256 digest for their own version before download and verify the archive **before** creating `.promptkit/`, extracting files, or running the installer. The release workflow independently checks the tagged archive, injects the digest, and checks the actual npm-packed courier before publication. A missing or mismatched digest stops installation; it is not a warning-only path.
41
+
42
+ **Historical exception:** the already-published immutable npm courier `promptkit-os@1.11.0` did not contain its own archive pin and warns/proceeds by default. npm package integrity/provenance authenticates that npm tarball, **not** the second-stage GitHub archive it downloads. Existing installed files are not retroactively checked or changed by the new policy. For `1.11.0`, an operator can independently obtain a trusted archive SHA-256 and set both `PROMPTKIT_TARBALL_SHA256=<trusted-64-hex-sha256>` and `PROMPTKIT_REQUIRE_INTEGRITY_PIN=1` (in PowerShell, set `$env:PROMPTKIT_TARBALL_SHA256` and `$env:PROMPTKIT_REQUIRE_INTEGRITY_PIN`); the published courier verifies the override before extraction. New couriers also accept an explicitly supplied trusted `PROMPTKIT_TARBALL_SHA256` for archive rotation. The override is an **operator-controlled trust input**: do not derive it from the same untrusted download, and investigate a mismatch rather than disabling verification.
43
+
44
+ GitHub-generated tag archives are not guaranteed byte-for-byte immutable forever. A legitimate archive-regeneration change may require a separately verified override or a new courier release; a fixed tag name alone does not guarantee reproducible archive bytes.
41
45
 
42
46
  ## Removal
43
47
 
@@ -24,15 +24,11 @@ const KIT_DIR = ".promptkit";
24
24
  // a sidecar would either be excluded from the published package (useless) or
25
25
  // require a workflow change to allowlist.
26
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).
27
+ // Release pins are inserted and checked by the repository release workflow after
28
+ // it checks out the tag and before npm publication. Do not mint pins by hand. A
29
+ // missing or invalid own-version pin fails closed before download. An explicitly
30
+ // supplied PROMPTKIT_TARBALL_SHA256 overrides the map as an operator-controlled
31
+ // trust input for testing or archive rotation.
36
32
  //
37
33
  // CAVEAT: these digests pin GitHub's *generated* archive for a tag. That archive
38
34
  // is reproducible today (verified by double download when each pin was minted)
@@ -42,6 +38,9 @@ const KIT_DIR = ".promptkit";
42
38
  // record before treating it as tampering; rotate with PROMPTKIT_TARBALL_SHA256.
43
39
  const TARBALL_SHA256_BY_VERSION = {
44
40
  "1.10.1": "1e55459914a1c46aa71df8f9cb8ce4c9ff37d1c02d67f70e5607b3bfd4f48d4b",
41
+ "1.11.0": "6503fdfcde286055b8c053aa0ffa8bdf244cf77df3321cccd6a8d8f780195ade",
42
+ "2.0.0": "4fb5b9d85e0f19bfe333c361e00465705b60636a8026edd62c15c6f2d61cbf4b",
43
+ // @pin-insert
45
44
  };
46
45
 
47
46
  function die(msg) {
@@ -105,20 +104,28 @@ function projectRootFrom(args) {
105
104
  return path.resolve(positional[0] || process.cwd());
106
105
  }
107
106
 
107
+ function normalizedDigest(digest, version) {
108
+ if (typeof digest !== "string" || !/^[0-9a-fA-F]{64}$/.test(digest)) {
109
+ die(`invalid SHA-256 digest for v${version} — expected exactly 64 hexadecimal characters; refusing to download, extract or execute`);
110
+ }
111
+ return digest.toLowerCase();
112
+ }
113
+
108
114
  function expectedTarballDigest(version) {
109
115
  const override = process.env.PROMPTKIT_TARBALL_SHA256;
110
- if (override !== undefined && override !== "") return override;
111
- return TARBALL_SHA256_BY_VERSION[version];
116
+ const digest = override === undefined ? TARBALL_SHA256_BY_VERSION[version] : override;
117
+ if (digest === undefined) {
118
+ die(`no integrity pin for v${version} — refusing to download, extract or execute; supply a trusted PROMPTKIT_TARBALL_SHA256`);
119
+ }
120
+ return normalizedDigest(digest, version);
112
121
  }
113
122
 
114
123
  // 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().
124
+ // `expectedHex`. Comparison is constant-time. Must be called after download()
125
+ // and before extract().
117
126
  function verifyTarballIntegrity(tarball, expectedHex, version) {
127
+ const expected = Buffer.from(normalizedDigest(expectedHex, version), "hex");
118
128
  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
129
  if (expected.length !== actual.length || !crypto.timingSafeEqual(actual, expected)) {
123
130
  die(
124
131
  `tarball integrity mismatch for v${version} ` +
@@ -162,6 +169,10 @@ function runInstaller(kitDir, args) {
162
169
  }
163
170
  }
164
171
 
172
+ function installerArgs(args, version) {
173
+ return [...args, `--engine-version=v${version}`];
174
+ }
175
+
165
176
  async function main() {
166
177
  const version = PKG.version;
167
178
  if (version === "0.0.0") {
@@ -198,32 +209,20 @@ async function main() {
198
209
  const url = `https://github.com/${REPO}/archive/refs/tags/v${version}.tar.gz`;
199
210
  const kitDir = path.join(root, KIT_DIR);
200
211
  assertInstallTargetIsClean(kitDir, force);
201
- fs.mkdirSync(kitDir, { recursive: true });
202
212
 
203
- const tarball = await download(url);
204
213
  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
- }
214
+ const tarball = await download(url);
215
+ verifyTarballIntegrity(tarball, expectedDigest, version);
216
+ process.stderr.write(`[promptkit-os] verified release tarball integrity (sha256) for v${version}\n`);
217
+ fs.mkdirSync(kitDir, { recursive: true });
219
218
  await extract(tarball, kitDir);
220
219
  process.stderr.write(`[promptkit-os] extracted release v${version} into ${KIT_DIR}/\n`);
221
220
 
222
- runInstaller(kitDir, args);
221
+ runInstaller(kitDir, installerArgs(args, version));
223
222
  }
224
223
 
225
224
  if (require.main === module) {
226
225
  main().catch((err) => die(err && err.message ? err.message : String(err)));
227
226
  } else {
228
- module.exports = { verifyTarballIntegrity, expectedTarballDigest, TARBALL_SHA256_BY_VERSION };
227
+ module.exports = { verifyTarballIntegrity, expectedTarballDigest, TARBALL_SHA256_BY_VERSION, installerArgs };
229
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptkit-os",
3
- "version": "1.11.0",
3
+ "version": "2.0.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",