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