@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.
- package/package.json +3 -3
- 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.
|
|
4
|
-
"description": "Pulsar OTA release CLI
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
161
|
-
res.
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
}
|