ugly-app 0.1.682 → 0.1.684

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.
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.682";
1
+ export declare const CLI_VERSION = "0.1.684";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.682";
2
+ export const CLI_VERSION = "0.1.684";
3
3
  //# sourceMappingURL=version.js.map
@@ -17,7 +17,7 @@ export type { TerminalFacade } from './facades/terminal.js';
17
17
  export { MCP_CATALOG } from './mcp-catalog.js';
18
18
  export { createProxyClient, nativeSigner } from './proxyClient.js';
19
19
  export type { ProxyClient, ProxyClientOpts, ProxySocket, HostInfo, RTCFactory, } from './proxyClient.js';
20
- export { isDesktopChannel, withProxy, dispatchNativeEvent, DESKTOP_NAMESPACES } from './proxy.js';
20
+ export { isDesktopChannel, withProxy, enableUglyProxy, installProxyClient, dispatchNativeEvent, DESKTOP_NAMESPACES } from './proxy.js';
21
21
  export type { WithProxyOpts } from './proxy.js';
22
22
  export { fpTag, verifyFpTag, parseFingerprint, mskSigner } from './proxyCrypto.js';
23
23
  export type { ProxyRole, ProxySigner } from './proxyCrypto.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/native/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,OAAO,EACP,WAAW,EACX,SAAS,EACT,QAAQ,EACR,MAAM,EACN,SAAS,EACT,cAAc,EACd,aAAa,EACb,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,aAAa,EACb,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,eAAe,EACf,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,aAAa,EACb,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtF,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EACV,WAAW,EACX,eAAe,EACf,WAAW,EACX,QAAQ,EACR,UAAU,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAClG,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACnF,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/native/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,OAAO,EACP,WAAW,EACX,SAAS,EACT,QAAQ,EACR,MAAM,EACN,SAAS,EACT,cAAc,EACd,aAAa,EACb,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,aAAa,EACb,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,eAAe,EACf,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,aAAa,EACb,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtF,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EACV,WAAW,EACX,eAAe,EACf,WAAW,EACX,QAAQ,EACR,UAAU,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,eAAe,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACvI,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACnF,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -6,6 +6,6 @@ export { native, permissions, nativePlatform, isNativeAvailable } from './wrappe
6
6
  export { MCP_CATALOG } from './mcp-catalog.js';
7
7
  // ── Ugly Proxy (remote UglyNative over WebRTC) ──
8
8
  export { createProxyClient, nativeSigner } from './proxyClient.js';
9
- export { isDesktopChannel, withProxy, dispatchNativeEvent, DESKTOP_NAMESPACES } from './proxy.js';
9
+ export { isDesktopChannel, withProxy, enableUglyProxy, installProxyClient, dispatchNativeEvent, DESKTOP_NAMESPACES } from './proxy.js';
10
10
  export { fpTag, verifyFpTag, parseFingerprint, mskSigner } from './proxyCrypto.js';
11
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/native/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE/E,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAStF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,mDAAmD;AACnD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAQnE,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAElG,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/native/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE/E,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAStF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,mDAAmD;AACnD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAQnE,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,eAAe,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEvI,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import type { UglyNative } from './protocol.js';
2
- import type { ProxyClient } from './proxyClient.js';
2
+ import { type ProxyClient, type ProxySocket, type RTCFactory } from './proxyClient.js';
3
3
  /** Namespaces that require a desktop host (RCE-grade / filesystem / long-running tasks). */
4
4
  export declare const DESKTOP_NAMESPACES: ReadonlySet<string>;
5
5
  export declare function isDesktopChannel(channel: string): boolean;
@@ -20,6 +20,33 @@ export interface WithProxyOpts {
20
20
  * to a `dispatchNativeEvent` (below) — then `base.subscribe` delivers them unchanged.
21
21
  */
22
22
  export declare function withProxy(base: UglyNative, opts: WithProxyOpts): UglyNative;
23
+ /**
24
+ * Full mobile/web client bootstrap: returns an UglyNative whose desktop-capability calls
25
+ * lazily connect to a host and proxy. On the first such call (no host yet) it drives the
26
+ * native host-picker (proxy.pickHost) + status overlay (proxy.setStatus); afterwards it
27
+ * reconnects to the same host silently. Device-capability calls stay local. Wire the
28
+ * client's `onEvent` to dispatchNativeEvent so host events reach `subscribe`.
29
+ */
30
+ export declare function enableUglyProxy(opts: {
31
+ native: UglyNative;
32
+ client: ProxyClient;
33
+ }): UglyNative;
34
+ /**
35
+ * One-call client bootstrap: build the WebRTC proxy client (signer = the native keychain
36
+ * channel, events fed back to the subscribe bus) and return an UglyNative that lazily
37
+ * connects + proxies desktop capabilities. Mobile/web shells call this once at startup and
38
+ * assign the result to `globalThis.UglyNative`.
39
+ */
40
+ export declare function installProxyClient(opts: {
41
+ native: UglyNative;
42
+ socket: ProxySocket;
43
+ selfDeviceId: string;
44
+ iceServers?: RTCIceServer[];
45
+ rtcFactory?: RTCFactory;
46
+ win?: {
47
+ dispatchEvent(e: unknown): boolean;
48
+ };
49
+ }): UglyNative;
23
50
  /** Dispatch a proxy event onto the same 'uglyNative' CustomEvent bus base.subscribe reads. */
24
51
  export declare function dispatchNativeEvent(win: {
25
52
  dispatchEvent(e: unknown): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/native/proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAYjD,CAAC;AAEH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,+EAA+E;IAC/E,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,GAAG,UAAU,CAc3E;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CACjC,GAAG,EAAE;IAAE,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;CAAE,EAC3C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,GACZ,IAAI,CAON"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/native/proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAmC,KAAK,WAAW,EAAE,KAAK,WAAW,EAAiB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEvI,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAYjD,CAAC;AAEH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,+EAA+E;IAC/E,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,GAAG,UAAU,CAc3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;CACrB,GAAG,UAAU,CAyDb;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE;QAAE,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;CAC9C,GAAG,UAAU,CAWb;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CACjC,GAAG,EAAE;IAAE,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;CAAE,EAC3C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,GACZ,IAAI,CAON"}
@@ -1,4 +1,5 @@
1
1
  import { NativeUnavailableError } from './protocol.js';
2
+ import { createProxyClient, nativeSigner } from './proxyClient.js';
2
3
  /** Namespaces that require a desktop host (RCE-grade / filesystem / long-running tasks). */
3
4
  export const DESKTOP_NAMESPACES = new Set([
4
5
  'fs',
@@ -38,6 +39,97 @@ export function withProxy(base, opts) {
38
39
  subscribe: base.subscribe,
39
40
  };
40
41
  }
42
+ /**
43
+ * Full mobile/web client bootstrap: returns an UglyNative whose desktop-capability calls
44
+ * lazily connect to a host and proxy. On the first such call (no host yet) it drives the
45
+ * native host-picker (proxy.pickHost) + status overlay (proxy.setStatus); afterwards it
46
+ * reconnects to the same host silently. Device-capability calls stay local. Wire the
47
+ * client's `onEvent` to dispatchNativeEvent so host events reach `subscribe`.
48
+ */
49
+ export function enableUglyProxy(opts) {
50
+ const { native, client } = opts;
51
+ let connecting = null;
52
+ let lastLabel;
53
+ const setStatus = (state, hostLabel) => {
54
+ void native.invoke('proxy.setStatus', { state, hostLabel }).catch(() => { });
55
+ };
56
+ async function ensureConnected() {
57
+ if (client.isOpen())
58
+ return true;
59
+ if (connecting)
60
+ return connecting;
61
+ connecting = (async () => {
62
+ try {
63
+ const existing = client.hostDeviceId();
64
+ if (existing) {
65
+ // Silent reconnect to the previously chosen host.
66
+ setStatus('reconnecting', lastLabel);
67
+ await client.connect(existing);
68
+ setStatus('connected', lastLabel);
69
+ return true;
70
+ }
71
+ setStatus('connecting');
72
+ const hosts = await client.listHosts();
73
+ const picked = (await native.invoke('proxy.pickHost', {
74
+ hosts: hosts.map((h) => ({ deviceId: h.deviceId, label: h.label, online: true, lastSeen: h.lastSeen })),
75
+ }));
76
+ if (!picked) {
77
+ setStatus('none');
78
+ return false;
79
+ }
80
+ lastLabel = hosts.find((h) => h.deviceId === picked.deviceId)?.label;
81
+ await client.connect(picked.deviceId);
82
+ setStatus('connected', lastLabel);
83
+ return true;
84
+ }
85
+ catch {
86
+ setStatus(client.hostDeviceId() ? 'offline' : 'none');
87
+ return false;
88
+ }
89
+ finally {
90
+ connecting = null;
91
+ }
92
+ })();
93
+ return connecting;
94
+ }
95
+ return {
96
+ platform: native.platform,
97
+ subscribe: native.subscribe,
98
+ invoke: (async (channel, payload) => {
99
+ if (!isDesktopChannel(channel))
100
+ return native.invoke(channel, payload);
101
+ if (!client.isOpen() && !(await ensureConnected()))
102
+ throw new NativeUnavailableError(channel);
103
+ try {
104
+ return await client.invoke(channel, payload);
105
+ }
106
+ catch (e) {
107
+ // Mid-session drop: surface the overlay, reconnect to the same host, retry once.
108
+ if (!client.isOpen() && (await ensureConnected()))
109
+ return client.invoke(channel, payload);
110
+ throw e;
111
+ }
112
+ }),
113
+ };
114
+ }
115
+ /**
116
+ * One-call client bootstrap: build the WebRTC proxy client (signer = the native keychain
117
+ * channel, events fed back to the subscribe bus) and return an UglyNative that lazily
118
+ * connects + proxies desktop capabilities. Mobile/web shells call this once at startup and
119
+ * assign the result to `globalThis.UglyNative`.
120
+ */
121
+ export function installProxyClient(opts) {
122
+ const win = opts.win ?? globalThis;
123
+ const client = createProxyClient({
124
+ socket: opts.socket,
125
+ selfDeviceId: opts.selfDeviceId,
126
+ signer: nativeSigner(opts.native),
127
+ iceServers: opts.iceServers,
128
+ rtcFactory: opts.rtcFactory,
129
+ onEvent: (event, data) => dispatchNativeEvent(win, event, data),
130
+ });
131
+ return enableUglyProxy({ native: opts.native, client });
132
+ }
41
133
  /** Dispatch a proxy event onto the same 'uglyNative' CustomEvent bus base.subscribe reads. */
42
134
  export function dispatchNativeEvent(win, event, data) {
43
135
  const detail = { event, data };
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/native/proxy.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGvD,4FAA4F;AAC5F,MAAM,CAAC,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC;IAC7D,IAAI;IACJ,SAAS;IACT,KAAK;IACL,OAAO;IACP,SAAS;IACT,UAAU;IACV,SAAS;IACT,KAAK;IACL,SAAS;IACT,QAAQ;IACR,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IACpF,OAAO,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AACpC,CAAC;AAcD;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAgB,EAAE,IAAmB;IAC7D,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,EAAU,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACvG,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,CAAC,CAAC,OAAe,EAAE,OAAgB,EAAE,EAAE;YAC7C,IAAI,WAAW,CAAC,OAAO,CAAC;gBAAE,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACtE,IAAI,IAAI,CAAC,wBAAwB,IAAI,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/D,0EAA0E;gBAC1E,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC;YAC7D,CAAC;YACD,OAAO,IAAI,CAAC,MAAM,CAAC,OAAgB,EAAE,OAAgB,CAAC,CAAC;QACzD,CAAC,CAAyB;QAC1B,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC;AACJ,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,mBAAmB,CACjC,GAA2C,EAC3C,KAAa,EACb,IAAa;IAEb,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/B,MAAM,EAAE,GACN,OAAO,WAAW,KAAK,UAAU;QAC/B,CAAC,CAAC,IAAI,WAAW,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,CAAC;QAC3C,CAAC,CAAE,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAuB,CAAC;IAC3D,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;AACxB,CAAC"}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/native/proxy.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-app",
3
- "version": "0.1.682",
3
+ "version": "0.1.684",
4
4
  "type": "module",
5
5
  "comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
6
6
  "files": [
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.682";
2
+ export const CLI_VERSION = "0.1.684";
@@ -58,7 +58,7 @@ export type {
58
58
  HostInfo,
59
59
  RTCFactory,
60
60
  } from './proxyClient.js';
61
- export { isDesktopChannel, withProxy, dispatchNativeEvent, DESKTOP_NAMESPACES } from './proxy.js';
61
+ export { isDesktopChannel, withProxy, enableUglyProxy, installProxyClient, dispatchNativeEvent, DESKTOP_NAMESPACES } from './proxy.js';
62
62
  export type { WithProxyOpts } from './proxy.js';
63
63
  export { fpTag, verifyFpTag, parseFingerprint, mskSigner } from './proxyCrypto.js';
64
64
  export type { ProxyRole, ProxySigner } from './proxyCrypto.js';
@@ -5,7 +5,7 @@
5
5
  // base UglyNative with `withProxy(...)`; the core install.ts is untouched.
6
6
  import type { UglyNative } from './protocol.js';
7
7
  import { NativeUnavailableError } from './protocol.js';
8
- import type { ProxyClient } from './proxyClient.js';
8
+ import { createProxyClient, nativeSigner, type ProxyClient, type ProxySocket, type HostInfo, type RTCFactory } from './proxyClient.js';
9
9
 
10
10
  /** Namespaces that require a desktop host (RCE-grade / filesystem / long-running tasks). */
11
11
  export const DESKTOP_NAMESPACES: ReadonlySet<string> = new Set([
@@ -60,6 +60,101 @@ export function withProxy(base: UglyNative, opts: WithProxyOpts): UglyNative {
60
60
  };
61
61
  }
62
62
 
63
+ /**
64
+ * Full mobile/web client bootstrap: returns an UglyNative whose desktop-capability calls
65
+ * lazily connect to a host and proxy. On the first such call (no host yet) it drives the
66
+ * native host-picker (proxy.pickHost) + status overlay (proxy.setStatus); afterwards it
67
+ * reconnects to the same host silently. Device-capability calls stay local. Wire the
68
+ * client's `onEvent` to dispatchNativeEvent so host events reach `subscribe`.
69
+ */
70
+ export function enableUglyProxy(opts: {
71
+ native: UglyNative;
72
+ client: ProxyClient;
73
+ }): UglyNative {
74
+ const { native, client } = opts;
75
+ let connecting: Promise<boolean> | null = null;
76
+ let lastLabel: string | undefined;
77
+
78
+ const setStatus = (state: string, hostLabel?: string): void => {
79
+ void native.invoke('proxy.setStatus' as never, { state, hostLabel } as never).catch(() => {});
80
+ };
81
+
82
+ async function ensureConnected(): Promise<boolean> {
83
+ if (client.isOpen()) return true;
84
+ if (connecting) return connecting;
85
+ connecting = (async () => {
86
+ try {
87
+ const existing = client.hostDeviceId();
88
+ if (existing) {
89
+ // Silent reconnect to the previously chosen host.
90
+ setStatus('reconnecting', lastLabel);
91
+ await client.connect(existing);
92
+ setStatus('connected', lastLabel);
93
+ return true;
94
+ }
95
+ setStatus('connecting');
96
+ const hosts = await client.listHosts();
97
+ const picked = (await native.invoke('proxy.pickHost' as never, {
98
+ hosts: hosts.map((h: HostInfo) => ({ deviceId: h.deviceId, label: h.label, online: true, lastSeen: h.lastSeen })),
99
+ } as never)) as { deviceId: string } | null;
100
+ if (!picked) { setStatus('none'); return false; }
101
+ lastLabel = hosts.find((h) => h.deviceId === picked.deviceId)?.label;
102
+ await client.connect(picked.deviceId);
103
+ setStatus('connected', lastLabel);
104
+ return true;
105
+ } catch {
106
+ setStatus(client.hostDeviceId() ? 'offline' : 'none');
107
+ return false;
108
+ } finally {
109
+ connecting = null;
110
+ }
111
+ })();
112
+ return connecting;
113
+ }
114
+
115
+ return {
116
+ platform: native.platform,
117
+ subscribe: native.subscribe,
118
+ invoke: (async (channel: string, payload: unknown) => {
119
+ if (!isDesktopChannel(channel)) return native.invoke(channel as never, payload as never);
120
+ if (!client.isOpen() && !(await ensureConnected())) throw new NativeUnavailableError(channel);
121
+ try {
122
+ return await client.invoke(channel, payload);
123
+ } catch (e) {
124
+ // Mid-session drop: surface the overlay, reconnect to the same host, retry once.
125
+ if (!client.isOpen() && (await ensureConnected())) return client.invoke(channel, payload);
126
+ throw e;
127
+ }
128
+ }) as UglyNative['invoke'],
129
+ };
130
+ }
131
+
132
+ /**
133
+ * One-call client bootstrap: build the WebRTC proxy client (signer = the native keychain
134
+ * channel, events fed back to the subscribe bus) and return an UglyNative that lazily
135
+ * connects + proxies desktop capabilities. Mobile/web shells call this once at startup and
136
+ * assign the result to `globalThis.UglyNative`.
137
+ */
138
+ export function installProxyClient(opts: {
139
+ native: UglyNative;
140
+ socket: ProxySocket;
141
+ selfDeviceId: string;
142
+ iceServers?: RTCIceServer[];
143
+ rtcFactory?: RTCFactory;
144
+ win?: { dispatchEvent(e: unknown): boolean };
145
+ }): UglyNative {
146
+ const win = opts.win ?? (globalThis as unknown as { dispatchEvent(e: unknown): boolean });
147
+ const client = createProxyClient({
148
+ socket: opts.socket,
149
+ selfDeviceId: opts.selfDeviceId,
150
+ signer: nativeSigner(opts.native),
151
+ iceServers: opts.iceServers,
152
+ rtcFactory: opts.rtcFactory,
153
+ onEvent: (event, data) => dispatchNativeEvent(win, event, data),
154
+ });
155
+ return enableUglyProxy({ native: opts.native, client });
156
+ }
157
+
63
158
  /** Dispatch a proxy event onto the same 'uglyNative' CustomEvent bus base.subscribe reads. */
64
159
  export function dispatchNativeEvent(
65
160
  win: { dispatchEvent(e: unknown): boolean },
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { enableUglyProxy } from './proxy.js';
3
+ import { NativeUnavailableError } from './protocol.js';
4
+ import type { UglyNative } from './protocol.js';
5
+ import type { ProxyClient } from './proxyClient.js';
6
+
7
+ function fakeNative(pick: { deviceId: string } | null): {
8
+ native: UglyNative;
9
+ calls: Array<{ channel: string; payload: unknown }>;
10
+ } {
11
+ const calls: Array<{ channel: string; payload: unknown }> = [];
12
+ const native: UglyNative = {
13
+ platform: 'ios',
14
+ subscribe: vi.fn(() => () => {}) as unknown as UglyNative['subscribe'],
15
+ invoke: (async (channel: string, payload: unknown) => {
16
+ calls.push({ channel, payload });
17
+ if (channel === 'proxy.pickHost') return pick;
18
+ if (channel === 'proxy.setStatus') return undefined;
19
+ return { local: channel }; // device-cap local impl
20
+ }) as unknown as UglyNative['invoke'],
21
+ };
22
+ return { native, calls };
23
+ }
24
+
25
+ function fakeClient(): ProxyClient & { _open: boolean; _host: string | null } {
26
+ const c = {
27
+ _open: false,
28
+ _host: null as string | null,
29
+ listHosts: vi.fn(async () => [{ deviceId: 'h1', label: 'My Mac', isHost: true, lastSeen: 1 }]),
30
+ connect: vi.fn(async (id: string) => { c._open = true; c._host = id; }),
31
+ invoke: vi.fn(async (channel: string, payload: unknown) => ({ proxied: channel, payload })),
32
+ subscribe: vi.fn(async () => ({ snapshot: undefined, unsubscribe: () => {} })),
33
+ isOpen: () => c._open,
34
+ hostDeviceId: () => c._host,
35
+ close: () => { c._open = false; },
36
+ };
37
+ return c as ProxyClient & { _open: boolean; _host: string | null };
38
+ }
39
+
40
+ const statuses = (calls: Array<{ channel: string; payload: unknown }>) =>
41
+ calls.filter((c) => c.channel === 'proxy.setStatus').map((c) => (c.payload as { state: string }).state);
42
+
43
+ describe('enableUglyProxy', () => {
44
+ it('keeps device-cap calls local', async () => {
45
+ const { native } = fakeNative({ deviceId: 'h1' });
46
+ const client = fakeClient();
47
+ const n = enableUglyProxy({ native, client });
48
+ expect(await n.invoke('clipboard.write' as never, { text: 'x' } as never)).toEqual({ local: 'clipboard.write' });
49
+ expect(client.connect).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it('on first desktop-cap call: picks a host, connects, proxies, and reports status', async () => {
53
+ const { native, calls } = fakeNative({ deviceId: 'h1' });
54
+ const client = fakeClient();
55
+ const n = enableUglyProxy({ native, client });
56
+ const r = await n.invoke('fs.readFile' as never, { path: '/p' } as never);
57
+ expect(client.listHosts).toHaveBeenCalled();
58
+ expect(calls.some((c) => c.channel === 'proxy.pickHost')).toBe(true);
59
+ expect(client.connect).toHaveBeenCalledWith('h1');
60
+ expect(r).toEqual({ proxied: 'fs.readFile', payload: { path: '/p' } });
61
+ expect(statuses(calls)).toEqual(['connecting', 'connected']);
62
+ });
63
+
64
+ it('throws + reports none when the user cancels the picker', async () => {
65
+ const { native, calls } = fakeNative(null);
66
+ const client = fakeClient();
67
+ const n = enableUglyProxy({ native, client });
68
+ await expect(n.invoke('process.spawn' as never, {} as never)).rejects.toBeInstanceOf(NativeUnavailableError);
69
+ expect(client.connect).not.toHaveBeenCalled();
70
+ expect(statuses(calls)).toEqual(['connecting', 'none']);
71
+ });
72
+
73
+ it('reconnects silently to the same host on a later call (no re-pick)', async () => {
74
+ const { native, calls } = fakeNative({ deviceId: 'h1' });
75
+ const client = fakeClient();
76
+ const n = enableUglyProxy({ native, client });
77
+ await n.invoke('fs.readFile' as never, {} as never); // connect once
78
+ client._open = false; // simulate drop
79
+ const r = await n.invoke('fs.readFile' as never, { again: true } as never);
80
+ expect(r).toMatchObject({ proxied: 'fs.readFile' });
81
+ expect(calls.filter((c) => c.channel === 'proxy.pickHost')).toHaveLength(1); // only the first time
82
+ expect(statuses(calls)).toEqual(['connecting', 'connected', 'reconnecting', 'connected']);
83
+ });
84
+ });