@ekrooh/bare 0.5.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 +15 -5
- package/dist/index.mjs +1 -1
- package/dist/{messages-BbpF3wNH.mjs → messages-CevD8qkV.mjs} +7 -5
- package/dist/{plugins-DZu_NWVp.mjs → plugins-Ddp2AgOe.mjs} +6 -3
- package/dist/plugins-discovery-events.mjs +1 -1
- package/dist/plugins-health-events.mjs +1 -1
- package/dist/plugins-logs-events.mjs +1 -1
- package/dist/plugins-media-events.mjs +1 -1
- package/dist/plugins-permissions-events.mjs +1 -1
- package/dist/plugins.d.mts +1 -1
- package/dist/plugins.mjs +1 -1
- package/dist/runtime.d.mts +42 -33
- package/dist/runtime.mjs +871 -596
- package/dist/{static-file-server-C8pIX1JD.d.mts → static-file-server-COxd-PFR.d.mts} +41 -36
- package/dist/transports.d.mts +3 -5
- package/dist/transports.mjs +2 -3
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
**One soul, many platforms.** One codebase and identity across Android, iOS,
|
|
4
4
|
web, and desktop.
|
|
5
5
|
|
|
6
|
+
**Docs: <https://anderscan.github.io/ekrooh>**
|
|
7
|
+
|
|
6
8
|
[](https://github.com/AndersCan/ekrooh/actions/workflows/test.yml)
|
|
9
|
+
[](https://github.com/AndersCan/ekrooh/releases/latest)
|
|
7
10
|
|
|
8
11
|
The boring bootstrap for cross-platform apps on the Bare runtime (holepunch): a
|
|
9
12
|
binary wire protocol, plugin kernel, RPC messenger, transports, and native host
|
|
10
|
-
bridges, plus a reference app.
|
|
13
|
+
bridges, plus a reference app.
|
|
11
14
|
|
|
12
15
|
- [`vision.md`](vision.md): what this project is and is not.
|
|
13
16
|
- [`AGENTS.md`](AGENTS.md): how to develop it and the canonical commands.
|
|
@@ -99,16 +102,23 @@ ephemeral port. Every request is gated by a per-session token:
|
|
|
99
102
|
|
|
100
103
|
1. The shell injects `window.__ekrooh = { bootstrap }`, a single-use nonce (never the raw token).
|
|
101
104
|
2. The page calls `POST /login` with the nonce and receives a
|
|
102
|
-
`
|
|
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.
|
|
103
108
|
3. The cookie authorizes `<img>`, `<video>`, `fetch`, and the WS upgrade.
|
|
104
109
|
Non-browser clients send `X-Bare-Token`. There is no `?token=` fallback
|
|
105
110
|
(see `docs/adr/0003-loopback-auth-surface.md`).
|
|
106
111
|
|
|
107
112
|
Only the bootstrap mount at `/` is public pre-auth. The worklet writes
|
|
108
|
-
`{ origin, port,
|
|
109
|
-
the
|
|
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.
|
|
110
119
|
|
|
111
|
-
The dev backend runs the same bundle with auth off on
|
|
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).
|
|
112
122
|
|
|
113
123
|
## Testing
|
|
114
124
|
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { h as CoreError, g as ErrorCode, _ as MAX_FRAME_BYTES, v as MAX_HEADER_BYTES, o as MessageProtocol, y as MessageType, b as VERSION, f as coerceErrorCode, n as createBareRuntimeContext, l as createPluginBus, u as createPluginRegistry, d as createPluginRouter, s as createProtocolMessenger, r as definePlugin, i as dispatchEvent, p as err, t as installWebConsoleCapture, a as invokeEvent, m as ok } from "./messages-
|
|
1
|
+
export { h as CoreError, g as ErrorCode, _ as MAX_FRAME_BYTES, v as MAX_HEADER_BYTES, o as MessageProtocol, y as MessageType, b as VERSION, f as coerceErrorCode, n as createBareRuntimeContext, l as createPluginBus, u as createPluginRegistry, d as createPluginRouter, s as createProtocolMessenger, r as definePlugin, i as dispatchEvent, p as err, t as installWebConsoleCapture, a as invokeEvent, m as ok } from "./messages-CevD8qkV.mjs";
|
|
@@ -296,9 +296,9 @@ function isPluginInvokeResponseHeader(header) {
|
|
|
296
296
|
/**
|
|
297
297
|
* CSPRNG-backed correlation id for RPC requests.
|
|
298
298
|
*
|
|
299
|
-
* Shared by the
|
|
300
|
-
*
|
|
301
|
-
*
|
|
299
|
+
* Shared by the RPC messenger (`rpc-messenger.ts`) and the host IPC bridge
|
|
300
|
+
* (`host-ipc.ts`) so the unguessable, collision-resistant guarantee can never
|
|
301
|
+
* regress in a single copy. Request
|
|
302
302
|
* IDs must never be derived from `Math.random()` — a guessable id lets a peer
|
|
303
303
|
* spoof or collide with an in-flight request and corrupt correlation. See
|
|
304
304
|
* ekrooh#141.
|
|
@@ -747,9 +747,11 @@ function dispatchEvent(spec, args, payload) {
|
|
|
747
747
|
// core/messages/create-bare-runtime-context.ts
|
|
748
748
|
function createBareRuntimeContext(plugins = [], routerOptions) {
|
|
749
749
|
const runtime = "bare";
|
|
750
|
+
const textEncoder = new TextEncoder$1();
|
|
751
|
+
const textDecoder = new TextDecoder$1();
|
|
750
752
|
const protocol = new MessageProtocol({
|
|
751
|
-
encode: (str) =>
|
|
752
|
-
decode: (bytes) =>
|
|
753
|
+
encode: (str) => textEncoder.encode(str),
|
|
754
|
+
decode: (bytes) => textDecoder.decode(bytes)
|
|
753
755
|
});
|
|
754
756
|
const pluginRegistry = createPluginRegistry();
|
|
755
757
|
for (const plugin of plugins) pluginRegistry.register(plugin);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as ErrorCode, m as ok, p as err, r as definePlugin } from "./messages-
|
|
1
|
+
import { g as ErrorCode, m as ok, p as err, r as definePlugin } from "./messages-CevD8qkV.mjs";
|
|
2
2
|
import { discoverySpecs } from "./plugins-discovery-events.mjs";
|
|
3
3
|
import { healthSpecs } from "./plugins-health-events.mjs";
|
|
4
4
|
import { logsSpecs } from "./plugins-logs-events.mjs";
|
|
@@ -84,6 +84,7 @@ function createLogsPlugin(deps) {
|
|
|
84
84
|
* mounted at once and evict the least-recently-used so picked/captured files
|
|
85
85
|
* do not accumulate on the loopback server for the whole session. */
|
|
86
86
|
const MEDIA_MOUNT_CAP = 4;
|
|
87
|
+
const MEDIA_KINDS = ["image", "video"];
|
|
87
88
|
function newRequestId() {
|
|
88
89
|
const rand = crypto.randomBytes(16).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
89
90
|
return `${Date.now().toString(36)}-${rand}`;
|
|
@@ -108,18 +109,20 @@ function createMediaPlugin(deps) {
|
|
|
108
109
|
}
|
|
109
110
|
};
|
|
110
111
|
const serve = async (event, args) => {
|
|
112
|
+
const kind = args?.kind;
|
|
113
|
+
if (kind !== void 0 && !MEDIA_KINDS.includes(kind)) return err(ErrorCode.PLUGIN_ERROR, `Unknown media kind for ${event}`);
|
|
111
114
|
const host = await deps.invokeOnHost({
|
|
112
115
|
type: "INVOKE_REQUEST",
|
|
113
116
|
pluginId: "vendor.media",
|
|
114
117
|
event,
|
|
115
118
|
requestId: newRequestId(),
|
|
116
|
-
args
|
|
119
|
+
args: kind === void 0 ? {} : { kind }
|
|
117
120
|
}, /* @__PURE__ */ new Uint8Array(0));
|
|
118
121
|
if (!host) return err(ErrorCode.HOST_ERROR, `Host did not answer ${event}`);
|
|
119
122
|
if (host.error) return err(host.error.code || ErrorCode.HOST_ERROR, host.error.message ?? `Host failed ${event}`);
|
|
120
123
|
const path = host.result?.path;
|
|
121
124
|
if (!path) return err(ErrorCode.HOST_ERROR, `Host returned no path for ${event}`);
|
|
122
|
-
const id = `${
|
|
125
|
+
const id = `${kind ?? "media"}-${newRequestId()}`;
|
|
123
126
|
mountMedia(id, path);
|
|
124
127
|
const url = await deps.staticServer.url(`/media/${id}`);
|
|
125
128
|
return ok({ url });
|
package/dist/plugins.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { F as CapabilityDescriptor, Q as PluginManifest, X as PluginInvokeRequestHeader, Z as PluginInvokeResponseHeader, a as LogStore, et as RuntimeTarget } from "./index-BkwUzwVt.mjs";
|
|
2
|
-
import { n as LoopbackServer } from "./static-file-server-
|
|
2
|
+
import { n as LoopbackServer } from "./static-file-server-COxd-PFR.mjs";
|
|
3
3
|
type DiscoveryPluginDeps = {
|
|
4
4
|
listBareCapabilities: () => Array<{
|
|
5
5
|
pluginId: string;
|
package/dist/plugins.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as createDefaultPlugins } from "./plugins-
|
|
1
|
+
export { t as createDefaultPlugins } from "./plugins-Ddp2AgOe.mjs";
|
package/dist/runtime.d.mts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { C as createPluginRegistry, F as CapabilityDescriptor, K as MessageHeader, Q as PluginManifest, X as PluginInvokeRequestHeader, Z as PluginInvokeResponseHeader, g as MessageProtocol, o as BareRuntimeContext, tt as WireMessage, w as createPluginRouter } from "./index-BkwUzwVt.mjs";
|
|
2
|
-
import { i as createLoopbackServer, n as LoopbackServer, r as collectRequestBody, t as LoopbackRouteHandler } from "./static-file-server-
|
|
2
|
+
import { i as createLoopbackServer, n as LoopbackServer, r as collectRequestBody, t as LoopbackRouteHandler } from "./static-file-server-COxd-PFR.mjs";
|
|
3
3
|
type BareIpcLike = {
|
|
4
|
-
write(data: Uint8Array |
|
|
4
|
+
write(data: Uint8Array | string): boolean;
|
|
5
5
|
};
|
|
6
6
|
declare function createHostIpcBridge(config: {
|
|
7
7
|
ipc: BareIpcLike;
|
|
8
8
|
protocol: MessageProtocol;
|
|
9
9
|
}): {
|
|
10
10
|
tryConsumeDownstream: (parsed: WireMessage) => boolean;
|
|
11
|
-
tryConsumeDownstreamFromHost(raw: Uint8Array | ArrayBuffer
|
|
11
|
+
tryConsumeDownstreamFromHost(raw: Uint8Array | ArrayBuffer): boolean;
|
|
12
12
|
queryCapabilities(timeoutMs?: number): Promise<CapabilityDescriptor[]>;
|
|
13
13
|
invokeOnHost(header: PluginInvokeRequestHeader, payload: Uint8Array, timeoutMs?: number): Promise<PluginInvokeResponseHeader | null>;
|
|
14
14
|
};
|
|
15
|
+
/** Envelope encoder bound to a loopback server's connected protocol socket —
|
|
16
|
+
* the server-initiated (backend → web) push seam. */
|
|
15
17
|
type LoopbackPush = (header: MessageHeader, payload?: Uint8Array | ArrayBuffer | null) => boolean;
|
|
16
18
|
/** Builds the push seam for the server's connected protocol socket: encodes a
|
|
17
19
|
* `DISPATCH`-style envelope and writes it, mirroring how `attachWebSocketProtocol`
|
|
@@ -20,8 +22,9 @@ export declare function createLoopbackPush(server: LoopbackServer, protocol: Mes
|
|
|
20
22
|
/**
|
|
21
23
|
* Attaches the framed-protocol socket to the unified loopback server. The
|
|
22
24
|
* server owns the WebSocket upgrade (origin check, cookie/token auth, single
|
|
23
|
-
* client, idle timeout); this layer
|
|
24
|
-
*
|
|
25
|
+
* client, idle timeout); this layer runs one frame channel per socket. A peer
|
|
26
|
+
* that sends a malformed frame is not recoverable on that socket, so it is
|
|
27
|
+
* torn down and the client reconnects.
|
|
25
28
|
*/
|
|
26
29
|
export declare function attachWebSocketProtocol(server: LoopbackServer, context: BareRuntimeContext): void;
|
|
27
30
|
/**
|
|
@@ -29,36 +32,36 @@ export declare function attachWebSocketProtocol(server: LoopbackServer, context:
|
|
|
29
32
|
*/
|
|
30
33
|
interface BareIPC extends NodeJS.EventEmitter {
|
|
31
34
|
read(n?: number): Buffer | null;
|
|
32
|
-
write(data:
|
|
35
|
+
write(data: Uint8Array | string): boolean;
|
|
33
36
|
resume?(): void;
|
|
34
37
|
pause?(): void;
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
|
-
* Returns the active IPC channel.
|
|
38
|
-
* In BareKit (Android), it uses BareKit.IPC.
|
|
40
|
+
* Returns the active IPC channel, or null when there is no host.
|
|
41
|
+
* In BareKit (Android, iOS), it uses BareKit.IPC.
|
|
39
42
|
* In Bare (Sidecar), it uses Bare.IPC.
|
|
40
43
|
*/
|
|
41
|
-
export declare function getIPC(): BareIPC;
|
|
44
|
+
export declare function getIPC(): BareIPC | null;
|
|
42
45
|
/** Resolves the host-provided worklet configuration from `start(...)`
|
|
43
46
|
* arguments. On-device hosts pass `["<webAssets>", "<storage>", "<cache>"]`
|
|
44
|
-
* (three existing directories, in that order), landing at `Bare.argv[0..2]
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* device and dev runs. */
|
|
47
|
+
* (three existing directories, in that order), landing at `Bare.argv[0..2]`.
|
|
48
|
+
* Falls back to the labeled CLI token syntax (see `resolveCliConfig`) when the
|
|
49
|
+
* positional shape doesn't match, so the same entry works for device and dev
|
|
50
|
+
* runs. Nothing here turns auth off: the dev backend is selected only by an
|
|
51
|
+
* explicit `mode=dev` token. */
|
|
50
52
|
export declare function resolveWorkletConfig(): WorkletRuntimeOptions;
|
|
51
53
|
/**
|
|
52
|
-
* Parses
|
|
53
|
-
* `webassets=<dir>`, `storage=<dir>`, `cache=<dir>`,
|
|
54
|
-
* `port=<n>`, `auth=<on|off|true|false|1|0
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* Parses labeled-token configuration from bare CLI arguments, one `key=value`
|
|
55
|
+
* per token: `webassets=<dir>`, `storage=<dir>`, `cache=<dir>`,
|
|
56
|
+
* `host=<addr>`, `port=<n>`, `auth=<on|off|true|false|1|0>`,
|
|
57
|
+
* `mode=<device|dev>` and `origin=<scheme://host[:port]>` (repeatable; an
|
|
58
|
+
* extra allowed page origin, e.g. the Vite dev server). Unknown labels (e.g.
|
|
59
|
+
* `bootstrap=127.0.0.1:49737`) are ignored and stay consumer-owned.
|
|
57
60
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* no labeled tokens are present
|
|
61
|
+
* Without `mode=dev` the result keeps the secure device defaults (auth on, an
|
|
62
|
+
* ephemeral port), and `auth=off` is ignored with a warning. `mode=dev` selects the dev backend: `deviceMode: false`,
|
|
63
|
+
* `auth: false`, `port: 8080`, each overridable by a token. Returns `{}` when
|
|
64
|
+
* no labeled tokens are present.
|
|
62
65
|
*/
|
|
63
66
|
export declare function resolveCliConfig(argv?: readonly string[]): WorkletRuntimeOptions;
|
|
64
67
|
export type WorkletRuntimeOptions = {
|
|
@@ -73,16 +76,21 @@ export type WorkletRuntimeOptions = {
|
|
|
73
76
|
/** Port to bind; `0` picks an ephemeral port (on-device). Defaults to an
|
|
74
77
|
* ephemeral port in device mode, 8080 in dev mode. */
|
|
75
78
|
port?: number;
|
|
76
|
-
/** Require the per-session token/cookie. Defaults to on in device mode
|
|
79
|
+
/** Require the per-session token/cookie. Defaults to on in device mode and
|
|
80
|
+
* off in dev mode. The origin check applies either way. */
|
|
77
81
|
auth?: boolean;
|
|
78
|
-
/**
|
|
79
|
-
* (
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
+
/** Extra page origins allowed to call the loopback server, e.g. the Vite dev
|
|
83
|
+
* server (`http://localhost:5173`). The bound origin and its loopback
|
|
84
|
+
* aliases are always allowed. */
|
|
85
|
+
allowedOrigins?: readonly string[];
|
|
86
|
+
/** Write `{ origin, port, bootstrap }` to `<storage>/handoff.json` for the
|
|
87
|
+
* host to read (auth on only; the session token is never written). Defaults
|
|
88
|
+
* to true; a host that receives them over its own channel (the desktop host
|
|
89
|
+
* reads them from a pipe) sets false so nothing touches disk. */
|
|
82
90
|
handoffFile?: boolean;
|
|
83
|
-
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
91
|
+
/** Device mode (the default): auth on, an ephemeral port, and the handoff
|
|
92
|
+
* file when `storage` is set. Pass `false` (or the `mode=dev` CLI token) for
|
|
93
|
+
* the dev backend: auth off and port 8080 unless set explicitly. */
|
|
86
94
|
deviceMode?: boolean;
|
|
87
95
|
/** Additional plugin manifests to register after the canonical defaults
|
|
88
96
|
* (consumer product plugins, e.g. `app.photos`). */
|
|
@@ -93,7 +101,8 @@ export type WorkletRuntimeOptions = {
|
|
|
93
101
|
* is destroyed, freeing the slot. Defaults to 10s. */
|
|
94
102
|
wsDeadPeerTimeoutMs?: number;
|
|
95
103
|
/** Minimum gap (ms) before an accepted protocol socket may be replaced by a
|
|
96
|
-
* new authenticated one
|
|
104
|
+
* new authenticated one; an upgrade that arrives sooner is held until then.
|
|
105
|
+
* Defaults to 1s. */
|
|
97
106
|
wsReplaceMinIntervalMs?: number;
|
|
98
107
|
/** Bytes queued behind a protocol socket whose reader stopped reading before
|
|
99
108
|
* server pushes are dropped (newest first). Defaults to 2 MiB. */
|