@novacraft-engineering/pulsar-cli 1.2.2 → 1.2.3

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 (2) hide show
  1. package/package.json +3 -3
  2. package/pulsar.js +23 -6
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novacraft-engineering/pulsar-cli",
3
- "version": "1.2.2",
4
- "description": "Pulsar OTA release CLI \u2014 bundle, hash, and ship React Native updates over-the-air.",
3
+ "version": "1.2.3",
4
+ "description": "Pulsar OTA release CLI — bundle, hash, and ship React Native updates over-the-air.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pulsar": "./pulsar.js"
@@ -26,7 +26,7 @@
26
26
  "node": ">=18"
27
27
  },
28
28
  "scripts": {
29
- "test": "node test/hash.test.js && node test/security.test.js"
29
+ "test": "node test/hash.test.js && node test/security.test.js && node test/signing.test.js"
30
30
  },
31
31
  "keywords": [
32
32
  "cli",
package/pulsar.js CHANGED
@@ -9,7 +9,10 @@ import crypto from 'node:crypto';
9
9
  import {computePackageHash, fileHashes} from './hash.js';
10
10
 
11
11
  // Code signing: sign the package hash into an RS256 JWT the client verifies against its
12
- // CodePushPublicKey (Info.plist). Written to `.codepushrelease` inside the bundle.
12
+ // CodePushPublicKey (Info.plist). Both native SDKs read it from <update>/CodePush/.codepushrelease
13
+ // and nowhere else; a signature anywhere else counts as no signature.
14
+ const SIGNATURE_PATH = 'CodePush/.codepushrelease';
15
+
13
16
  function signCodePushJWT(privateKeyPem, contentHash) {
14
17
  const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
15
18
  const signingInput = `${encode({alg: 'RS256', typ: 'JWT'})}.${encode({contentHash})}`;
@@ -246,8 +249,11 @@ async function buildDiffPackage(baseZip, newDir) {
246
249
  fs.writeFileSync(path.join(diffDir, 'hotcodepush.json'), JSON.stringify({deletedFiles}));
247
250
  // The signature isn't part of the content hash, so it's excluded above — but the patched bundle
248
251
  // still needs it to verify, so carry it into the diff when the release is signed.
249
- const signature = path.join(newDir, '.codepushrelease');
250
- if (fs.existsSync(signature)) fs.copyFileSync(signature, path.join(diffDir, '.codepushrelease'));
252
+ const signature = path.join(newDir, SIGNATURE_PATH);
253
+ if (fs.existsSync(signature)) {
254
+ fs.mkdirSync(path.dirname(path.join(diffDir, SIGNATURE_PATH)), {recursive: true});
255
+ fs.copyFileSync(signature, path.join(diffDir, SIGNATURE_PATH));
256
+ }
251
257
  return diffDir;
252
258
  }
253
259
 
@@ -417,9 +423,20 @@ function buildOnce(args) {
417
423
  const contentsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulsar-build-'));
418
424
  bundle(platform, contentsDir);
419
425
  const packageHash = computePackageHash(contentsDir);
426
+ // CI secrets arrive as environment variables holding the key itself, not as files, so the key can
427
+ // come from PULSAR_SIGNING_KEY as well as --private-key <file>. An unset or empty secret means
428
+ // "don't sign", exactly as before.
420
429
  const privateKeyPath = getFlag(args, 'private-key');
421
- if (privateKeyPath) {
422
- fs.writeFileSync(path.join(contentsDir, '.codepushrelease'), signCodePushJWT(fs.readFileSync(privateKeyPath, 'utf8'), packageHash));
430
+ const signingKey = privateKeyPath ? fs.readFileSync(privateKeyPath, 'utf8') : (process.env.PULSAR_SIGNING_KEY || '').trim();
431
+ if (signingKey) {
432
+ let signature;
433
+ try {
434
+ signature = signCodePushJWT(signingKey, packageHash);
435
+ } catch (error) {
436
+ die(`could not sign with the provided key (${error.message}) — nothing was uploaded`);
437
+ }
438
+ fs.mkdirSync(path.join(contentsDir, 'CodePush'), {recursive: true});
439
+ fs.writeFileSync(path.join(contentsDir, SIGNATURE_PATH), signature);
423
440
  console.log('• signed release — devices with CodePushPublicKey set will verify it');
424
441
  }
425
442
  const zip = fs.readFileSync(zipContents(contentsDir));
@@ -594,7 +611,7 @@ async function cmdKeys(args) {
594
611
  console.log(`✓ wrote ${Object.keys(vars).length} key(s) to ${target} (${written.size} updated, ${missing.length} added)`);
595
612
  }
596
613
 
597
- export {saveConfig, browserLogin};
614
+ export {saveConfig, browserLogin, signCodePushJWT, SIGNATURE_PATH};
598
615
 
599
616
  // Only dispatch when run as the CLI — importing the module (e.g. from tests) must not execute a command.
600
617
  if (import.meta.url === `file://${process.argv[1]}`) {