@scalebun/cli 1.0.0 → 1.4.0

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 (57) hide show
  1. package/lib/api/client.d.ts +8 -0
  2. package/lib/api/client.d.ts.map +1 -1
  3. package/lib/api/client.js +70 -2
  4. package/lib/api/client.js.map +1 -1
  5. package/lib/commands/ci-init.d.ts +0 -6
  6. package/lib/commands/ci-init.d.ts.map +1 -1
  7. package/lib/commands/ci-init.js +172 -28
  8. package/lib/commands/ci-init.js.map +1 -1
  9. package/lib/commands/doctor.d.ts +8 -0
  10. package/lib/commands/doctor.d.ts.map +1 -0
  11. package/lib/commands/doctor.js +252 -0
  12. package/lib/commands/doctor.js.map +1 -0
  13. package/lib/commands/login.d.ts +28 -4
  14. package/lib/commands/login.d.ts.map +1 -1
  15. package/lib/commands/login.js +132 -51
  16. package/lib/commands/login.js.map +1 -1
  17. package/lib/commands/ota-bundle.d.ts.map +1 -1
  18. package/lib/commands/ota-bundle.js +2 -0
  19. package/lib/commands/ota-bundle.js.map +1 -1
  20. package/lib/commands/ota-delete-bundle.d.ts +8 -0
  21. package/lib/commands/ota-delete-bundle.d.ts.map +1 -1
  22. package/lib/commands/ota-delete-bundle.js +60 -0
  23. package/lib/commands/ota-delete-bundle.js.map +1 -1
  24. package/lib/commands/ota-keys.d.ts +14 -0
  25. package/lib/commands/ota-keys.d.ts.map +1 -0
  26. package/lib/commands/ota-keys.js +200 -0
  27. package/lib/commands/ota-keys.js.map +1 -0
  28. package/lib/commands/ota-publish.d.ts +10 -0
  29. package/lib/commands/ota-publish.d.ts.map +1 -1
  30. package/lib/commands/ota-publish.js +251 -34
  31. package/lib/commands/ota-publish.js.map +1 -1
  32. package/lib/commands/ota-rollout.d.ts +3 -0
  33. package/lib/commands/ota-rollout.d.ts.map +1 -0
  34. package/lib/commands/ota-rollout.js +235 -0
  35. package/lib/commands/ota-rollout.js.map +1 -0
  36. package/lib/commands/quickstart.d.ts +4 -0
  37. package/lib/commands/quickstart.d.ts.map +1 -0
  38. package/lib/commands/quickstart.js +274 -0
  39. package/lib/commands/quickstart.js.map +1 -0
  40. package/lib/index.js +20 -1
  41. package/lib/index.js.map +1 -1
  42. package/lib/utils/bundle.d.ts +86 -0
  43. package/lib/utils/bundle.d.ts.map +1 -1
  44. package/lib/utils/bundle.js +244 -14
  45. package/lib/utils/bundle.js.map +1 -1
  46. package/lib/utils/credentials.d.ts +12 -1
  47. package/lib/utils/credentials.d.ts.map +1 -1
  48. package/lib/utils/credentials.js +40 -1
  49. package/lib/utils/credentials.js.map +1 -1
  50. package/lib/utils/transportSecurity.d.ts +36 -0
  51. package/lib/utils/transportSecurity.d.ts.map +1 -0
  52. package/lib/utils/transportSecurity.js +74 -0
  53. package/lib/utils/transportSecurity.js.map +1 -0
  54. package/lib/utils/ui.d.ts.map +1 -1
  55. package/lib/utils/ui.js +2 -0
  56. package/lib/utils/ui.js.map +1 -1
  57. package/package.json +1 -1
@@ -19,13 +19,78 @@ export interface BundleOptions {
19
19
  * step will be mandatory when the patch engine ships.
20
20
  */
21
21
  hermes?: boolean;
22
+ /**
23
+ * Permit shipping a plain-JS bundle when `--hermes` was asked for but
24
+ * hermesc could not run. Default false: a failed compile is an ERROR.
25
+ *
26
+ * It used to warn and ship the uncompiled bundle, which is a silent
27
+ * downgrade of the artifact — the publish "succeeds" and nothing downstream
28
+ * can tell that what shipped is not what was requested. Two concrete costs:
29
+ * cold start is 20-30% slower for every user on that release, and the delta
30
+ * engine diffs the new bundle against the previous one, so a channel that
31
+ * flips between bytecode and plain JS produces patches with nothing in
32
+ * common and every device falls back to a full download.
33
+ *
34
+ * A broken toolchain should stop the publish, not quietly change what it
35
+ * publishes. The escape hatch stays available for anyone who genuinely
36
+ * wants the uncompiled artifact — it just has to be asked for.
37
+ */
38
+ allowPlainJs?: boolean;
22
39
  /** If true, generate a sourcemap alongside the bundle. */
23
40
  sourcemap?: boolean;
41
+ /**
42
+ * Identity marker to compile into the bundle. Injected into the JS source
43
+ * BEFORE hermesc runs — Hermes bytecode is a binary format, so text cannot be
44
+ * prepended after compilation without corrupting the file.
45
+ *
46
+ * At runtime the SDK reads `globalThis.__SCALEBUN_OTA_ID__` and compares it
47
+ * against the slot it believes is active. That comparison is the only way to
48
+ * prove from JS that the bundle we installed is the bundle actually running —
49
+ * which catches a host app that never wired `getJSBundleFile()`, and the
50
+ * bridgeless runtimes below RN 0.76.1 that ignore it entirely.
51
+ */
52
+ identityToken?: string;
24
53
  }
25
54
  export interface BundleResult {
26
55
  bundlePath: string;
27
56
  assetsPath: string;
57
+ /**
58
+ * Asset files Metro emitted for this bundle, relative to `assetsPath`.
59
+ * OTA ships the JS artifact only — these resolve from the installed binary,
60
+ * so the list exists to WARN about assets added since the last store build.
61
+ */
62
+ assets: string[];
63
+ /** True when the artifact really is Hermes bytecode (verified from the header). */
64
+ hermesCompiled: boolean;
65
+ /** HBC format version read from the compiled header, or null for plain JS. */
66
+ hermesBytecodeVersion: number | null;
28
67
  }
68
+ /**
69
+ * Read the HBC format version straight out of the compiled file header.
70
+ *
71
+ * Parsed from the artifact rather than from `hermesc --version` output, because
72
+ * the header is what the device's VM actually compares against — and its format
73
+ * is stable, whereas the CLI's version banner is not.
74
+ *
75
+ * Returns null when the file is not Hermes bytecode.
76
+ */
77
+ export declare function readHermesBytecodeVersion(filePath: string): number | null;
78
+ /**
79
+ * Locate the `hermesc` binary that ships inside React Native.
80
+ *
81
+ * `hermesc` is NOT an npm package — `npx hermesc` fails with
82
+ * "No versions available for hermesc" on every project, which meant `--hermes`
83
+ * silently degraded to a plain JS bundle every single time it was used. React
84
+ * Native ships a prebuilt binary per host platform under `sdks/hermesc/`.
85
+ *
86
+ * Using the copy inside the project's own `react-native` is also the correct
87
+ * choice for a second reason: it is the compiler matched to the Hermes runtime
88
+ * in that app's binary. Compiling with any other hermesc is what produces
89
+ * "Wrong bytecode version. Expected 96 but got 98" on device.
90
+ */
91
+ export declare function resolveHermesc(cwd?: string): string | null;
92
+ /** React Native version of the project being published, from its node_modules. */
93
+ export declare function detectProjectRnVersion(cwd?: string): string | null;
29
94
  /**
30
95
  * Shell out to `npx react-native bundle` to produce a JS bundle.
31
96
  *
@@ -33,5 +98,26 @@ export interface BundleResult {
33
98
  * The native runtime detects the format from magic bytes (\x1f\x\xbc —
34
99
  * Hermes bytecode header), so the same .jsbundle extension works for both.
35
100
  */
101
+ /**
102
+ * True when this project is an Expo app.
103
+ *
104
+ * Expo projects do not ship `@react-native-community/cli`, so `npx react-native
105
+ * bundle` fails outright with a "react-native depends on
106
+ * @react-native-community/cli" error. Expo's equivalent is `expo export:embed`,
107
+ * which takes the same flags and produces the same artifact. Detecting this is
108
+ * not a nicety — without it `ota publish` cannot build a bundle in an Expo app
109
+ * at all, which is a large share of the React Native ecosystem.
110
+ */
111
+ export declare function isExpoProject(cwd?: string): boolean;
36
112
  export declare function buildBundle(opts: BundleOptions): BundleResult;
113
+ /**
114
+ * Every asset file Metro emitted for this bundle, relative to the assets root.
115
+ *
116
+ * OTA ships ONE artifact: the JS/bytecode bundle. Assets are resolved from what
117
+ * the installed binary already contains, so an image or font added since the
118
+ * last store build is referenced by the new JS and is simply not there — the
119
+ * update installs cleanly and renders a missing image. `ota publish` uses this
120
+ * list to say so out loud rather than discarding the directory in silence.
121
+ */
122
+ export declare function listAssets(root: string): string[];
37
123
  //# sourceMappingURL=bundle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,SAAS,GAAG,KAAK,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,OAAO,CAAC;IACb;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CA+D7D"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,SAAS,GAAG,KAAK,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,OAAO,CAAC;IACb;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,mFAAmF;IACnF,cAAc,EAAE,OAAO,CAAC;IACxB,8EAA8E;IAC9E,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAQD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAkBzE;AAED,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CASjF;AAED;;;;;;GAMG;AACH;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAelE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAwK7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBjD"}
@@ -33,11 +33,92 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readHermesBytecodeVersion = readHermesBytecodeVersion;
37
+ exports.resolveHermesc = resolveHermesc;
38
+ exports.detectProjectRnVersion = detectProjectRnVersion;
39
+ exports.isExpoProject = isExpoProject;
36
40
  exports.buildBundle = buildBundle;
41
+ exports.listAssets = listAssets;
37
42
  const childProcess = __importStar(require("child_process"));
38
43
  const fs = __importStar(require("fs"));
39
44
  const path = __importStar(require("path"));
40
45
  const os = __importStar(require("os"));
46
+ /**
47
+ * Hermes bytecode file magic, little-endian on disk.
48
+ * Source: `const uint64_t MAGIC = 0x1F1903C103BC1FC6` in Hermes.
49
+ */
50
+ const HBC_MAGIC = Buffer.from([0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03, 0x19, 0x1f]);
51
+ /**
52
+ * Read the HBC format version straight out of the compiled file header.
53
+ *
54
+ * Parsed from the artifact rather than from `hermesc --version` output, because
55
+ * the header is what the device's VM actually compares against — and its format
56
+ * is stable, whereas the CLI's version banner is not.
57
+ *
58
+ * Returns null when the file is not Hermes bytecode.
59
+ */
60
+ function readHermesBytecodeVersion(filePath) {
61
+ let fd;
62
+ try {
63
+ fd = fs.openSync(filePath, 'r');
64
+ const header = Buffer.alloc(12);
65
+ const read = fs.readSync(fd, header, 0, 12, 0);
66
+ if (read < 12)
67
+ return null;
68
+ if (!header.subarray(0, 8).equals(HBC_MAGIC))
69
+ return null;
70
+ return header.readUInt32LE(8);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ finally {
76
+ if (fd !== undefined) {
77
+ try {
78
+ fs.closeSync(fd);
79
+ }
80
+ catch {
81
+ /* ignore */
82
+ }
83
+ }
84
+ }
85
+ }
86
+ /**
87
+ * Locate the `hermesc` binary that ships inside React Native.
88
+ *
89
+ * `hermesc` is NOT an npm package — `npx hermesc` fails with
90
+ * "No versions available for hermesc" on every project, which meant `--hermes`
91
+ * silently degraded to a plain JS bundle every single time it was used. React
92
+ * Native ships a prebuilt binary per host platform under `sdks/hermesc/`.
93
+ *
94
+ * Using the copy inside the project's own `react-native` is also the correct
95
+ * choice for a second reason: it is the compiler matched to the Hermes runtime
96
+ * in that app's binary. Compiling with any other hermesc is what produces
97
+ * "Wrong bytecode version. Expected 96 but got 98" on device.
98
+ */
99
+ function resolveHermesc(cwd = process.cwd()) {
100
+ const dir = process.platform === 'win32'
101
+ ? 'win64-bin'
102
+ : process.platform === 'darwin'
103
+ ? 'osx-bin'
104
+ : 'linux64-bin';
105
+ const bin = process.platform === 'win32' ? 'hermesc.exe' : 'hermesc';
106
+ const candidate = path.join(cwd, 'node_modules', 'react-native', 'sdks', 'hermesc', dir, bin);
107
+ return fs.existsSync(candidate) ? candidate : null;
108
+ }
109
+ /** React Native version of the project being published, from its node_modules. */
110
+ function detectProjectRnVersion(cwd = process.cwd()) {
111
+ try {
112
+ const pkgPath = path.join(cwd, 'node_modules', 'react-native', 'package.json');
113
+ if (!fs.existsSync(pkgPath))
114
+ return null;
115
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
116
+ return typeof pkg.version === 'string' ? pkg.version : null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
41
122
  /**
42
123
  * Shell out to `npx react-native bundle` to produce a JS bundle.
43
124
  *
@@ -45,23 +126,49 @@ const os = __importStar(require("os"));
45
126
  * The native runtime detects the format from magic bytes (\x1f\x\xbc —
46
127
  * Hermes bytecode header), so the same .jsbundle extension works for both.
47
128
  */
129
+ /**
130
+ * True when this project is an Expo app.
131
+ *
132
+ * Expo projects do not ship `@react-native-community/cli`, so `npx react-native
133
+ * bundle` fails outright with a "react-native depends on
134
+ * @react-native-community/cli" error. Expo's equivalent is `expo export:embed`,
135
+ * which takes the same flags and produces the same artifact. Detecting this is
136
+ * not a nicety — without it `ota publish` cannot build a bundle in an Expo app
137
+ * at all, which is a large share of the React Native ecosystem.
138
+ */
139
+ function isExpoProject(cwd = process.cwd()) {
140
+ try {
141
+ if (fs.existsSync(path.join(cwd, 'node_modules', 'expo', 'package.json'))) {
142
+ return true;
143
+ }
144
+ const pkgPath = path.join(cwd, 'package.json');
145
+ if (!fs.existsSync(pkgPath))
146
+ return false;
147
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
148
+ return Boolean(pkg.dependencies?.expo ?? pkg.devDependencies?.expo);
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ }
48
154
  function buildBundle(opts) {
49
155
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scalebun-bundle-'));
50
156
  const bundlePath = path.join(tmpDir, 'bundle.jsbundle');
51
157
  const assetsPath = path.join(tmpDir, 'assets');
52
158
  fs.mkdirSync(assetsPath, { recursive: true });
53
- const args = [
54
- 'react-native', 'bundle',
55
- '--platform', opts.platform,
56
- '--dev', String(opts.dev),
57
- '--entry-file', opts.entryFile,
58
- '--bundle-output', bundlePath,
59
- '--assets-dest', assetsPath,
60
- ];
159
+ // `expo export:embed` is the Expo-side equivalent of `react-native bundle`
160
+ // and accepts the same flags. Both emit a plain JS bundle; Hermes compilation
161
+ // is a separate step below in either case.
162
+ const expo = isExpoProject();
163
+ const args = expo
164
+ ? ['expo', 'export:embed']
165
+ : ['react-native', 'bundle'];
166
+ args.push('--platform', opts.platform, '--dev', String(opts.dev), '--entry-file', opts.entryFile, '--bundle-output', bundlePath, '--assets-dest', assetsPath);
61
167
  // Sprint 8: Generate sourcemap if requested
62
168
  if (opts.sourcemap) {
63
169
  args.push('--sourcemap-output', bundlePath + '.map');
64
170
  }
171
+ console.log(` Detected ${expo ? 'Expo' : 'bare React Native'} project`);
65
172
  console.log(` Running: npx ${args.join(' ')}`);
66
173
  const result = childProcess.spawnSync('npx', args, {
67
174
  stdio: 'inherit',
@@ -69,26 +176,149 @@ function buildBundle(opts) {
69
176
  timeout: 300000, // 5 minutes
70
177
  });
71
178
  if (result.status !== 0) {
72
- throw new Error(`react-native bundle failed with exit code ${result.status}`);
179
+ throw new Error(`${expo ? 'expo export:embed' : 'react-native bundle'} failed with exit code ${result.status}`);
73
180
  }
74
181
  if (!fs.existsSync(bundlePath)) {
75
182
  throw new Error(`Bundle file not found at ${bundlePath}`);
76
183
  }
184
+ // Identity marker — injected into the JS source, before any Hermes step
185
+ // (after hermesc the artifact is binary and text would corrupt the header).
186
+ //
187
+ // APPENDED, not prepended. Metro has already written the sourcemap by this
188
+ // point, and a prepended line shifts every generated line down by one —
189
+ // silently corrupting the line numbers of every symbolicated stack trace
190
+ // from this bundle. An appended statement adds no mappings and moves none;
191
+ // it still executes during bundle evaluation, and the SDK reads the global
192
+ // at init(), long after evaluation completes.
193
+ if (opts.identityToken) {
194
+ const marker = `\nglobalThis.__SCALEBUN_OTA_ID__=${JSON.stringify(opts.identityToken)};\n`;
195
+ fs.appendFileSync(bundlePath, marker, 'utf-8');
196
+ }
77
197
  // Hermes bytecode compilation (opt-in)
78
198
  if (opts.hermes) {
79
199
  console.log(' Compiling to Hermes bytecode…');
80
- const hbcPath = bundlePath; // overwrite in-place same extension
81
- const hermescResult = childProcess.spawnSync('npx', ['hermesc', '-emit-binary', '-out', hbcPath, bundlePath], { stdio: 'inherit', shell: true, timeout: 120000 });
82
- if (hermescResult.status !== 0) {
83
- console.warn(` ⚠️ hermesc failed (exit ${hermescResult.status}) shipping plain JS bundle`);
200
+ // Compile to a sibling path, then rename. hermesc reading and writing the
201
+ // same path in one invocation is not guaranteed to be safe, and a truncated
202
+ // in-place write would produce a bundle that hashes fine and never loads.
203
+ const hbcPath = bundlePath + '.hbc';
204
+ const hermesc = resolveHermesc();
205
+ // A missing hermesc is handled with the compile failures below, so that
206
+ // "cannot compile" has exactly one outcome regardless of the reason.
207
+ // Output is CAPTURED, not inherited. hermesc emits one warning per
208
+ // undeclared global per module ("the variable Buffer was not declared…"),
209
+ // and each warning quotes the offending source line — which in a minified
210
+ // bundle is a single line of 100KB+. Two consequences shaped this code:
211
+ // 1. Inherited stdio drowned the publish summary in megabytes of noise.
212
+ // 2. spawnSync's DEFAULT maxBuffer is 1MB; the quoted warnings blow past
213
+ // it, Node kills hermesc (ENOBUFS), and `--hermes` silently degrades
214
+ // to plain JS — the same bug as `npx hermesc`, via a different door.
215
+ // Hit live on the very first publish after the capture change.
216
+ const hermescResult = hermesc
217
+ ? childProcess.spawnSync(hermesc, ['-emit-binary', '-out', hbcPath, bundlePath], {
218
+ stdio: ['ignore', 'pipe', 'pipe'],
219
+ shell: false,
220
+ timeout: 120000,
221
+ encoding: 'utf-8',
222
+ maxBuffer: 256 * 1024 * 1024,
223
+ })
224
+ : { status: 1 };
225
+ if (hermesc && hermescResult.status === 0) {
226
+ const warnings = `${hermescResult.stdout ?? ''}${hermescResult.stderr ?? ''}`
227
+ .split('\n')
228
+ .filter((l) => l.includes('warning:')).length;
229
+ if (warnings > 0) {
230
+ console.log(` (${warnings} hermesc warnings suppressed — expected for RN bundles)`);
231
+ }
232
+ }
233
+ if (hermescResult.status !== 0 || !fs.existsSync(hbcPath)) {
234
+ // Collect the reason before deciding what to do with it — the same
235
+ // detail is worth printing whether this ends as an error or a warning.
236
+ const reason = hermesc
237
+ ? `hermesc failed (exit ${hermescResult.status}` +
238
+ `${hermescResult.error ? `, ${hermescResult.error.message}` : ''})`
239
+ : 'hermesc not found under node_modules/react-native/sdks/hermesc';
240
+ // Only lines that carry the error itself, each clipped: a quoted
241
+ // minified source line is 100KB of noise per line.
242
+ const errLines = `${hermescResult.stderr ?? ''}`
243
+ .split('\n')
244
+ .filter((l) => l.includes('error:'))
245
+ .slice(0, 5)
246
+ .map((l) => (l.length > 200 ? l.slice(0, 200) + '…' : l));
247
+ try {
248
+ if (fs.existsSync(hbcPath))
249
+ fs.unlinkSync(hbcPath);
250
+ }
251
+ catch {
252
+ /* ignore */
253
+ }
254
+ if (!opts.allowPlainJs) {
255
+ // STOP. --hermes was explicitly requested; shipping the uncompiled
256
+ // bundle instead would publish a different artifact than the one
257
+ // asked for, and nothing downstream could tell.
258
+ for (const l of errLines)
259
+ console.error(` ${l}`);
260
+ throw new Error(`${reason}.\n` +
261
+ ' --hermes was requested, so this is a hard failure rather than a silent\n' +
262
+ ' downgrade: shipping plain JS would slow every user\'s cold start by\n' +
263
+ ' 20-30% and break delta patching against previous bytecode releases.\n' +
264
+ ' Fix the toolchain, or pass --allow-plain-js to publish uncompiled.');
265
+ }
266
+ console.warn(` ⚠️ ${reason} — shipping plain JS bundle (--allow-plain-js)`);
267
+ for (const l of errLines)
268
+ console.warn(` ${l}`);
84
269
  }
85
270
  else {
271
+ fs.renameSync(hbcPath, bundlePath);
86
272
  console.log(' ✅ Hermes bytecode compiled');
87
273
  }
88
274
  }
89
275
  else {
90
276
  console.log(' ℹ️ Shipping plain JS (no hermesc). Use --hermes for bytecode compilation.');
91
277
  }
92
- return { bundlePath, assetsPath };
278
+ // Read the format back off the artifact rather than trusting the flag. When
279
+ // hermesc fails we fall back to plain JS, and recording `hermes: true` anyway
280
+ // would tell the backend to gate devices on a bytecode version this bundle
281
+ // does not have.
282
+ const hermesBytecodeVersion = readHermesBytecodeVersion(bundlePath);
283
+ if (opts.hermes && hermesBytecodeVersion === null) {
284
+ console.warn(' ⚠️ Artifact is not Hermes bytecode — recording it as plain JS.');
285
+ }
286
+ return {
287
+ bundlePath,
288
+ assetsPath,
289
+ assets: listAssets(assetsPath),
290
+ hermesCompiled: hermesBytecodeVersion !== null,
291
+ hermesBytecodeVersion,
292
+ };
293
+ }
294
+ /**
295
+ * Every asset file Metro emitted for this bundle, relative to the assets root.
296
+ *
297
+ * OTA ships ONE artifact: the JS/bytecode bundle. Assets are resolved from what
298
+ * the installed binary already contains, so an image or font added since the
299
+ * last store build is referenced by the new JS and is simply not there — the
300
+ * update installs cleanly and renders a missing image. `ota publish` uses this
301
+ * list to say so out loud rather than discarding the directory in silence.
302
+ */
303
+ function listAssets(root) {
304
+ const out = [];
305
+ const walk = (dir, prefix) => {
306
+ let entries;
307
+ try {
308
+ entries = fs.readdirSync(dir, { withFileTypes: true });
309
+ }
310
+ catch {
311
+ return; // no assets dir at all is the common, healthy case
312
+ }
313
+ for (const entry of entries) {
314
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
315
+ if (entry.isDirectory())
316
+ walk(path.join(dir, entry.name), rel);
317
+ else
318
+ out.push(rel);
319
+ }
320
+ };
321
+ walk(root, '');
322
+ return out;
93
323
  }
94
324
  //# sourceMappingURL=bundle.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,kCA+DC;AAzGD,4DAA8C;AAC9C,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAgCzB;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,IAAmB;IAC7C,MAAM,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC1E,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACxD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAE/C,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9C,MAAM,IAAI,GAAG;QACX,cAAc,EAAE,QAAQ;QACxB,YAAY,EAAE,IAAI,CAAC,QAAQ;QAC3B,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;QACzB,cAAc,EAAE,IAAI,CAAC,SAAS;QAC9B,iBAAiB,EAAE,UAAU;QAC7B,eAAe,EAAE,UAAU;KAC5B,CAAC;IAEF,4CAA4C;IAC5C,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,UAAU,GAAG,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhD,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;QACjD,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,IAAI;QACX,OAAO,EAAE,MAAO,EAAE,YAAY;KAC/B,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,6CAA6C,MAAM,CAAC,MAAM,EAAE,CAC7D,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,4BAA4B,UAAU,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,uCAAuC;IACvC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,sCAAsC;QAClE,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,CAC1C,KAAK,EACL,CAAC,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,EACxD,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAO,EAAE,CACpD,CAAC;QAEF,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,CACV,8BAA8B,aAAa,CAAC,MAAM,8BAA8B,CACjF,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CACT,8EAA8E,CAC/E,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/utils/bundle.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyFA,8DAoBC;AAeD,wCAkBC;AAGD,wDASC;AAmBD,sCAeC;AAED,kCAwKC;AAWD,gCAiBC;AAlYD,4DAA8C;AAC9C,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAuEzB;;;GAGG;AACH,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAEhF;;;;;;;;GAQG;AACH,SAAgB,yBAAyB,CAAC,QAAgB;IACxD,IAAI,EAAsB,CAAC;IAC3B,IAAI,CAAC;QACH,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAChC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAC/C,IAAI,IAAI,GAAG,EAAE;YAAE,OAAO,IAAI,CAAC;QAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1D,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,IAAI,CAAC;gBACH,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACnB,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY;YACd,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAgB,cAAc,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IACxD,MAAM,GAAG,GACP,OAAO,CAAC,QAAQ,KAAK,OAAO;QAC1B,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ;YAC7B,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,aAAa,CAAC;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;IACrE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CACzB,GAAG,EACH,cAAc,EACd,cAAc,EACd,MAAM,EACN,SAAS,EACT,GAAG,EACH,GAAG,CACJ,CAAC;IACF,OAAO,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;AACrD,CAAC;AAED,kFAAkF;AAClF,SAAgB,sBAAsB,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAChE,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;QAC/E,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAyB,CAAC;QAClF,OAAO,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH;;;;;;;;;GASG;AACH,SAAgB,aAAa,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IACvD,IAAI,CAAC;QACH,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;YAC1E,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QAC/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAGvD,CAAC;QACF,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,IAAI,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAgB,WAAW,CAAC,IAAmB;IAC7C,MAAM,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAC1E,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACxD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAE/C,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9C,2EAA2E;IAC3E,8EAA8E;IAC9E,2CAA2C;IAC3C,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI;QACf,CAAC,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC;QAC1B,CAAC,CAAC,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAE/B,IAAI,CAAC,IAAI,CACP,YAAY,EAAE,IAAI,CAAC,QAAQ,EAC3B,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EACzB,cAAc,EAAE,IAAI,CAAC,SAAS,EAC9B,iBAAiB,EAAE,UAAU,EAC7B,eAAe,EAAE,UAAU,CAC5B,CAAC;IAEF,4CAA4C;IAC5C,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,UAAU,GAAG,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,mBAAmB,UAAU,CAAC,CAAC;IACzE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhD,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;QACjD,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,IAAI;QACX,OAAO,EAAE,MAAO,EAAE,YAAY;KAC/B,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBAAqB,0BAA0B,MAAM,CAAC,MAAM,EAAE,CAC/F,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,4BAA4B,UAAU,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,wEAAwE;IACxE,4EAA4E;IAC5E,EAAE;IACF,2EAA2E;IAC3E,wEAAwE;IACxE,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,8CAA8C;IAC9C,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,oCAAoC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;QAC3F,EAAE,CAAC,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IAED,uCAAuC;IACvC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;QAC/C,0EAA0E;QAC1E,4EAA4E;QAC5E,0EAA0E;QAC1E,MAAM,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;QACpC,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;QAEjC,wEAAwE;QACxE,qEAAqE;QAErE,mEAAmE;QACnE,0EAA0E;QAC1E,0EAA0E;QAC1E,wEAAwE;QACxE,0EAA0E;QAC1E,2EAA2E;QAC3E,0EAA0E;QAC1E,0EAA0E;QAC1E,oEAAoE;QACpE,MAAM,aAAa,GAAG,OAAO;YAC3B,CAAC,CAAC,YAAY,CAAC,SAAS,CACpB,OAAO,EACP,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,EAC7C;gBACE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;gBACjC,KAAK,EAAE,KAAK;gBACZ,OAAO,EAAE,MAAO;gBAChB,QAAQ,EAAE,OAAO;gBACjB,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;aAC7B,CACF;YACH,CAAC,CAAE,EAAE,MAAM,EAAE,CAAC,EAA4C,CAAC;QAE7D,IAAI,OAAO,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1C,MAAM,QAAQ,GAAG,GAAG,aAAa,CAAC,MAAM,IAAI,EAAE,GAAG,aAAa,CAAC,MAAM,IAAI,EAAE,EAAE;iBAC1E,KAAK,CAAC,IAAI,CAAC;iBACX,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;YAChD,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,MAAM,QAAQ,yDAAyD,CAAC,CAAC;YACvF,CAAC;QACH,CAAC;QAED,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1D,mEAAmE;YACnE,uEAAuE;YACvE,MAAM,MAAM,GAAG,OAAO;gBACpB,CAAC,CAAC,wBAAwB,aAAa,CAAC,MAAM,EAAE;oBAC9C,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG;gBACrE,CAAC,CAAC,gEAAgE,CAAC;YACrE,iEAAiE;YACjE,mDAAmD;YACnD,MAAM,QAAQ,GAAG,GAAG,aAAa,CAAC,MAAM,IAAI,EAAE,EAAE;iBAC7C,KAAK,CAAC,IAAI,CAAC;iBACX,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;iBACnC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;iBACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAE5D,IAAI,CAAC;gBACH,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;oBAAE,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YACrD,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY;YACd,CAAC;YAED,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;gBACvB,mEAAmE;gBACnE,iEAAiE;gBACjE,gDAAgD;gBAChD,KAAK,MAAM,CAAC,IAAI,QAAQ;oBAAE,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBACtD,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,KAAK;oBACZ,4EAA4E;oBAC5E,yEAAyE;oBACzE,yEAAyE;oBACzE,sEAAsE,CACzE,CAAC;YACJ,CAAC;YAED,OAAO,CAAC,IAAI,CAAC,SAAS,MAAM,gDAAgD,CAAC,CAAC;YAC9E,KAAK,MAAM,CAAC,IAAI,QAAQ;gBAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CACT,8EAA8E,CAC/E,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,8EAA8E;IAC9E,2EAA2E;IAC3E,iBAAiB;IACjB,MAAM,qBAAqB,GAAG,yBAAyB,CAAC,UAAU,CAAC,CAAC;IACpE,IAAI,IAAI,CAAC,MAAM,IAAI,qBAAqB,KAAK,IAAI,EAAE,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAC;IACpF,CAAC;IAED,OAAO;QACL,UAAU;QACV,UAAU;QACV,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC;QAC9B,cAAc,EAAE,qBAAqB,KAAK,IAAI;QAC9C,qBAAqB;KACtB,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,UAAU,CAAC,IAAY;IACrC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,MAAc,EAAE,EAAE;QAC3C,IAAI,OAAoB,CAAC;QACzB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,mDAAmD;QAC7D,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;YAC5D,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;;gBAC1D,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACf,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -8,7 +8,18 @@ export interface Credentials {
8
8
  * 2. ~/.scalebun/credentials.json (from `scalebun login`)
9
9
  */
10
10
  export declare function readCredentials(): Credentials | null;
11
- /** Store credentials to ~/.scalebun/credentials.json */
11
+ /**
12
+ * Store credentials to ~/.scalebun/credentials.json.
13
+ *
14
+ * The `mode: 0o600` below is real on macOS and Linux and a NO-OP ON WINDOWS —
15
+ * Node cannot express a Windows ACL through a Unix mode, so it silently drops
16
+ * it and the file lands with whatever the parent directory grants. That is how
17
+ * a token ends up readable by every other account on a shared machine while
18
+ * the code looks like it locked the file down. So on Windows the ACL is set
19
+ * explicitly afterwards, and a failure to do so is reported rather than
20
+ * swallowed: the user is entitled to know their token is sitting in a
21
+ * world-readable file.
22
+ */
12
23
  export declare function writeCredentials(creds: Credentials): void;
13
24
  /** Delete stored credentials. */
14
25
  export declare function clearCredentials(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CAkBpD;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CASzD;AAED,iCAAiC;AACjC,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CAkBpD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAUzD;AAgCD,iCAAiC;AACjC,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC"}
@@ -39,6 +39,7 @@ exports.clearCredentials = clearCredentials;
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const os = __importStar(require("os"));
42
+ const childProcess = __importStar(require("child_process"));
42
43
  const CREDENTIALS_DIR = path.join(os.homedir(), '.scalebun');
43
44
  const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
44
45
  /**
@@ -67,12 +68,50 @@ function readCredentials() {
67
68
  return null;
68
69
  }
69
70
  }
70
- /** Store credentials to ~/.scalebun/credentials.json */
71
+ /**
72
+ * Store credentials to ~/.scalebun/credentials.json.
73
+ *
74
+ * The `mode: 0o600` below is real on macOS and Linux and a NO-OP ON WINDOWS —
75
+ * Node cannot express a Windows ACL through a Unix mode, so it silently drops
76
+ * it and the file lands with whatever the parent directory grants. That is how
77
+ * a token ends up readable by every other account on a shared machine while
78
+ * the code looks like it locked the file down. So on Windows the ACL is set
79
+ * explicitly afterwards, and a failure to do so is reported rather than
80
+ * swallowed: the user is entitled to know their token is sitting in a
81
+ * world-readable file.
82
+ */
71
83
  function writeCredentials(creds) {
72
84
  if (!fs.existsSync(CREDENTIALS_DIR)) {
73
85
  fs.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
74
86
  }
75
87
  fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2) + '\n', { mode: 0o600 });
88
+ restrictToCurrentUser(CREDENTIALS_FILE);
89
+ }
90
+ /**
91
+ * Windows-only ACL tightening: drop inherited ACEs and grant the current user
92
+ * alone full control. On POSIX the file mode already did this and this is a
93
+ * no-op.
94
+ */
95
+ function restrictToCurrentUser(file) {
96
+ if (process.platform !== 'win32')
97
+ return;
98
+ const user = process.env.USERNAME
99
+ ? `${process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\` : ''}${process.env.USERNAME}`
100
+ : null;
101
+ if (!user)
102
+ return;
103
+ try {
104
+ // /inheritance:r removes inherited permissions — without it the grant is
105
+ // added ON TOP of whatever the profile directory already allows, which is
106
+ // usually the point being missed.
107
+ childProcess.execFileSync('icacls', [file, '/inheritance:r', '/grant:r', `${user}:F`], { stdio: 'ignore', timeout: 10000 });
108
+ }
109
+ catch {
110
+ console.warn(`⚠️ Could not restrict permissions on ${file}.\n` +
111
+ ' It contains an access token in plaintext and may be readable by other\n' +
112
+ ' accounts on this machine. Fix it with:\n' +
113
+ ` icacls "${file}" /inheritance:r /grant:r "%USERNAME%:F"`);
114
+ }
76
115
  }
77
116
  /** Delete stored credentials. */
78
117
  function clearCredentials() {
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBA,0CAkBC;AAGD,4CASC;AAGD,4CAIC;AAtDD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAEzB,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;AAC7D,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,kBAAkB,CAAC,CAAC;AAOxE;;;;GAIG;AACH,SAAgB,eAAe;IAC7B,oCAAoC;IACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC5C,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAC;QACvB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IAC7C,CAAC;IAED,qCAAqC;IACrC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC;QAAE,OAAO,IAAI,CAAC;IAClD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACjD,OAAO,MAAqB,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,wDAAwD;AACxD,SAAgB,gBAAgB,CAAC,KAAkB;IACjD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACpC,EAAE,CAAC,SAAS,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,EAAE,CAAC,aAAa,CACd,gBAAgB,EAChB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EACrC,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAC;AACJ,CAAC;AAED,iCAAiC;AACjC,SAAgB,gBAAgB;IAC9B,IAAI,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACpC,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAClC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * HTTPS enforcement for everything the CLI authenticates to.
3
+ *
4
+ * WHAT WAS WRONG. The API client picked its transport straight off the URL
5
+ * scheme — `parsed.protocol === 'https:' ? https : http` — with nothing
6
+ * requiring HTTPS in the first place, and every request attaches
7
+ * `Authorization: Bearer <token>`. `scalebun login` accepts an arbitrary
8
+ * `--api-url` and POSTs an email and password to it. So a CLI pointed at an
9
+ * `http://` URL — by a typo, a copied CI snippet, or a tampered config —
10
+ * would put a personal access token, or the user's actual password, on the
11
+ * wire in cleartext, with no warning and no failure.
12
+ *
13
+ * THE RULE. Credentials only travel over TLS. The single exception is
14
+ * loopback, because local development against `http://localhost:3000` is a
15
+ * real workflow and traffic that never leaves the machine cannot be
16
+ * intercepted on the network. Anything else is refused before a socket is
17
+ * opened, rather than warned about — a warning printed above a successful
18
+ * request is not a control.
19
+ *
20
+ * Loopback is matched on resolved literals, not on name. `localhost` and the
21
+ * reserved 127/8 and ::1 literals are loopback by definition; an arbitrary
22
+ * hostname that merely happens to resolve to 127.0.0.1 today is not, because
23
+ * whoever controls the DNS answer controls where the token goes tomorrow.
24
+ */
25
+ export declare class InsecureApiUrlError extends Error {
26
+ constructor(url: string, hostname: string);
27
+ }
28
+ /**
29
+ * Validate an API base URL before it is used or persisted.
30
+ *
31
+ * @returns the normalised URL (trailing slashes stripped)
32
+ * @throws InsecureApiUrlError for a non-loopback `http://` URL
33
+ * @throws Error for a URL that is malformed or uses a non-HTTP scheme
34
+ */
35
+ export declare function assertSecureApiUrl(rawUrl: string): string;
36
+ //# sourceMappingURL=transportSecurity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transportSecurity.d.ts","sourceRoot":"","sources":["../../src/utils/transportSecurity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAYH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAW1C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAkBzD"}