@condorinst/mobile-bridge 0.13.2 → 0.15.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/CHANGELOG.md CHANGED
@@ -7,6 +7,108 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.0] — 2026-08-31
11
+
12
+ ### Added
13
+
14
+ - **`SensorLogQueryPayload` — the request side of `sensorLog` is now typed.**
15
+ The command already accepted these arguments (`send` is permissive by
16
+ design), but nothing described them, so every client rediscovered the shape
17
+ by reading the native handler. The type also carries the two parameters the
18
+ backend added in the v2 range contract: `startTime` (inclusive) and
19
+ `endTime` (exclusive), both `YYYY-MM-DD HH:MM:SS`.
20
+
21
+ ⚠️ Those timestamps are **local device time, not UTC** — `log_time` is a
22
+ passthrough of the device RTC. Build the range with the local `Date` getters
23
+ and never with `toISOString()`: converting shifts every series by the
24
+ timezone offset, with plausible values and no visible error.
25
+
26
+ - **`BridgeClient.fetchSensorLog(query, { timeoutMs })`** — resolves with the
27
+ page whose `requestId` matches, so clients stop hand-rolling the
28
+ `requestId → resolve` map (the demo had two copies of it). It resolves on
29
+ `success: false` too — that is an answer, not a transport failure — and only
30
+ rejects on timeout (default 10 s, sized for the ~2.7 s worst-case page).
31
+
32
+ Paging over a period means chaining calls on the returned `startingAfter`
33
+ until `hasMore` is `false`; the cursors are dependent, so pages cannot be
34
+ fetched in parallel.
35
+
36
+ ### Fixed
37
+
38
+ - **`condor-mobile-bridge build` corrupted bundles whose JS contains markup.**
39
+ The inliner used `set_content()`, which **parses** what it receives as HTML.
40
+ Every client bundle carries markup inside template literals, and the parser
41
+ rewrote it: `<stop … />` became `<stop …></stop>`, a minified comparison like
42
+ `a<T` was read as an opening tag, and its closing tag was emitted at the end
43
+ of the script. The JS came out syntactically broken.
44
+
45
+ Nothing failed loudly. The zip was produced, the hash printed, the upload
46
+ succeeded — and the app then aborted the Home with *"Security failure: native
47
+ bridges reachable from the iframe"*, because the broken client never got to
48
+ answer the shell's `verify-sandbox`. A message pointing at security, for what
49
+ was a corrupted bundle.
50
+
51
+ Whether a given bundle broke depended on how the minifier happened to name
52
+ its variables, so the same source could build fine one day and not the next.
53
+ The script is now inserted as raw text (`textContent`), byte for byte.
54
+
55
+ ### Changed
56
+
57
+ - `SensorLogPayload` docs corrected: the timestamps were described as "UTC
58
+ naive", which is wrong and was the most expensive kind of wrong — a silent
59
+ 3-hour shift. Also documents that with a range, `hasMore`/`hasPrevious` are
60
+ computed **within the period**, and that `error` is a diagnostic code, never
61
+ user-facing text.
62
+
63
+ ## [0.14.0] — 2026-08-27
64
+
65
+ ### Added
66
+
67
+ - **`language` observable key — the client can finally translate.** Until now
68
+ the language never crossed the native→web boundary: the app could be running
69
+ in Portuguese while the client bundle rendered English, inside the same
70
+ screen. The new key carries the app's language state, emitted on attach,
71
+ again right after `handshake-ack` (so subscribing late still gets a value),
72
+ and on every language change in the app Settings — a page already loaded
73
+ re-renders without a restart.
74
+
75
+ The payload carries **two independent sources plus the app's own
76
+ resolution**, and the distinction is the point of the channel:
77
+
78
+ - `deviceLocale` — the raw device locale as a BCP-47 tag (`"pt-BR"`,
79
+ `"es-419"`), region included, never passed through the app's
80
+ supported-language fallback.
81
+ - `appLanguage` — the language explicitly chosen in the app Settings, or
82
+ `null` when the user never chose one. Never a fallback in disguise, so
83
+ `null` is a real signal you can branch on.
84
+ - `resolvedLanguage` — what the native chrome is rendering, for clients that
85
+ would rather stay visually consistent with the frame around them.
86
+
87
+ The app ships four languages (`en`, `pt`, `de`, `fr`) and collapses anything
88
+ else to English; a client bundle has no such limit. Sending only the resolved
89
+ language would mean a device set to Spanish reaching the client as `en` —
90
+ rendering English while holding a Spanish bundle. Both raw values are sent so
91
+ the precedence rule stays the client's own decision.
92
+
93
+ It is **push + pull**. The app pushes on attach, after the handshake ack, and
94
+ on every Settings change — but the first push happens while the WebView is
95
+ still being created, before the client bundle exists. Send the `language`
96
+ command once after your handshake to pull the current state; without it a
97
+ first launch would sit on the client's default until the user changed the
98
+ language by hand.
99
+
100
+ There is no version negotiation: a native host older than `0.14.0` never
101
+ emits this key, so the event simply never arrives — keep a default language
102
+ for that case and read `nativeVersion` from the handshake ack if you need to
103
+ branch on it. `client_demo` subscribes to the key and drives its date
104
+ formatting from it, and its mock previews the interesting case (device in
105
+ Spanish, no explicit choice).
106
+
107
+ ### Documentation
108
+
109
+ - The README key catalog was missing the `systemStatus` row; both it and
110
+ `language` are now listed.
111
+
10
112
  ## [0.13.2] — 2026-08-26
11
113
 
12
114
  ### Added
package/README.md CHANGED
@@ -219,7 +219,9 @@ are what the JS receives via `window.CondorBridge.emit(key, payload)`.
219
219
  | `deviceConnection` | `'deviceConnection'` | `DeviceConnectionPayload = { serialNo, isConnect }` | BG → JS (status); JS → BG (`{type, serialNo}`, permissive) |
220
220
  | `sensorLog` | `'sensorLog'` | `SensorLogPayload` | BG → JS |
221
221
  | `dfu` | `'dfu'` | `Record<string, unknown>` (fallback) | BG → JS |
222
+ | `systemStatus` | `'systemStatus'` | `SystemStatusPayload = { bluetooth, internet, permission, hasDevices, missingOptionalPermissions[], missingEssentialPermissions[], deviceSyncError }` | native → JS (on change + snapshot after handshake) |
222
223
  | `navigationBack` | `'navigationBack'` | `NavigationBackPayload = {}` (OS back pressed) | BG → JS (via `BridgeClient.onBack`) |
224
+ | `language` | `'language'` | `LanguagePayload = { deviceLocale, appLanguage, resolvedLanguage }` | native → JS (on change + snapshot after handshake) |
223
225
 
224
226
  **Automatic inference on `Bridge.on`:**
225
227
 
package/dist/cli.cjs CHANGED
@@ -64,7 +64,7 @@ function inlineScripts(root, htmlRoot) {
64
64
  "<\\/script>"
65
65
  );
66
66
  el.removeAttribute("src");
67
- el.set_content(js);
67
+ el.textContent = js;
68
68
  }
69
69
  }
70
70
  function inlineStylesheets(root, htmlRoot) {
package/dist/cli.cjs.map CHANGED
@@ -1 +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⚠️ WARNING: the .zip (${result.size} bytes) exceeds the ${limitMb} MB limit — ` +\n 'the app will REJECT this bundle (falls back to the default screen). Reduce inline assets/media.',\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 — builds the self-contained white-label .zip.\n\nUsage:\n npx condor-mobile-bridge build -i <html> [-o <zip>]\n\nOptions:\n -i, --html <path> Input HTML (e.g. dist/index.html). Required.\n -o, --out <path> Output .zip (default: ${DEFAULT_OUT}).\n --max-bytes <n> Warning threshold in bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).\n -h, --help Show this help.`;\n"],"mappings":"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","names":["import_node_fs","import_node_path"]}
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⚠️ WARNING: the .zip (${result.size} bytes) exceeds the ${limitMb} MB limit — ` +\n 'the app will REJECT this bundle (falls back to the default screen). Reduce inline assets/media.',\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 // `textContent`, nunca `set_content`: este último **parseia** o que recebe\n // como HTML. Todo bundle carrega markup dentro de strings JS, e o parser o\n // reescreve — `<stop … />` vira `<stop …></stop>`, uma comparação `a<T` é\n // lida como abertura de tag e o fechamento correspondente é cuspido no fim\n // do script. O JS sai sintaticamente quebrado, o zip é gerado sem erro, e\n // o app aborta a Home com \"native bridges reachable from the iframe\" —\n // porque o cliente nunca chegou a responder o verify-sandbox.\n el.textContent = 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 — builds the self-contained white-label .zip.\n\nUsage:\n npx condor-mobile-bridge build -i <html> [-o <zip>]\n\nOptions:\n -i, --html <path> Input HTML (e.g. dist/index.html). Required.\n -o, --out <path> Output .zip (default: ${DEFAULT_OUT}).\n --max-bytes <n> Warning threshold in bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).\n -h, --help Show this help.`;\n"],"mappings":"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","names":["import_node_fs","import_node_path"]}
package/dist/cli.js CHANGED
@@ -63,7 +63,7 @@ function inlineScripts(root, htmlRoot) {
63
63
  "<\\/script>"
64
64
  );
65
65
  el.removeAttribute("src");
66
- el.set_content(js);
66
+ el.textContent = js;
67
67
  }
68
68
  }
69
69
  function inlineStylesheets(root, htmlRoot) {
package/dist/cli.js.map CHANGED
@@ -1 +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⚠️ WARNING: the .zip (${result.size} bytes) exceeds the ${limitMb} MB limit — ` +\n 'the app will REJECT this bundle (falls back to the default screen). Reduce inline assets/media.',\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 — builds the self-contained white-label .zip.\n\nUsage:\n npx condor-mobile-bridge build -i <html> [-o <zip>]\n\nOptions:\n -i, --html <path> Input HTML (e.g. dist/index.html). Required.\n -o, --out <path> Output .zip (default: ${DEFAULT_OUT}).\n --max-bytes <n> Warning threshold in bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).\n -h, --help Show this help.`;\n"],"mappings":"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","names":["dirname","resolve","resolve","dirname"]}
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⚠️ WARNING: the .zip (${result.size} bytes) exceeds the ${limitMb} MB limit — ` +\n 'the app will REJECT this bundle (falls back to the default screen). Reduce inline assets/media.',\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 // `textContent`, nunca `set_content`: este último **parseia** o que recebe\n // como HTML. Todo bundle carrega markup dentro de strings JS, e o parser o\n // reescreve — `<stop … />` vira `<stop …></stop>`, uma comparação `a<T` é\n // lida como abertura de tag e o fechamento correspondente é cuspido no fim\n // do script. O JS sai sintaticamente quebrado, o zip é gerado sem erro, e\n // o app aborta a Home com \"native bridges reachable from the iframe\" —\n // porque o cliente nunca chegou a responder o verify-sandbox.\n el.textContent = 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 — builds the self-contained white-label .zip.\n\nUsage:\n npx condor-mobile-bridge build -i <html> [-o <zip>]\n\nOptions:\n -i, --html <path> Input HTML (e.g. dist/index.html). Required.\n -o, --out <path> Output .zip (default: ${DEFAULT_OUT}).\n --max-bytes <n> Warning threshold in bytes (default: ${DEFAULT_MAX_BYTES} = 2 MB).\n -h, --help Show this help.`;\n"],"mappings":"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","names":["dirname","resolve","resolve","dirname"]}
package/dist/index.cjs CHANGED
@@ -56,7 +56,15 @@ var BRIDGE_KEYS = {
56
56
  * UMA página interna. Emitido só quando o nativo decidiu delegar o back
57
57
  * ao cliente. Vide `BridgeClient.onBack`.
58
58
  */
59
- navigationBack: "navigationBack"
59
+ navigationBack: "navigationBack",
60
+ /**
61
+ * Native → client: the app's language state. Carries the raw device locale
62
+ * AND the language explicitly chosen in the app Settings, so the client can
63
+ * apply its own precedence rule — including rendering a language the native
64
+ * chrome does not support. Emitted on attach, after `handshake-ack`, and on
65
+ * every change in Settings. Vide `LanguagePayload`.
66
+ */
67
+ language: "language"
60
68
  };
61
69
 
62
70
  // src/internal/dev-panel.ts
@@ -623,6 +631,7 @@ var Bridge = {
623
631
  // src/bridge-client.ts
624
632
  var PROTOCOL_VERSION2 = "0.5.0";
625
633
  var HANDSHAKE_TIMEOUT_MS = 5e3;
634
+ var SENSOR_LOG_TIMEOUT_MS = 1e4;
626
635
  var subscribers2 = /* @__PURE__ */ new Map();
627
636
  var pendingHandshake2 = null;
628
637
  var messageListenerInstalled = false;
@@ -709,6 +718,30 @@ var BridgeClient = {
709
718
  openSettings() {
710
719
  this.send("openSettings");
711
720
  },
721
+ fetchSensorLog(query, opts) {
722
+ const timeoutMs = opts?.timeoutMs ?? SENSOR_LOG_TIMEOUT_MS;
723
+ return new Promise((resolve, reject) => {
724
+ let settled = false;
725
+ const unsubscribe = this.on(BRIDGE_KEYS.sensorLog, (payload) => {
726
+ if (settled || payload.requestId !== query.requestId) return;
727
+ settled = true;
728
+ clearTimeout(timer);
729
+ unsubscribe();
730
+ resolve(payload);
731
+ });
732
+ const timer = setTimeout(() => {
733
+ if (settled) return;
734
+ settled = true;
735
+ unsubscribe();
736
+ reject(
737
+ new Error(
738
+ `BridgeClient: sensorLog timeout after ${timeoutMs}ms (requestId=${query.requestId})`
739
+ )
740
+ );
741
+ }, timeoutMs);
742
+ this.send("sensorLog", { ...query });
743
+ });
744
+ },
712
745
  requestPermission(permission) {
713
746
  this.send("requestPermission", { permission });
714
747
  },