feat: desktop-substrate — Electron preload + transport substrate for sense:// (15 commits)#27
Open
garrettmflynn wants to merge 228 commits into
Open
feat: desktop-substrate — Electron preload + transport substrate for sense:// (15 commits)#27garrettmflynn wants to merge 228 commits into
garrettmflynn wants to merge 228 commits into
Conversation
…checks work on Mac
Percentages are misleading since they depend on the app's own code size. Show absolute overhead range (20–63 KB) instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Service hot-reload: - Enable stdin pipe to Electron child process (was 'ignore', now 'pipe') - Add sendCommand() helper to send JSON commands to running Electron - Extend Vite handleHotUpdate to detect service file changes and send reload commands via stdin - Add onServiceReload callback to setupStdinCommands in lifecycle module - Wire up in main.ts: close service, restart, update sanitized URLs, notify renderer windows of updated service URLs Test fix: - Fix pre-existing failure in plugins.test.ts: plugin src is null in renderer context because import.meta.url doesn't resolve after bundling into commoners.config.mjs (browser bundle strips src) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use __IS_DESKTOP__ compile-time guard for event system branching: web builds exclude Electron events, desktop builds exclude web events - onload.mjs: 4.6 KB → 3.7 KB for hello world (was 14 KB before tree-shaking work) - Remove redundant rows from why.md comparison table (Auto-bundle, Local + remote services, Frontend framework, Overhead) - Add CLI display overhaul to roadmap Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use __IS_DEV__ compile-time guard to exclude WebSocket dev server setup and plugin hot-reload handler from production builds. Demo app onload.mjs drops from 12 KB to 8.5 KB in production. Total onload.mjs reduction across all optimizations: - Hello world: 14 KB → 3.7 KB (74%) - Demo (plugins, prod): 13 KB → 8.5 KB (35%) Also adds CLI display overhaul to roadmap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update checkAssets to expect .cjs only in Electron builds - Fix "Source file is resolved" in shared test utils to expect null - Close stdin pipe in Electron cleanup to prevent process hanging - Unref stdin in setupStdinCommands to not block process exit - Remove unused build parameter from checkAssets Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Electron .cjs config bundle was not overriding import.meta.url, causing getDirname() to resolve paths relative to the bundle output directory (.commoners/.tmp/assets/) instead of the project root. This broke Python service resolution in desktop dev mode — the source files couldn't be found. Fix: add import.meta.url define to the Vite/Rollup config bundling, matching the existing override in the Node.js config loading step. Python service echo tests (basic-python, numpy) now pass in desktop mode. 673/700 tests pass (remaining: tauri-driver missing, demo icon too small for Electron builder). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Electron builder requires icons to be at least 512x512. The demo icon was 32x32, causing desktop-build and desktop-zlaunch tests to fail. All 33 test files now pass (693 tests, 7 skipped). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The process/browser polyfill used in browser config bundles provides an empty env object, so process.env.VITE_* references were always undefined at runtime. Now these values are inlined at bundle time via Vite's define map, fixing support for patterns like `process.env.VITE_BASE_PATH || '/'` in commoners.config.ts. Adds vite-integration.test.ts covering process.env in config bundling, vite.base path forwarding, and VITE_* env var inlining. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Guard servicesModule usage behind a null check so apps without services still create the main window. Also externalize @commoners/solidarity and commoners from config esbuild to prevent bundling the entire framework when users import defineConfig. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Externalize @commoners/solidarity from config esbuild to prevent bundling the entire framework when users import defineConfig - Support object-form CSP overrides (Record<string, string[]>) that merge per-directive into the default policy - Format esbuild errors with color-coded output instead of raw dumps - Validate project has a config file or index.html before launching - Validate config defines at least one page or service Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous integrity check hashed each executable service binary at build time and compared bytes at spawn. This couldn't survive code signing — signtool/codesign mutate the bytes after the hash was computed, so every legitimate signed binary failed verification and the service was silently rejected. Apps that signed their builds saw services fail to spawn with no visible error (only "Starting <service>..." then nothing), because the hash mismatch was emitted only as a hook event. Replaces the byte-hash design with OS-native code-signature verification: PowerShell Get-AuthenticodeSignature on Windows, codesign --verify on macOS, skipped on platforms without native code signing (Linux). The signing identity must contain an expected publisher substring declared in electron.security.expectedPublisher; that field lives in service-trust.json sealed inside app.asar via ASAR integrity, so an attacker cannot redirect the trust without invalidating the asar. - assets/utils/sign-verify.ts — new platform-aware verifier - ElectronBuildStrategy.generateServiceTrustManifest replaces generateServiceHashManifest; emits service-trust.json keyed by service id - assets/electron/main.ts reads service-trust.json instead of service-hashes.json and threads it through start() as serviceTrust - assets/services/index.ts replaces hashManifest comparison with verifySignature(); failures still emit security:service:integrity:fail with reason, plus a console.error so misconfigurations surface in logs - New event security:service:integrity:skipped for unsupported platforms - ElectronSecuritySettings.expectedPublisher added to types - Drive-by: rename two unused 'target' params to '_target' to satisfy the no-unused-vars rule (pre-existing lint errors in the same file) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CONTRIBUTING.md only covered the contributor flow (add a changeset to your PR). The maintainer flow — taking accumulated changesets (or a manual version bump) on dev and turning it into a published npm artifact — was implicit. That's a problem when a downstream consumer cites a specific commoners alpha version and expects the release procedure that produced it to be retraceable. docs/RELEASE.md covers: - When to cut a pre-1.0 alpha vs (eventual) post-1.0 stable release - The linked-package versioning model from .changeset/config.json (all linked packages share one version) - Path A: changeset-driven release (pnpm changeset version + pnpm release) - Path B: manual bump path that's been used in practice when no changesets accumulated, with the caveat that the changelog must be filled in by hand - Verification checklist (npm view, git tag, dist.tarball reachable, CHANGELOG entries) before announcing CONTRIBUTING.md gets a "Releases" section pointing at the new doc so maintainers can find it without grepping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The renderer-side onload populated `loaded[id] = load.call(...)` and then
`await loaded[id]` on the next line. The await ensures load() completed but
does not mutate the slot — so `loaded[id]` (and therefore `ENV.PLUGINS.<id>`,
which consumers receive from `await commoners.READY`) was the *Promise* itself
rather than the resolved manager/handle.
Most consumers happen to await the call chain (`await commoners.READY.then(p
=> p.foo.bar())`) and the Promise unwraps transparently. But any consumer
that destructures and reads sub-properties synchronously hits `undefined`:
const PLUGINS = await commoners.READY
const { windows } = PLUGINS // Promise
windows.popup.create() // TypeError: undefined.create
This was masked by a "race condition" escape hatch in the windows-plugin
test that returned early when `'popup' in plugins.windows` was false (which
is always false for a Promise). Removed that escape hatch and added a
generic regression test that asserts no `loaded[id]` value has a `.then`
method, so any future async-load plugin trips the same trap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`loadPage` resolved the URL via `getPageLocation` (which uses fileURLToPath
and existsSync) and then called `loadURL(pathToFileURL(loc).href)`. The
query string and fragment from the originating navigation URL were dropped
on the floor — `window.location.href = "file:///.../profile.html?id=xyz"`
in the renderer landed at `file:///.../profile.html` with no search.
Fixed in two places (mirror of each other):
- The `will-navigate` handler at line 357 now passes `urlObj.search` and
`urlObj.hash` through to `loadPage`, which appends them to the URL it
hands `loadURL`.
- The custom-protocol handler at line 739 already captured search/hash
into `__location` per-window state but didn't pass them to `loadPage`
either; now does.
Affects in-window page-to-page navigation in dev mode (file:// origin) and
production mode (custom protocol). Without this fix, any
`navigate("page", { search })` call silently strips the query, and the
destination page sees an empty `window.location.search`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a downstream consumer builds with SKIP_SIGNING=1 to produce an unsigned dev-test artifact, also skip writing service-trust.json. The runtime signature-verification block in assets/services/index.ts only fires when opts.serviceTrust[id] is present, so omitting the manifest is enough to bypass it cleanly. Without this, an unsigned bundled service would fail spawn-time verification (\`actual: '(no signer)'\`) and the app couldn't launch at all in unsigned mode. Counterpart change lives in the consumer's afterSign hook (the place that decides whether to actually invoke signtool). Together they let downstreams trade signature integrity for build speed during local iteration. Production builds always sign — never set SKIP_SIGNING. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Includes the five commits since alpha.3: service trust manifest skip under SKIP_SIGNING, navigation search/hash preservation, plugin loader Promise unwrap, release-process docs, and Authenticode-based service binary verification. Widens the @commoners/solidarity peer constraint in serial and windows to accept alpha.4 alongside alpha.3 + future 1.x finals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Instance Single-instance was previously hardcoded — every consumer app blocked concurrent instances regardless of preference. Add `electron.singleInstance` (default true, preserves existing behavior). Setting it to false skips makeSingleInstance entirely. Useful in dev when an old Electron process hasn't released the OS lock, and for apps that genuinely want concurrent instances. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…paired devices The renderer's reconnect path relies on `navigator.bluetooth.getDevices()` returning the list of previously-selected devices. In Electron that list is gated by `session.setDevicePermissionHandler` — without one, the call returns [] regardless of prior `requestDevice()` grants. Mirroring the serial plugin's approach (which has had `setDevicePermissionHandler(() => true)` from the start), the BLE plugin now grants Bluetooth-typed device permissions session-wide. Effect: pair a Muse once via the picker; on the next connect the renderer's `getDevices()` returns it directly and `gatt.connect()` re-opens the link without showing the picker again. Scope is in-session only — cross-restart persistence requires a userData JSON store and is left for a follow-up. Matches serial's current posture exactly. Drive-by: tighten two pre-existing `Function` type usages flagged by the pre-commit ESLint hook on this file. Same behavior, narrower types.
The previous commit added both `setPermissionCheckHandler` and `setDevicePermissionHandler`. The former covers many permission types (clipboard, media, notifications) and returning false for non-bluetooth ones causes Chromium to terminate the renderer with `bad IPC message, reason 105` when the renderer requests anything besides bluetooth. setDevicePermissionHandler alone is sufficient for the BLE reconnect path — it's what `navigator.bluetooth.getDevices()` consults to decide which devices to surface. Leaving the broader permission machinery to Electron's default keeps clipboard/media/etc. working.
Adds `postMessage(channel, message, transferList)` to the commoners
scoped-IPC API alongside the existing `send/invoke/on/once`. Plugins
needed to expose `ipcRenderer.postMessage` to renderer code in order
to transfer transferable objects (MessagePort, ArrayBuffer) to the
main process; the existing `send` accepts only structured-cloneable
args.
Two-line addition:
- `packages/core/assets/electron/preload.ts` — exposes
`TEMP_COMMONERS.postMessage` wrapping `ipcRenderer.postMessage`,
guarded by the same `isAllowedChannel` check the other IPC
wrappers use.
- `packages/core/assets/onload.ts` — adds `postMessage` to the
per-plugin scoped renderer ctx alongside `send`, scoping the
channel as `plugins:<id>:<channel>`.
No main-side changes needed: `scopedOn` in
`packages/core/assets/electron/modules/ipc.ts` already forwards
`IpcMainEvent` untouched, so handlers read `event.ports[]` when
expecting transferables.
Motivating use case: sense:// broker subscriber ports between
BrowserWindows in apps/desktop. The popover renderer creates a
MessageChannel, hands port2 to a DedicatedWorker-hosted broker, and
transfers port1 to the main process via
`commoners.<plugin>.postMessage('sense-port-to-overlay', null, [port1])`.
Main re-transfers to the overlay window. Generic addition — any
commoners plugin can now transfer MessagePorts.
See `roadmap/design/desktop-sense-transport.md` in the
universalbrain/sdk repo for the consumer pattern.
New `@commoners/haptics` plugin exposes a uniform haptic API across the three target runtimes: - Mobile (Capacitor): native intensity via @capacitor/haptics — light/medium/heavy impact, vibrate, taptic-engine selection feedback - Web: navigator.vibrate fallback for vibrate(); intensity-aware impact() is unsupported (returns no-op) - Desktop: not supported (no haptic hardware on the platform) API: `impact(style)`, `vibrate(duration)`, `selectionStart/Changed/End`. Same shape as the other commoners device-style plugins (one renderer backend per runtime, isSupported gating, capacitor configuration).
Pivots away from the WebBluetoothNewPermissionsBackend approach
(earlier on this branch in `4cd3756` + `e3ceb41`). The picker-skip /
silent-reconnect UX the switch was enabling isn't load-bearing for
the consumer apps that drove the original need; the runtime-
permission + permission-handler stack already covers the operator-
visible cases.
Removes:
- `commandLineSwitches: { 'enable-features': 'WebBluetoothNewPermissionsBackend' }`
export from packages/plugins/devices/ble/index.ts
- The plugin-iteration loop in packages/core/assets/electron/main.ts
that applied those switches pre-app.whenReady()
The generic `commandLineSwitches` plugin contract goes with this
revert — no plugin in the tree was using it for anything besides
BLE. If a future plugin needs Chromium feature flags, we'll re-
introduce the contract under its own design discussion.
Also widens the WIN_STATES.select + callbacks Record signatures to
match the post-revert runtime shape (eslint-clean `(...args: unknown[]) => unknown`
in place of the original narrow MACAddress callback).
The TEMP_COMMONERS.postMessage exposed via contextBridge cannot transfer MessagePort across the main-world ↔ isolated-world boundary — Electron's IPC raises "Invalid value for transfer" because contextBridge's V8 serialization doesn't preserve MessagePort identity. Documented Electron limitation; see https://www.electronjs.org/docs/latest/tutorial/message-ports. Surfaced during runtime testing of the sdk's desktop-sense-transport branch: 6 sink ops fire correctly, broker subscriber registers correctly, but the `commoners.overlayCanvas .postSubscriberPort(port)` call throws "Invalid value for transfer" on the renderer→main IPC hop. Fix: route port transfers through window.postMessage (which DOES support MessagePort across the world boundary), then a new preload listener catches the message + forwards via ipcRenderer.postMessage in the isolated world where it works cleanly. Two edits: - `preload.ts` adds a `window.addEventListener('message', ...)` that watches for `__commoners_port_transfer` envelopes + their ports, validates the channel name against the IPC allowlist, then forwards via `ipcRenderer.postMessage`. - `onload.ts` per-plugin scoped `postMessage` detects MessagePort in the transfer list + dispatches via `window.postMessage` instead of TEMP_COMMONERS.postMessage. Non-port transfers (ArrayBuffer- only) take the direct path since ArrayBuffer survives contextBridge. Generic addition — works for any commoners plugin shipping MessagePort across renderers.
esbuild bundles the user's commoners.config.ts into a `.mjs` ESM
snapshot. If that config statically imports a wasm-pack `--target
nodejs` output (CJS using `__dirname` + `require('fs')`), esbuild
wraps the CJS module but the `__dirname` reference has no value in
ESM scope → "__dirname is not defined in ES module scope" with no
stack at launch.
Wasm-pack Node outputs are not structurally bundlable into ESM
without rewriting their sync-load path. The cleanest fix is to mark
them external so Node's real resolver loads them at runtime, where
their nested package.json (`"type": "commonjs"` if the consumer set
it) is honored.
Widens the config-bundle externals from a hardcoded list to also
match `*-wasm` and `wasm-*` globs. esbuild's `external` field
supports prefix/suffix wildcards but not regex — `*-wasm` matches
`ub-sense-wasm`, `muse-wasm`, etc.; `wasm-*` mirrors for the other
naming convention.
Caller pattern (verified working in apps/desktop):
package.json:
"type": "module",
"exports": {
".": {
"node": "./node/ub_sense_wasm.js",
"browser": "./web/ub_sense_wasm.js",
"default": "./web/ub_sense_wasm.js"
}
}
node/package.json:
{ "type": "commonjs" }
When a window ships its own preload (e.g. an ambient-feedback overlay
using raw Electron IPC for `senseBridge` / `overlayBridge` rather
than the commoners contextBridge), `globalThis.__commoners` is
undefined → `TEMP_COMMONERS = {}` → `.send` is missing.
`registerPluginAsLoaded` then threw at every plugin, surfaced as
`[commoners] <plugin> (load) failed to execute: TypeError:
TEMP_COMMONERS.send is not a function` for the full plugin list in
the overlay window's devtools.
Custom-preload renderers DON'T want to participate in the commoners
IPC graph by design — that's the whole point of shipping a custom
preload. Skip the notify rather than throwing. The plugin's own
`load()` closure still runs (returns the consumer-facing API);
consumers that DON'T touch the missing `.send`/`.on` keep working.
Consumers that DO touch them still get a clear "X is not a function"
at the call site, just not at plugin-load time.
Follow-up to the registerPluginAsLoaded fix: every plugin's
renderer-side ctx (`this.send`, `this.on`, `this.invoke`, etc.)
was hard-wired to `TEMP_COMMONERS.X(...)`. In a custom-preload
renderer (e.g. the desktop app's transparent overlay using
`window.senseBridge` / `window.overlayBridge` via raw Electron
IPC), `TEMP_COMMONERS = {}` and these calls threw at every
plugin that eagerly registered listeners in its renderer-side
`load()` body — the commoners BLE + serial plugins both do this,
so the overlay devtools spammed:
[commoners] bluetooth plugin (load) failed to execute:
TypeError: TEMP_COMMONERS.on is not a function
[commoners] serial plugin (load) failed to execute:
TypeError: TEMP_COMMONERS.on is not a function
Replace direct `TEMP_COMMONERS.X(...)` with a `tempCall(method,
fallback)` helper. If the underlying TEMP_COMMONERS method is a
function, use it; otherwise return the fallback (mostly no-op).
The plugin's renderer-side `load()` body completes; any consumer
that actually calls `commoners.<plugin>.X(...)` and depends on
the IPC will see the no-op behavior (e.g. an `.invoke()` resolves
undefined) — they can still detect missing commoners IPC at the
call site.
Combined with the prior `registerPluginAsLoaded` guard, the overlay
canvas window now boots clean: no plugin-load TypeError, sense://
subscriber-port preload bridge active and awaiting frames.
Last residual from the custom-preload-renderer fix series. The events bootstrap at `onload.ts:32-34` immediately calls `createElectronRendererEvents(TEMP_COMMONERS.send, TEMP_COMMONERS.on)` which (line 17) registered a listener on `on(EVENTS_RECEIVE_CHANNEL, ...)` — undefined call → `Uncaught (in promise) TypeError: on is not a function` in the overlay devtools. Make the parameters optional. If `on` is missing, skip the incoming-message subscription (cross-window events from other renderers won't reach this one — fine, the overlay doesn't participate in commoners cross-window events anyway). If `send` is missing, `emit()` only fires same-window listeners (still useful for in-window pub/sub). Same-window `on/off/once` continue to work via the local `listeners` Map regardless. Combined with `646af06` (registerPluginAsLoaded guard) and `a924a47` (tempCall-wrapped ctx methods), the desktop overlay window now boots completely clean — zero TypeError, sense:// subscriber port ready, awaiting frames.
Previously each main-process console.{log,warn,error} call was
wrapped in `console.groupCollapsed('Commoners Electron Process')`
in the renderer devtools. Hidden by default; surface only when the
operator clicks the group header.
This collides with debugging via exported logs: DevTools' "Save
as..." serializes collapsed groups to just the header line and
drops everything inside. Recurring failure mode — engineers add
`console.log` to main code for diagnostics, ship a log export to
investigate a bug, and find their diagnostic prints missing.
Replace with a `[main]` prefix. Same visual signal in devtools (one
short token tells you it's from main, not the renderer), but the
output is flat and survives export verbatim. Reviewers can grep
the exported file directly.
Without explicit `worker-src`, browsers fall back to `script-src`
for worker creation. The default CSP's script-src is
`'self' 'unsafe-inline' 'wasm-unsafe-eval'` — no `blob:`. But Vite
in dev mode resolves the canonical `new Worker(new URL('./w.ts',
import.meta.url), { type: 'module' })` pattern to a `blob:` URL
(it wraps the module body as a blob to serve as a worker source).
Result: `new Worker(...)` throws "Refused to create a worker from
blob:..." in dev and the worker never starts. Reported via a Vite
HMR-channel CSP violation log:
Creating a worker from 'blob:http://localhost:5173/<uuid>' violates
the following Content Security Policy directive:
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'". Note that
'worker-src' was not explicitly set, so 'script-src' is used as a
fallback. The action has been blocked.
Add `worker-src 'self' blob:` to the default CSP. 'self' covers
production bundles where the worker file is served from the same
origin. blob: is what dev mode needs. This unblocks any commoners
app using the standard module-worker pattern (which is broadly the
recommended Vite-supported way to ship workers).
Found while debugging desktop-sense-transport: the sense:// broker
worker silently never spawned in the popover, so the entire output
path stayed dark with no usable diagnostic. The broker-host code
DID call `new Worker(...)` — the CSP killed it before our own logs
could fire.
Companion to e1a04b7 (config-bundle esbuild external). That commit covered loadConfigFromFile's `.mjs` snapshot. But commoners has a SECOND config-bundle path — `bundleConfig` in utils/assets.ts uses Vite/Rollup and emits both `.mjs` (renderer) AND `.cjs` (Electron main). The `.cjs` is what `assets/electron/modules/config.ts` loads in main process and the Vite/Rollup external list didn't have the wasm-pack carve-out. Result: `ub-sense-wasm`'s web-target init (which calls `fetch()` for the .wasm bytes) gets bundled into `commoners.config.cjs`. At runtime in Electron main, the bundled code calls fetch on a data/blob URL → Node's undici throws "not implemented... yet..." → `main-subscriber attach failed: TypeError: fetch failed`. This was caught while debugging desktop-sense-transport: the main-process sense:// subscriber couldn't initialize the codec because the wrong wasm-pack target was bundled. The overlay subscriber (running in a renderer) was fine because the web target works there. Switch the Rollup `external` from an array to a function so we can match suffix/prefix patterns. Keep the existing nodeExternals list intact (just lift into the same check).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Long-running branch carrying substrate-level changes that landed in the UB SDK desktop app (
universalbrain/sdk→ branchdesktop-sense-transport) over the past two sessions. Each commit is independently scoped + landable; bundling them so reviewers can see the full picture of what apps/desktop now relies on.What's in here
Custom-preload renderer tolerance (4 commits —
646af06,a924a47,bfc1cc5,bc917cb):When an Electron app spawns a BrowserWindow with its own preload (bypassing the commoners contextBridge in favor of raw Electron IPC — common for transparent overlay canvases, custom picker dialogs, etc.), commoners' renderer-side scaffolding assumed every renderer would have
__commonersglobal. It didn't, so every plugin's renderer-side `load()` threw `TEMP_COMMONERS.send/on is not a function` immediately on mount. Four small fixes make commoners gracefully no-op in these renderers:CSP for Vite module workers (
40b443e):Default CSP had `script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'` but no `worker-src`. Vite's canonical worker pattern (`new Worker(new URL('./w.ts', import.meta.url), { type: 'module' })`) resolves to a `blob:` URL in dev; browsers fall back to `script-src` for worker creation; blob: not allowed → worker throws "Refused to create a worker" + silently never spawns. Added `worker-src 'self' blob:` to defaults so this just works.
wasm-pack external in config bundlers (2 commits —
e1a04b7,ca2bee8):wasm-pack's `--target nodejs` produces CJS using `__dirname` + `require('fs')` for sync WASM load. When commoners' two separate config-bundle paths (loadConfigFromFile's esbuild → mjs, bundleConfig's Vite/Rollup → mjs+cjs) include a wasm-pack output in the bundle, the CJS reference to `__dirname` collapses into "is not defined in ES module scope". Both bundler paths now externalize `-wasm` / `wasm-` globs so Node's runtime resolver picks the right per-target build via the package's `exports` conditions.
Plus 8 prior commits on the same branch (BLE permissions / single-instance opt-out / haptics plugin / postMessage MessagePort routing / alpha.4 version bump / etc.) — all landed via apps/desktop usage at `universalbrain/sdk`'s `desktop-sense-transport` branch.
Where this was needed
The UB SDK's `apps/desktop` (menubar shell with always-on-top transparent overlay) is the first commoners app that:
Each of these probably comes up for other commoners apps eventually. Landing them as defaults rather than per-app workarounds.
Test plan
🤖 Generated with Claude Code