@ekrooh/bare 0.1.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/LICENSE +201 -0
- package/README.md +228 -0
- package/dist/index-DjulYFJL.d.mts +261 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +512 -0
- package/dist/plugins-1KweEBct.mjs +125 -0
- package/dist/plugins-discovery-events.d.mts +21 -0
- package/dist/plugins-discovery-events.mjs +13 -0
- package/dist/plugins-health-events.d.mts +57 -0
- package/dist/plugins-health-events.mjs +35 -0
- package/dist/plugins-media-events.d.mts +39 -0
- package/dist/plugins-media-events.mjs +27 -0
- package/dist/plugins-permissions-events.d.mts +39 -0
- package/dist/plugins-permissions-events.mjs +26 -0
- package/dist/plugins.d.mts +26 -0
- package/dist/plugins.mjs +2 -0
- package/dist/runtime.d.mts +96 -0
- package/dist/runtime.mjs +637 -0
- package/dist/static-file-server-CxeI_mQr.d.mts +74 -0
- package/dist/transports.d.mts +38 -0
- package/dist/transports.mjs +384 -0
- package/package.json +112 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import "bare-http1";
|
|
2
|
+
//#region core/server/static-file-server.d.ts
|
|
3
|
+
type WebSocketLike = {
|
|
4
|
+
on(event: 'data', listener: (chunk: unknown) => void): unknown;
|
|
5
|
+
on(event: 'close', listener: () => void): unknown;
|
|
6
|
+
on(event: 'error', listener: (err: Error) => void): unknown;
|
|
7
|
+
write(data: unknown): boolean;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
};
|
|
10
|
+
/** Minimal shape of the HTTP request object handed to WS upgrade handlers. */
|
|
11
|
+
type LoopbackRequest = {
|
|
12
|
+
url?: string;
|
|
13
|
+
method?: string;
|
|
14
|
+
headers: Record<string, string | number>;
|
|
15
|
+
};
|
|
16
|
+
type LoopbackConnectionHandler = (socket: WebSocketLike, request: LoopbackRequest) => void;
|
|
17
|
+
interface LoopbackServerOptions {
|
|
18
|
+
/** Bind address. Defaults to `127.0.0.1`. */
|
|
19
|
+
host?: string;
|
|
20
|
+
/** Port to bind; `0` picks an ephemeral port (on-device). */
|
|
21
|
+
port?: number;
|
|
22
|
+
/** Require the per-session token/cookie on every request and WS upgrade.
|
|
23
|
+
* The dev backend turns this off (the browser cannot know the token). */
|
|
24
|
+
auth?: boolean;
|
|
25
|
+
/** Fixed session token (dev/tests); otherwise a random 32-byte token is
|
|
26
|
+
* generated on first bind. */
|
|
27
|
+
token?: string;
|
|
28
|
+
/** Idle timeout (ms) after which an authenticated WebSocket connection is
|
|
29
|
+
* dropped. Defaults to 60s. */
|
|
30
|
+
wsIdleTimeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
interface LoopbackServer {
|
|
33
|
+
/** Resolves to the server origin (`http://127.0.0.1:<port>`), binding on
|
|
34
|
+
* first use. */
|
|
35
|
+
origin(): Promise<string>;
|
|
36
|
+
/** Resolves to a URL for `path` (`<origin><path>`). The per-session cookie
|
|
37
|
+
* authorizes media loads; no token is embedded in the URL anymore. */
|
|
38
|
+
url(path: string): Promise<string>;
|
|
39
|
+
/** The per-session token (generated on first bind). */
|
|
40
|
+
token(): string;
|
|
41
|
+
/** Resolves to the bound origin, port and token — written to the handoff
|
|
42
|
+
* file for the host to read before loading the page. */
|
|
43
|
+
credentials(): Promise<{
|
|
44
|
+
origin: string;
|
|
45
|
+
port: number;
|
|
46
|
+
token: string;
|
|
47
|
+
}>;
|
|
48
|
+
/** Serves `filePath` at `http://<origin><path>` (exact match). */
|
|
49
|
+
mount(path: string, filePath: string): void;
|
|
50
|
+
unmount(path: string): void;
|
|
51
|
+
/** Serves every file under `dirPath` below `prefix` (longest-prefix match),
|
|
52
|
+
* with an SPA fallback to `dirPath/index.html` for unknown GET paths. */
|
|
53
|
+
mountDir(prefix: string, dirPath: string): void;
|
|
54
|
+
/** Registers a handler for authenticated, handshaken WebSocket connections
|
|
55
|
+
* (the protocol socket). */
|
|
56
|
+
onConnection(handler: LoopbackConnectionHandler): void;
|
|
57
|
+
/** Stops accepting connections and closes the server. */
|
|
58
|
+
close(cb?: (err?: Error | null) => void): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The single loopback HTTP+WS server for the worklet. One origin serves the
|
|
62
|
+
* web app (`/`), the media files mounted by plugins, and the framed-protocol
|
|
63
|
+
* WebSocket socket the page connects to. On device it binds `127.0.0.1` on an
|
|
64
|
+
* ephemeral port. The bundled web app (directory mounts) is public content — a
|
|
65
|
+
* fresh WebView must load the page before the page can `POST /login`. Mounted
|
|
66
|
+
* session files (media) and the WS upgrade are gated by a per-session token,
|
|
67
|
+
* accepted as a `bare_session` cookie (set via `POST /login`), the
|
|
68
|
+
* `X-Bare-Token` header, or a `?token=` query param — defense in depth for
|
|
69
|
+
* non-browser clients. All responses send `Referrer-Policy: no-referrer` so
|
|
70
|
+
* token-bearing URLs never leak through a Referer header.
|
|
71
|
+
*/
|
|
72
|
+
declare function createLoopbackServer(options?: LoopbackServerOptions): LoopbackServer;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { createLoopbackServer as n, LoopbackServer as t };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { V as MessageHeader, X as WireMessage, tt as MessageTypeValue } from "./index-DjulYFJL.mjs";
|
|
2
|
+
//#region web/websocket-client.d.ts
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
/** Per-session bridge state injected by the embedding shell on-device.
|
|
6
|
+
* `token` is exchanged for a session cookie via `POST /login`, which then
|
|
7
|
+
* rides every same-origin request — including the WebSocket upgrade.
|
|
8
|
+
* Browser dev has none (the dev backend runs without auth). */
|
|
9
|
+
__ekrooh?: {
|
|
10
|
+
token?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
};
|
|
13
|
+
/** Set by the embedding shell on-device. */
|
|
14
|
+
BareShell?: boolean;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
interface CreateWebSocketTransportOptions {
|
|
18
|
+
/** WebSocket URL. Defaults to the page's same-origin (`ws://location.host`).
|
|
19
|
+
* Browser dev passes `VITE_BARE_WS_URL` explicitly. */
|
|
20
|
+
url?: string;
|
|
21
|
+
/** Whether to bootstrap the session cookie via `POST /login` when a token
|
|
22
|
+
* is present. Defaults to `true`. */
|
|
23
|
+
login?: boolean;
|
|
24
|
+
/** Reconnect attempts after an unexpected close (boot-window). Default 5. */
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
/** Initial reconnect backoff in ms, doubling to a 2s cap. Default 250. */
|
|
27
|
+
backoffMs?: number;
|
|
28
|
+
}
|
|
29
|
+
interface MessageTransport {
|
|
30
|
+
send(type: MessageTypeValue, header: MessageHeader, payload?: Uint8Array | ArrayBuffer | string | null): void;
|
|
31
|
+
subscribe(handler: (message: WireMessage) => void): () => void;
|
|
32
|
+
}
|
|
33
|
+
declare function createWebSocketTransport(urlOrOptions?: string | CreateWebSocketTransportOptions): MessageTransport;
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region web/transports/mock.d.ts
|
|
36
|
+
declare function createMockTransport(): MessageTransport;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { type MessageTransport, createMockTransport, createWebSocketTransport };
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { ErrorCode, MessageProtocol, MessageType } from "./index.mjs";
|
|
2
|
+
import { Actor, RealClock, event, state } from "@mantaq/core";
|
|
3
|
+
//#region web/connection-machine.ts
|
|
4
|
+
const idleState = state("idle")();
|
|
5
|
+
const openingState = state("opening")();
|
|
6
|
+
const connectedState = state("connected")();
|
|
7
|
+
const backoffState = state("backoff")();
|
|
8
|
+
const gaveUpState = state("gaveUp")().final();
|
|
9
|
+
const loginOk = event("LOGIN_OK")();
|
|
10
|
+
const loginFail = event("LOGIN_FAIL")();
|
|
11
|
+
const socketOpen = event("SOCKET_OPEN")();
|
|
12
|
+
const socketClose = event("SOCKET_CLOSE")();
|
|
13
|
+
const retryTimer = event("RETRY_TIMER")();
|
|
14
|
+
/** Append the session token to the URL as a `?token=...` query param, keeping
|
|
15
|
+
* any query the URL already carries. */
|
|
16
|
+
function withToken(url, token) {
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
parsed.searchParams.set("token", token);
|
|
19
|
+
return parsed.toString();
|
|
20
|
+
}
|
|
21
|
+
function createConnectionMachine(options) {
|
|
22
|
+
const context = {
|
|
23
|
+
url: options.url,
|
|
24
|
+
baseUrl: options.url,
|
|
25
|
+
token: options.token,
|
|
26
|
+
maxRetries: options.maxRetries,
|
|
27
|
+
initialBackoffMs: options.initialBackoffMs,
|
|
28
|
+
maxBackoffMs: options.maxBackoffMs,
|
|
29
|
+
retries: 0,
|
|
30
|
+
tokenTried: false
|
|
31
|
+
};
|
|
32
|
+
const actor = new Actor({
|
|
33
|
+
inputs: [
|
|
34
|
+
loginOk,
|
|
35
|
+
loginFail,
|
|
36
|
+
socketOpen,
|
|
37
|
+
socketClose
|
|
38
|
+
],
|
|
39
|
+
internal: [retryTimer],
|
|
40
|
+
outputs: [],
|
|
41
|
+
states: [
|
|
42
|
+
idleState,
|
|
43
|
+
openingState,
|
|
44
|
+
connectedState,
|
|
45
|
+
backoffState,
|
|
46
|
+
gaveUpState
|
|
47
|
+
],
|
|
48
|
+
initial: idleState,
|
|
49
|
+
clock: options.clock ?? new RealClock(),
|
|
50
|
+
context,
|
|
51
|
+
setup: (m) => {
|
|
52
|
+
m.effect(backoffState, ({ signal, context, clock, emit }) => {
|
|
53
|
+
const s = context.get();
|
|
54
|
+
const delay = Math.min(s.initialBackoffMs * 2 ** (s.retries - 1), s.maxBackoffMs);
|
|
55
|
+
clock.setTimeout(delay, () => {
|
|
56
|
+
if (signal.aborted) return;
|
|
57
|
+
emit({ type: "RETRY_TIMER" });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
m.on(idleState, loginOk, () => ({ state: openingState }));
|
|
61
|
+
m.on(idleState, loginFail, (_event, opts) => {
|
|
62
|
+
const s = opts.context.get();
|
|
63
|
+
const tokenUrl = s.token && s.token.length > 0 ? withToken(s.baseUrl, s.token) : s.url;
|
|
64
|
+
opts.context.set({
|
|
65
|
+
...s,
|
|
66
|
+
url: tokenUrl,
|
|
67
|
+
tokenTried: true
|
|
68
|
+
});
|
|
69
|
+
return { state: openingState };
|
|
70
|
+
});
|
|
71
|
+
m.on(openingState, socketOpen, (_event, opts) => {
|
|
72
|
+
const s = opts.context.get();
|
|
73
|
+
opts.context.set({
|
|
74
|
+
...s,
|
|
75
|
+
retries: 0
|
|
76
|
+
});
|
|
77
|
+
return { state: connectedState };
|
|
78
|
+
});
|
|
79
|
+
m.on(openingState, socketClose, (_event, opts) => {
|
|
80
|
+
const s = opts.context.get();
|
|
81
|
+
if (!s.tokenTried && typeof s.token === "string" && s.token.length > 0) {
|
|
82
|
+
opts.context.set({
|
|
83
|
+
...s,
|
|
84
|
+
url: withToken(s.baseUrl, s.token),
|
|
85
|
+
tokenTried: true
|
|
86
|
+
});
|
|
87
|
+
return { state: openingState };
|
|
88
|
+
}
|
|
89
|
+
if (s.retries >= s.maxRetries) return { state: gaveUpState };
|
|
90
|
+
opts.context.set({
|
|
91
|
+
...s,
|
|
92
|
+
retries: s.retries + 1
|
|
93
|
+
});
|
|
94
|
+
return { state: backoffState };
|
|
95
|
+
});
|
|
96
|
+
m.on(connectedState, socketClose, (_event, opts) => {
|
|
97
|
+
const s = opts.context.get();
|
|
98
|
+
if (s.retries >= s.maxRetries) return { state: gaveUpState };
|
|
99
|
+
opts.context.set({
|
|
100
|
+
...s,
|
|
101
|
+
retries: s.retries + 1
|
|
102
|
+
});
|
|
103
|
+
return { state: backoffState };
|
|
104
|
+
});
|
|
105
|
+
m.on(backoffState, retryTimer, () => ({ state: openingState }));
|
|
106
|
+
m.on(backoffState, socketClose, () => ({}));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const name = (snapshot) => snapshot.path[0];
|
|
110
|
+
return {
|
|
111
|
+
url: () => actor.context.url,
|
|
112
|
+
state: () => name(actor.snapshot()),
|
|
113
|
+
isConnected: () => name(actor.snapshot()) === "connected",
|
|
114
|
+
isGaveUp: () => name(actor.snapshot()) === "gaveUp",
|
|
115
|
+
sendLoginOk: () => actor.send(loginOk.create()),
|
|
116
|
+
sendLoginFail: () => actor.send(loginFail.create()),
|
|
117
|
+
sendOpen: () => actor.send(socketOpen.create()),
|
|
118
|
+
sendClose: () => actor.send(socketClose.create()),
|
|
119
|
+
onChange(fn) {
|
|
120
|
+
return actor.on("change", (snapshot) => fn(name(snapshot)));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region web/websocket-client.ts
|
|
126
|
+
const protocol = new MessageProtocol();
|
|
127
|
+
const WS_DISCONNECTED_MESSAGE = "WebSocket disconnected before request could be sent";
|
|
128
|
+
const DEFAULT_RETRIES = 5;
|
|
129
|
+
const DEFAULT_BACKOFF_MS = 250;
|
|
130
|
+
const MAX_BACKOFF_MS = 2e3;
|
|
131
|
+
const LOGIN_TIMEOUT_MS = 2e3;
|
|
132
|
+
/** Same-origin default: on-device the page is served by the loopback server,
|
|
133
|
+
* so `ws://<location.host>` is the protocol socket with no mixed content and
|
|
134
|
+
* no custom scheme. */
|
|
135
|
+
function defaultWsUrl() {
|
|
136
|
+
if (typeof window !== "undefined" && window.location?.host) return `ws://${window.location.host}`;
|
|
137
|
+
return "ws://localhost:8080";
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* `websocket.send` needs a `BufferSource` backed by a plain `ArrayBuffer`;
|
|
141
|
+
* the codec may produce a view into a pooled Buffer, so slice a standalone
|
|
142
|
+
* copy of just the frame.
|
|
143
|
+
*/
|
|
144
|
+
function toArrayBuffer(bytes) {
|
|
145
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
146
|
+
}
|
|
147
|
+
function createWebSocketTransport(urlOrOptions = {}) {
|
|
148
|
+
const options = typeof urlOrOptions === "string" ? { url: urlOrOptions } : urlOrOptions;
|
|
149
|
+
const url = options.url ?? defaultWsUrl();
|
|
150
|
+
const maxRetries = options.maxRetries ?? DEFAULT_RETRIES;
|
|
151
|
+
const initialBackoff = options.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
152
|
+
const token = typeof window !== "undefined" ? window.__ekrooh?.token : void 0;
|
|
153
|
+
const shouldLogin = options.login !== false && typeof token === "string" && token.length > 0;
|
|
154
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
155
|
+
const queued = [];
|
|
156
|
+
let socket = null;
|
|
157
|
+
const machine = createConnectionMachine({
|
|
158
|
+
url,
|
|
159
|
+
token: typeof token === "string" ? token : void 0,
|
|
160
|
+
maxRetries,
|
|
161
|
+
initialBackoffMs: initialBackoff,
|
|
162
|
+
maxBackoffMs: MAX_BACKOFF_MS
|
|
163
|
+
});
|
|
164
|
+
const emitTransportError = (header, message) => {
|
|
165
|
+
if (header.type !== "INVOKE_REQUEST") return;
|
|
166
|
+
for (const listener of listeners) listener({
|
|
167
|
+
type: MessageType.ENVELOPE,
|
|
168
|
+
header: {
|
|
169
|
+
type: "INVOKE_RESPONSE",
|
|
170
|
+
pluginId: header.pluginId,
|
|
171
|
+
event: header.event,
|
|
172
|
+
requestId: header.requestId,
|
|
173
|
+
error: {
|
|
174
|
+
code: ErrorCode.TRANSPORT_ERROR,
|
|
175
|
+
message
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
payload: /* @__PURE__ */ new Uint8Array(0)
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
const failQueuedMessages = (message) => {
|
|
182
|
+
for (const queuedMessage of queued) emitTransportError(queuedMessage.header, message);
|
|
183
|
+
queued.length = 0;
|
|
184
|
+
};
|
|
185
|
+
const flushQueued = () => {
|
|
186
|
+
for (const queuedMessage of queued) socket?.send(toArrayBuffer(queuedMessage.bytes));
|
|
187
|
+
queued.length = 0;
|
|
188
|
+
};
|
|
189
|
+
/** Opens a WebSocket for the machine's current URL and wires it to the
|
|
190
|
+
* machine's lifecycle events. */
|
|
191
|
+
function openSocket() {
|
|
192
|
+
socket = new WebSocket(machine.url());
|
|
193
|
+
socket.binaryType = "arraybuffer";
|
|
194
|
+
socket.onopen = () => {
|
|
195
|
+
machine.sendOpen();
|
|
196
|
+
flushQueued();
|
|
197
|
+
};
|
|
198
|
+
socket.onmessage = (event) => {
|
|
199
|
+
if ((event.data?.byteLength ?? 0) === 0) return;
|
|
200
|
+
try {
|
|
201
|
+
const message = protocol.decode(event.data);
|
|
202
|
+
for (const listener of listeners) listener(message);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error("Failed to parse WS message:", err);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
socket.onclose = () => {
|
|
208
|
+
machine.sendClose();
|
|
209
|
+
};
|
|
210
|
+
socket.onerror = () => {};
|
|
211
|
+
}
|
|
212
|
+
machine.onChange((state) => {
|
|
213
|
+
if (state === "opening") openSocket();
|
|
214
|
+
else if (state === "gaveUp") failQueuedMessages(WS_DISCONNECTED_MESSAGE);
|
|
215
|
+
});
|
|
216
|
+
async function bootstrap() {
|
|
217
|
+
if (shouldLogin) {
|
|
218
|
+
const loginTimeout = new Promise((resolve) => {
|
|
219
|
+
setTimeout(() => resolve("timeout"), LOGIN_TIMEOUT_MS);
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
if (await Promise.race([fetch("/login", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
body: token
|
|
225
|
+
}).then((response) => response.ok ? "ok" : "rejected"), loginTimeout]) === "ok") machine.sendLoginOk();
|
|
226
|
+
else machine.sendLoginFail();
|
|
227
|
+
} catch {
|
|
228
|
+
machine.sendLoginFail();
|
|
229
|
+
}
|
|
230
|
+
} else machine.sendLoginOk();
|
|
231
|
+
}
|
|
232
|
+
bootstrap();
|
|
233
|
+
return {
|
|
234
|
+
send(type, header, payload) {
|
|
235
|
+
const encoded = protocol.encode(type, header, payload);
|
|
236
|
+
if (machine.isConnected() && socket && socket.readyState === WebSocket.OPEN) {
|
|
237
|
+
socket.send(toArrayBuffer(encoded));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (machine.isGaveUp()) {
|
|
241
|
+
emitTransportError(header, WS_DISCONNECTED_MESSAGE);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
queued.push({
|
|
245
|
+
bytes: encoded,
|
|
246
|
+
header
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
subscribe(handler) {
|
|
250
|
+
listeners.add(handler);
|
|
251
|
+
return () => {
|
|
252
|
+
listeners.delete(handler);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region web/transports/mock-handlers.ts
|
|
259
|
+
function createHealthInvokeHandlers() {
|
|
260
|
+
return {
|
|
261
|
+
"health.ping": (args) => ({
|
|
262
|
+
message: String(args?.message ?? "pong"),
|
|
263
|
+
ts: Date.now()
|
|
264
|
+
}),
|
|
265
|
+
"health.payloadEcho": (args, payload) => {
|
|
266
|
+
const payloadSize = typeof payload === "string" ? new TextEncoder().encode(payload).byteLength : payload instanceof ArrayBuffer ? payload.byteLength : payload?.byteLength ?? 0;
|
|
267
|
+
return {
|
|
268
|
+
label: String(args?.label ?? "payload"),
|
|
269
|
+
payloadSize
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
"health.roundtrip": () => ({
|
|
273
|
+
pong: true,
|
|
274
|
+
ts: Date.now()
|
|
275
|
+
})
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region web/transports/mock.ts
|
|
280
|
+
const MOCK_MEDIA_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAbUlEQVR4nO3O7UkCABiAQcd5hjXSyCKLPrCiklRswHOM/rw3wS0s4yquYxXruInb2MRd3MdDbOMxnuI5XuI13mIX7/ERn/EV3/ET+/iNQxzjFOf4y2ICE5jABCYwgQlMYAITmMAEJjCBCfx34ALvwBam5OydHgAAAABJRU5ErkJggg==";
|
|
281
|
+
const permissionOf = (args) => typeof args?.permission === "string" ? args.permission : "storage";
|
|
282
|
+
const mockInvokeHandlers = {
|
|
283
|
+
...createHealthInvokeHandlers(),
|
|
284
|
+
"discovery.list": () => ({
|
|
285
|
+
schemaVersion: 1,
|
|
286
|
+
capabilities: [
|
|
287
|
+
{
|
|
288
|
+
pluginId: "core.health",
|
|
289
|
+
capabilities: [],
|
|
290
|
+
events: [
|
|
291
|
+
"health.ping",
|
|
292
|
+
"health.payloadEcho",
|
|
293
|
+
"health.roundtrip"
|
|
294
|
+
],
|
|
295
|
+
runtimes: ["bare"]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
pluginId: "core.discovery",
|
|
299
|
+
capabilities: ["discovery"],
|
|
300
|
+
events: ["discovery.list"],
|
|
301
|
+
runtimes: ["bare"]
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
pluginId: "core.permissions",
|
|
305
|
+
capabilities: ["permissions"],
|
|
306
|
+
events: ["permissions.request", "permissions.status"],
|
|
307
|
+
runtimes: []
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
pluginId: "vendor.media",
|
|
311
|
+
capabilities: ["media"],
|
|
312
|
+
events: ["media.pick", "media.capture"],
|
|
313
|
+
runtimes: ["bare"]
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
}),
|
|
317
|
+
"permissions.request": (args) => ({
|
|
318
|
+
permission: permissionOf(args),
|
|
319
|
+
status: "granted"
|
|
320
|
+
}),
|
|
321
|
+
"permissions.status": (args) => ({
|
|
322
|
+
permission: permissionOf(args),
|
|
323
|
+
status: "granted"
|
|
324
|
+
}),
|
|
325
|
+
"media.pick": () => ({
|
|
326
|
+
url: MOCK_MEDIA_DATA_URI,
|
|
327
|
+
path: ""
|
|
328
|
+
}),
|
|
329
|
+
"media.capture": () => ({
|
|
330
|
+
url: MOCK_MEDIA_DATA_URI,
|
|
331
|
+
path: ""
|
|
332
|
+
})
|
|
333
|
+
};
|
|
334
|
+
function createMockTransport() {
|
|
335
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
336
|
+
const invokeHandlers = mockInvokeHandlers;
|
|
337
|
+
const emit = (message) => {
|
|
338
|
+
queueMicrotask(() => {
|
|
339
|
+
for (const listener of listeners) listener(message);
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
return {
|
|
343
|
+
send(type, header, payload) {
|
|
344
|
+
if (type !== MessageType.ENVELOPE || header.type !== "INVOKE_REQUEST") return;
|
|
345
|
+
const handler = invokeHandlers[header.event];
|
|
346
|
+
if (!handler) {
|
|
347
|
+
emit({
|
|
348
|
+
type: MessageType.ENVELOPE,
|
|
349
|
+
header: {
|
|
350
|
+
type: "INVOKE_RESPONSE",
|
|
351
|
+
pluginId: header.pluginId,
|
|
352
|
+
event: header.event,
|
|
353
|
+
requestId: header.requestId,
|
|
354
|
+
error: {
|
|
355
|
+
code: ErrorCode.UNSUPPORTED_EVENT,
|
|
356
|
+
message: `Unsupported mock event: ${header.event}`
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
payload: /* @__PURE__ */ new Uint8Array(0)
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
emit({
|
|
364
|
+
type: MessageType.ENVELOPE,
|
|
365
|
+
header: {
|
|
366
|
+
type: "INVOKE_RESPONSE",
|
|
367
|
+
pluginId: header.pluginId,
|
|
368
|
+
event: header.event,
|
|
369
|
+
requestId: header.requestId,
|
|
370
|
+
result: handler(header.args, payload)
|
|
371
|
+
},
|
|
372
|
+
payload: /* @__PURE__ */ new Uint8Array(0)
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
subscribe(handler) {
|
|
376
|
+
listeners.add(handler);
|
|
377
|
+
return () => {
|
|
378
|
+
listeners.delete(handler);
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
export { createMockTransport, createWebSocketTransport };
|
package/package.json
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekrooh/bare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "One soul, many platforms — the boring bootstrap for cross-platform apps on the Bare runtime: wire protocol, plugin kernel, RPC messenger, transports, and native host bridges.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/AndersCan/ekrooh.git"
|
|
10
|
+
},
|
|
11
|
+
"workspaces": [
|
|
12
|
+
"examples/*",
|
|
13
|
+
"e2e"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.mjs",
|
|
17
|
+
"./core": "./dist/index.mjs",
|
|
18
|
+
"./runtime": "./dist/runtime.mjs",
|
|
19
|
+
"./plugins": "./dist/plugins.mjs",
|
|
20
|
+
"./plugins/health/events": "./dist/plugins-health-events.mjs",
|
|
21
|
+
"./plugins/discovery/events": "./dist/plugins-discovery-events.mjs",
|
|
22
|
+
"./plugins/permissions/events": "./dist/plugins-permissions-events.mjs",
|
|
23
|
+
"./plugins/media/events": "./dist/plugins-media-events.mjs",
|
|
24
|
+
"./transports": "./dist/transports.mjs",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"check": "vp check",
|
|
32
|
+
"lint": "vp fmt . --fix",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "vp test && npm run test:e2e:web && npm run test:e2e:harness",
|
|
35
|
+
"test:unit": "vp test",
|
|
36
|
+
"test:e2e:web": "node scripts/playwright-local-browsers.mjs test",
|
|
37
|
+
"dev": "concurrently \"vp -C examples/web dev\" \"npm run dev:bare\"",
|
|
38
|
+
"predev": "npm run build:pkg",
|
|
39
|
+
"dev:bare": "node scripts/dev-bare.mjs",
|
|
40
|
+
"dev:mock": "VITE_TRANSPORT_MODE=mock vp -C examples/web dev",
|
|
41
|
+
"predev:mock": "npm run build:pkg",
|
|
42
|
+
"dev:ws": "vp -C examples/web dev",
|
|
43
|
+
"predev:ws": "npm run build:pkg",
|
|
44
|
+
"dev:e2e": "vp -C examples/web dev --host 127.0.0.1 --port 4173 --strictPort",
|
|
45
|
+
"predev:e2e": "npm run build:pkg",
|
|
46
|
+
"build": "npm run build:android-assets",
|
|
47
|
+
"build:pkg": "vp pack",
|
|
48
|
+
"prepack": "npm run build:pkg",
|
|
49
|
+
"prepublishOnly": "npm run build:pkg",
|
|
50
|
+
"build:web": "npm run build:pkg && vp -C examples/web build",
|
|
51
|
+
"build:core": "npm run build:bare:app",
|
|
52
|
+
"build:android-assets": "npm run build:core && npm run build:web",
|
|
53
|
+
"build:ios": "node scripts/build-ios-app.mjs",
|
|
54
|
+
"test:ios": "node scripts/test-ios.mjs",
|
|
55
|
+
"build:bare": "npm run build:core",
|
|
56
|
+
"build:bare:app": "esbuild core/main.core.ts --bundle --packages=external --platform=node --format=esm --outfile=core/main.core.gen.js",
|
|
57
|
+
"build:harness": "esbuild core/harness.core.ts --bundle --packages=external --platform=node --format=esm --outfile=core/harness.core.gen.js",
|
|
58
|
+
"harness:e2e": "npm run build:harness && npm run build:web && node scripts/harness-e2e.mjs",
|
|
59
|
+
"test:e2e:harness": "node scripts/playwright-local-browsers.mjs test --config playwright.harness.config.ts",
|
|
60
|
+
"playwright:install": "node scripts/playwright-local-browsers.mjs install chromium",
|
|
61
|
+
"smoke:loopback": "esbuild scripts/smoke-loopback.mjs --bundle --packages=external --platform=node --format=esm --outfile=scripts/smoke-loopback.gen.mjs && bare scripts/smoke-loopback.gen.mjs",
|
|
62
|
+
"smoke:p2p": "npm run build:p2p-verify && rm -rf .p2p-smoke && mkdir .p2p-smoke && bare core/p2p-verify.core.gen.js .p2p-smoke",
|
|
63
|
+
"build:p2p-verify": "esbuild core/p2p-verify.core.ts --bundle --packages=external --platform=node --format=esm --outfile=core/p2p-verify.core.gen.js",
|
|
64
|
+
"prebuilds": "node scripts/fetch-prebuilds.mjs",
|
|
65
|
+
"prepare": "vp config"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/node": "^26.2.0",
|
|
69
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
70
|
+
"bare": "^1.28.0",
|
|
71
|
+
"bare-link": "^3.3.0",
|
|
72
|
+
"bare-pack": "^2.2.1",
|
|
73
|
+
"bare-runtime": "^1.28.0",
|
|
74
|
+
"concurrently": "^10.0.4",
|
|
75
|
+
"corestore": "^7.12.0",
|
|
76
|
+
"esbuild": "^0.28.0",
|
|
77
|
+
"hyperdrive": "^13.3.3",
|
|
78
|
+
"hyperswarm": "^4.17.0",
|
|
79
|
+
"typescript": "^7.0.2",
|
|
80
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
|
|
81
|
+
"vite-plus": "0.2.8"
|
|
82
|
+
},
|
|
83
|
+
"dependencies": {
|
|
84
|
+
"@mantaq/core": "^0.2.0",
|
|
85
|
+
"@mantaq/sugar": "^0.2.0",
|
|
86
|
+
"bare-crypto": "^1.15.3",
|
|
87
|
+
"bare-encoding": "^1.0.3",
|
|
88
|
+
"bare-env": "^3.0.0",
|
|
89
|
+
"bare-fs": "^4.8.0",
|
|
90
|
+
"bare-http1": "^4.5.7",
|
|
91
|
+
"bare-path": "^3.1.1",
|
|
92
|
+
"bare-tcp": "^2.2.7",
|
|
93
|
+
"bare-ws": "^3.0.0",
|
|
94
|
+
"fs-native-extensions": "1.5.0",
|
|
95
|
+
"quickbit-native": "2.4.8",
|
|
96
|
+
"rabin-native": "2.0.0",
|
|
97
|
+
"rocksdb-native": "3.17.4",
|
|
98
|
+
"simdle-native": "1.3.9",
|
|
99
|
+
"sodium-native": "5.1.0",
|
|
100
|
+
"udx-native": "1.21.0"
|
|
101
|
+
},
|
|
102
|
+
"devEngines": {
|
|
103
|
+
"packageManager": {
|
|
104
|
+
"name": "npm",
|
|
105
|
+
"version": "12.0.2",
|
|
106
|
+
"onFail": "warn"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"overrides": {
|
|
110
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.8"
|
|
111
|
+
}
|
|
112
|
+
}
|