Repository navigation
This release changes behaviour at stability-contract
boundaries (plugin id rules, error codes and messages, FRAME_INVALID
handling, option meanings, Kotlin host behaviour), and in 0.x a breaking change
bumps the minor version (as ADR 0006 did with 0.6.0).
Security and correctness fixes from the November 2026 review. No export was
removed and the wire VERSION is still 1, but several defaults and error
paths changed. Entries marked "Can break" can break a consumer that relied on
the old behaviour; read them before upgrading.
Security
- Can break —
createWorkletRuntime({ auth: false })throws unless
deviceMode: falseis also set. Before, it silently ran device mode without
auth. - Can break — Fetch-metadata gate: with auth on, a gated request or
WebSocket upgrade carryingSec-Fetch-Site: same-siteorcross-siteis
refused with 403. Session cookies are not port-isolated, so a page on another
127.0.0.1port could otherwise load gated files with our cookie. Requests
without the header (non-browser clients) are unchanged, and a request whose
Originis exactly anallowedOriginsentry is exempt. - Can break — bootstrap nonce TTL: an unredeemed nonce expires after 120 s
(BOOTSTRAP_TTL_MS) of running time counted from the first page request
(a/loginbefore any page load is accepted only inside the 600 s unloaded
window); the clock pauses while the Bare
worklet is suspended. A nonce whose page never loads expires after 600 s of
running time (BOOTSTRAP_UNLOADED_TTL_MS).
LoopbackServerOptions.bootstrapTtlMsoverrides the 120 s (an invalid value
falls back to it; capped at 600 s,BOOTSTRAP_TTL_MAX_MS). A/loginafter
expiry is a 401; an existing session cookie keeps working. - Can break —
mountDiroverlap throws: a gated directory mount whose root
is inside or equal to the public root is refused, in either mount order
(real paths; case- and normalization-insensitive only on macOS, iOS and
Windows,foldPathCaseoverrides). A gated root strictly around the public
one is allowed. The public mount 404s files inside a gated root. - Can break —
registerRoutepaths are normalized like mount paths
(decoded, trailing slash dropped); a path no request could match throws. - Can break — generic error messages (error text is part of the plugin
contract): a plugin or host handler that throws
is answered with a fixed message (<pluginId>.<event> failed); the detail is
logged. Returnerr(code, message)to send a specific message. Host error
codes outside the canonical set or the app-scoped form (ns.x/code, ≤ 64
chars) becomeHOST_ERROR; host messages are capped at 512 chars (vendor.media: 500; it passes app-scoped host codes such as
app.media/cancelledthrough). Oversized-response fallbacks no longer echo the
plugin, event or reason. - Can break — plugin id format and reserved namespaces:
registerrejects
an id that is not two or more dot-separated[A-Za-z0-9_-]segments (≤ 256
UTF-8 bytes), and anycore.*/vendor.*id other than the framework's own
plugins. Rename consumer plugins to your own namespace (app.photos). - Can break —
FRAME_INVALIDbehaviour:pluginId/event/requestId
over 256 UTF-8 bytes and invalid UTF-8 in a header (was U+FFFD) are
FRAME_INVALID. A content-invalid frame is skipped and answered
FRAME_INVALIDinstead of closing the protocol socket; on the host IPC a bad
host response fails its call at once and no longer drops the good frames
coalesced with it. iOS and Android answer malformed IPC fields with
FRAME_INVALIDtoo. - Can break — iOS storage moved: the reference app and
BareStorage.prepare()keep storage inApplication Support/bare(was
Documents/bare, moved once on first launch) and the cache in
Caches/bare-cache. Storage exclusion from backup is fatal if it fails. - Can break — Android storage moved: the reference app keeps durable
storage innoBackupFilesDir/bare(wascacheDir/bare, where "Clear cache"
wiped the p2p identity), moved once on upgrade. Backup and device-transfer
rules now exclude every domain,rootanddevice_*included. - Can break — iOS permissions fail closed: a missing or unknown permission
id isunsupported(was answered asstorage, i.e.granted). - Constant-time credential comparison, the 1024 pending-host-call cap and the
forged-HOST_*_RESPONSErule are now pinned by tests. - Files are served from a verified descriptor (real path +
dev/ino
re-checked after open), so a symlink swapped in after the check is refused;
an unreadable file-mount target is a 404. - Responses from routes and the framework send
nosniff, and, with auth on, gated
responses sendCross-Origin-Resource-Policy: same-origin(also when
allowedOriginsis set; off in dev). A route that fails after its response
ended is only logged, and a route's 500 drops the headers the handler set. An oversized/loginbody is a 413 and closes the connection. - Upgrade-rejection logs are rate limited (10 per 10 s) and truncated to 200
chars; captured backend console messages are capped at 16 KiB;logs.view
rejects a non-numbertailand unknownlevel/source, andLogStore.view
returns nothing for an invalidtail(was everything). handoff.jsonis written atomically with mode0600.- Android host:
BareWebViewClientalso blocks off-origin POST
navigations (empty 403), opens external links only on a user gesture,
refuses userinfo URLs; the reference app declaresCAMERA, runs the
worklet once per process in theApplication(singleTopactivity that
survives UI-mode, density, font-scale, locale and layout-direction changes;
later WebViews get no nonce; a late handoff is still loaded; a failed
storage migration falls back and is retried), feature-checks
addDocumentStartJavaScript, reads the handoff throughBareHandoff(never
logged), and keeps the p2p verify bundle out of the APK.
requireSupportedAbi()also throws in a 32-bit process. - iOS host: confinement, bootstrap injection, handoff deletion and backup
exclusion move intoBareHost(BareNavigationDelegate,BareWebHost,
BareHandoff,BareStorage); a non-JSON handler result is
INVALID_RESPONSEinstead of a crash. A same-origintarget=_blankloads
in place; a failedDocuments/baremigration is retried next launch; the
asset cache moves toCaches/bare-cache/asset-cache. - Can break — iOS host timeouts:
HostPluginRegistry.registertraps
(precondition) on a non-positive or NaN timeout; timeouts are clamped to
maxInvokeTimeout(one day). - Desktop: the window is confined to the backend origin, external links
open in the system browser, clipboard read is off, and the
EKROOH_TEST_HANDOFF_COPYhook exists only in-Dtest-hooks=truebuilds. - Dev harness: the supervisor refuses a non-loopback bind, caps live
instances at 32 (429), cleans up a failed allocation and answers a 500 with
no detail. - Release pipeline:
release.ymlis split into a credential-free
buildjob (checks the tag equals the version and is onmain, then runs
build:pkg,docs:check,vp check,vp testand the Gradle build), an
OIDC-onlypublish-npmjob that publishes the packed tarball with no
checkout or install, and apublish-mavenjob that runs only the Gradle
publish tasks. Gradle dependency verification is on.publish-npmfails
early on npm older than 11.5.1. The OSV vuln scan ignores the Maven
ecosystem (its only input is the Gradle build-tool hash list); npm lockfiles
are still scanned. The docs-only classifier is rename-aware, and install
detection insecurity.test.tsis segment-aware (gems included). Workflow-level
permissions are read-only, every checkout drops its token, and
CODEOWNERScovers the trust boundaries. - Maven publishing runs no Gradle next to the secrets: the Gradle builds
no longer sign or upload (thesigningand Nexus publish plugins are gone).
The credential-free build job stages the unsigned publication with
publishMavenAarPublicationToLocalStagingRepository; the publish jobs in
release.ymlandrelease-bare-kit-runtime.ymlsign it withgpgand
upload it withcurl(.github/scripts/publish-maven.sh, Central Portal
publisher API). CI dry-runs the signing path with a throwaway key. - Merge queue ready: the workflows holding required checks also run on
merge_group. - Can break —
npm run prebuildsno longer needsghor a token (it
fetches the public release URL), replacesprebuilds/androidand
prebuilds/ioswholesale, refuses absolute,..and symlink archive
entries, and a version argument must equal the lockfile's. - Can break — deploy templates:
DEPLOY_APPROVERSis removed (it gated
nothing). The templates need the environmentsandroid-signing,
internal,production,ios-signing,testflight, each limited to the
default branch and holding its own secrets.
Changed
- The Bare Kit runtime is a separate Android artifact (ADR 0007):
bare-hostno longer bundleslibbare-kit.soand the Bare Kitclasses.jar.
It depends onio.github.anderscan.ekrooh:bare-kit-runtime:2.5.5instead,
which Gradle resolves from Maven Central automatically. Thebare-hostAAR
goes from about 69 MB to about 40 KB. The runtime is published only when
bare-kit is bumped, because Maven Central limits monthly release size. No
consumer changes are needed. - Can break — host timeouts: a host call that misses its deadline is
TIMEOUT(wasHOST_ERROR). Event deadlines now reach the host bridge:vendor.media
waits 4 min 55 s andpermissions.request5 min (was a fixed 30 s, which
dropped a photo picked after 30 s). iOS and Android hosts honour per-handler
timeouts. - Web transport reconnect: sent invokes fail with
TRANSPORT_ERRORas soon
as the socket is lost (they waited out their timeout). Retries use equal
jitter; a never-connected client gives up only after 5 retries and 10 s;
the retry count resets after 5 s of stable open; a socket stuck connecting is
dropped after 10 s;WAKErecovers a client that gave up. A socket the
server replaced is closed with code 4001"replaced", and the client waits
30 s or a foreground before reconnecting, so two documents no longer evict
each other forever. At most 256 requests queue while connecting. - Give-up and invoke-timeout errors no longer include server log lines unless
diagnostics: trueis set. - Can break — per-channel cap: the router caps calls per channel (48 of
64,maxInflightPerChannel); over it isTRANSPORT_ERROR. A per-plugin cap
(maxInflightPerPlugin) is available but off by default. Ids are capped at
256 UTF-8 bytes; an undecodable host capabilities answer fails the query
withFRAME_INVALID; timer delays are capped at 2^31−1 ms. - Can break: a declared event with no handler is
UNSUPPORTED_EVENT(was
PLUGIN_ERROR). - A route handler that rejects is answered with a 500 (it used to abort the
worklet). - Can break —
runtime.start()rejects when the handoff cannot be
written, whenmountDirthrows for the web app (the overlap rule), and
afterclose(); it is idempotent. A failedcreateWorkletRuntimeno longer
leaves the console patched.close()is idempotent, detaches the IPC
listener, fails pending host calls, and the console is restored when the
last runtime closes. - The mock transport runs the real plugin kernel and manifests: unknown
permission ids areunsupported, unknown media kinds are refused,
discovery listscore.logs, and it warns inside an app shell. It ships in
the@ekrooh/bare/transportsbundle, so select it only behind a dev/test
guard. - The reference apps fold server log lines into errors only in dev or when
built withVITE_BARE_DIAGNOSTICS=true(the device CI jobs set it). The
replaced-socket error reads "WebSocket replaced by another page holding the
session; reconnects in 30 s or when this page returns to the foreground". - IPv6 hosts are bracketed in the origin.
Added
@ekrooh/bare/sync: peer-to-peer sync of one JSON value per device.
createSyncreturns invite/join/publish/values/onChange; devices that share
an invite code see each other's latest value.hyperswarmis now a runtime
dependency.EventSpec.validate(runtime args check),EventSpec.hostTimeoutMs,
PluginManifest.hostTimeoutsMs, thetimeoutMsargument of
delegateToHost, routermaxInflightPerChannel/maxInflightPerPlugin, and
the same two options onWorkletRuntimeOptions.MessageTransport.close()(optional) and thecreateWebSocketTransport
optionsminBootWindowMs,openTimeoutMs,maxQueued,diagnostics;
diagnosticsoncreateProtocolMessenger.LoopbackServerOptions.bootstrapTtlMsandfoldPathCase;
LoopbackServer.pauseBootstrapClock?()/resumeBootstrapClock?()(the
runtime wires them to Baresuspend/resume);
HostCapabilitiesResponseHeader.error?.- Swift:
BareStorage,BareStorageDirectories,BareStorageError,
BareHandoff,BareNavigationPolicy,BareNavigationDecision,
BareNavigationDelegate,BareWebHost,HostIpcRouter,
HostPluginRegistry.register(…timeout:…),invokeTimeout,
defaultInvokeTimeout,userMediatedInvokeTimeout,
BareProtocol.buildErrorResponse(…type:),HostPluginRegistry.maxInvokeTimeout,
BareNavigationDecision.loadInPlace. - Kotlin:
BareHandoff,HostIpcCoordinator.Port(+ constructor and
stop()),HostPluginRegistrytimeout overload,timeoutFor,
DEFAULT_TIMEOUT_MS,INTERACTIVE_TIMEOUT_MS,FrameDecoder(onInvalidFrame),
BareHostAbi.isSupportedRuntime,BareWebViewClient.shouldOpenExternally,
shouldBlockMainFrameRequest,onRenderProcessGone;BareWebViewClientis
nowopen. - API snapshots: the JS type surface (
api/public-api.d.mts.snap,
npm run api:snapshot), the Kotlin API (android/api/bare-host.api,
apiDump/apiCheck) and the Swift API (ios/public-api.swift.snap). The
exportskeys are frozen. security.android.test.ts,security.apple.test.tsand
error-codes-parity.test.ts;docs:checktype-checks every docs snippet
againstdist/and rejects known retired APIs.
Fixed
- npm license metadata:
package.jsondeclaredMIT, but theLICENSE
file shipped in every tarball, the Maven POMs and upstream Bare / Bare Kit
are all Apache-2.0. The field now saysApache-2.0. The license itself is
unchanged.
Removed
web/transport-log.ts(dead code) and the mock-only handlers module.
Docs
- Fixed snippets that called APIs that do not exist (
transport.invoke,
discovery.summary, flat builder paths), the frame layout (payloadLen),
the AAR coordinates, the iOS embedding steps, and the media guide (CSPRNG
mount ids, no nativepathin results). ADRs 0005 and 0006 are marked
accepted; 0003 notes what 0006 superseded.