@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.
@@ -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
+ }