@condorinst/mobile-bridge 0.14.0 → 0.15.2

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,101 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.2] — 2026-08-31
11
+
12
+ ### Fixed
13
+
14
+ - **`client_demo` (examples-only): the other device's BLE event wiped the
15
+ history charts.** Second cause of the same symptom as `0.15.1`, found on
16
+ device after that fix: the charts vanished on their own, seconds after being
17
+ drawn, with nobody touching the screen.
18
+
19
+ `handleDeviceConnection` guarded its rerender with
20
+ `chartsAreLive() && selectedSerialNo === payload.serialNo`, which let through
21
+ exactly the event of the **other** device — the 7047 was on screen and the
22
+ 4800 connecting over BLE re-rendered the page and destroyed the five
23
+ canvases. The guard meant to protect the visible device ended up protecting
24
+ only the case that could not hurt it.
25
+
26
+ With charts alive there is now no full rerender: the visible device patches
27
+ its badge, any other device only updates state — its card lives on the home,
28
+ which is rendered when the user goes back.
29
+
30
+ `handleRealTime` has the same shape but genuinely needs its rerender (the
31
+ real-time divs enter and leave the DOM), so it now remounts the history
32
+ charts afterwards instead of leaving the loaded week erased.
33
+
34
+ ## [0.15.1] — 2026-08-31
35
+
36
+ ### Fixed
37
+
38
+ - **`client_demo` (examples-only): the history charts went blank the moment the
39
+ week finished loading.** `onDone` called the full `rerender()`, which replaces
40
+ the page's `innerHTML` and destroys the live canvases, and then relied on a
41
+ single `requestAnimationFrame` to mount them again. The WebView does not
42
+ always run that frame — a fling suspends them — and when it was skipped
43
+ nothing ever redrew: the section stayed empty, legends gone, until the user
44
+ navigated away. Reproduced on device, on the week that takes ~24 s to load.
45
+
46
+ The end of a load changes exactly one thing on screen: the progress bar goes
47
+ away. That is now a surgical DOM update, so the canvases are never torn down
48
+ and there is no remount to miss. `rerender()` is kept only for the empty week,
49
+ where the structure really does change. `onPage` also self-heals: if the
50
+ charts are not mounted by the time a page lands, it mounts them.
51
+
52
+ ## [0.15.0] — 2026-08-31
53
+
54
+ ### Added
55
+
56
+ - **`SensorLogQueryPayload` — the request side of `sensorLog` is now typed.**
57
+ The command already accepted these arguments (`send` is permissive by
58
+ design), but nothing described them, so every client rediscovered the shape
59
+ by reading the native handler. The type also carries the two parameters the
60
+ backend added in the v2 range contract: `startTime` (inclusive) and
61
+ `endTime` (exclusive), both `YYYY-MM-DD HH:MM:SS`.
62
+
63
+ ⚠️ Those timestamps are **local device time, not UTC** — `log_time` is a
64
+ passthrough of the device RTC. Build the range with the local `Date` getters
65
+ and never with `toISOString()`: converting shifts every series by the
66
+ timezone offset, with plausible values and no visible error.
67
+
68
+ - **`BridgeClient.fetchSensorLog(query, { timeoutMs })`** — resolves with the
69
+ page whose `requestId` matches, so clients stop hand-rolling the
70
+ `requestId → resolve` map (the demo had two copies of it). It resolves on
71
+ `success: false` too — that is an answer, not a transport failure — and only
72
+ rejects on timeout (default 10 s, sized for the ~2.7 s worst-case page).
73
+
74
+ Paging over a period means chaining calls on the returned `startingAfter`
75
+ until `hasMore` is `false`; the cursors are dependent, so pages cannot be
76
+ fetched in parallel.
77
+
78
+ ### Fixed
79
+
80
+ - **`condor-mobile-bridge build` corrupted bundles whose JS contains markup.**
81
+ The inliner used `set_content()`, which **parses** what it receives as HTML.
82
+ Every client bundle carries markup inside template literals, and the parser
83
+ rewrote it: `<stop … />` became `<stop …></stop>`, a minified comparison like
84
+ `a<T` was read as an opening tag, and its closing tag was emitted at the end
85
+ of the script. The JS came out syntactically broken.
86
+
87
+ Nothing failed loudly. The zip was produced, the hash printed, the upload
88
+ succeeded — and the app then aborted the Home with *"Security failure: native
89
+ bridges reachable from the iframe"*, because the broken client never got to
90
+ answer the shell's `verify-sandbox`. A message pointing at security, for what
91
+ was a corrupted bundle.
92
+
93
+ Whether a given bundle broke depended on how the minifier happened to name
94
+ its variables, so the same source could build fine one day and not the next.
95
+ The script is now inserted as raw text (`textContent`), byte for byte.
96
+
97
+ ### Changed
98
+
99
+ - `SensorLogPayload` docs corrected: the timestamps were described as "UTC
100
+ naive", which is wrong and was the most expensive kind of wrong — a silent
101
+ 3-hour shift. Also documents that with a range, `hasMore`/`hasPrevious` are
102
+ computed **within the period**, and that `error` is a diagnostic code, never
103
+ user-facing text.
104
+
10
105
  ## [0.14.0] — 2026-08-27
11
106
 
12
107
  ### Added
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":";;;;AAOA,IAAAA,kBAAyC;AACzC,IAAAC,oBAAiC;;;ACUjC,yBAA2B;AAC3B,qBAA6B;AAC7B,uBAA0C;AAC1C,oBAAiC;AACjC,8BAAwC;AAGjC,IAAM,oBAAoB,IAAI,OAAO;AAG5C,IAAM,cAAc,oBAAI,KAAK,sBAAsB;AAG5C,IAAM,aAAqC;AAAA,EAChD,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AACX;AAGA,SAAS,mBAAmB,KAAsB;AAChD,SAAO,mBAAmB,KAAK,GAAG,KAAK,UAAU,KAAK,GAAG,KAAK,qBAAqB,KAAK,GAAG;AAC7F;AAGA,SAAS,eAAe,KAAqB;AAC3C,SAAO,IAAI,MAAM,MAAM,EAAE,CAAC,KAAK;AACjC;AAQA,SAAS,aAAa,UAAkB,aAAqB,KAAqB;AAChF,QAAM,QAAQ,eAAe,GAAG;AAChC,SAAO,MAAM,WAAW,GAAG,QAAI,0BAAQ,UAAU,IAAI,KAAK,EAAE,QAAI,0BAAQ,aAAa,KAAK;AAC5F;AAGO,SAAS,UAAU,UAA0B;AAClD,QAAM,YAAQ,6BAAa,QAAQ;AACnC,QAAM,OAAO,eAAW,0BAAQ,QAAQ,EAAE,YAAY,CAAC,KAAK;AAC5D,SAAO,QAAQ,IAAI,WAAW,MAAM,SAAS,QAAQ,CAAC;AACxD;AAMO,SAAS,cAAc,KAAa,UAAkB,QAAwB;AACnF,SAAO,IAAI,QAAQ,qCAAqC,CAAC,OAAO,QAAQ,WAAmB;AACzF,QAAI,mBAAmB,MAAM,EAAG,QAAO;AACvC,UAAM,WAAW,aAAa,UAAU,QAAQ,MAAM;AACtD,WAAO,OAAO,UAAU,QAAQ,CAAC;AAAA,EACnC,CAAC;AACH;AAGA,SAAS,cAAc,MAAmB,UAAwB;AAChE,aAAW,MAAM,KAAK,iBAAiB,aAAa,GAAG;AACrD,UAAM,MAAM,GAAG,aAAa,KAAK;AACjC,QAAI,CAAC,OAAO,mBAAmB,GAAG,EAAG;AACrC,UAAM,SAAK,6BAAa,aAAa,UAAU,UAAU,GAAG,GAAG,OAAO,EAAE;AAAA,MACtE;AAAA,MACA;AAAA,IACF;AACA,OAAG,gBAAgB,KAAK;AACxB,OAAG,YAAY,EAAE;AAAA,EACnB;AACF;AAGA,SAAS,kBAAkB,MAAmB,UAAwB;AACpE,aAAW,MAAM,KAAK,iBAAiB,wBAAwB,GAAG;AAChE,UAAM,OAAO,GAAG,aAAa,MAAM;AACnC,QAAI,CAAC,QAAQ,mBAAmB,IAAI,EAAG;AACvC,UAAM,UAAU,aAAa,UAAU,UAAU,IAAI;AACrD,UAAM,MAAM,kBAAc,6BAAa,SAAS,OAAO,GAAG,cAAU,0BAAQ,OAAO,CAAC;AACpF,OAAG,YAAY,OAAG,+BAAM,UAAU,GAAG,UAAU,EAAE,UAAU;AAAA,EAC7D;AACF;AAGA,SAAS,0BAA0B,MAAmB,UAAwB;AAC5E,aAAW,MAAM,KAAK,iBAAiB,OAAO,GAAG;AAC/C,UAAM,MAAM,GAAG;AACf,QAAI,CAAC,IAAI,SAAS,MAAM,EAAG;AAC3B,OAAG,YAAY,cAAc,KAAK,UAAU,QAAQ,CAAC;AAAA,EACvD;AACF;AAGA,SAAS,sBAAsB,MAAmB,UAAwB;AACxE,QAAM,UAAmD;AAAA,IACvD,CAAC,YAAY,KAAK;AAAA,IAClB,CAAC,eAAe,KAAK;AAAA,IACrB,CAAC,cAAc,KAAK;AAAA,IACpB,CAAC,cAAc,KAAK;AAAA,IACpB,CAAC,oBAAoB,MAAM;AAAA,IAC3B,CAAC,6BAA6B,MAAM;AAAA,IACpC,CAAC,gCAAgC,MAAM;AAAA,IACvC,CAAC,uBAAuB,MAAM;AAAA,EAChC;AACA,aAAW,CAAC,UAAU,IAAI,KAAK,SAAS;AACtC,eAAW,MAAM,KAAK,iBAAiB,QAAQ,GAAG;AAChD,YAAM,MAAM,GAAG,aAAa,IAAI;AAChC,UAAI,CAAC,OAAO,mBAAmB,GAAG,EAAG;AACrC,SAAG,aAAa,MAAM,UAAU,aAAa,UAAU,UAAU,GAAG,CAAC,CAAC;AAAA,IACxE;AAAA,EACF;AACF;AAMO,SAAS,WAAW,UAA0B;AACnD,QAAM,eAAW,8BAAQ,0BAAQ,QAAQ,CAAC;AAC1C,QAAM,WAAO,mCAAM,6BAAa,UAAU,OAAO,GAAG;AAAA,IAClD,SAAS;AAAA,IACT,SAAS,EAAE,cAAc,KAAK;AAAA,EAChC,CAAC;AACD,gBAAc,MAAM,QAAQ;AAC5B,oBAAkB,MAAM,QAAQ;AAChC,4BAA0B,MAAM,QAAQ;AACxC,wBAAsB,MAAM,QAAQ;AACpC,SAAO,KAAK,SAAS;AACvB;AAoBO,SAAS,YAAY,UAAkB,WAAmB,mBAAiC;AAChG,QAAM,OAAO,WAAW,QAAQ;AAChC,QAAM,eAAW,uBAAQ,EAAE,cAAc,KAAC,uBAAQ,IAAI,GAAG,EAAE,OAAO,YAAY,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AAChG,QAAM,OAAO,SAAS;AACtB,QAAM,OAAO,cAAU,+BAAW,QAAQ,EAAE,OAAO,QAAQ,EAAE,OAAO,KAAK,CAAC;AAC1E,SAAO,EAAE,MAAM,UAAU,MAAM,MAAM,WAAW,OAAO,SAAS;AAClE;AASA,IAAM,cAAc;AAGb,SAAS,UAAU,MAAyB;AACjD,QAAM,OAAgB;AAAA,IACpB,UAAU;AAAA,IACV,SAAS;AAAA,IACT,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AACA,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,QAAQ,KAAK,CAAC;AACpB,YAAQ,OAAO;AAAA,MACb,KAAK;AAAA,MACL,KAAK;AACH,aAAK,OAAO;AACZ;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,aAAK,WAAW,KAAK,EAAE,CAAC,KAAK;AAC7B;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,aAAK,UAAU,KAAK,EAAE,CAAC,KAAK;AAC5B;AAAA,MACF,KAAK;AACH,aAAK,WAAW,OAAO,SAAS,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,KAAK;AACxD;AAAA,MACF,KAAK;AAEH;AAAA,MACF;AAEE,YAAI,SAAS,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,SAAU,MAAK,WAAW;AAAA,IAC3E;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+CAO0B,WAAW;AAAA,8DACI,iBAAiB;AAAA;;;ADzO/E,SAAS,OAAa;AACpB,QAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAE5C,MAAI,KAAK,QAAQ,CAAC,KAAK,UAAU;AAC/B,YAAQ,IAAI,KAAK;AACjB,YAAQ,KAAK,KAAK,OAAO,IAAI,CAAC;AAAA,EAChC;AAEA,QAAM,SAAS,YAAY,KAAK,UAAU,KAAK,QAAQ;AAEvD,QAAM,cAAU,2BAAQ,KAAK,OAAO;AACpC,qCAAU,2BAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AAC/C,qCAAc,SAAS,OAAO,QAAQ;AAEtC,QAAM,MAAM,OAAO,WAAW,OAAO,IAAI,IAAI,MAAM,QAAQ,CAAC;AAC5D,UAAQ,IAAI,SAAS,KAAK,OAAO,EAAE;AACnC,UAAQ,IAAI,wBAAwB,EAAE,qBAAqB;AAC3D,UAAQ,IAAI,uBAAuB,OAAO,IAAI,QAAQ;AACtD,UAAQ,IAAI,uBAAuB,OAAO,IAAI,EAAE;AAEhD,MAAI,OAAO,WAAW;AACpB,UAAM,WAAW,KAAK,YAAY,OAAO,OAAO,QAAQ,CAAC;AACzD,YAAQ;AAAA,MACN;AAAA,mCAA4B,OAAO,IAAI,uBAAuB,OAAO;AAAA,IAEvE;AAAA,EACF;AACF;AAEA,KAAK;","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
@@ -631,6 +631,7 @@ var Bridge = {
631
631
  // src/bridge-client.ts
632
632
  var PROTOCOL_VERSION2 = "0.5.0";
633
633
  var HANDSHAKE_TIMEOUT_MS = 5e3;
634
+ var SENSOR_LOG_TIMEOUT_MS = 1e4;
634
635
  var subscribers2 = /* @__PURE__ */ new Map();
635
636
  var pendingHandshake2 = null;
636
637
  var messageListenerInstalled = false;
@@ -717,6 +718,30 @@ var BridgeClient = {
717
718
  openSettings() {
718
719
  this.send("openSettings");
719
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
+ },
720
745
  requestPermission(permission) {
721
746
  this.send("requestPermission", { permission });
722
747
  },