bunite-core 0.8.0 → 0.9.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.
Files changed (57) hide show
  1. package/package.json +9 -7
  2. package/src/{bun → host}/core/App.ts +45 -81
  3. package/src/{bun → host}/core/BrowserView.ts +64 -64
  4. package/src/{bun → host}/core/BrowserWindow.ts +14 -14
  5. package/src/host/core/Socket.ts +98 -0
  6. package/src/host/core/SurfaceBrowserIPC.ts +7 -0
  7. package/src/host/core/SurfaceManager.ts +154 -0
  8. package/src/host/encryptedPipe.ts +62 -0
  9. package/src/{bun → host}/events/appEvents.ts +0 -1
  10. package/src/host/index.ts +29 -0
  11. package/src/{bun/proc → host}/native.ts +38 -52
  12. package/src/{shared → host}/paths.ts +20 -26
  13. package/src/{bun/preload/inline.ts → host/preloadBundle.ts} +2 -2
  14. package/src/host/serveWeb.ts +81 -0
  15. package/src/native/linux/bunite_linux_runtime.cpp +2 -2
  16. package/src/native/mac/bunite_mac_ffi.mm +2 -2
  17. package/src/native/shared/ffi_exports.h +1 -1
  18. package/src/native/win/native_host_ffi.cpp +2 -2
  19. package/src/preload/runtime.built.js +1 -1
  20. package/src/preload/runtime.ts +54 -219
  21. package/src/preload/tsconfig.json +3 -10
  22. package/src/rpc/encrypt.ts +74 -0
  23. package/src/rpc/error.ts +58 -0
  24. package/src/rpc/framework.ts +132 -0
  25. package/src/rpc/hash.ts +142 -0
  26. package/src/rpc/index.ts +129 -0
  27. package/src/rpc/peer.ts +1055 -0
  28. package/src/rpc/renderer.ts +82 -0
  29. package/src/rpc/schema.ts +246 -0
  30. package/src/rpc/stream.ts +72 -0
  31. package/src/rpc/transport.ts +81 -0
  32. package/src/rpc/wire.ts +150 -0
  33. package/src/{preload/webviewElement.ts → webview/native.ts} +68 -48
  34. package/src/{shared/webviewPolyfill.ts → webview/polyfill.ts} +4 -7
  35. package/src/bun/core/Socket.ts +0 -187
  36. package/src/bun/core/SurfaceBrowserIPC.ts +0 -65
  37. package/src/bun/core/SurfaceManager.ts +0 -201
  38. package/src/bun/index.ts +0 -53
  39. package/src/bun/preload/index.ts +0 -73
  40. package/src/preload/tsconfig.tsbuildinfo +0 -1
  41. package/src/shared/rpc.ts +0 -424
  42. package/src/shared/rpcDemux.ts +0 -219
  43. package/src/shared/rpcWire.ts +0 -54
  44. package/src/shared/rpcWireConstants.ts +0 -3
  45. package/src/shared/webRpcHandler.ts +0 -77
  46. package/src/shared/webSocketTransport.ts +0 -26
  47. package/src/view/index.ts +0 -196
  48. /package/src/{shared → host}/cefVersion.ts +0 -0
  49. /package/src/{bun → host}/core/SurfaceRegistry.ts +0 -0
  50. /package/src/{bun → host}/core/singleInstanceLock.ts +0 -0
  51. /package/src/{bun → host}/core/windowIds.ts +0 -0
  52. /package/src/{bun → host}/events/event.ts +0 -0
  53. /package/src/{bun → host}/events/eventEmitter.ts +0 -0
  54. /package/src/{bun → host}/events/webviewEvents.ts +0 -0
  55. /package/src/{bun → host}/events/windowEvents.ts +0 -0
  56. /package/src/{shared → host}/log.ts +0 -0
  57. /package/src/{shared → host}/platform.ts +0 -0
@@ -0,0 +1,74 @@
1
+ import type { BytesPipe } from "./transport";
2
+
3
+ const VERSION = 1;
4
+ const IV_LENGTH = 12;
5
+ const HEADER_LENGTH = 1 + IV_LENGTH;
6
+
7
+ function toBufferSource(view: Uint8Array): ArrayBuffer {
8
+ const out = new ArrayBuffer(view.byteLength);
9
+ new Uint8Array(out).set(view);
10
+ return out;
11
+ }
12
+
13
+ async function importAesGcmKey(rawKey: Uint8Array): Promise<CryptoKey> {
14
+ return crypto.subtle.importKey("raw", toBufferSource(rawKey), "AES-GCM", false, ["encrypt", "decrypt"]);
15
+ }
16
+
17
+ // WebCrypto AES-256-GCM (browser / preload). For Bun-side use `host/encryptedPipe.ts` (node:crypto).
18
+ export async function createEncryptedPipe(base: BytesPipe, rawKey: Uint8Array): Promise<BytesPipe> {
19
+ const key = await importAesGcmKey(rawKey);
20
+ let downstream: ((bytes: Uint8Array) => void) | undefined;
21
+ let sendChain: Promise<void> = Promise.resolve();
22
+ let recvChain: Promise<void> = Promise.resolve();
23
+ let closed = false;
24
+ const closeOnce = () => { if (!closed) { closed = true; base.close(); } };
25
+
26
+ base.setReceive((frame) => {
27
+ if (closed) return;
28
+ if (frame.length < HEADER_LENGTH || frame[0] !== VERSION) {
29
+ closeOnce();
30
+ return;
31
+ }
32
+ const iv = toBufferSource(frame.subarray(1, HEADER_LENGTH));
33
+ const payload = toBufferSource(frame.subarray(HEADER_LENGTH));
34
+ recvChain = recvChain.then(async () => {
35
+ if (closed) return;
36
+ try {
37
+ const buf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, payload);
38
+ downstream?.(new Uint8Array(buf));
39
+ } catch {
40
+ closeOnce();
41
+ }
42
+ });
43
+ });
44
+
45
+ return {
46
+ send(bytes) {
47
+ if (closed) return;
48
+ const payload = toBufferSource(bytes);
49
+ sendChain = sendChain.then(async () => {
50
+ if (closed) return;
51
+ try {
52
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
53
+ const ivBuf = toBufferSource(iv);
54
+ const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv: ivBuf }, key, payload);
55
+ const encArr = new Uint8Array(encrypted);
56
+ const out = new Uint8Array(HEADER_LENGTH + encArr.byteLength);
57
+ out[0] = VERSION;
58
+ out.set(iv, 1);
59
+ out.set(encArr, HEADER_LENGTH);
60
+ base.send(out);
61
+ } catch {
62
+ closeOnce();
63
+ }
64
+ });
65
+ },
66
+ setReceive(handler) {
67
+ downstream = handler;
68
+ },
69
+ close() {
70
+ closeOnce();
71
+ },
72
+ };
73
+ }
74
+
@@ -0,0 +1,58 @@
1
+ export type IpcCode =
2
+ | "ok"
3
+ | "cancelled"
4
+ | "unknown"
5
+ | "invalid_argument"
6
+ | "deadline_exceeded"
7
+ | "not_found"
8
+ | "not_supported"
9
+ | "already_exists"
10
+ | "permission_denied"
11
+ | "unauthenticated"
12
+ | "resource_exhausted"
13
+ | "failed_precondition"
14
+ | "unavailable"
15
+ | "protocol_error";
16
+
17
+ export type FailedPreconditionReason =
18
+ | "cap_disposed"
19
+ | "cap_revoked"
20
+ | "owner_disconnect"
21
+ | "protocol_violation";
22
+
23
+ export type RetrySpec =
24
+ | { kind: "never" }
25
+ | { kind: "transparent" }
26
+ | { kind: "after-cooldown"; minMs: number }
27
+ | { kind: "after-resync" };
28
+
29
+ export interface IpcStatus {
30
+ code: IpcCode;
31
+ message?: string;
32
+ details?: unknown;
33
+ retry?: RetrySpec;
34
+ }
35
+
36
+ export class IpcError extends Error {
37
+ readonly code: IpcCode;
38
+ readonly details: unknown;
39
+ readonly retry?: RetrySpec;
40
+
41
+ constructor(status: IpcStatus) {
42
+ super(status.message ?? status.code);
43
+ this.name = "IpcError";
44
+ this.code = status.code;
45
+ this.details = status.details;
46
+ this.retry = status.retry;
47
+ }
48
+
49
+ toStatus(): IpcStatus {
50
+ return {
51
+ code: this.code,
52
+ message: this.message,
53
+ details: this.details,
54
+ retry: this.retry,
55
+ };
56
+ }
57
+ }
58
+
@@ -0,0 +1,132 @@
1
+ import { call, defineCap, stream, cap } from "./schema";
2
+
3
+ export const BrowserWindowCap = defineCap({
4
+ focus: call<void, void>(),
5
+ close: call<void, void>(),
6
+ setBounds: call<{ x: number; y: number; w: number; h: number }, void>(),
7
+ setTitle: call<{ title: string }, void>(),
8
+ id: call<void, number>({ idempotent: true }),
9
+ label: call<void, string>({ idempotent: true }),
10
+ });
11
+
12
+ export const WindowCap = defineCap({
13
+ create: call<WindowCreateOpts, typeof BrowserWindowCap>({ returns: cap(BrowserWindowCap) }),
14
+ list: call<void, typeof BrowserWindowCap>({ returns: cap.array(BrowserWindowCap), idempotent: true }),
15
+ focus: call<{ id?: number; label?: string }, void>(),
16
+ close: call<{ id?: number; label?: string }, void>(),
17
+ });
18
+
19
+ export interface WindowCreateOpts {
20
+ url: string;
21
+ title?: string;
22
+ bounds?: { x?: number; y?: number; w?: number; h?: number };
23
+ label?: string;
24
+ }
25
+
26
+ export const FileRefCap = defineCap({
27
+ text: call<void, string>({ idempotent: true }),
28
+ bytes: call<void, Uint8Array>({ idempotent: true }),
29
+ path: call<void, string>({ idempotent: true }),
30
+ revoke: call<void, void>(),
31
+ }, { disposal: { method: "revoke", async: true } });
32
+
33
+ export const DialogsCap = defineCap({
34
+ openFile: call<DialogOpenFileOpts, typeof FileRefCap>({ returns: cap.array(FileRefCap) }),
35
+ saveFile: call<DialogSaveFileOpts, typeof FileRefCap>({ returns: cap(FileRefCap) }),
36
+ showMessage: call<DialogMessageOpts, "primary" | "secondary" | "tertiary">(),
37
+ });
38
+
39
+ export interface DialogOpenFileOpts {
40
+ title?: string;
41
+ filters?: { name: string; extensions: string[] }[];
42
+ multiple?: boolean;
43
+ startDir?: string;
44
+ }
45
+
46
+ export interface DialogSaveFileOpts {
47
+ title?: string;
48
+ defaultName?: string;
49
+ filters?: { name: string; extensions: string[] }[];
50
+ }
51
+
52
+ export interface DialogMessageOpts {
53
+ title?: string;
54
+ body: string;
55
+ primary?: string;
56
+ secondary?: string;
57
+ tertiary?: string;
58
+ }
59
+
60
+ export const ClipboardCap = defineCap({
61
+ readText: call<void, string>({ idempotent: true }),
62
+ writeText: call<{ text: string }, void>(),
63
+ readBytes: call<{ mime: string }, Uint8Array>({ idempotent: true }),
64
+ writeBytes: call<{ mime: string; data: Uint8Array }, void>(),
65
+ });
66
+
67
+ export const ShellCap = defineCap({
68
+ openExternal: call<{ url: string }, boolean>(),
69
+ showItemInFolder: call<{ path: string }, void>(),
70
+ });
71
+
72
+ export type SurfaceMask = { x: number; y: number; w: number; h: number };
73
+
74
+ export const SurfaceCap = defineCap({
75
+ init: call<{
76
+ src: string;
77
+ x: number;
78
+ y: number;
79
+ width: number;
80
+ height: number;
81
+ hidden?: boolean;
82
+ }, { surfaceId: number }>(),
83
+ resize: call<{ surfaceId: number; x: number; y: number; w: number; h: number }, void>(),
84
+ remove: call<{ surfaceId: number }, void>(),
85
+ setHidden: call<{ surfaceId: number; hidden: boolean }, void>(),
86
+ setMasks: call<{ surfaceId: number; masks: SurfaceMask[] }, void>(),
87
+ setAllPassthrough: call<{ passthrough: boolean }, void>(),
88
+ bringAllVisiblesToFront: call<void, void>(),
89
+ navigate: call<{ surfaceId: number; url: string }, void>(),
90
+ goBack: call<{ surfaceId: number }, void>(),
91
+ reload: call<{ surfaceId: number }, void>(),
92
+ didNavigate: stream<void, { surfaceId: number; url: string }>(),
93
+ });
94
+
95
+ export const RuntimeCap = defineCap({
96
+ window: call<void, typeof WindowCap>({ returns: cap(WindowCap), idempotent: true }),
97
+ dialogs: call<void, typeof DialogsCap>({ returns: cap(DialogsCap), idempotent: true }),
98
+ clipboard: call<void, typeof ClipboardCap>({ returns: cap(ClipboardCap), idempotent: true }),
99
+ shell: call<void, typeof ShellCap>({ returns: cap(ShellCap), idempotent: true }),
100
+ appName: call<void, string>({ idempotent: true }),
101
+ appVersion: call<void, string>({ idempotent: true }),
102
+ theme: call<void, "light" | "dark">({ idempotent: true }),
103
+ themeWatch: stream<void, "light" | "dark">(),
104
+ surface: call<void, typeof SurfaceCap>({ returns: cap(SurfaceCap), idempotent: true }),
105
+ });
106
+
107
+ export const FRAMEWORK_TYPE_IDS = {
108
+ Runtime: 1,
109
+ Window: 2,
110
+ Dialogs: 3,
111
+ FileRef: 4,
112
+ Clipboard: 5,
113
+ Shell: 6,
114
+ BrowserWindow: 7,
115
+ } as const;
116
+
117
+ import type { CapDef } from "./schema";
118
+
119
+ const FRAMEWORK_CAP_TYPE_IDS = new Map<CapDef<any, any>, number>([
120
+ [RuntimeCap, FRAMEWORK_TYPE_IDS.Runtime],
121
+ [WindowCap, FRAMEWORK_TYPE_IDS.Window],
122
+ [DialogsCap, FRAMEWORK_TYPE_IDS.Dialogs],
123
+ [FileRefCap, FRAMEWORK_TYPE_IDS.FileRef],
124
+ [ClipboardCap, FRAMEWORK_TYPE_IDS.Clipboard],
125
+ [ShellCap, FRAMEWORK_TYPE_IDS.Shell],
126
+ [BrowserWindowCap, FRAMEWORK_TYPE_IDS.BrowserWindow],
127
+ [SurfaceCap, 8],
128
+ ]);
129
+
130
+ export function frameworkTypeIdOf(cap: CapDef<any, any>): number | undefined {
131
+ return FRAMEWORK_CAP_TYPE_IDS.get(cap);
132
+ }
@@ -0,0 +1,142 @@
1
+ import {
2
+ type Schema,
3
+ type CapDef,
4
+ type MethodDef,
5
+ type DisposalSpec,
6
+ type AnyCapToken,
7
+ type ReturnsKind,
8
+ isCallDef,
9
+ isStreamDef,
10
+ isCapRef,
11
+ isCapArray,
12
+ isCapRecord,
13
+ _bindTopologyHash,
14
+ } from "./schema";
15
+
16
+ type CanonicalReturns =
17
+ | { kind: "type" }
18
+ | { kind: "cap" | "capArray" | "capRecord"; capIndex: number };
19
+
20
+ type CanonicalMethod =
21
+ | { name: string; kind: "call"; idempotent: boolean; returns: CanonicalReturns }
22
+ | { name: string; kind: "stream" };
23
+
24
+ type CanonicalCap = {
25
+ methods: CanonicalMethod[];
26
+ disposal?: { method: string; async: boolean };
27
+ };
28
+
29
+ type CanonicalRoot = {
30
+ name: string;
31
+ capIndex: number;
32
+ };
33
+
34
+ type CanonicalSchema = {
35
+ v: 1;
36
+ roots: CanonicalRoot[];
37
+ caps: CanonicalCap[];
38
+ };
39
+
40
+ export function canonicalize(schema: Schema<any>): CanonicalSchema {
41
+ const capIndex = new Map<CapDef<any, any>, number>();
42
+ const caps: CanonicalCap[] = [];
43
+
44
+ for (const declared of schema.caps) {
45
+ if (capIndex.has(declared)) continue;
46
+ capIndex.set(declared, caps.length);
47
+ caps.push(null as unknown as CanonicalCap);
48
+ }
49
+
50
+ const intern = (c: CapDef<any, any>): number => {
51
+ const existing = capIndex.get(c);
52
+ if (existing !== undefined) return existing;
53
+ const idx = caps.length;
54
+ capIndex.set(c, idx);
55
+ caps.push(null as unknown as CanonicalCap);
56
+ return idx;
57
+ };
58
+
59
+ for (let i = 0; i < caps.length; i++) {
60
+ if (caps[i] !== null) continue;
61
+ const declared = schema.caps[i];
62
+ caps[i] = capToCanonical(declared, intern);
63
+ }
64
+
65
+ const roots: CanonicalRoot[] = Object.keys(schema.roots).map((name) => ({
66
+ name,
67
+ capIndex: intern(schema.roots[name]),
68
+ }));
69
+
70
+ for (let i = 0; i < caps.length; i++) {
71
+ if (caps[i] !== null) continue;
72
+ const cap = [...capIndex.keys()][i];
73
+ caps[i] = capToCanonical(cap, intern);
74
+ }
75
+
76
+ return { v: 1, roots, caps };
77
+ }
78
+
79
+ function capToCanonical(c: CapDef<any, any>, intern: (c: CapDef<any, any>) => number): CanonicalCap {
80
+ const methods: CanonicalMethod[] = Object.keys(c.methods).map((name) => {
81
+ const m = c.methods[name] as MethodDef;
82
+ if (isCallDef(m)) {
83
+ return {
84
+ name,
85
+ kind: "call",
86
+ idempotent: m.idempotent,
87
+ returns: returnsToCanonical(m.returns, intern),
88
+ };
89
+ }
90
+ if (isStreamDef(m)) {
91
+ return { name, kind: "stream" };
92
+ }
93
+ throw new Error(`Unknown method def for "${name}"`);
94
+ });
95
+
96
+ const result: CanonicalCap = { methods };
97
+ const disposal = c.disposal as DisposalSpec | undefined;
98
+ if (disposal) {
99
+ result.disposal = { method: disposal.method, async: !!disposal.async };
100
+ }
101
+ return result;
102
+ }
103
+
104
+ function returnsToCanonical(
105
+ ret: AnyCapToken | undefined,
106
+ intern: (c: CapDef<any, any>) => number
107
+ ): CanonicalReturns {
108
+ if (!ret) return { kind: "type" };
109
+ if (isCapRef(ret)) return { kind: "cap", capIndex: intern(ret.cap) };
110
+ if (isCapArray(ret)) return { kind: "capArray", capIndex: intern(ret.cap) };
111
+ if (isCapRecord(ret)) return { kind: "capRecord", capIndex: intern(ret.cap) };
112
+ return { kind: "type" };
113
+ }
114
+
115
+ function canonicalJSON(value: unknown): string {
116
+ if (Array.isArray(value)) {
117
+ return "[" + value.map(canonicalJSON).join(",") + "]";
118
+ }
119
+ if (value && typeof value === "object") {
120
+ const keys = Object.keys(value as object).sort();
121
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJSON((value as Record<string, unknown>)[k])).join(",") + "}";
122
+ }
123
+ return JSON.stringify(value);
124
+ }
125
+
126
+ export async function topologyHash(schema: Schema<any>): Promise<string> {
127
+ const canonical = canonicalize(schema);
128
+ const json = canonicalJSON(canonical);
129
+ const bytes = new TextEncoder().encode(json);
130
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
131
+ return toHex(new Uint8Array(digest));
132
+ }
133
+
134
+ function toHex(bytes: Uint8Array): string {
135
+ let out = "";
136
+ for (let i = 0; i < bytes.length; i++) {
137
+ out += bytes[i].toString(16).padStart(2, "0");
138
+ }
139
+ return out;
140
+ }
141
+
142
+ _bindTopologyHash(topologyHash);
@@ -0,0 +1,129 @@
1
+ export {
2
+ call,
3
+ stream,
4
+ cap,
5
+ defineCap,
6
+ defineSchema,
7
+ isCallDef,
8
+ isStreamDef,
9
+ isCapDef,
10
+ isSchema,
11
+ isCapRef,
12
+ isCapArray,
13
+ isCapRecord,
14
+ returnsKindOf,
15
+ } from "./schema";
16
+
17
+ export type {
18
+ CallDef,
19
+ StreamDef,
20
+ CapDef,
21
+ CapRefToken,
22
+ CapArrayToken,
23
+ CapRecordToken,
24
+ AnyCapToken,
25
+ MethodDef,
26
+ MethodsRecord,
27
+ DisposalSpec,
28
+ Schema,
29
+ SchemaShape,
30
+ ImplsOf,
31
+ ServerDescriptor,
32
+ ReturnsKind,
33
+ CallCtx,
34
+ Attestation,
35
+ ExportedCap,
36
+ ClientOf,
37
+ ImplOf,
38
+ ClientReturn,
39
+ } from "./schema";
40
+
41
+ export { topologyHash, canonicalize } from "./hash";
42
+
43
+ export {
44
+ CapRef,
45
+ CAP_REF_EXT,
46
+ createCodec,
47
+ isFrame,
48
+ DEFAULT_MAX_BYTES,
49
+ PROTOCOL_VERSION,
50
+ } from "./wire";
51
+
52
+ export type {
53
+ Frame,
54
+ CallFrame,
55
+ ResultFrame,
56
+ StreamFrame,
57
+ CancelFrame,
58
+ DropFrame,
59
+ HelloFrame,
60
+ GoAwayFrame,
61
+ StreamEvent,
62
+ Target,
63
+ CallMeta,
64
+ CodecPair,
65
+ u32,
66
+ u53,
67
+ } from "./wire";
68
+
69
+ export { IpcError } from "./error";
70
+
71
+ export type {
72
+ IpcCode,
73
+ IpcStatus,
74
+ RetrySpec,
75
+ FailedPreconditionReason,
76
+ } from "./error";
77
+
78
+ export {
79
+ CapTable,
80
+ USER_ROOTS_CAP_ID,
81
+ RUNTIME_CAP_ID,
82
+ USER_ROOTS_TYPE_ID,
83
+ RUNTIME_TYPE_ID,
84
+ FIRST_USER_CAP_ID,
85
+ FIRST_USER_TYPE_ID,
86
+ MAX_CAPS_PER_CONNECTION,
87
+ createConnection,
88
+ } from "./peer";
89
+
90
+ export {
91
+ RuntimeCap,
92
+ WindowCap,
93
+ BrowserWindowCap,
94
+ DialogsCap,
95
+ FileRefCap,
96
+ ClipboardCap,
97
+ ShellCap,
98
+ SurfaceCap,
99
+ FRAMEWORK_TYPE_IDS,
100
+ } from "./framework";
101
+
102
+ export type {
103
+ WindowCreateOpts,
104
+ DialogOpenFileOpts,
105
+ DialogSaveFileOpts,
106
+ DialogMessageOpts,
107
+ } from "./framework";
108
+
109
+ export type {
110
+ Transport,
111
+ Connection,
112
+ ConnectionOptions,
113
+ CapTableEntry,
114
+ PendingCall,
115
+ } from "./peer";
116
+
117
+ export {
118
+ createFrameTransport,
119
+ createWebSocketPipe,
120
+ } from "./transport";
121
+
122
+ export type {
123
+ BytesPipe,
124
+ WebSocketLike,
125
+ } from "./transport";
126
+
127
+ export { createEncryptedPipe } from "./encrypt";
128
+
129
+ export { Stream } from "./stream";