@scalebun/cli 1.0.0 → 1.0.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/lib/api/client.d.ts +2 -0
- package/lib/api/client.d.ts.map +1 -1
- package/lib/api/client.js +14 -0
- package/lib/api/client.js.map +1 -1
- package/lib/commands/ci-init.d.ts +0 -6
- package/lib/commands/ci-init.d.ts.map +1 -1
- package/lib/commands/ci-init.js +164 -29
- package/lib/commands/ci-init.js.map +1 -1
- package/lib/commands/doctor.d.ts +3 -0
- package/lib/commands/doctor.d.ts.map +1 -0
- package/lib/commands/doctor.js +243 -0
- package/lib/commands/doctor.js.map +1 -0
- package/lib/commands/login.d.ts +16 -4
- package/lib/commands/login.d.ts.map +1 -1
- package/lib/commands/login.js +83 -25
- package/lib/commands/login.js.map +1 -1
- package/lib/commands/ota-keys.d.ts +11 -0
- package/lib/commands/ota-keys.d.ts.map +1 -0
- package/lib/commands/ota-keys.js +199 -0
- package/lib/commands/ota-keys.js.map +1 -0
- package/lib/commands/ota-publish.d.ts.map +1 -1
- package/lib/commands/ota-publish.js +154 -34
- package/lib/commands/ota-publish.js.map +1 -1
- package/lib/commands/ota-rollout.d.ts +3 -0
- package/lib/commands/ota-rollout.d.ts.map +1 -0
- package/lib/commands/ota-rollout.js +235 -0
- package/lib/commands/ota-rollout.js.map +1 -0
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -1
- package/lib/utils/bundle.d.ts +53 -0
- package/lib/utils/bundle.d.ts.map +1 -1
- package/lib/utils/bundle.js +199 -14
- package/lib/utils/bundle.js.map +1 -1
- package/package.json +1 -1
package/lib/commands/login.js
CHANGED
|
@@ -35,21 +35,37 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.registerLoginCommand = registerLoginCommand;
|
|
37
37
|
const readline = __importStar(require("readline"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
38
39
|
const credentials_1 = require("../utils/credentials");
|
|
39
40
|
const client_1 = require("../api/client");
|
|
40
41
|
/**
|
|
41
|
-
* `scalebun login` — authenticate
|
|
42
|
+
* `scalebun login` — authenticate and store a DURABLE credential.
|
|
42
43
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* ── Why the PAT exchange matters ────────────────────────────────────────────
|
|
45
|
+
*
|
|
46
|
+
* The backend's session JWTs expire in ~15 minutes. This command used to store
|
|
47
|
+
* that JWT directly, so every publish that started more than a few minutes
|
|
48
|
+
* after login failed with an opaque 401 — hit live during the first real
|
|
49
|
+
* device-test run. Meanwhile the backend has had a full personal-access-token
|
|
50
|
+
* system all along (`POST /auth/tokens`, hashed at rest, scoped, revocable)
|
|
51
|
+
* that the CLI never used.
|
|
52
|
+
*
|
|
53
|
+
* Now: email/password buys a short-lived JWT, which is immediately spent on
|
|
54
|
+
* minting a PAT named after this machine. The PAT is what gets stored — it
|
|
55
|
+
* survives for `--expires-days` (default 90) and is revocable from the
|
|
56
|
+
* dashboard (Settings → Access Tokens) if this machine is ever lost.
|
|
57
|
+
*
|
|
58
|
+
* Pasting an existing `scalebun_pat_…` still works and skips the exchange.
|
|
46
59
|
*/
|
|
47
60
|
function registerLoginCommand(program) {
|
|
48
61
|
program
|
|
49
62
|
.command('login')
|
|
50
|
-
.description('Authenticate with your Scalebun backend
|
|
63
|
+
.description('Authenticate with your Scalebun backend (stores a durable access token)')
|
|
51
64
|
.option('--api-url <url>', 'Backend API URL (e.g. https://api.scalebun.com/api/v1)')
|
|
52
65
|
.option('--token <pat>', 'Personal access token (scalebun_pat_…)')
|
|
66
|
+
.option('--email <email>', 'Account email (prompts for password unless --password is set)')
|
|
67
|
+
.option('--password <password>', 'Account password (prefer the prompt — flags leak into shell history)')
|
|
68
|
+
.option('--expires-days <n>', 'Lifetime of the minted CLI token in days (0 = never expires)', '90')
|
|
53
69
|
.action(async (opts) => {
|
|
54
70
|
const rl = readline.createInterface({
|
|
55
71
|
input: process.stdin,
|
|
@@ -66,37 +82,72 @@ function registerLoginCommand(program) {
|
|
|
66
82
|
process.exit(1);
|
|
67
83
|
}
|
|
68
84
|
let token = opts.token;
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
let email = opts.email;
|
|
86
|
+
if (!token && !email) {
|
|
87
|
+
const input = await ask('Email or token (scalebun_pat_…): ');
|
|
71
88
|
if (!input) {
|
|
72
|
-
console.error('❌ Email or
|
|
89
|
+
console.error('❌ Email or token is required');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (input.includes('@'))
|
|
93
|
+
email = input;
|
|
94
|
+
else
|
|
95
|
+
token = input;
|
|
96
|
+
}
|
|
97
|
+
// ── Email/password → JWT → durable PAT ────────────────────────────
|
|
98
|
+
if (!token && email) {
|
|
99
|
+
const password = opts.password || (await ask('Password: '));
|
|
100
|
+
if (!password) {
|
|
101
|
+
console.error('❌ Password is required');
|
|
73
102
|
process.exit(1);
|
|
74
103
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
console.log(' Signing in…');
|
|
105
|
+
const unauthClient = new client_1.ApiClient({ apiUrl, token: '' });
|
|
106
|
+
const loginRes = await unauthClient.post('/auth/login', { email, password });
|
|
107
|
+
if (!loginRes.accessToken) {
|
|
108
|
+
if (loginRes.mfaRequired) {
|
|
109
|
+
console.error('❌ This account has MFA enabled, which the CLI cannot complete.\n' +
|
|
110
|
+
' Create a token in the dashboard instead (Settings → Access Tokens)\n' +
|
|
111
|
+
' and run: scalebun login --token scalebun_pat_…');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.error('❌ Sign-in did not return a token.');
|
|
80
115
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
// Spend the 15-minute JWT on something durable.
|
|
119
|
+
const expiresDays = Math.max(0, parseInt(opts.expiresDays, 10) || 0);
|
|
120
|
+
const jwtClient = new client_1.ApiClient({ apiUrl, token: loginRes.accessToken });
|
|
121
|
+
try {
|
|
122
|
+
const pat = await jwtClient.post('/auth/tokens', {
|
|
123
|
+
name: `Scalebun CLI on ${os.hostname()}`,
|
|
124
|
+
scopes: ['read', 'write'],
|
|
125
|
+
expiresInDays: expiresDays > 0 ? expiresDays : null,
|
|
86
126
|
});
|
|
87
|
-
token =
|
|
127
|
+
token = pat.token;
|
|
128
|
+
console.log(` 🔑 Created CLI access token (…${pat.last4}, ` +
|
|
129
|
+
`${expiresDays > 0 ? `expires in ${expiresDays} days` : 'no expiry'}). ` +
|
|
130
|
+
'Revocable from the dashboard.');
|
|
88
131
|
}
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
catch (patErr) {
|
|
133
|
+
// An older backend without /auth/tokens still logs in — but the
|
|
134
|
+
// session dies in minutes, and pretending otherwise reproduces
|
|
135
|
+
// the exact bug this exchange exists to fix. Say so.
|
|
136
|
+
token = loginRes.accessToken;
|
|
137
|
+
console.warn(` ⚠️ Could not create a durable token (${patErr.message}).\n` +
|
|
138
|
+
' Falling back to the session token, which EXPIRES IN ~15 MINUTES.');
|
|
91
139
|
}
|
|
92
140
|
}
|
|
93
|
-
// Validate
|
|
141
|
+
// ── Validate whatever we ended up with ─────────────────────────────
|
|
94
142
|
console.log(' Validating token…');
|
|
95
|
-
const client = new client_1.ApiClient({ apiUrl, token });
|
|
143
|
+
const client = new client_1.ApiClient({ apiUrl, token: token });
|
|
96
144
|
const me = await client.get('/auth/me');
|
|
97
|
-
(0, credentials_1.writeCredentials)({ apiUrl, token });
|
|
145
|
+
(0, credentials_1.writeCredentials)({ apiUrl, token: token });
|
|
98
146
|
console.log(`✅ Logged in as ${me.email} (${me.role ?? 'User'})`);
|
|
99
|
-
console.log(
|
|
147
|
+
console.log(' Credentials saved to ~/.scalebun/credentials.json');
|
|
148
|
+
console.log('\nNext steps:');
|
|
149
|
+
console.log(' scalebun doctor # verify your project is OTA-ready');
|
|
150
|
+
console.log(' scalebun ota publish --app-id <id> # ship your first update');
|
|
100
151
|
}
|
|
101
152
|
catch (err) {
|
|
102
153
|
console.error(`❌ Login failed: ${err.message}`);
|
|
@@ -106,5 +157,12 @@ function registerLoginCommand(program) {
|
|
|
106
157
|
rl.close();
|
|
107
158
|
}
|
|
108
159
|
});
|
|
160
|
+
program
|
|
161
|
+
.command('logout')
|
|
162
|
+
.description('Remove stored credentials from this machine')
|
|
163
|
+
.action(() => {
|
|
164
|
+
(0, credentials_1.clearCredentials)();
|
|
165
|
+
console.log('✅ Logged out. (The token still exists server-side — revoke it in the dashboard if this machine is compromised.)');
|
|
166
|
+
});
|
|
109
167
|
}
|
|
110
168
|
//# sourceMappingURL=login.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export declare function registerOtaKeysCommand(ota: Command): void;
|
|
3
|
+
/**
|
|
4
|
+
* Sign a bundle hash with the locally-held private key.
|
|
5
|
+
*
|
|
6
|
+
* Returns null when no key is present — publishing without signing stays valid
|
|
7
|
+
* for apps that have not adopted it, and `ota publish` reports which happened
|
|
8
|
+
* rather than failing.
|
|
9
|
+
*/
|
|
10
|
+
export declare function signBundleHash(sha256Hex: string, keyDir?: string): string | null;
|
|
11
|
+
//# sourceMappingURL=ota-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ota-keys.d.ts","sourceRoot":"","sources":["../../src/commands/ota-keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuCpC,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA0JzD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAcf"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerOtaKeysCommand = registerOtaKeysCommand;
|
|
37
|
+
exports.signBundleHash = signBundleHash;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const credentials_1 = require("../utils/credentials");
|
|
42
|
+
const client_1 = require("../api/client");
|
|
43
|
+
/**
|
|
44
|
+
* Bundle signing keys (OTA-03).
|
|
45
|
+
*
|
|
46
|
+
* The security model, which is the reason this command exists at all: the
|
|
47
|
+
* private key is generated on the developer's machine and NEVER leaves it. Only
|
|
48
|
+
* the public half is uploaded. A server that cannot sign cannot forge a bundle,
|
|
49
|
+
* so a compromise of ScaleBun's infrastructure still cannot push code to a
|
|
50
|
+
* customer's users.
|
|
51
|
+
*
|
|
52
|
+
* `scalebun ota generate-key-pair` was referenced in the product documentation
|
|
53
|
+
* before it existed — the docs described verification the SDK never performed
|
|
54
|
+
* and a command the CLI never registered. This implements it.
|
|
55
|
+
*/
|
|
56
|
+
const KEY_DIR = '.scalebun';
|
|
57
|
+
const PRIVATE_KEY_FILE = 'ota-signing-key.pem';
|
|
58
|
+
/** Hex-encoded raw 32-byte ed25519 public key from a Node KeyObject. */
|
|
59
|
+
function rawPublicKeyHex(publicKey) {
|
|
60
|
+
// SPKI for ed25519 is a fixed 12-byte header followed by the 32-byte key.
|
|
61
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
62
|
+
return spki.subarray(spki.length - 32).toString('hex');
|
|
63
|
+
}
|
|
64
|
+
function registerOtaKeysCommand(ota) {
|
|
65
|
+
ota
|
|
66
|
+
.command('generate-key-pair')
|
|
67
|
+
.description('Generate an ed25519 signing keypair locally and register the public half')
|
|
68
|
+
.option('--app-id <id>', 'Register the public key against this app')
|
|
69
|
+
.option('--name <name>', 'Label for the key in the dashboard')
|
|
70
|
+
.option('--out <dir>', `Directory for the private key (default: ./${KEY_DIR})`)
|
|
71
|
+
.option('--force', 'Overwrite an existing private key')
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
const outDir = path.resolve(opts.out ?? KEY_DIR);
|
|
74
|
+
const privatePath = path.join(outDir, PRIVATE_KEY_FILE);
|
|
75
|
+
if (fs.existsSync(privatePath) && !opts.force) {
|
|
76
|
+
console.error(`❌ A signing key already exists at ${privatePath}.\n` +
|
|
77
|
+
` Overwriting it means every bundle signed with the old key can no longer be\n` +
|
|
78
|
+
` verified by apps carrying the old public key. Pass --force if that is intended.`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
82
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
83
|
+
fs.writeFileSync(privatePath, privateKey.export({ type: 'pkcs8', format: 'pem' }),
|
|
84
|
+
// Owner read/write only. Irrelevant on Windows, correct everywhere else.
|
|
85
|
+
{ mode: 0o600 });
|
|
86
|
+
const publicHex = rawPublicKeyHex(publicKey);
|
|
87
|
+
console.log(`\n🔑 ed25519 signing keypair generated.\n`);
|
|
88
|
+
console.log(` Private key : ${privatePath}`);
|
|
89
|
+
console.log(` NEVER commit this. Add ${KEY_DIR}/ to .gitignore.`);
|
|
90
|
+
console.log(` Public key : ${publicHex}\n`);
|
|
91
|
+
if (opts.appId) {
|
|
92
|
+
const creds = (0, credentials_1.readCredentials)();
|
|
93
|
+
if (!creds) {
|
|
94
|
+
console.error('❌ Not logged in. Run `scalebun login` first.');
|
|
95
|
+
console.error(' The keypair was still written; register it later with:');
|
|
96
|
+
console.error(` scalebun ota register-key --app-id <id> --public-key ${publicHex}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const client = new client_1.ApiClient(creds);
|
|
100
|
+
try {
|
|
101
|
+
await client.post(`/saas/apps/${opts.appId}/ota/signing-keys`, {
|
|
102
|
+
publicKey: publicHex,
|
|
103
|
+
name: opts.name ?? `CLI ${new Date().toISOString().slice(0, 10)}`,
|
|
104
|
+
});
|
|
105
|
+
console.log('✅ Public key registered with ScaleBun.\n');
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error(`❌ Failed to register public key: ${err.message}`);
|
|
109
|
+
console.error(` Retry with: scalebun ota register-key --app-id ${opts.appId} --public-key ${publicHex}\n`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log('ℹ️ Pass --app-id to register the public key automatically.\n');
|
|
115
|
+
}
|
|
116
|
+
console.log('Next: embed the PUBLIC key in your app and enforce it at init:\n');
|
|
117
|
+
console.log(' ScaleBun.init({');
|
|
118
|
+
console.log(' // …');
|
|
119
|
+
console.log(' ota: {');
|
|
120
|
+
console.log(` publicSigningKey: '${publicHex}',`);
|
|
121
|
+
console.log(' verifySignature: myEd25519Verifier,');
|
|
122
|
+
console.log(' },');
|
|
123
|
+
console.log(' })\n');
|
|
124
|
+
});
|
|
125
|
+
ota
|
|
126
|
+
.command('register-key')
|
|
127
|
+
.description('Register an existing ed25519 public key with an app')
|
|
128
|
+
.requiredOption('--app-id <id>', 'Scalebun app ID')
|
|
129
|
+
.requiredOption('--public-key <hex>', 'ed25519 public key (hex or base64)')
|
|
130
|
+
.option('--name <name>', 'Label for the key in the dashboard')
|
|
131
|
+
.action(async (opts) => {
|
|
132
|
+
const creds = (0, credentials_1.readCredentials)();
|
|
133
|
+
if (!creds) {
|
|
134
|
+
console.error('❌ Not logged in. Run `scalebun login` first.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const client = new client_1.ApiClient(creds);
|
|
138
|
+
try {
|
|
139
|
+
await client.post(`/saas/apps/${opts.appId}/ota/signing-keys`, {
|
|
140
|
+
publicKey: opts.publicKey,
|
|
141
|
+
name: opts.name ?? 'CLI',
|
|
142
|
+
});
|
|
143
|
+
console.log('✅ Public key registered.\n');
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.error(`❌ Failed to register public key: ${err.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
ota
|
|
151
|
+
.command('keys')
|
|
152
|
+
.description('List signing keys registered for an app')
|
|
153
|
+
.requiredOption('--app-id <id>', 'Scalebun app ID')
|
|
154
|
+
.action(async (opts) => {
|
|
155
|
+
const creds = (0, credentials_1.readCredentials)();
|
|
156
|
+
if (!creds) {
|
|
157
|
+
console.error('❌ Not logged in. Run `scalebun login` first.');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const client = new client_1.ApiClient(creds);
|
|
161
|
+
try {
|
|
162
|
+
const keys = await client.get(`/saas/apps/${opts.appId}/ota/signing-keys`);
|
|
163
|
+
console.log(`\n🔑 Signing keys for app ${opts.appId}:\n`);
|
|
164
|
+
if (!keys?.length) {
|
|
165
|
+
console.log(' (none — run `scalebun ota generate-key-pair --app-id <id>`)\n');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const k of keys) {
|
|
169
|
+
const state = k.revokedAt ? 'REVOKED' : 'active';
|
|
170
|
+
console.log(` • ${(k.name ?? 'unnamed').padEnd(20)} ${state.padEnd(8)} ${k.publicKey.slice(0, 16)}…`);
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
console.error(`❌ Failed to list keys: ${err.message}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Sign a bundle hash with the locally-held private key.
|
|
182
|
+
*
|
|
183
|
+
* Returns null when no key is present — publishing without signing stays valid
|
|
184
|
+
* for apps that have not adopted it, and `ota publish` reports which happened
|
|
185
|
+
* rather than failing.
|
|
186
|
+
*/
|
|
187
|
+
function signBundleHash(sha256Hex, keyDir) {
|
|
188
|
+
const privatePath = path.join(path.resolve(keyDir ?? KEY_DIR), PRIVATE_KEY_FILE);
|
|
189
|
+
if (!fs.existsSync(privatePath))
|
|
190
|
+
return null;
|
|
191
|
+
const privateKey = crypto.createPrivateKey(fs.readFileSync(privatePath));
|
|
192
|
+
// The signature covers the bundle's SHA-256, which the device already has from
|
|
193
|
+
// the check response and re-computes from the downloaded bytes. Signing the
|
|
194
|
+
// hash rather than the bundle keeps the CLI from re-reading 20 MB, and gives
|
|
195
|
+
// the device one value to verify that transitively covers the whole artifact.
|
|
196
|
+
const signature = crypto.sign(null, Buffer.from(sha256Hex, 'hex'), privateKey);
|
|
197
|
+
return signature.toString('hex');
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=ota-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ota-keys.js","sourceRoot":"","sources":["../../src/commands/ota-keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ota-publish.d.ts","sourceRoot":"","sources":["../../src/commands/ota-publish.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ota-publish.d.ts","sourceRoot":"","sources":["../../src/commands/ota-publish.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6BpC,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAE7E;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAuW5D"}
|