specrails-desktop 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/client/dist/assets/{ActivityFeedPage-BTYWMRwB.js → ActivityFeedPage-CoWwVcty.js} +1 -1
  2. package/client/dist/assets/{AgentsPage-BfOCeHHt.js → AgentsPage-CgPvynWc.js} +1 -1
  3. package/client/dist/assets/{AnalyticsPage-AbVXKh9v.js → AnalyticsPage-ioz3Ub2D.js} +1 -1
  4. package/client/dist/assets/{BarChart-DlshJN3Z.js → BarChart-BKXQPcoW.js} +1 -1
  5. package/client/dist/assets/{CodePage-DJCjDG4I.js → CodePage-CYhXRKiI.js} +1 -1
  6. package/client/dist/assets/{DesktopAnalyticsPage-CTqZ9mbB.js → DesktopAnalyticsPage-CBfPCT3q.js} +1 -1
  7. package/client/dist/assets/{DocsDialog-KiJOSRvX.js → DocsDialog-uRTBV-3T.js} +1 -1
  8. package/client/dist/assets/{DocsPage-B17CR54A.js → DocsPage-gH0Lc54I.js} +1 -1
  9. package/client/dist/assets/{ExportDropdown-BAu6z3b6.js → ExportDropdown-DAp7zWib.js} +1 -1
  10. package/client/dist/assets/{IntegrationsPage-CCG64Q-6.js → IntegrationsPage-D40Si_7s.js} +1 -1
  11. package/client/dist/assets/{JobDetailPage-BnGJSMiS.js → JobDetailPage-DSxAvB1n.js} +1 -1
  12. package/client/dist/assets/{JobsPage-B-tn4CIf.js → JobsPage-ZMBc1BHE.js} +1 -1
  13. package/client/dist/assets/{dist-js-B16c3VyT.js → dist-js-CKqmDyXR.js} +1 -1
  14. package/client/dist/assets/{dist-js-P2FkJ6fA.js → dist-js-bTZuok_W.js} +1 -1
  15. package/client/dist/assets/{index-AfVF6BgE.js → index-B9IKK_QQ.js} +45 -45
  16. package/client/dist/assets/index-BqAXaTbC.css +2 -0
  17. package/client/dist/assets/{lib-rNNmltMb.js → lib-B5mjOeEi.js} +1 -1
  18. package/client/dist/assets/{settings-D3LurcR5.js → settings-BI_cVCqN.js} +1 -1
  19. package/client/dist/assets/{settings-5tzo0Rn3.js → settings-BRaLLSVi.js} +1 -1
  20. package/client/dist/assets/{settings-BEWv3VEu.js → settings-BcqH0oea.js} +1 -1
  21. package/client/dist/assets/settings-C0-7Fpxg.js +1 -0
  22. package/client/dist/assets/{settings-BORg56um.js → settings-D6QMBlGQ.js} +1 -1
  23. package/client/dist/assets/{settings-DcqWIEM6.js → settings-GOBKOTGl.js} +1 -1
  24. package/client/dist/assets/{settings-BDAW3trC.js → settings-pT3MzfRu.js} +1 -1
  25. package/client/dist/assets/{settings-Dfz8QbZS.js → settings-u-16ISHt.js} +1 -1
  26. package/client/dist/assets/{setup-D3rNZA9A.js → setup-BIIkb-_K.js} +1 -1
  27. package/client/dist/assets/{setup-C1IA-9YS.js → setup-BeQxu9kD.js} +1 -1
  28. package/client/dist/assets/{setup-pjgmYHx6.js → setup-CPa6GnlI.js} +1 -1
  29. package/client/dist/assets/{setup-gzLG8T6F.js → setup-CZl4OEJx.js} +1 -1
  30. package/client/dist/assets/{setup-C0dzw8j4.js → setup-ChpodNfn.js} +1 -1
  31. package/client/dist/assets/{setup-WP6WOYQh.js → setup-D_fjJH6u.js} +1 -1
  32. package/client/dist/assets/{setup-UD2aanGs.js → setup-YzD8DX4O.js} +1 -1
  33. package/client/dist/assets/{setup-CpfjaNut.js → setup-fRpDozmq.js} +1 -1
  34. package/client/dist/assets/{useProjectCache-Cid_GxRM.js → useProjectCache-Cf83MBQh.js} +1 -1
  35. package/client/dist/index.html +4 -4
  36. package/docs/internals/api-reference.md +4 -7
  37. package/package.json +2 -2
  38. package/server/dist/mobile/index.js +5 -5
  39. package/server/dist/mobile/mobile-admin-router.js +28 -35
  40. package/server/dist/mobile/mobile-datachannel.js +167 -0
  41. package/server/dist/mobile/mobile-gateway.js +72 -98
  42. package/server/dist/mobile/mobile-router.js +4 -35
  43. package/server/dist/mobile/mobile-signal-reconnect.js +84 -0
  44. package/server/dist/mobile/mobile-types.js +5 -5
  45. package/server/dist/mobile/mobile-webrtc-peer.js +129 -0
  46. package/server/dist/mobile/mobile-webrtc.js +117 -0
  47. package/client/dist/assets/index-NlH5BbXJ.css +0 -2
  48. package/client/dist/assets/settings-yMubjqYw.js +0 -1
  49. package/server/dist/mobile/mobile-mdns.js +0 -81
  50. package/server/dist/mobile/mobile-pairing.js +0 -179
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MobileWebrtcGateway = void 0;
4
+ const werift_1 = require("werift");
5
+ const auth_1 = require("../auth");
6
+ const mobile_datachannel_1 = require("./mobile-datachannel");
7
+ class MobileWebrtcGateway {
8
+ _deps;
9
+ _pending = null;
10
+ _active = new Set();
11
+ constructor(_deps) {
12
+ this._deps = _deps;
13
+ }
14
+ /** Drop the open (un-answered) offer. */
15
+ clearOffer() {
16
+ const p = this._pending;
17
+ this._pending = null;
18
+ if (p)
19
+ this._close(p.pc);
20
+ }
21
+ /** Create a fresh pairing offer (peer + DataChannel + gathered offer SDP).
22
+ * Replaces any previously-open offer. Returns the SDP to embed in the QR. */
23
+ async createOffer(secret) {
24
+ this.clearOffer();
25
+ const pc = new werift_1.RTCPeerConnection({ iceServers: [] });
26
+ const dc = pc.createDataChannel('mobile');
27
+ // The companion peer activates when the channel opens. It validates `hello`
28
+ // against THIS connection's offer secret (captured in this closure) — not a
29
+ // global pending secret, which is already null by the time hello arrives.
30
+ new mobile_datachannel_1.DataChannelPeer(toChannelLike(dc), {
31
+ registerDevice: async (input) => input.secret && (0, auth_1.safeEqual)(input.secret, secret)
32
+ ? this._deps.registerDevice({ deviceName: input.deviceName, platform: input.platform, token: input.token })
33
+ : { ok: false },
34
+ rpcDispatch: this._deps.rpcDispatch,
35
+ bridge: this._deps.bridge,
36
+ });
37
+ pc.connectionStateChange.subscribe((s) => {
38
+ console.log('[webrtc] pc connectionState:', s);
39
+ if (s === 'failed' || s === 'closed' || s === 'disconnected') {
40
+ this._active.delete(pc);
41
+ if (this._pending?.pc === pc)
42
+ this._pending = null;
43
+ }
44
+ });
45
+ const offer = await pc.createOffer();
46
+ await pc.setLocalDescription(offer);
47
+ await waitIceComplete(pc);
48
+ const sdp = pc.localDescription?.sdp;
49
+ if (!sdp) {
50
+ this._close(pc);
51
+ throw new Error('no local description after ICE gathering');
52
+ }
53
+ console.log(`[webrtc] offer ready: ${(sdp.match(/a=candidate/g) ?? []).length} ICE candidates`);
54
+ this._pending = { pc, secret };
55
+ return { sdp };
56
+ }
57
+ /** Apply the companion's scanned answer SDP to the open offer and keep the
58
+ * connection alive for the session. */
59
+ async acceptAnswer(sdp) {
60
+ const p = this._pending;
61
+ if (!p)
62
+ return { ok: false };
63
+ try {
64
+ console.log(`[webrtc] applying answer: ${(sdp.match(/a=candidate/g) ?? []).length} ICE candidates`);
65
+ await p.pc.setRemoteDescription({ type: 'answer', sdp });
66
+ this._active.add(p.pc);
67
+ this._pending = null;
68
+ return { ok: true };
69
+ }
70
+ catch {
71
+ return { ok: false };
72
+ }
73
+ }
74
+ /** Tear down every peer (gateway stopping / disabled). */
75
+ stop() {
76
+ this.clearOffer();
77
+ for (const pc of this._active)
78
+ this._close(pc);
79
+ this._active.clear();
80
+ }
81
+ _close(pc) {
82
+ try {
83
+ void pc.close();
84
+ }
85
+ catch {
86
+ /* ignore */
87
+ }
88
+ }
89
+ }
90
+ exports.MobileWebrtcGateway = MobileWebrtcGateway;
91
+ function toChannelLike(dc) {
92
+ dc.stateChanged.subscribe((s) => console.log('[webrtc] datachannel state:', s));
93
+ return {
94
+ get readyState() {
95
+ return dc.readyState;
96
+ },
97
+ send: (data) => dc.send(data),
98
+ onMessage: (cb) => {
99
+ dc.onMessage.subscribe((d) => cb(typeof d === 'string' ? d : d.toString('utf8')));
100
+ },
101
+ onClose: (cb) => {
102
+ dc.stateChanged.subscribe((s) => {
103
+ if (s === 'closed')
104
+ cb();
105
+ });
106
+ },
107
+ };
108
+ }
109
+ /** Vanilla ICE: resolve once gathering completes so the offer SDP carries all
110
+ * candidates (there's no trickle channel — the SDP travels by QR). */
111
+ function waitIceComplete(pc) {
112
+ if (pc.iceGatheringState === 'complete')
113
+ return Promise.resolve();
114
+ return new Promise((resolve) => {
115
+ let done = false;
116
+ const finish = () => {
117
+ if (!done) {
118
+ done = true;
119
+ resolve();
120
+ }
121
+ };
122
+ pc.iceGatheringStateChange.subscribe((s) => {
123
+ if (s === 'complete')
124
+ finish();
125
+ });
126
+ const t = setTimeout(finish, 3000);
127
+ t.unref?.();
128
+ });
129
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildRegisterDevice = buildRegisterDevice;
7
+ exports.buildRpcDispatch = buildRpcDispatch;
8
+ exports.createLoopbackFetch = createLoopbackFetch;
9
+ const https_1 = __importDefault(require("https"));
10
+ const crypto_1 = require("crypto");
11
+ const mobile_devices_1 = require("./mobile-devices");
12
+ // Glue for the serverless WebRTC companion, kept dependency-injected so the
13
+ // security-relevant bits (secret check, device creation, /v1 dispatch) are
14
+ // unit-tested without a real WebRTC peer, the DB driver, or HTTP. The werift
15
+ // runtime peer lives in ./mobile-webrtc-peer.ts and consumes these.
16
+ function toPlatform(p) {
17
+ return p === 'android' ? 'android' : p === 'ios' ? 'ios' : 'web';
18
+ }
19
+ /** Persists a paired device + issues its token. The single-use pairing-secret
20
+ * check is done per-connection by [MobileWebrtcGateway] (which holds each
21
+ * offer's secret for the life of that connection — the secret must NOT live on
22
+ * the global "pending offer", which is cleared the moment the answer is
23
+ * accepted, well before the companion's `hello` arrives). Approval is implicit:
24
+ * the desktop user actively scanned the phone's answer QR. */
25
+ function buildRegisterDevice(deps) {
26
+ const genToken = deps.genToken ?? (() => (0, crypto_1.randomBytes)(32).toString('hex'));
27
+ return async (input) => {
28
+ // Reconnect: a known, non-revoked device token → reuse that device (no new
29
+ // row, no growing the paired-devices list on every reconnect).
30
+ if (input.token) {
31
+ const existing = (0, mobile_devices_1.getActiveDeviceByTokenHash)(deps.db, (0, mobile_devices_1.hashToken)(input.token));
32
+ if (existing) {
33
+ return {
34
+ ok: true,
35
+ deviceId: existing.id,
36
+ token: input.token,
37
+ hubName: deps.desktopName(),
38
+ hubInstanceId: deps.desktopInstanceId(),
39
+ };
40
+ }
41
+ }
42
+ const token = genToken();
43
+ const row = (0, mobile_devices_1.createDevice)(deps.db, {
44
+ name: (input.deviceName || 'Web companion').slice(0, 80),
45
+ platform: toPlatform(input.platform),
46
+ tokenHash: (0, mobile_devices_1.hashToken)(token),
47
+ certFingerprint: deps.currentFingerprint(),
48
+ });
49
+ return {
50
+ ok: true,
51
+ deviceId: row.id,
52
+ token,
53
+ hubName: deps.desktopName(),
54
+ hubInstanceId: deps.desktopInstanceId(),
55
+ };
56
+ };
57
+ }
58
+ /** Builds the `rpcDispatch` used by [DataChannelPeer] on `rpc`. A thin proxy to
59
+ * the EXISTING `/v1` allow-list (which already validates params, forwards to the
60
+ * internal API, and redacts), authenticated with the device's bearer token. */
61
+ function buildRpcDispatch(deps) {
62
+ return async (input) => {
63
+ // Only the /v1 surface is reachable; the gateway enforces the allow-list.
64
+ if (input.path !== '/v1' && !input.path.startsWith('/v1/')) {
65
+ return { status: 404, json: { error: 'not found' } };
66
+ }
67
+ const method = input.method.toUpperCase();
68
+ const headers = { Authorization: `Bearer ${input.token}` };
69
+ let body;
70
+ if (input.body !== undefined && method !== 'GET' && method !== 'DELETE') {
71
+ headers['Content-Type'] = 'application/json';
72
+ body = JSON.stringify(input.body);
73
+ }
74
+ try {
75
+ const res = await deps.doFetch(deps.gatewayBase + input.path, { method, headers, body });
76
+ const text = await res.text();
77
+ let json = {};
78
+ try {
79
+ json = text ? JSON.parse(text) : {};
80
+ }
81
+ catch {
82
+ json = {};
83
+ }
84
+ return { status: res.status, json };
85
+ }
86
+ catch {
87
+ return { status: 502, json: { error: 'gateway unreachable' } };
88
+ }
89
+ };
90
+ }
91
+ /** A loopback `fetch` for the local /v1 gateway. It deliberately ignores the
92
+ * gateway's self-signed cert — the connection never leaves 127.0.0.1, and the
93
+ * device bearer token (not the TLS cert) is the auth boundary. */
94
+ function createLoopbackFetch() {
95
+ return (url, init) => new Promise((resolve, reject) => {
96
+ const u = new URL(url);
97
+ const req = https_1.default.request({
98
+ hostname: u.hostname,
99
+ port: u.port,
100
+ path: u.pathname + u.search,
101
+ method: init.method,
102
+ headers: init.headers,
103
+ rejectUnauthorized: false,
104
+ }, (res) => {
105
+ let data = '';
106
+ res.setEncoding('utf8');
107
+ res.on('data', (c) => {
108
+ data += c;
109
+ });
110
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, text: async () => data }));
111
+ });
112
+ req.on('error', reject);
113
+ if (init.body)
114
+ req.write(init.body);
115
+ req.end();
116
+ });
117
+ }