@condorinst/mobile-bridge 0.8.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.
package/README.md ADDED
@@ -0,0 +1,396 @@
1
+ # @condorinst/mobile-bridge
2
+
3
+ Canonical TypeScript SDK for the JS↔Native bridge of the Condor white-label
4
+ mobile app.
5
+
6
+ > **Purpose:** give the JavaScript code that runs inside the white-label app's
7
+ > WebView a typed, runtime-validated, testable API to talk to the native
8
+ > Flutter side — versioned handshake, observation of keys emitted by the
9
+ > background, commands to the foreground service, and hooks into native
10
+ > screens (Settings).
11
+
12
+ > **New here?** Start with the [Client Guide](./docs/client-guide.md) —
13
+ > _"Build your White Label Home"_ — and the runnable
14
+ > [`examples/minimal-home`](./examples/minimal-home).
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @condorinst/mobile-bridge
20
+ ```
21
+
22
+ The published package ships a prebuilt `dist/` (ESM + CJS + type
23
+ declarations) — **no build toolchain is required on your side**.
24
+
25
+ > **Versioning during the pilot.** The package is on `0.x` (beta). Pin
26
+ > `"@condorinst/mobile-bridge": "^0.8.0"`. Under semver, while on `0.x` a
27
+ > caret range auto-updates **patches only** (`0.8.x`); moving to `0.9.0` is
28
+ > an intentional opt-in. `1.0.0` is reserved for production.
29
+
30
+ ## Quickstart
31
+
32
+ ```typescript
33
+ import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
34
+
35
+ // Versioned handshake with the native side (can be called at any time;
36
+ // recommended on the client's DOMContentLoaded)
37
+ const ack = await Bridge.handshake({ clientVersion: '1.0.0' });
38
+ console.log('Native bridge:', ack.nativeVersion, 'accepted:', ack.accepted);
39
+
40
+ // Subscribe to an observable key coming from the background
41
+ const unsubscribe = Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
42
+ console.log('Device:', payload);
43
+ });
44
+
45
+ // Command: enable real-time
46
+ Bridge.send('realTime', { enabled: true });
47
+
48
+ // Native hook: open the app's native Settings screen
49
+ Bridge.openSettings();
50
+
51
+ // Cleanup
52
+ unsubscribe();
53
+ ```
54
+
55
+ > The recommended model for white-label clients is **`BridgeClient`** (sandbox
56
+ > iframe), not the legacy `Bridge`. See below.
57
+
58
+ ## Sandbox iframe — `BridgeClient` + `BridgeShell`
59
+
60
+ The recommended model isolates the external client code inside an
61
+ `<iframe sandbox="allow-scripts">`, communicating over `window.postMessage`
62
+ with schema-validated messages and a default-deny policy.
63
+
64
+ ### For the external client — `BridgeClient`
65
+
66
+ API **identical** to the legacy `Bridge`, but over a postMessage transport.
67
+ The client imports it and never notices it is sandboxed:
68
+
69
+ ```typescript
70
+ import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
71
+
72
+ const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
73
+ console.log('Shell:', ack.shellVersion, 'accepted:', ack.accepted);
74
+
75
+ const unsubscribe = BridgeClient.on(BRIDGE_KEYS.userDevice, (payload) => {
76
+ console.log('Devices:', payload);
77
+ });
78
+
79
+ BridgeClient.send('realTime', { serialNo: 1234, value: true });
80
+ BridgeClient.openSettings();
81
+ ```
82
+
83
+ **Differences vs `Bridge`:**
84
+ - Does not expose `mockEmit`, `installDevPanel`, `isMock`, `getMockState` —
85
+ dev/mock APIs live only on the shell.
86
+ - `handshake` resolves with `{ shellVersion, accepted }` (not `nativeVersion`).
87
+ - Commands not allowed by the `ShellPolicy` are silently dropped (with a
88
+ `console.warn`). The external client has no visibility into which commands
89
+ do or do not exist.
90
+
91
+ ### For the Condor-controlled shell — `BridgeShell`
92
+
93
+ ```typescript
94
+ import { BridgeShell, BRIDGE_KEYS, type ShellPolicy } from '@condorinst/mobile-bridge';
95
+
96
+ const iframe = document.getElementById('client-frame') as HTMLIFrameElement;
97
+
98
+ const policy: ShellPolicy = {
99
+ readOnlyKeys: [
100
+ BRIDGE_KEYS.userDevice,
101
+ BRIDGE_KEYS.deviceConnection,
102
+ BRIDGE_KEYS.deviceLightSensors,
103
+ BRIDGE_KEYS.realTime,
104
+ ],
105
+ allowedCommands: ['userDevice', 'realTime', 'openSettings'],
106
+ requiresConfirmation: ['syncTime', 'deleteMemory'],
107
+ deniedCommands: ['dfu'],
108
+ };
109
+
110
+ const shell = new BridgeShell({
111
+ iframe,
112
+ policy,
113
+ onConfirmationRequest: async ({ command, args }) => {
114
+ return await showConfirmationModal(command, args); // shell UI
115
+ },
116
+ });
117
+
118
+ // Layer 1 — bootstrap with an override prefix.
119
+ //
120
+ // Use bootstrapClientIframe(clientPath) when HTML comes over HTTP:
121
+ await shell.bootstrapClientIframe('https://api.condor.example/whitelabel/bundle.html');
122
+ //
123
+ // Use bootstrapClientIframeWithHtml(htmlString) when the HTML is already in
124
+ // memory — required for an asset bundled at file:// (Android WebView cannot
125
+ // fetch over file://), a build-time inlined bundle, etc:
126
+ declare const __CLIENT_HTML__: string; // e.g. via esbuild.define
127
+ await shell.bootstrapClientIframeWithHtml(__CLIENT_HTML__);
128
+
129
+ // Layer 2 — iframe attestation.
130
+ const verify = await shell.verifySandbox();
131
+ if (!verify.ok) {
132
+ // Abort the boot — Layer 1 failed.
133
+ throw new Error('Sandbox compromised: bridges still reachable from iframe.');
134
+ }
135
+
136
+ // From here on, BridgeShell routes postMessage ↔ native Bridge
137
+ // automatically. Nothing else to do at boot.
138
+ ```
139
+
140
+ ### Layered defense (propagation of `window.flutter_inappwebview`)
141
+
142
+ | Layer | Where | What it does |
143
+ |---|---|---|
144
+ | **1** | `BridgeShell.bootstrapClientIframe` | `srcdoc` with a prefix that deletes `window.flutter_inappwebview` + `window.CondorBridge` before any client script runs. |
145
+ | **2** | `BridgeShell.verifySandbox` + `BridgeClient` handler | Round-trip `postMessage` to attest the iframe state. |
146
+ | **3** | (Native side) | Shell nonce validated natively in the Flutter bridge. |
147
+ | **4** | (Fallback if 1+2 fail) | Dual `InAppWebView` — shell and client in separate WebViews. |
148
+
149
+ ## Build CLI — `condor-mobile-bridge build`
150
+
151
+ Generates the **self-contained `.zip` bundle** that the white-label app
152
+ consumes. The app injects the client HTML via `iframe.srcdoc` (origin `null`,
153
+ no base URL) → **relative references do not resolve** and the CSP blocks
154
+ external fetches; therefore the `.zip` must contain **a single `index.html`**
155
+ with inline JS/CSS and media as `data:` URIs.
156
+
157
+ The CLI does this from your build's `dist/`: it reads `index.html`, **inlines**
158
+ `<script src>` (escaping `</script>`) and `<link rel=stylesheet>`, resolves CSS
159
+ `url()` and local `<img>`/icons to `data:` URIs, and packs a `.zip` with
160
+ `index.html` at the root. External refs (`https://…`) are **preserved** (and
161
+ blocked by the app's CSP at runtime).
162
+
163
+ ```bash
164
+ npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
165
+ ```
166
+
167
+ | Option | Description |
168
+ |---|---|
169
+ | `-i, --html <path>` | Input HTML (e.g. `dist/index.html`). Required. |
170
+ | `-o, --out <path>` | Output `.zip` (default: `dist/whitelabel-bundle.zip`). |
171
+ | `--max-bytes <n>` | **Warning** threshold in bytes (default: `2097152` = 2 MB). |
172
+
173
+ Output (for registering the bundle with the backend — `bundleSize`/`bundleHash`):
174
+
175
+ ```
176
+ Built dist/bundle.zip
177
+ entry: index.html (123.4 KB self-contained)
178
+ bundleSize (zip): 45678 bytes
179
+ bundleHash (zip): sha256-<hex>
180
+ ```
181
+
182
+ The hash is **deterministic** (fixed mtime on the zip entry) — the same input
183
+ produces the same `bundleHash`. Above the threshold, the CLI **warns** (does
184
+ not block): remember that **the app rejects** a `.zip` larger than 2 MB and
185
+ falls back to the mock.
186
+
187
+ > ⚠️ **Constraint: single-chunk build.** The inliner is post-build and assumes
188
+ > a `dist` with **one JS + one CSS + assets**. Builds with **code-splitting /
189
+ > ES modules** that `import` across chunks **break** in `srcdoc` (the imports
190
+ > do not resolve). Configure your build to emit a single file:
191
+ > - **Vite:** `vite-plugin-singlefile` (or `build.rollupOptions.output.inlineDynamicImports: true` + `manualChunks: undefined`).
192
+ > - **CRA:** build without code-splitting (no `React.lazy`/dynamic `import()` in routes), single bundle.
193
+ >
194
+ > Inlining inflates `data:` media by ~33% (base64) — size against the 2 MB limit.
195
+
196
+ ## Key catalog (strictly typed)
197
+
198
+ Mirrors the JS-facing side of the native foreground com keys. The strings here
199
+ are what the JS receives via `window.CondorBridge.emit(key, payload)`.
200
+
201
+ | `BRIDGE_KEYS.*` | Wire (JS) | Typed payload | Typical direction |
202
+ |---|---|---|---|
203
+ | `pingPong` | `'pingPong'` | `PingPongPayload = { data: 'ping' \| 'pong' }` | bidirectional |
204
+ | `userDevice` | `'userDevice'` | `UserDevicePayload = { lastUpdate, devicesConfig[], devicesTelemetry[], devicesUserConfig[] }` | BG → JS |
205
+ | `deviceLightSensors` | `'deviceLightSensors'` | `DeviceLightSensorsPayload = { serialNo, type, data }` | BG → JS |
206
+ | `syncTime` | `'syncTime'` | `SyncTimeAckPayload = { serialNo, success }` | JS → BG (cmd) / BG → JS (ack) |
207
+ | `realTime` | `'realTime'` | `RealTimeAckPayload = { serialNo, success, value }` | JS → BG (cmd) / BG → JS (state) |
208
+ | `deleteMemory` | `'deleteMemory'` | `DeleteMemoryAckPayload = { serialNo, success }` | JS → BG (cmd) / BG → JS (ack) |
209
+ | `deviceNotification` | `'deviceNotification'` | `DeviceNotificationPayload = { serialNo, isConnect, message, messageCode, isError }` | BG → JS |
210
+ | `deviceConnection` | `'deviceConnection'` | `DeviceConnectionPayload = { serialNo, isConnect }` | BG → JS (status); JS → BG (`{type, serialNo}`, permissive) |
211
+ | `sensorLog` | `'sensorLog'` | `SensorLogPayload` | BG → JS |
212
+ | `dfu` | `'dfu'` | `Record<string, unknown>` (fallback) | BG → JS |
213
+
214
+ **Automatic inference on `Bridge.on`:**
215
+
216
+ ```typescript
217
+ Bridge.on(BRIDGE_KEYS.realTime, (payload) => {
218
+ // payload: RealTimeAckPayload — type-safe!
219
+ console.log(payload.serialNo, payload.success, payload.value);
220
+ });
221
+ ```
222
+
223
+ **`Bridge.send` commands** remain permissive (heterogeneous shapes per type).
224
+ The client passes `Record<string, unknown>`.
225
+
226
+ ## Versioning policy (semver)
227
+
228
+ - **PATCH** (`0.8.x`): bugfix with no API change.
229
+ - **MINOR** (`0.x.0`): new observable key, new command, backward-compatible API.
230
+ > While on `0.x`, a minor bump may carry breaking changes (semver treats
231
+ > pre-1.0 minors as breaking). Pin `^0.8.0` to stay on safe patches.
232
+ - **MAJOR** (`x.0.0`): incompatible schema change (key rename, payload shape
233
+ change, public API change). `1.0.0` marks the production-stable contract.
234
+
235
+ The native side declares `nativeVersion` in the `handshake`; the client
236
+ declares `clientVersion`. This release does **not** enforce hard semver
237
+ validation — the native side accepts any `clientVersion`. Real validation will
238
+ land once there are 2+ versions in the field.
239
+
240
+ ## Host detection
241
+
242
+ The package detects automatically:
243
+
244
+ - **Android / iOS** — `window.flutter_inappwebview` (via the `flutter_inappwebview` package on the Flutter side).
245
+ - **Browser outside the app** (dev/test): **mock mode** activates automatically.
246
+
247
+ ## Mock mode
248
+
249
+ When the package runs outside the Flutter WebView (regular browser, Vite dev
250
+ server, Storybook), the adapter activates mock mode automatically. You can
251
+ develop in the browser without building the APK.
252
+
253
+ ```typescript
254
+ import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
255
+
256
+ if (Bridge.isMock) {
257
+ // Subscribe to a listener as usual
258
+ Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
259
+ console.log('Device:', payload);
260
+ });
261
+
262
+ // Simulate the native side sending an event
263
+ Bridge.mockEmit(BRIDGE_KEYS.deviceConnection, {
264
+ serialNo: 1234,
265
+ connected: true,
266
+ });
267
+ }
268
+ ```
269
+
270
+ ### Handshake in mock is EXPLICIT
271
+
272
+ ```typescript
273
+ // In mock, handshake does NOT resolve by itself — the client/test responds.
274
+ const ackPromise = Bridge.handshake({ clientVersion: '1.0.0' });
275
+
276
+ if (Bridge.isMock) {
277
+ Bridge.mockEmit('handshake-ack', {
278
+ nativeVersion: '0.1.0',
279
+ accepted: true,
280
+ });
281
+ }
282
+
283
+ const ack = await ackPromise;
284
+ ```
285
+
286
+ Design decision: it forces tests to be intentional about the native state. In
287
+ real mode (inside the Flutter WebView), the ack arrives naturally from native.
288
+
289
+ ### `Bridge.send` in mock
290
+
291
+ Messages sent via `Bridge.send` (or helpers like `openSettings`) are recorded
292
+ in an internal FIFO buffer (cap 100) + `console.debug`. Inspect via
293
+ `Bridge.getMockState()`:
294
+
295
+ ```typescript
296
+ Bridge.send('realTime', { enabled: true });
297
+
298
+ const state = Bridge.getMockState();
299
+ console.log(state.messages);
300
+ // → [{ handlerName: 'CondorBridge', payload: {...}, timestamp: ... }]
301
+ console.log(state.subscriberKeys);
302
+ // → ['deviceConnection']
303
+ ```
304
+
305
+ ## Dev panel
306
+
307
+ An opt-in HTML panel to inspect the bridge in the browser during development.
308
+
309
+ ```typescript
310
+ import { Bridge } from '@condorinst/mobile-bridge';
311
+
312
+ if (Bridge.isMock) {
313
+ Bridge.installDevPanel({
314
+ position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-*'
315
+ maxMessages: 20, // last N shown
316
+ theme: 'dark', // 'dark' | 'light'
317
+ startCollapsed: false,
318
+ });
319
+ }
320
+ ```
321
+
322
+ Contents:
323
+
324
+ - Header with a "MOCK" badge, sent counter, minimize button.
325
+ - Sent log — last N messages (timestamp, type, truncated payload).
326
+ - Trigger emit — dropdown of `BRIDGE_KEYS` + JSON textarea + "Emit" button
327
+ (fires `Bridge.mockEmit`) + "Clear sent" button.
328
+
329
+ Idempotent — calling `installDevPanel` again replaces the previous panel.
330
+ Throws if called in real mode (`!Bridge.isMock`).
331
+
332
+ ### Auto-respond toggle
333
+
334
+ To close the full loop in the browser (client clicks a button → native
335
+ responds → subscriber fires), pass a **policy** that maps `send` to `emit`.
336
+ The package provides the **mechanism** (checkbox UI + hook); you provide the
337
+ **policy** (app-specific mappings).
338
+
339
+ ```typescript
340
+ import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
341
+
342
+ if (Bridge.isMock) {
343
+ Bridge.installDevPanel({
344
+ startAutoRespondEnabled: true, // default false
345
+ autoRespondDelayMs: 50, // default 50 (simulates IPC)
346
+ onSendForAutoRespond(msg) {
347
+ const payload = msg.payload as { type?: string };
348
+ switch (payload?.type) {
349
+ case 'handshake':
350
+ return {
351
+ key: 'handshake-ack',
352
+ payload: { nativeVersion: '0.1.0 (mock)', accepted: true },
353
+ };
354
+ case 'ping':
355
+ return {
356
+ key: BRIDGE_KEYS.pingPong,
357
+ payload: { data: 'pong' },
358
+ };
359
+ default:
360
+ return null; // do not auto-respond to this send
361
+ }
362
+ },
363
+ });
364
+ }
365
+ ```
366
+
367
+ Behavior:
368
+
369
+ - The toggle appears in the panel **only when** `onSendForAutoRespond` is
370
+ passed — without a policy, the panel keeps the manual-emit UX.
371
+ - The toggle state persists in memory for the panel's lifetime. Re-install
372
+ resets to `startAutoRespondEnabled`.
373
+ - Errors thrown by the policy or the internal `triggerEmit` are caught and
374
+ logged (`console.error`) — they do not destabilize the bridge.
375
+ - Use `BRIDGE_KEYS` to avoid string-magic in the mappings.
376
+
377
+ ## Local development (SDK contributors)
378
+
379
+ ```bash
380
+ npm install # installs deps + runs prepare (build)
381
+ npm run typecheck # tsc --noEmit
382
+ npm run lint # biome check
383
+ npm run format # biome format --write
384
+ npm run test # vitest run
385
+ npm run build # tsup (ESM + CJS + types in dist/)
386
+ ```
387
+
388
+ ## Further reading
389
+
390
+ - [Client Guide — Build your White Label Home](./docs/client-guide.md)
391
+ - [`examples/minimal-home`](./examples/minimal-home) — runnable minimal client
392
+ - [CHANGELOG](./CHANGELOG.md)
393
+
394
+ ## License
395
+
396
+ Proprietary — Condor Instruments. See [LICENSE](./LICENSE).
package/dist/cli.cjs ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli.ts
5
+ var import_node_fs2 = require("fs");
6
+ var import_node_path2 = require("path");
7
+
8
+ // src/cli-core.ts
9
+ var import_node_crypto = require("crypto");
10
+ var import_node_fs = require("fs");
11
+ var import_node_path = require("path");
12
+ var import_fflate = require("fflate");
13
+ var import_node_html_parser = require("node-html-parser");
14
+ var DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
15
+ var FIXED_MTIME = /* @__PURE__ */ new Date("2000-01-01T00:00:00Z");
16
+ var MIME_TYPES = {
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".jpeg": "image/jpeg",
20
+ ".gif": "image/gif",
21
+ ".webp": "image/webp",
22
+ ".avif": "image/avif",
23
+ ".svg": "image/svg+xml",
24
+ ".ico": "image/x-icon",
25
+ ".bmp": "image/bmp",
26
+ ".woff": "font/woff",
27
+ ".woff2": "font/woff2",
28
+ ".ttf": "font/ttf",
29
+ ".otf": "font/otf",
30
+ ".eot": "application/vnd.ms-fontobject",
31
+ ".mp3": "audio/mpeg",
32
+ ".mp4": "video/mp4",
33
+ ".webm": "video/webm",
34
+ ".json": "application/json"
35
+ };
36
+ function isExternalOrInline(url) {
37
+ return /^(https?:)?\/\//i.test(url) || /^data:/i.test(url) || /^(mailto:|tel:|#)/i.test(url);
38
+ }
39
+ function stripQueryHash(url) {
40
+ return url.split(/[?#]/)[0] ?? url;
41
+ }
42
+ function resolveLocal(htmlRoot, resourceDir, url) {
43
+ const clean = stripQueryHash(url);
44
+ return clean.startsWith("/") ? (0, import_node_path.resolve)(htmlRoot, `.${clean}`) : (0, import_node_path.resolve)(resourceDir, clean);
45
+ }
46
+ function toDataUri(filePath) {
47
+ const bytes = (0, import_node_fs.readFileSync)(filePath);
48
+ const mime = MIME_TYPES[(0, import_node_path.extname)(filePath).toLowerCase()] ?? "application/octet-stream";
49
+ return `data:${mime};base64,${bytes.toString("base64")}`;
50
+ }
51
+ function inlineCssUrls(css, htmlRoot, cssDir) {
52
+ return css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (match, _quote, rawUrl) => {
53
+ if (isExternalOrInline(rawUrl)) return match;
54
+ const filePath = resolveLocal(htmlRoot, cssDir, rawUrl);
55
+ return `url(${toDataUri(filePath)})`;
56
+ });
57
+ }
58
+ function inlineScripts(root, htmlRoot) {
59
+ for (const el of root.querySelectorAll("script[src]")) {
60
+ const src = el.getAttribute("src");
61
+ if (!src || isExternalOrInline(src)) continue;
62
+ const js = (0, import_node_fs.readFileSync)(resolveLocal(htmlRoot, htmlRoot, src), "utf-8").replace(
63
+ /<\/script>/gi,
64
+ "<\\/script>"
65
+ );
66
+ el.removeAttribute("src");
67
+ el.set_content(js);
68
+ }
69
+ }
70
+ function inlineStylesheets(root, htmlRoot) {
71
+ for (const el of root.querySelectorAll('link[rel="stylesheet"]')) {
72
+ const href = el.getAttribute("href");
73
+ if (!href || isExternalOrInline(href)) continue;
74
+ const cssPath = resolveLocal(htmlRoot, htmlRoot, href);
75
+ const css = inlineCssUrls((0, import_node_fs.readFileSync)(cssPath, "utf-8"), htmlRoot, (0, import_node_path.dirname)(cssPath));
76
+ el.replaceWith(...(0, import_node_html_parser.parse)(`<style>${css}</style>`).childNodes);
77
+ }
78
+ }
79
+ function inlineExistingStyleBlocks(root, htmlRoot) {
80
+ for (const el of root.querySelectorAll("style")) {
81
+ const css = el.innerHTML;
82
+ if (!css.includes("url(")) continue;
83
+ el.set_content(inlineCssUrls(css, htmlRoot, htmlRoot));
84
+ }
85
+ }
86
+ function inlineMediaAttributes(root, htmlRoot) {
87
+ const targets = [
88
+ ["img[src]", "src"],
89
+ ["source[src]", "src"],
90
+ ["audio[src]", "src"],
91
+ ["video[src]", "src"],
92
+ ['link[rel="icon"]', "href"],
93
+ ['link[rel="shortcut icon"]', "href"],
94
+ ['link[rel="apple-touch-icon"]', "href"],
95
+ ['link[rel="preload"]', "href"]
96
+ ];
97
+ for (const [selector, attr] of targets) {
98
+ for (const el of root.querySelectorAll(selector)) {
99
+ const ref = el.getAttribute(attr);
100
+ if (!ref || isExternalOrInline(ref)) continue;
101
+ el.setAttribute(attr, toDataUri(resolveLocal(htmlRoot, htmlRoot, ref)));
102
+ }
103
+ }
104
+ }
105
+ function inlineHtml(htmlPath) {
106
+ const htmlRoot = (0, import_node_path.dirname)((0, import_node_path.resolve)(htmlPath));
107
+ const root = (0, import_node_html_parser.parse)((0, import_node_fs.readFileSync)(htmlPath, "utf-8"), {
108
+ comment: true,
109
+ voidTag: { closingSlash: true }
110
+ });
111
+ inlineScripts(root, htmlRoot);
112
+ inlineStylesheets(root, htmlRoot);
113
+ inlineExistingStyleBlocks(root, htmlRoot);
114
+ inlineMediaAttributes(root, htmlRoot);
115
+ return root.toString();
116
+ }
117
+ function buildBundle(htmlPath, maxBytes = DEFAULT_MAX_BYTES) {
118
+ const html = inlineHtml(htmlPath);
119
+ const zipBytes = (0, import_fflate.zipSync)({ "index.html": [(0, import_fflate.strToU8)(html), { mtime: FIXED_MTIME }] }, { level: 6 });
120
+ const size = zipBytes.length;
121
+ const hash = `sha256-${(0, import_node_crypto.createHash)("sha256").update(zipBytes).digest("hex")}`;
122
+ return { html, zipBytes, size, hash, overLimit: size > maxBytes };
123
+ }
124
+ var DEFAULT_OUT = "dist/whitelabel-bundle.zip";
125
+ function parseArgs(argv) {
126
+ const args = {
127
+ htmlPath: "",
128
+ outPath: DEFAULT_OUT,
129
+ maxBytes: DEFAULT_MAX_BYTES,
130
+ help: false
131
+ };
132
+ for (let i = 0; i < argv.length; i++) {
133
+ const token = argv[i];
134
+ switch (token) {
135
+ case "--help":
136
+ case "-h":
137
+ args.help = true;
138
+ break;
139
+ case "--html":
140
+ case "-i":
141
+ args.htmlPath = argv[++i] ?? "";
142
+ break;
143
+ case "--out":
144
+ case "-o":
145
+ args.outPath = argv[++i] ?? DEFAULT_OUT;
146
+ break;
147
+ case "--max-bytes":
148
+ args.maxBytes = Number.parseInt(argv[++i] ?? "", 10) || DEFAULT_MAX_BYTES;
149
+ break;
150
+ case "build":
151
+ break;
152
+ default:
153
+ if (token && !token.startsWith("-") && !args.htmlPath) args.htmlPath = token;
154
+ }
155
+ }
156
+ return args;
157
+ }
158
+ var USAGE = `condor-mobile-bridge build \u2014 gera o .zip white-label self-contained.
159
+
160
+ Uso:
161
+ npx condor-mobile-bridge build -i <html> [-o <zip>]
162
+
163
+ Op\xE7\xF5es:
164
+ -i, --html <path> HTML de entrada (ex.: dist/index.html). Obrigat\xF3rio.
165
+ -o, --out <path> .zip de sa\xEDda (default: ${DEFAULT_OUT}).
166
+ --max-bytes <n> Limite de aviso em bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).
167
+ -h, --help Mostra esta ajuda.`;
168
+
169
+ // src/cli.ts
170
+ function main() {
171
+ const args = parseArgs(process.argv.slice(2));
172
+ if (args.help || !args.htmlPath) {
173
+ console.log(USAGE);
174
+ process.exit(args.help ? 0 : 1);
175
+ }
176
+ const result = buildBundle(args.htmlPath, args.maxBytes);
177
+ const outPath = (0, import_node_path2.resolve)(args.outPath);
178
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(outPath), { recursive: true });
179
+ (0, import_node_fs2.writeFileSync)(outPath, result.zipBytes);
180
+ const kb = (Buffer.byteLength(result.html) / 1024).toFixed(1);
181
+ console.log(`Built ${args.outPath}`);
182
+ console.log(` entry: index.html (${kb} KB self-contained)`);
183
+ console.log(` bundleSize (zip): ${result.size} bytes`);
184
+ console.log(` bundleHash (zip): ${result.hash}`);
185
+ if (result.overLimit) {
186
+ const limitMb = (args.maxBytes / (1024 * 1024)).toFixed(1);
187
+ console.warn(
188
+ `
189
+ \u26A0\uFE0F AVISO: o .zip (${result.size} bytes) excede o limite de ${limitMb} MB \u2014 o app vai REJEITAR este bundle (cai no mock). Reduza assets/m\xEDdias inline.`
190
+ );
191
+ }
192
+ }
193
+ main();
194
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/cli-core.ts"],"sourcesContent":["/**\n * cli.ts — Entry `bin` do `condor-mobile-bridge build`.\n *\n * Fino: parse de argv → `buildBundle` → escreve o `.zip` (mkdir recursivo) →\n * imprime tamanho + SHA-256 (formato que espelha `web-mock-src/build-demo.mjs`)\n * e avisa se passar do limite. Toda a lógica testável vive em `cli-core.ts`.\n */\nimport { mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { buildBundle, parseArgs, USAGE } from './cli-core';\n\nfunction main(): void {\n const args = parseArgs(process.argv.slice(2));\n\n if (args.help || !args.htmlPath) {\n console.log(USAGE);\n process.exit(args.help ? 0 : 1);\n }\n\n const result = buildBundle(args.htmlPath, args.maxBytes);\n\n const outPath = resolve(args.outPath);\n mkdirSync(dirname(outPath), { recursive: true });\n writeFileSync(outPath, result.zipBytes);\n\n const kb = (Buffer.byteLength(result.html) / 1024).toFixed(1);\n console.log(`Built ${args.outPath}`);\n console.log(` entry: index.html (${kb} KB self-contained)`);\n console.log(` bundleSize (zip): ${result.size} bytes`);\n console.log(` bundleHash (zip): ${result.hash}`);\n\n if (result.overLimit) {\n const limitMb = (args.maxBytes / (1024 * 1024)).toFixed(1);\n console.warn(\n `\\n⚠️ AVISO: o .zip (${result.size} bytes) excede o limite de ${limitMb} MB — ` +\n 'o app vai REJEITAR este bundle (cai no mock). Reduza assets/mídias inline.',\n );\n }\n}\n\nmain();\n","/**\n * cli-core.ts — Núcleo (puro/testável) do CLI `condor-mobile-bridge build`.\n *\n * Materializa a Frente 1 da Opção A do bundle white-label: pega um\n * `dist/index.html` (build React/Vite do cliente), inlina JS em `<script>`,\n * CSS em `<style>` (resolvendo `url()` em `data:` URI), converte mídias\n * locais em `data:` URI e empacota num `.zip` com um único `index.html` na\n * raiz. Não toca o filesystem de saída — quem escreve o `.zip` é `cli.ts`.\n *\n * Modelo de render do app: `iframe.srcdoc` (origem `null`, sem base URL) só\n * resolve conteúdo inline + `data:` URI — por isso o bundle precisa ser\n * self-contained. Ver `condor_wiki/decisions/2026-06-22_whitelabel-bundle-format.md`.\n *\n * LIMITAÇÃO conhecida: o inliner pós-build assume `dist` single-chunk\n * (um JS + um CSS + assets). Builds com code-split / ES modules que fazem\n * `import` entre chunks quebram em `srcdoc` — o cliente deve buildar\n * single-file (Vite: `vite-plugin-singlefile`; CRA: sem split). Ver README.\n */\nimport { createHash } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { dirname, extname, resolve } from 'node:path';\nimport { strToU8, zipSync } from 'fflate';\nimport { type HTMLElement, parse } from 'node-html-parser';\n\n/** Default do tamanho máximo do `.zip` (espelha `WhitelabelBundleLimits.maxZipBytes` no app). */\nexport const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;\n\n/** Mtime fixo (>=1980, limite do formato ZIP) → SHA-256 reprodutível entre builds. */\nconst FIXED_MTIME = new Date('2000-01-01T00:00:00Z');\n\n/** Mapa de extensão → MIME para montar `data:` URIs de mídias embutidas. */\nexport const MIME_TYPES: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.bmp': 'image/bmp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.eot': 'application/vnd.ms-fontobject',\n '.mp3': 'audio/mpeg',\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n '.json': 'application/json',\n};\n\n/** Refs que NÃO devem ser inlinadas: absolutas externas, protocol-relative, data: já-inline. */\nfunction isExternalOrInline(url: string): boolean {\n return /^(https?:)?\\/\\//i.test(url) || /^data:/i.test(url) || /^(mailto:|tel:|#)/i.test(url);\n}\n\n/** Remove query/hash de uma ref local (`main.js?v=abc#x` → `main.js`). */\nfunction stripQueryHash(url: string): string {\n return url.split(/[?#]/)[0] ?? url;\n}\n\n/**\n * Resolve uma ref local para caminho absoluto no filesystem.\n * `htmlRoot` é a raiz do site (dir do `index.html`); `resourceDir` é o dir do\n * recurso que contém a ref (p/ `url()` no CSS, é o dir do `.css`). Refs que\n * começam com `/` são relativas à raiz do site.\n */\nfunction resolveLocal(htmlRoot: string, resourceDir: string, url: string): string {\n const clean = stripQueryHash(url);\n return clean.startsWith('/') ? resolve(htmlRoot, `.${clean}`) : resolve(resourceDir, clean);\n}\n\n/** Lê um arquivo local e devolve uma `data:` URI base64. */\nexport function toDataUri(filePath: string): string {\n const bytes = readFileSync(filePath);\n const mime = MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n return `data:${mime};base64,${bytes.toString('base64')}`;\n}\n\n/**\n * Resolve `url(...)` dentro de um bloco CSS, trocando refs locais por `data:`\n * URIs. `cssDir` é o dir do arquivo CSS (ou do HTML, p/ `<style>` inline).\n */\nexport function inlineCssUrls(css: string, htmlRoot: string, cssDir: string): string {\n return css.replace(/url\\(\\s*(['\"]?)([^'\")]+)\\1\\s*\\)/gi, (match, _quote, rawUrl: string) => {\n if (isExternalOrInline(rawUrl)) return match;\n const filePath = resolveLocal(htmlRoot, cssDir, rawUrl);\n return `url(${toDataUri(filePath)})`;\n });\n}\n\n/** Inlina `<script src=\"local\">` → `<script>…</script>` (escapando `</script>`). */\nfunction inlineScripts(root: HTMLElement, htmlRoot: string): void {\n for (const el of root.querySelectorAll('script[src]')) {\n const src = el.getAttribute('src');\n if (!src || isExternalOrInline(src)) continue;\n const js = readFileSync(resolveLocal(htmlRoot, htmlRoot, src), 'utf-8').replace(\n /<\\/script>/gi,\n '<\\\\/script>',\n );\n el.removeAttribute('src');\n el.set_content(js);\n }\n}\n\n/** Inlina `<link rel=\"stylesheet\" href=\"local\">` → `<style>…</style>` (com `url()` resolvido). */\nfunction inlineStylesheets(root: HTMLElement, htmlRoot: string): void {\n for (const el of root.querySelectorAll('link[rel=\"stylesheet\"]')) {\n const href = el.getAttribute('href');\n if (!href || isExternalOrInline(href)) continue;\n const cssPath = resolveLocal(htmlRoot, htmlRoot, href);\n const css = inlineCssUrls(readFileSync(cssPath, 'utf-8'), htmlRoot, dirname(cssPath));\n el.replaceWith(...parse(`<style>${css}</style>`).childNodes);\n }\n}\n\n/** Resolve `url()` em blocos `<style>` já existentes no HTML. */\nfunction inlineExistingStyleBlocks(root: HTMLElement, htmlRoot: string): void {\n for (const el of root.querySelectorAll('style')) {\n const css = el.innerHTML;\n if (!css.includes('url(')) continue;\n el.set_content(inlineCssUrls(css, htmlRoot, htmlRoot));\n }\n}\n\n/** Converte mídias locais referenciadas por atributo (`src`/`href`) em `data:` URIs. */\nfunction inlineMediaAttributes(root: HTMLElement, htmlRoot: string): void {\n const targets: Array<[selector: string, attr: string]> = [\n ['img[src]', 'src'],\n ['source[src]', 'src'],\n ['audio[src]', 'src'],\n ['video[src]', 'src'],\n ['link[rel=\"icon\"]', 'href'],\n ['link[rel=\"shortcut icon\"]', 'href'],\n ['link[rel=\"apple-touch-icon\"]', 'href'],\n ['link[rel=\"preload\"]', 'href'],\n ];\n for (const [selector, attr] of targets) {\n for (const el of root.querySelectorAll(selector)) {\n const ref = el.getAttribute(attr);\n if (!ref || isExternalOrInline(ref)) continue;\n el.setAttribute(attr, toDataUri(resolveLocal(htmlRoot, htmlRoot, ref)));\n }\n }\n}\n\n/**\n * Lê o HTML de entrada e devolve o HTML consolidado self-contained\n * (JS/CSS inline, mídias em `data:` URI). Não escreve nada em disco.\n */\nexport function inlineHtml(htmlPath: string): string {\n const htmlRoot = dirname(resolve(htmlPath));\n const root = parse(readFileSync(htmlPath, 'utf-8'), {\n comment: true,\n voidTag: { closingSlash: true },\n });\n inlineScripts(root, htmlRoot);\n inlineStylesheets(root, htmlRoot);\n inlineExistingStyleBlocks(root, htmlRoot);\n inlineMediaAttributes(root, htmlRoot);\n return root.toString();\n}\n\nexport interface BundleResult {\n /** HTML consolidado self-contained. */\n html: string;\n /** Bytes do `.zip` (entry único `index.html` na raiz). */\n zipBytes: Uint8Array;\n /** Tamanho do `.zip` em bytes. */\n size: number;\n /** SHA-256 dos bytes do `.zip`, no formato `sha256-<hex>`. */\n hash: string;\n /** `true` se `size` excede `maxBytes`. */\n overLimit: boolean;\n}\n\n/**\n * Constrói o bundle a partir do `index.html` de entrada: inlina, zipa\n * (entry `index.html` na raiz, mtime fixo p/ hash determinístico) e calcula\n * tamanho + SHA-256. Puro — não escreve o `.zip` (responsabilidade do caller).\n */\nexport function buildBundle(htmlPath: string, maxBytes: number = DEFAULT_MAX_BYTES): BundleResult {\n const html = inlineHtml(htmlPath);\n const zipBytes = zipSync({ 'index.html': [strToU8(html), { mtime: FIXED_MTIME }] }, { level: 6 });\n const size = zipBytes.length;\n const hash = `sha256-${createHash('sha256').update(zipBytes).digest('hex')}`;\n return { html, zipBytes, size, hash, overLimit: size > maxBytes };\n}\n\nexport interface CliArgs {\n htmlPath: string;\n outPath: string;\n maxBytes: number;\n help: boolean;\n}\n\nconst DEFAULT_OUT = 'dist/whitelabel-bundle.zip';\n\n/** Parse mínimo de argv (sem deps): `--html/-i`, `--out/-o`, `--max-bytes`, `--help/-h`. */\nexport function parseArgs(argv: string[]): CliArgs {\n const args: CliArgs = {\n htmlPath: '',\n outPath: DEFAULT_OUT,\n maxBytes: DEFAULT_MAX_BYTES,\n help: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n switch (token) {\n case '--help':\n case '-h':\n args.help = true;\n break;\n case '--html':\n case '-i':\n args.htmlPath = argv[++i] ?? '';\n break;\n case '--out':\n case '-o':\n args.outPath = argv[++i] ?? DEFAULT_OUT;\n break;\n case '--max-bytes':\n args.maxBytes = Number.parseInt(argv[++i] ?? '', 10) || DEFAULT_MAX_BYTES;\n break;\n case 'build':\n // Subcomando (`condor-mobile-bridge build …`) — ignorado aqui.\n break;\n default:\n // Primeiro posicional vira o html de entrada (ergonomia: `build dist/index.html`).\n if (token && !token.startsWith('-') && !args.htmlPath) args.htmlPath = token;\n }\n }\n return args;\n}\n\nexport const USAGE = `condor-mobile-bridge build — gera o .zip white-label self-contained.\n\nUso:\n npx condor-mobile-bridge build -i <html> [-o <zip>]\n\nOpções:\n -i, --html <path> HTML de entrada (ex.: dist/index.html). Obrigatório.\n -o, --out <path> .zip de saída (default: ${DEFAULT_OUT}).\n --max-bytes <n> Limite de aviso em bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).\n -h, --help Mostra esta ajuda.`;\n"],"mappings":"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","names":["import_node_fs","import_node_path"]}