@novacraft-engineering/pulsar-cli 1.2.0 → 1.2.1

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 +46 -31
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novacraft-engineering/pulsar-cli",
3
- "version": "1.2.0",
4
- "description": "Pulsar OTA release CLI — bundle, hash, and ship React Native updates over-the-air.",
3
+ "version": "1.2.1",
4
+ "description": "Pulsar OTA release CLI \u2014 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"
29
+ "test": "node test/hash.test.js && node test/security.test.js"
30
30
  },
31
31
  "keywords": [
32
32
  "react-native",
package/pulsar.js CHANGED
@@ -23,7 +23,7 @@ function cmdKeygen() {
23
23
  publicKeyEncoding: {type: 'spki', format: 'pem'},
24
24
  privateKeyEncoding: {type: 'pkcs8', format: 'pem'},
25
25
  });
26
- fs.writeFileSync('pulsar-private.pem', privateKey);
26
+ fs.writeFileSync('pulsar-private.pem', privateKey, {mode: 0o600});
27
27
  fs.writeFileSync('pulsar-public.pem', publicKey);
28
28
  console.log('✓ wrote pulsar-private.pem (keep secret, use with --private-key) and pulsar-public.pem');
29
29
  console.log(' Put the public key in Info.plist <key>CodePushPublicKey</key> to require signed updates.');
@@ -41,8 +41,11 @@ function loadConfig() {
41
41
  }
42
42
 
43
43
  function saveConfig(config) {
44
- fs.mkdirSync(CONFIG_DIR, {recursive: true});
45
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
44
+ // The config holds a bearer token — keep the dir and file owner-only so other local users on a
45
+ // shared host can't read the session.
46
+ fs.mkdirSync(CONFIG_DIR, {recursive: true, mode: 0o700});
47
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {mode: 0o600});
48
+ try { fs.chmodSync(CONFIG_FILE, 0o600); } catch { /* best-effort on pre-existing files */ }
46
49
  }
47
50
 
48
51
  async function api(pathname, {method = 'GET', body, headers = {}, raw, allowAnonymous, onUploadProgress} = {}) {
@@ -153,19 +156,26 @@ function dashboardUrl(apiUrl) {
153
156
  // Browser login: spin up a throwaway localhost server, open the dashboard's /cli-login page which
154
157
  // (once you're signed in) redirects back here with a token. No password ever touches the terminal.
155
158
  function browserLogin(apiUrl) {
159
+ // A random nonce ties the callback to THIS login: the dashboard echoes it back and we reject any
160
+ // callback whose state doesn't match, so a stray page spraying the loopback port range can't inject
161
+ // its own token into our config (login fixation).
162
+ const state = crypto.randomUUID();
156
163
  return new Promise((resolve, reject) => {
157
164
  const server = http.createServer((req, res) => {
158
165
  const requestUrl = new URL(req.url, 'http://127.0.0.1');
166
+ if (requestUrl.pathname !== '/callback') { res.writeHead(204); res.end(); return; }
159
167
  const token = requestUrl.searchParams.get('token');
160
- res.writeHead(token ? 200 : 400, {'content-type': 'text/html'});
161
- res.end(`<!doctype html><meta charset="utf-8"><body style="font-family:system-ui;background:#080a0f;color:#eef2f7;display:grid;place-items:center;height:100vh;margin:0"><div style="text-align:center"><h2 style="font-weight:600">${token ? 'Pulsar CLI authorized ✓' : 'No token received'}</h2><p style="color:#8f98a6">You can close this tab and return to your terminal.</p></div></body>`);
168
+ const ok = token && requestUrl.searchParams.get('state') === state;
169
+ res.writeHead(ok ? 200 : 400, {'content-type': 'text/html'});
170
+ res.end(`<!doctype html><meta charset="utf-8"><body style="font-family:system-ui;background:#080a0f;color:#eef2f7;display:grid;place-items:center;height:100vh;margin:0"><div style="text-align:center"><h2 style="font-weight:600">${ok ? 'Pulsar CLI authorized ✓' : 'Authorization failed'}</h2><p style="color:#8f98a6">You can close this tab and return to your terminal.</p></div></body>`);
171
+ if (!ok) return;
162
172
  setTimeout(() => { try { server.close(); } catch {} }, 100);
163
- if (token) resolve(token); else reject(new Error('No token received from the browser'));
173
+ resolve(token);
164
174
  });
165
175
  server.on('error', reject);
166
176
  server.listen(0, '127.0.0.1', () => {
167
177
  const {port} = server.address();
168
- const authUrl = `${dashboardUrl(apiUrl)}/cli-login?port=${port}`;
178
+ const authUrl = `${dashboardUrl(apiUrl)}/cli-login?port=${port}&state=${state}`;
169
179
  console.log(`\n Authorize the CLI in your browser:\n ${authUrl}\n`);
170
180
  openBrowser(authUrl);
171
181
  console.log(' Waiting for authorization…');
@@ -511,28 +521,33 @@ async function cmdKeys(args) {
511
521
  console.log(`✓ wrote ${Object.keys(vars).length} key(s) to ${target} (${written.size} updated, ${missing.length} added)`);
512
522
  }
513
523
 
514
- const [command, ...rest] = process.argv.slice(2);
515
- const commands = {
516
- login: cmdLogin,
517
- apps: cmdApps,
518
- release: cmdRelease,
519
- 'release-train': cmdReleaseTrain,
520
- promote: cmdPromote,
521
- rollback: cmdRollback,
522
- deployment: cmdDeployment,
523
- history: cmdHistory,
524
- patch: cmdPatch,
525
- keygen: cmdKeygen,
526
- keys: cmdKeys,
527
- };
528
-
529
- const ANONYMOUS_COMMANDS = new Set(['login', 'keygen']);
530
-
531
- if (commands[command] && !ANONYMOUS_COMMANDS.has(command) && !loadConfig().token) {
532
- die('Not authenticated. Run: pulsar login <apiUrl>');
533
- }
524
+ export {saveConfig, browserLogin};
525
+
526
+ // Only dispatch when run as the CLI — importing the module (e.g. from tests) must not execute a command.
527
+ if (import.meta.url === `file://${process.argv[1]}`) {
528
+ const [command, ...rest] = process.argv.slice(2);
529
+ const commands = {
530
+ login: cmdLogin,
531
+ apps: cmdApps,
532
+ release: cmdRelease,
533
+ 'release-train': cmdReleaseTrain,
534
+ promote: cmdPromote,
535
+ rollback: cmdRollback,
536
+ deployment: cmdDeployment,
537
+ history: cmdHistory,
538
+ patch: cmdPatch,
539
+ keygen: cmdKeygen,
540
+ keys: cmdKeys,
541
+ };
542
+
543
+ const ANONYMOUS_COMMANDS = new Set(['login', 'keygen']);
534
544
 
535
- (commands[command] || (() => {
536
- console.log('pulsar <login|apps|keys|release|release-train|promote|rollback|deployment|history|patch|keygen>');
537
- process.exit(command ? 1 : 0);
538
- }))(rest);
545
+ if (commands[command] && !ANONYMOUS_COMMANDS.has(command) && !loadConfig().token) {
546
+ die('Not authenticated. Run: pulsar login <apiUrl>');
547
+ }
548
+
549
+ (commands[command] || (() => {
550
+ console.log('pulsar <login|apps|keys|release|release-train|promote|rollback|deployment|history|patch|keygen>');
551
+ process.exit(command ? 1 : 0);
552
+ }))(rest);
553
+ }