@ekrooh/bare 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,246 +1,190 @@
1
1
  # @ekrooh/bare
2
2
 
3
- **One soul, many platforms.** A single codebase and identity that lives
4
- across every platform at once — the same app, state, and soul on Android, iOS,
3
+ **One soul, many platforms.** One codebase and identity across Android, iOS,
5
4
  web, and desktop.
6
5
 
7
- [![CI](https://github.com/AndersCan/ekrooh/actions/workflows/test.yml/badge.svg)](https://github.com/AndersCan/ekrooh/actions/workflows/test.yml)
6
+ **Docs: <https://anderscan.github.io/ekrooh>**
8
7
 
9
- The **boring bootstrap** for cross-platform apps on the Bare runtime
10
- (holepunch): a binary wire protocol, plugin kernel, RPC messenger, transports,
11
- and native host bridges — plus a reference app that demonstrates all of it.
8
+ [![CI](https://github.com/AndersCan/ekrooh/actions/workflows/test.yml/badge.svg)](https://github.com/AndersCan/ekrooh/actions/workflows/test.yml)
9
+ [![Release](https://img.shields.io/github/v/release/AndersCan/ekrooh)](https://github.com/AndersCan/ekrooh/releases/latest)
12
10
 
13
- > Read `vision.md` for what this project is and is not, `AGENTS.md` for how to
14
- > develop it, `CONTRIBUTING.md` for how to contribute, and `RELEASING.md` for
15
- > how to cut a release.
11
+ The boring bootstrap for cross-platform apps on the Bare runtime (holepunch): a
12
+ binary wire protocol, plugin kernel, RPC messenger, transports, and native host
13
+ bridges, plus a reference app.
16
14
 
17
- ## The model (UI perspective)
15
+ - [`vision.md`](vision.md): what this project is and is not.
16
+ - [`AGENTS.md`](AGENTS.md): how to develop it and the canonical commands.
17
+ - [`CONTRIBUTING.md`](CONTRIBUTING.md): how to contribute.
18
+ - [`RELEASING.md`](RELEASING.md): how to cut a release.
19
+ - `docs/`: internal ADRs (`docs/adr/`) and agent operations; not the public site.
18
20
 
19
- The **frontend** (browser or WebView) is written as if there is only a
20
- **backend** reachable over a **WebSocket** (binary framed messages). It picks a
21
- transport automatically:
21
+ ## Model
22
22
 
23
- - **WebSocket** everywhere on-device: the worklet runs **one loopback HTTP+WS
24
- server** that serves the web app, the media files, and the protocol socket,
25
- so the page connects to the same origin it was served from (`ws://location.host`).
26
- - **Mock transport** when `VITE_TRANSPORT_MODE=mock` (tests).
27
- - Browser dev points the WebSocket at the dev backend explicitly
28
- (`VITE_BARE_WS_URL`).
23
+ The frontend (browser or WebView) is written as if only a backend is reachable
24
+ over a WebSocket carrying binary framed messages.
29
25
 
30
- The UI does **not** branch on worklets, Bare, or host IPC. Shared types and
31
- helpers (`@ekrooh/bare/core`) describe the **wire protocol** to the backend, not
32
- the runtime that implements it.
26
+ - On device, the worklet runs one loopback HTTP+WS server that serves the web
27
+ app, media files, and the protocol socket. The page connects to
28
+ `ws://location.host`.
29
+ - `VITE_TRANSPORT_MODE=mock` selects the mock transport (tests).
30
+ - Browser dev points the WebSocket at the dev backend via `VITE_BARE_WS_URL`.
33
31
 
34
- ## Glossary
32
+ The UI never branches on worklets, Bare, or host IPC. `@ekrooh/bare/core`
33
+ describes the wire protocol, not the runtime that implements it.
35
34
 
36
- | Term | Meaning |
37
- | ------------ | -------------------------------------------------------------------------------------------- |
38
- | **Frontend** | Web UI: Vite bundle in the browser; same bundle in the Android/iOS WebView. |
39
- | **Backend** | Logic that runs the core bundle and speaks the framed message protocol with the frontend. |
40
- | **Host** | Native shell (Android/iOS): starts the backend, owns system APIs, injects the session token. |
35
+ | Term | Meaning |
36
+ | ------------ | ------------------------------------------------------------------------------------------------ |
37
+ | **Frontend** | Web UI: Vite bundle in the browser, or the same bundle in a WebView. |
38
+ | **Backend** | Runs the core bundle and speaks the framed protocol with the frontend. |
39
+ | **Host** | Native shell (Android/iOS): starts the backend, owns system APIs, injects the session bootstrap. |
41
40
 
42
- ## Repository structure
41
+ ## Repository layout
43
42
 
44
- - `core/` — framework: wire protocol codec, plugin router, RPC messenger, host
45
- IPC (`core/messages`), the unified loopback server (HTTP + WS + cookie auth),
46
- Bare worklet entry.
47
- - `plugins/` — framework: canonical plugins (`core.health`, `core.discovery`,
43
+ - `core/`: wire codec, plugin router, RPC messenger, host IPC (`core/messages`),
44
+ loopback server (`core/server`), worklet entry (`core/main.core.ts`).
45
+ - `plugins/`: canonical plugins (`core.health`, `core.discovery`,
48
46
  `core.permissions`, `vendor.media`) and typed event builders.
49
- - `web/transports/` — framework: `MessageTransport` plus WebSocket and mock
50
- transports.
51
- - `android/` — framework: Android host **library** (`:bare-host`) — IPC
52
- coordinator, host plugin registry, WebView client.
53
- - `ios/` — framework: iOS host **Swift package** (`BareHost`) — IPC
54
- coordinator, host plugin registry.
55
- - `examples/` — reference app: `web/` (lit-html + nanostores + Tailwind UI),
56
- `android-app/` (the Android shell that embeds the backend and WebView), and
57
- `ios-app/` (the iOS shell).
58
- - `e2e/` — Playwright tests against the browser runtime on the mock transport.
59
- - `scripts/` — dev backend runner, Playwright browser wrapper, prebuilds
60
- fetcher, loopback smoke test.
61
- - `prebuilds/` — Bare Kit prebuilds (build output, gitignored).
62
-
63
- The framework's public surface is the `exports` map of the root `package.json`
64
- (`@ekrooh/bare/core`, `/runtime`, `/plugins`, `/plugins/*/events`, `/transports`).
65
- The package ships **compiled ESM JavaScript + type declarations** (`dist/`,
66
- built with `vp pack`) — consumers never see TypeScript source.
47
+ - `web/transports/`: `MessageTransport`, WebSocket and mock transports.
48
+ - `android/`: host library `:bare-host` (IPC coordinator, plugin registry, WebView client).
49
+ - `ios/`: host Swift package `BareHost` (root `Package.swift`).
50
+ - `examples/`: `web/`, `android-app/`, `ios-app/`, `consumer-basic/`.
51
+ - `e2e/`: Playwright specs on the mock transport. `scripts/`: dev runner, Playwright wrapper, prebuild fetcher.
52
+ - `apps/docs/`: public docs site (Astro Starlight, GitHub Pages).
53
+ - `templates/`, `research/`: shared deploy templates; design notes.
54
+ - `prebuilds/`: Bare Kit prebuilds (gitignored build output).
55
+
56
+ The public surface is the `exports` map in `package.json` (`@ekrooh/bare/core`,
57
+ `/runtime`, `/plugins`, `/plugins/*/events`, `/transports`). It points at
58
+ compiled ESM and type declarations in `dist/` (built with `vp pack`). Consumers
59
+ never see TypeScript source.
67
60
 
68
61
  ## Support matrix
69
62
 
70
- | Runtime | Status | Transport | Notes |
71
- | -------------- | ------------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72
- | Browser (dev) | first-class | WebSocket | Vite + local backend server (`VITE_BARE_WS_URL`) |
73
- | Browser (test) | first-class | Mock | Deterministic Playwright runs |
74
- | Android | first-class | WebSocket | Same-origin loopback socket; cookie auth via the injected token |
75
- | iOS | first-class (reference parity) | WebSocket | Same-origin loopback socket + cookie auth; host ships as source (`ios/`, SPM) on the same release tag; native picker/camera + Maestro journeys land before beta |
76
- | Desktop | contract | — | Adapters follow the same plugin contracts |
77
-
78
- ### Parity policy
79
-
80
- - Core protocol and plugin contracts are runtime-agnostic.
81
- - Capabilities are opt-in plugins and may be runtime-specific.
82
- - Plugins report unsupported behavior with deterministic errors.
83
- - Feature code calls plugin events, not raw platform APIs from shared UI code.
84
-
85
- ## Architecture
86
-
87
- ```mermaid
88
- flowchart LR
89
- subgraph frontend [Frontend]
90
- webUi[WebUI]
91
- end
92
- subgraph backend [Backend]
93
- coreProtocol[Message protocol]
94
- coreRuntime[Core + plugins]
95
- end
96
- subgraph host [Host native]
97
- hostIpc[Host IPC]
98
- end
99
- webUi -->|WebSocket framed messages| coreProtocol
100
- coreProtocol --> coreRuntime
101
- coreRuntime <-->|delegation| hostIpc
102
- ```
63
+ | Runtime | Status | Transport | Notes |
64
+ | -------------- | ------------------------------ | --------- | ----------------------------------------------------------------------------------------------- |
65
+ | Browser (dev) | first-class | WebSocket | Vite + local backend (`VITE_BARE_WS_URL`) |
66
+ | Browser (test) | first-class | Mock | Deterministic Playwright runs |
67
+ | Android | first-class | WebSocket | Same-origin loopback socket, cookie auth |
68
+ | iOS | first-class (reference parity) | WebSocket | Same-origin loopback socket, cookie auth; host ships as source (`ios/`, SPM) on the release tag |
69
+ | Desktop | reference (macOS) | WebSocket | Zig host + system webview + sidecar `bare`; see `examples/desktop-app` |
70
+
71
+ Parity policy: core protocol and plugin contracts are runtime-agnostic.
72
+ Capabilities are opt-in plugins and may be runtime-specific. Plugins report
73
+ unsupported behavior with deterministic errors. Feature code calls plugin
74
+ events, never raw platform APIs.
103
75
 
104
76
  ## Message protocol
105
77
 
106
- Binary frame:
107
-
108
- - `version` (1 byte)
109
- - `type` (1 byte)
110
- - `headerLen` (2 bytes, big-endian)
111
- - `header` (UTF-8 JSON)
112
- - `payload` (optional)
113
-
114
- Implementation: `MessageProtocol` in `core/messages` (see `wire-codec.ts`).
115
-
116
- ## Plugin-first architecture
117
-
118
- Core does not ship product features (camera, permissions, BLE, etc.). Teams
119
- add plugins with namespaced events and per-runtime handlers.
120
-
121
- - Plugin IDs: `vendor.plugin`; events are plugin-local (`health.ping`).
122
- - Header types: `DISPATCH`, `INVOKE_REQUEST`, `INVOKE_RESPONSE`; invoke
123
- responses echo `requestId`.
124
- - `dispatch` is fire-and-forget; `invoke` is request/response and timeout-bound.
125
- - Runtime adapters: `web`, `android`, `ios`, `bare`.
126
- - Canonical plugins: `core.health`, `core.discovery`, `core.permissions`, and
127
- `vendor.media` (reference for out-of-band binary transfer).
128
-
129
- ### Loopback server + cookie auth
130
-
131
- One loopback HTTP+WS server (`core/server/static-file-server.ts`) serves the
132
- web app at `/`, the media files mounted by plugins, and the framed-protocol
133
- WebSocket socket. On device it binds `127.0.0.1` on an ephemeral port and every
134
- request is gated by a per-session token:
135
-
136
- - The shell injects `window.__ekrooh = { token }` (plus `window.BareShell` as
137
- a presence marker) before the page loads.
138
- - The page exchanges it for a `bare_session` cookie via `POST /login`
139
- (`HttpOnly; SameSite=Lax; Path=/`).
140
- - The cookie then authorizes `<img>`/`<video>`/`fetch` and the WebSocket
141
- upgrade. `X-Bare-Token`/`?token=` remain as a fallback for non-browser
142
- clients.
143
- - The worklet writes `{ origin, port, token }` to `handoff.json` in the
144
- sandbox dir the host passed via `Worklet.Configuration.assets`; the host
145
- reads it, injects the token, and loads `http://127.0.0.1:<port>/index.html`.
146
-
147
- The dev backend runs the same bundle with auth off on a fixed port
148
- (`ws://localhost:8080`), so browser dev needs no token.
149
-
150
- ### Media (out-of-band bytes)
151
-
152
- Images/videos never cross the wire protocol (the frame cap is 16 MiB).
153
- `vendor.media` demonstrates the intended pattern: the host picks/captures a
154
- file natively and returns its path; the worklet mounts it on the loopback
155
- server and returns a URL. The web layer loads the URL directly (same-origin,
156
- cookie-authenticated) — one serving implementation for every runtime. The
157
- Android reference host wires a real native picker/camera; the iOS reference
158
- host's real picker/camera is part of the iOS parity worklist.
159
-
160
- ## Scripts
161
-
162
- | Script | Purpose |
163
- | ---------------------------- | ------------------------------------------------------ |
164
- | `npm run check` | Format + lint + type-check (`vp check`) |
165
- | `npm run typecheck` | `tsc --noEmit` only |
166
- | `npm run test` | Unit tests + Playwright e2e |
167
- | `npm run test:unit` | Unit tests (vitest) |
168
- | `npm run test:e2e:web` | Playwright e2e |
169
- | `npm run dev` | Vite dev server + Bare backend (watch restart) |
170
- | `npm run dev:mock` | Vite with mock transport |
171
- | `npm run dev:ws` | Vite only (backend started separately) |
172
- | `npm run build` | Core bundle + web assets for the Android app |
173
- | `npm run build:core` | Bundle `core/main.core.ts` with esbuild |
174
- | `npm run build:web` | Build `examples/web` into the Android app's assets |
175
- | `npm run build:ios` | iOS app inputs (addons, bundle, web assets) + xcodegen |
176
- | `npm run prebuilds` | Fetch Bare Kit prebuilds (requires `gh`) |
177
- | `npm run test:ios` | XCTest + UI test on the iOS simulator |
178
- | `npm run playwright:install` | Download Chromium into `.playwright-browsers/` |
179
- | `npm run smoke:loopback` | Smoke-test the unified loopback server under `bare` |
78
+ Binary frame: `version` (1 byte), `type` (1 byte), `headerLen` (2 bytes, BE),
79
+ `payloadLen` (3 bytes, BE), `header` (UTF-8 JSON), `payload` (optional).
80
+ Implemented by `MessageProtocol` in `core/messages` (`wire-codec.ts`).
81
+
82
+ Header types: `DISPATCH`, `INVOKE_REQUEST`, `INVOKE_RESPONSE` (responses echo
83
+ `requestId`). `dispatch` is fire-and-forget; `invoke` is request/response and
84
+ timeout-bound.
85
+
86
+ ## Plugins
87
+
88
+ Core ships no product features. Plugin IDs are `vendor.plugin`; events are
89
+ plugin-local (`health.ping`). Runtime adapters: `web`, `android`, `ios`, `bare`.
90
+ Canonical plugins are listed under Repository layout.
91
+
92
+ Media never crosses the wire (frame cap 16 MiB). `vendor.media` is the reference
93
+ for out-of-band bytes: the host picks or captures a file natively, the worklet
94
+ mounts it on the loopback server and returns a URL, and the web layer loads that
95
+ URL with cookie auth.
96
+
97
+ ## Loopback server and auth
98
+
99
+ One server (`core/server/static-file-server.ts`) serves the web app at `/`, plugin
100
+ media mounts, and the WebSocket upgrade. On device it binds `127.0.0.1` on an
101
+ ephemeral port. Every request is gated by a per-session token:
102
+
103
+ 1. The shell injects `window.__ekrooh = { bootstrap }`, a single-use nonce (never the raw token).
104
+ 2. The page calls `POST /login` with the nonce and receives a
105
+ `bare_session_<port>` cookie (`HttpOnly; SameSite=Strict; Path=/`). The port
106
+ in the name keeps two loopback servers in one cookie store apart. The raw
107
+ token is never a login credential.
108
+ 3. The cookie authorizes `<img>`, `<video>`, `fetch`, and the WS upgrade.
109
+ Non-browser clients send `X-Bare-Token`. There is no `?token=` fallback
110
+ (see `docs/adr/0003-loopback-auth-surface.md`).
111
+
112
+ Only the bootstrap mount at `/` is public pre-auth. The worklet writes
113
+ `{ origin, port, bootstrap }` to `handoff.json` in the host's sandbox dir (never
114
+ the session token); the host loads `http://127.0.0.1:<port>/index.html` and
115
+ deletes the file.
116
+
117
+ Every request must also target the bound loopback server (`Host`), and a page
118
+ `Origin` must be that server or an explicitly allowed one, in every mode.
119
+
120
+ The dev backend (`mode=dev`) runs the same bundle with auth off on
121
+ `ws://localhost:8080`, allowing the Vite dev origin (`origin=` tokens).
180
122
 
181
123
  ## Testing
182
124
 
183
- - **Unit** — vitest (`vp test`) for the protocol codec, plugin router, RPC
184
- messenger, and transports; JUnit for the Kotlin host; XCTest for the Swift
185
- host.
186
- - **Type check** — `tsc --noEmit` (part of `vp check`).
187
- - **Integration** — Playwright e2e against the mock transport exercising the
188
- real binary protocol (`npm run test:e2e:web`). Browsers resolve through
189
- `scripts/playwright-local-browsers.mjs`, which pins `PLAYWRIGHT_BROWSERS_PATH`
190
- to the repo-local `.playwright-browsers/`.
191
- - **Native end-to-end** — `npm run test:ios` runs the Swift host's XCTest suite
192
- plus a UI test against the reference app on the iOS simulator (discovery
193
- summary, Ping / Payload Echo / Roundtrip / Storage permission).
125
+ - Unit: vitest (`vp test`) for the codec, router, RPC messenger, transports;
126
+ JUnit for the Kotlin host; XCTest for the Swift host.
127
+ - Type check: `tsc --noEmit`, part of `vp check`.
128
+ - Integration: Playwright e2e on the mock transport (`npm run test:e2e:web`).
129
+ Browsers resolve through `scripts/playwright-local-browsers.mjs` into `.playwright-browsers/`.
130
+ - Native: `npm run test:ios` runs the Swift XCTest suite and a UI test on the simulator.
131
+
132
+ ## Commands
133
+
134
+ Canonical list: [`AGENTS.md`](AGENTS.md#canonical-commands). Common ones:
135
+
136
+ | Command | Purpose |
137
+ | ---------------------------- | -------------------------------------------------- |
138
+ | `vp check` | Format, lint, type-check |
139
+ | `vp test` | Unit tests |
140
+ | `npm run test` | Unit + Playwright e2e |
141
+ | `npm run dev` | Vite dev server + Bare backend (watch restart) |
142
+ | `npm run dev:mock` | Vite with mock transport |
143
+ | `npm run build:core` | Bundle `core/main.core.ts` (esbuild) |
144
+ | `npm run build:web` | Build `examples/web` into the Android app's assets |
145
+ | `npm run build:ios` | iOS inputs (addons, bundle, web assets, xcodegen) |
146
+ | `npm run prebuilds` | Fetch Bare Kit prebuilds (requires `gh`) |
147
+ | `npm run playwright:install` | Download Chromium into `.playwright-browsers/` |
148
+ | `npm run smoke:loopback` | Smoke-test the loopback server under `bare` |
194
149
 
195
150
  ## Android build
196
151
 
197
- Prerequisites: Android SDK (`local.properties` with `sdk.dir=...`) and Bare Kit
198
- prebuilds.
152
+ Needs the Android SDK (`local.properties` with `sdk.dir=...`) and Bare Kit prebuilds.
199
153
 
200
154
  ```bash
201
- npm ci
202
- npm run prebuilds # fetches prebuilds into prebuilds/ (gitignored)
155
+ vp install
156
+ npm run prebuilds
203
157
  npm run build # core bundle + web assets
204
158
  ./gradlew :examples:android-app:assembleDebug
205
159
  ```
206
160
 
207
- `./gradlew build` additionally runs the host library's JUnit tests.
161
+ `./gradlew build` also runs the host library's JUnit tests.
208
162
 
209
163
  ### Consuming the published AAR
210
164
 
211
- `io.github.anderscan.ekrooh:bare-host` is published to **Maven Central** (no auth) on the same
212
- tag as the npm package — pin it to your `@ekrooh/bare` version:
165
+ `io.github.anderscan.ekrooh:bare-host` is on Maven Central (no credentials).
166
+ Pin it to the same version as `@ekrooh/bare`:
213
167
 
214
168
  ```gradle
215
- repositories {
216
- mavenCentral() // default in modern Gradle; no credentials needed
217
- }
218
-
219
169
  dependencies {
220
170
  implementation "io.github.anderscan.ekrooh:bare-host:<version>"
221
171
  }
222
172
  ```
223
173
 
224
- GitHub Packages (`maven.pkg.github.com/AndersCan/ekrooh`) remains a fallback
225
- but requires a `read:packages` PAT — prefer Maven Central.
174
+ GitHub Packages is a fallback that requires a `read:packages` token. Prefer Maven Central.
226
175
 
227
176
  ## iOS build
228
177
 
229
- Prerequisites: Xcode (macOS), Bare Kit prebuilds, and `xcodegen`.
178
+ Needs Xcode, Bare Kit prebuilds, and `xcodegen`.
230
179
 
231
180
  ```bash
232
- npm ci
233
- npm run prebuilds # fetches prebuilds/ios/BareKit.xcframework too
234
- npm run build:ios # addons/, Resources/, and ios-app.xcodeproj
235
- npm run test:ios # xcodebuild test on the simulator
181
+ vp install
182
+ npm run prebuilds # also fetches prebuilds/ios/BareKit.xcframework
183
+ npm run build:ios
184
+ npm run test:ios
236
185
  ```
237
186
 
238
- `npm run test:ios` runs the Swift host's XCTest suite (hosted by the reference
239
- app) plus a UI test that verifies the health-checks page end to end.
240
-
241
- ## Asset hygiene
187
+ ## Notes
242
188
 
243
- - Vite cleans hashed `examples/android-app/src/main/assets/assets/main-*.{js,css}`
244
- before rebuilding web assets.
245
- - Treat files under `examples/android-app/src/main/assets/` as build output,
246
- not source.
189
+ - Treat `examples/android-app/src/main/assets/` as build output. Vite clears
190
+ hashed `assets/main-*.{js,css}` before rebuilding.
@@ -1,4 +1,3 @@
1
- //#region core/messages/constants.d.ts
2
1
  declare const VERSION = 1;
3
2
  declare const MessageType: {
4
3
  readonly ENVELOPE: 1;
@@ -25,8 +24,6 @@ declare const MAX_HEADER_BYTES = 65535;
25
24
  /** Upper bound on one encoded frame. Large binary transfers are a plugin-level
26
25
  * concern (out-of-band handles), not an in-band payload. */
27
26
  declare const MAX_FRAME_BYTES: number;
28
- //#endregion
29
- //#region core/messages/types.d.ts
30
27
  /** Decoded binary frame (version byte stripped); used by WebSocket and other transports. */
31
28
  interface WireMessage {
32
29
  type: MessageTypeValue;
@@ -126,6 +123,12 @@ type MessageHeader = PluginHeader | HostCapabilitiesQueryHeader | HostCapabiliti
126
123
  interface PluginContext {
127
124
  runtime: RuntimeTarget;
128
125
  payload: Uint8Array;
126
+ /** The registry-validated sender of the envelope. Always equals the
127
+ * wire `pluginId` once the router has matched a manifest — adapters can trust
128
+ * it without re-validating against the wire. */
129
+ sender?: {
130
+ pluginId: string;
131
+ };
129
132
  }
130
133
  interface PluginRuntimeAdapter {
131
134
  dispatch?: (event: string, args: Record<string, unknown> | undefined, context: PluginContext) => void | Promise<void>;
@@ -138,8 +141,6 @@ interface PluginManifest {
138
141
  events?: string[];
139
142
  runtimes: Partial<Record<RuntimeTarget, PluginRuntimeAdapter>>;
140
143
  }
141
- //#endregion
142
- //#region core/messages/errors.d.ts
143
144
  /** Success half of an `Either` tuple. */
144
145
  declare function ok<T>(result: T): [null, T];
145
146
  /** Failure half of an `Either` tuple. Accepts a canonical error code or any
@@ -153,8 +154,6 @@ declare function coerceErrorCode(code: string | undefined): ErrorCode;
153
154
  type Ok<T> = [null, T];
154
155
  type Err = [CoreError, null];
155
156
  type PluginResult<T> = Either<CoreError, T>;
156
- //#endregion
157
- //#region core/messages/rpc-messenger.d.ts
158
157
  type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
159
158
  type InternalDispatchRequest = (PluginDispatchHeader & {
160
159
  requestId: string;
@@ -169,8 +168,6 @@ interface ProtocolMessenger {
169
168
  handleIncoming(header: MessageHeader): void;
170
169
  }
171
170
  declare function createProtocolMessenger(send: (request: InternalDispatchRequest, payload?: Uint8Array | ArrayBuffer | string | null) => void): ProtocolMessenger;
172
- //#endregion
173
- //#region core/messages/plugin-kernel.d.ts
174
171
  interface PluginRegistry {
175
172
  register(plugin: PluginManifest): void;
176
173
  resolve(pluginId: string, runtime: RuntimeTarget): PluginManifest['runtimes'][RuntimeTarget] | undefined;
@@ -188,7 +185,27 @@ interface PluginRouter {
188
185
  }
189
186
  type PluginRouterOptions = {
190
187
  delegateToHost?: (header: PluginInvokeRequestHeader, payload: Uint8Array) => Promise<PluginInvokeResponseHeader | null>;
191
- logger?: Pick<Console, 'warn' | 'error'>;
188
+ /** Host-registered capabilities (from `HOST_CAPABILITIES_RESPONSE`).
189
+ * Delegation to the host is only permitted for events the host announces
190
+ * here — a deny-by-default allowlist. When omitted or empty, no
191
+ * host-delegated invokes are allowed. */
192
+ getHostCapabilities?: () => Promise<CapabilityDescriptor[]>;
193
+ logger?: Pick<Console, 'warn' | 'error' | 'debug'>;
194
+ /** Global cap on calls routed at once (running or queued behind a serial
195
+ * plugin). A call over the cap is answered with a `TRANSPORT_ERROR` instead
196
+ * of being queued, so a flood cannot grow memory without bound. Defaults to
197
+ * `DEFAULT_MAX_INFLIGHT`. */
198
+ maxInflight?: number;
199
+ /** Plugin ids whose calls must run one at a time, in arrival order. Every
200
+ * other plugin's calls run concurrently, so a slow call never delays them. */
201
+ serialPlugins?: readonly string[];
202
+ /** How long (ms) the host's announced capabilities are reused before the
203
+ * host is asked again. Delegating an invoke otherwise costs an IPC round
204
+ * trip per call. Capabilities the host adds are still picked up within
205
+ * `NEGATIVE_REFRESH_MS`, and a failed delegation drops the cache at once.
206
+ * `0` disables caching (ask the host on every delegation). Defaults to
207
+ * `DEFAULT_HOST_CAPABILITIES_TTL_MS`. */
208
+ hostCapabilitiesTtlMs?: number;
192
209
  };
193
210
  declare function createPluginRouter(registry: PluginRegistry, runtime: RuntimeTarget, options?: PluginRouterOptions): PluginRouter;
194
211
  interface PluginBus {
@@ -196,28 +213,29 @@ interface PluginBus {
196
213
  invoke<TResult>(envelope: InvokeEnvelope<string, Record<string, unknown>, TResult>): Promise<Either<CoreError, TResult>>;
197
214
  }
198
215
  declare function createPluginBus(messenger: ProtocolMessenger): PluginBus;
199
- //#endregion
200
- //#region core/messages/wire-codec.d.ts
201
216
  type Encoder = (str: string) => Uint8Array;
202
217
  type Decoder = (bytes: Uint8Array) => string;
203
218
  interface ProtocolOptions {
204
219
  encode?: Encoder;
205
220
  decode?: Decoder;
206
221
  allowUnknownTypes?: boolean;
207
- /** Override the default {@link MAX_FRAME_BYTES} cap. */
222
+ /** Override the default {@link MAX_FRAME_BYTES} cap. Clamped to a sane
223
+ * range: it must be large enough to hold the largest legal header, and never
224
+ * larger than {@link MAX_FRAME_BYTES}. `Infinity`/`NaN`/negative inputs
225
+ * collapse to the defaults so a caller cannot disable the decode-side cap. */
208
226
  maxFrameBytes?: number;
209
227
  }
210
228
  declare class MessageProtocol {
211
229
  private encodeStr;
212
230
  private decodeStr;
213
231
  private allowUnknownTypes;
214
- private maxFrameBytes;
232
+ /** Resolved, clamped frame cap. Public so callers/tests can inspect the
233
+ * effective limit after clamping. */
234
+ readonly maxFrameBytes: number;
215
235
  constructor(options?: ProtocolOptions);
216
236
  encode(type: MessageTypeValue, header: MessageHeader, payload?: Uint8Array | ArrayBuffer | string | null): Uint8Array;
217
237
  decode(data: ArrayBuffer | Uint8Array): WireMessage;
218
238
  }
219
- //#endregion
220
- //#region core/messages/define-plugin.d.ts
221
239
  /**
222
240
  * One event's wire identity and type shape — the single source of truth for
223
241
  * the manifest `events` list, the generated handler table, and the typed
@@ -249,13 +267,69 @@ declare function definePlugin<const TSpecs extends Record<string, EventSpec<any,
249
267
  declare function invokeEvent<TSpec extends EventSpec<any, any>>(spec: TSpec, args: TSpec['args'], payload?: Uint8Array | ArrayBuffer | string | null, timeoutMs?: number): InvokeEnvelope<TSpec['name'], TSpec['args'], TSpec['result']>;
250
268
  /** Typed builder: a dispatch envelope for a spec's event. */
251
269
  declare function dispatchEvent<TSpec extends EventSpec<any, any>>(spec: TSpec, args: TSpec['args'], payload?: Uint8Array | ArrayBuffer | string | null): DispatchEnvelope<TSpec['name'], TSpec['args']>;
252
- //#endregion
253
- //#region core/messages/create-bare-runtime-context.d.ts
254
270
  interface BareRuntimeContext {
255
271
  protocol: MessageProtocol;
256
272
  pluginRegistry: PluginRegistry;
257
273
  pluginRouter: PluginRouter;
258
274
  }
259
275
  declare function createBareRuntimeContext(plugins?: PluginManifest[], routerOptions?: PluginRouterOptions): BareRuntimeContext;
260
- //#endregion
261
- export { MAX_HEADER_BYTES as $, CapabilityDescriptor as A, InvokeEnvelope as B, createProtocolMessenger as C, coerceErrorCode as D, PluginResult as E, ErrorCodeOrString as F, PluginInvokeRequestHeader as G, PluginContext as H, HostCapabilitiesQueryHeader as I, PluginRuntimeAdapter as J, PluginInvokeResponseHeader as K, HostCapabilitiesResponseHeader as L, CoreErrorWire as M, DispatchEnvelope as N, err as O, Either as P, MAX_FRAME_BYTES as Q, HostInvokeRequestHeader as R, ProtocolMessenger as S, Ok as T, PluginDispatchHeader as U, MessageHeader as V, PluginHeader as W, WireMessage as X, RuntimeTarget as Y, ErrorCode as Z, createPluginBus as _, InvokeHandler as a, DispatchRequest as b, invokeEvent as c, MessageProtocol as d, MessageType as et, ProtocolOptions as f, PluginRouterOptions as g, PluginRouter as h, EventSpec as i, CoreError as j, ok as k, Decoder as l, PluginRegistry as m, createBareRuntimeContext as n, VERSION as nt, definePlugin as o, PluginBus as p, PluginManifest as q, DispatchHandler as r, dispatchEvent as s, BareRuntimeContext as t, MessageTypeValue as tt, Encoder as u, createPluginRegistry as v, Err as w, InvokeRequest as x, createPluginRouter as y, HostInvokeResponseHeader as z };
276
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
277
+ type LogSource = 'backend' | 'web';
278
+ interface LogEntry {
279
+ /** Monotonic sequence assigned by the store on append. */
280
+ seq: number;
281
+ /** Epoch millisecond timestamp. */
282
+ ts: number;
283
+ level: LogLevel;
284
+ source: LogSource;
285
+ tag?: string;
286
+ message: string;
287
+ }
288
+ interface LogViewOptions {
289
+ /** Most-recent N matching entries. Omit for all. */
290
+ tail?: number;
291
+ level?: LogLevel;
292
+ source?: LogSource;
293
+ }
294
+ interface LogStore {
295
+ /** Maximum number of entries retained (oldest dropped first). */
296
+ readonly capacity: number;
297
+ /** Appends an entry, assigning its `seq`. */
298
+ append(entry: Omit<LogEntry, 'seq'>): void;
299
+ /** Returns a snapshot of matching entries, newest-last, bounded by `tail`. */
300
+ view(options?: LogViewOptions): LogEntry[];
301
+ /** Empties the buffer. Returns the number of entries dropped. */
302
+ clear(): number;
303
+ }
304
+ type WebConsoleCaptureOptions = {
305
+ /** Ingest endpoint. Same-origin `/logs` on the loopback server. */
306
+ endpoint?: string;
307
+ /** Max entries buffered before a flush is issued. */
308
+ flushThreshold?: number;
309
+ /** Flush interval (ms); pass 0 to disable the interval. */
310
+ flushIntervalMs?: number;
311
+ /** Injectable ingest function (defaults to a same-origin POST fetch). */
312
+ ingest?: (payload: {
313
+ source: 'web';
314
+ entries: Array<{
315
+ level: LogLevel;
316
+ tag?: string;
317
+ message: string;
318
+ }>;
319
+ }) => void | Promise<void>;
320
+ };
321
+ type WebConsoleCapture = {
322
+ /** Flushes any buffered entries immediately (fire-and-forget). */
323
+ flush(): void;
324
+ /** Restores the original console methods captured at install time. */
325
+ restore(): void;
326
+ };
327
+ /**
328
+ * Web-side console capture: wraps the console methods, buffers formatted
329
+ * entries, and flushes them as a batch to the loopback `POST /logs` ingest
330
+ * route with `source: 'web'` (same-origin fetch). Every step is try/catch-
331
+ * wrapped so the seam can never break the app. Skip installing in mock mode,
332
+ * where there is no loopback backend to ingest into.
333
+ */
334
+ declare function installWebConsoleCapture(options?: WebConsoleCaptureOptions): WebConsoleCapture;
335
+ export { PluginRuntimeAdapter as $, Ok as A, ErrorCodeOrString as B, createPluginRegistry as C, ProtocolMessenger as D, InvokeRequest as E, CapabilityDescriptor as F, InvokeEnvelope as G, HostCapabilitiesResponseHeader as H, CoreError as I, PluginDispatchHeader as J, MessageHeader as K, CoreErrorWire as L, coerceErrorCode as M, err as N, createProtocolMessenger as O, ok as P, PluginManifest as Q, DispatchEnvelope as R, createPluginBus as S, DispatchRequest as T, HostInvokeRequestHeader as U, HostCapabilitiesQueryHeader as V, HostInvokeResponseHeader as W, PluginInvokeRequestHeader as X, PluginHeader as Y, PluginInvokeResponseHeader as Z, ProtocolOptions as _, LogStore as a, MessageType as at, PluginRouter as b, DispatchHandler as c, definePlugin as d, RuntimeTarget as et, dispatchEvent as f, MessageProtocol as g, Encoder as h, LogSource as i, MAX_HEADER_BYTES as it, PluginResult as j, Err as k, EventSpec as l, Decoder as m, LogEntry as n, ErrorCode as nt, BareRuntimeContext as o, MessageTypeValue as ot, invokeEvent as p, PluginContext as q, LogLevel as r, MAX_FRAME_BYTES as rt, createBareRuntimeContext as s, VERSION as st, installWebConsoleCapture as t, WireMessage as tt, InvokeHandler as u, PluginBus as v, createPluginRouter as w, PluginRouterOptions as x, PluginRegistry as y, Either as z };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { $ as MAX_HEADER_BYTES, A as CapabilityDescriptor, B as InvokeEnvelope, C as createProtocolMessenger, D as coerceErrorCode, E as PluginResult, F as ErrorCodeOrString, G as PluginInvokeRequestHeader, H as PluginContext, I as HostCapabilitiesQueryHeader, J as PluginRuntimeAdapter, K as PluginInvokeResponseHeader, L as HostCapabilitiesResponseHeader, M as CoreErrorWire, N as DispatchEnvelope, O as err, P as Either, Q as MAX_FRAME_BYTES, R as HostInvokeRequestHeader, S as ProtocolMessenger, T as Ok, U as PluginDispatchHeader, V as MessageHeader, W as PluginHeader, X as WireMessage, Y as RuntimeTarget, Z as ErrorCode, _ as createPluginBus, a as InvokeHandler, b as DispatchRequest, c as invokeEvent, d as MessageProtocol, et as MessageType, f as ProtocolOptions, g as PluginRouterOptions, h as PluginRouter, i as EventSpec, j as CoreError, k as ok, l as Decoder, m as PluginRegistry, n as createBareRuntimeContext, nt as VERSION, o as definePlugin, p as PluginBus, q as PluginManifest, r as DispatchHandler, s as dispatchEvent, t as BareRuntimeContext, tt as MessageTypeValue, u as Encoder, v as createPluginRegistry, w as Err, x as InvokeRequest, y as createPluginRouter, z as HostInvokeResponseHeader } from "./index-DjulYFJL.mjs";
2
- export { BareRuntimeContext, CapabilityDescriptor, CoreError, CoreErrorWire, Decoder, DispatchEnvelope, DispatchHandler, DispatchRequest, Either, Encoder, Err, ErrorCode, ErrorCodeOrString, EventSpec, HostCapabilitiesQueryHeader, HostCapabilitiesResponseHeader, HostInvokeRequestHeader, HostInvokeResponseHeader, InvokeEnvelope, InvokeHandler, InvokeRequest, MAX_FRAME_BYTES, MAX_HEADER_BYTES, MessageHeader, MessageProtocol, MessageType, MessageTypeValue, Ok, PluginBus, PluginContext, PluginDispatchHeader, PluginHeader, PluginInvokeRequestHeader, PluginInvokeResponseHeader, PluginManifest, PluginRegistry, PluginResult, PluginRouter, PluginRouterOptions, PluginRuntimeAdapter, ProtocolMessenger, ProtocolOptions, RuntimeTarget, VERSION, WireMessage, coerceErrorCode, createBareRuntimeContext, createPluginBus, createPluginRegistry, createPluginRouter, createProtocolMessenger, definePlugin, dispatchEvent, err, invokeEvent, ok };
1
+ import { $ as PluginRuntimeAdapter, A as Ok, B as ErrorCodeOrString, C as createPluginRegistry, D as ProtocolMessenger, E as InvokeRequest, F as CapabilityDescriptor, G as InvokeEnvelope, H as HostCapabilitiesResponseHeader, I as CoreError, J as PluginDispatchHeader, K as MessageHeader, L as CoreErrorWire, M as coerceErrorCode, N as err, O as createProtocolMessenger, P as ok, Q as PluginManifest, R as DispatchEnvelope, S as createPluginBus, T as DispatchRequest, U as HostInvokeRequestHeader, V as HostCapabilitiesQueryHeader, W as HostInvokeResponseHeader, X as PluginInvokeRequestHeader, Y as PluginHeader, Z as PluginInvokeResponseHeader, _ as ProtocolOptions, at as MessageType, b as PluginRouter, c as DispatchHandler, d as definePlugin, et as RuntimeTarget, f as dispatchEvent, g as MessageProtocol, h as Encoder, it as MAX_HEADER_BYTES, j as PluginResult, k as Err, l as EventSpec, m as Decoder, nt as ErrorCode, o as BareRuntimeContext, ot as MessageTypeValue, p as invokeEvent, q as PluginContext, rt as MAX_FRAME_BYTES, s as createBareRuntimeContext, st as VERSION, t as installWebConsoleCapture, tt as WireMessage, u as InvokeHandler, v as PluginBus, w as createPluginRouter, x as PluginRouterOptions, y as PluginRegistry, z as Either } from "./index-BkwUzwVt.mjs";
2
+ export { BareRuntimeContext, CapabilityDescriptor, CoreError, CoreErrorWire, Decoder, DispatchEnvelope, DispatchHandler, DispatchRequest, Either, Encoder, Err, ErrorCode, ErrorCodeOrString, EventSpec, HostCapabilitiesQueryHeader, HostCapabilitiesResponseHeader, HostInvokeRequestHeader, HostInvokeResponseHeader, InvokeEnvelope, InvokeHandler, InvokeRequest, MAX_FRAME_BYTES, MAX_HEADER_BYTES, MessageHeader, MessageProtocol, MessageType, MessageTypeValue, Ok, PluginBus, PluginContext, PluginDispatchHeader, PluginHeader, PluginInvokeRequestHeader, PluginInvokeResponseHeader, PluginManifest, PluginRegistry, PluginResult, PluginRouter, PluginRouterOptions, PluginRuntimeAdapter, ProtocolMessenger, ProtocolOptions, RuntimeTarget, VERSION, WireMessage, coerceErrorCode, createBareRuntimeContext, createPluginBus, createPluginRegistry, createPluginRouter, createProtocolMessenger, definePlugin, dispatchEvent, err, installWebConsoleCapture, invokeEvent, ok };