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.
- package/client/dist/assets/{ActivityFeedPage-BTYWMRwB.js → ActivityFeedPage-CoWwVcty.js} +1 -1
- package/client/dist/assets/{AgentsPage-BfOCeHHt.js → AgentsPage-CgPvynWc.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-AbVXKh9v.js → AnalyticsPage-ioz3Ub2D.js} +1 -1
- package/client/dist/assets/{BarChart-DlshJN3Z.js → BarChart-BKXQPcoW.js} +1 -1
- package/client/dist/assets/{CodePage-DJCjDG4I.js → CodePage-CYhXRKiI.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-CTqZ9mbB.js → DesktopAnalyticsPage-CBfPCT3q.js} +1 -1
- package/client/dist/assets/{DocsDialog-KiJOSRvX.js → DocsDialog-uRTBV-3T.js} +1 -1
- package/client/dist/assets/{DocsPage-B17CR54A.js → DocsPage-gH0Lc54I.js} +1 -1
- package/client/dist/assets/{ExportDropdown-BAu6z3b6.js → ExportDropdown-DAp7zWib.js} +1 -1
- package/client/dist/assets/{IntegrationsPage-CCG64Q-6.js → IntegrationsPage-D40Si_7s.js} +1 -1
- package/client/dist/assets/{JobDetailPage-BnGJSMiS.js → JobDetailPage-DSxAvB1n.js} +1 -1
- package/client/dist/assets/{JobsPage-B-tn4CIf.js → JobsPage-ZMBc1BHE.js} +1 -1
- package/client/dist/assets/{dist-js-B16c3VyT.js → dist-js-CKqmDyXR.js} +1 -1
- package/client/dist/assets/{dist-js-P2FkJ6fA.js → dist-js-bTZuok_W.js} +1 -1
- package/client/dist/assets/{index-AfVF6BgE.js → index-B9IKK_QQ.js} +45 -45
- package/client/dist/assets/index-BqAXaTbC.css +2 -0
- package/client/dist/assets/{lib-rNNmltMb.js → lib-B5mjOeEi.js} +1 -1
- package/client/dist/assets/{settings-D3LurcR5.js → settings-BI_cVCqN.js} +1 -1
- package/client/dist/assets/{settings-5tzo0Rn3.js → settings-BRaLLSVi.js} +1 -1
- package/client/dist/assets/{settings-BEWv3VEu.js → settings-BcqH0oea.js} +1 -1
- package/client/dist/assets/settings-C0-7Fpxg.js +1 -0
- package/client/dist/assets/{settings-BORg56um.js → settings-D6QMBlGQ.js} +1 -1
- package/client/dist/assets/{settings-DcqWIEM6.js → settings-GOBKOTGl.js} +1 -1
- package/client/dist/assets/{settings-BDAW3trC.js → settings-pT3MzfRu.js} +1 -1
- package/client/dist/assets/{settings-Dfz8QbZS.js → settings-u-16ISHt.js} +1 -1
- package/client/dist/assets/{setup-D3rNZA9A.js → setup-BIIkb-_K.js} +1 -1
- package/client/dist/assets/{setup-C1IA-9YS.js → setup-BeQxu9kD.js} +1 -1
- package/client/dist/assets/{setup-pjgmYHx6.js → setup-CPa6GnlI.js} +1 -1
- package/client/dist/assets/{setup-gzLG8T6F.js → setup-CZl4OEJx.js} +1 -1
- package/client/dist/assets/{setup-C0dzw8j4.js → setup-ChpodNfn.js} +1 -1
- package/client/dist/assets/{setup-WP6WOYQh.js → setup-D_fjJH6u.js} +1 -1
- package/client/dist/assets/{setup-UD2aanGs.js → setup-YzD8DX4O.js} +1 -1
- package/client/dist/assets/{setup-CpfjaNut.js → setup-fRpDozmq.js} +1 -1
- package/client/dist/assets/{useProjectCache-Cid_GxRM.js → useProjectCache-Cf83MBQh.js} +1 -1
- package/client/dist/index.html +4 -4
- package/docs/internals/api-reference.md +4 -7
- package/package.json +2 -2
- package/server/dist/mobile/index.js +5 -5
- package/server/dist/mobile/mobile-admin-router.js +28 -35
- package/server/dist/mobile/mobile-datachannel.js +167 -0
- package/server/dist/mobile/mobile-gateway.js +72 -98
- package/server/dist/mobile/mobile-router.js +4 -35
- package/server/dist/mobile/mobile-signal-reconnect.js +84 -0
- package/server/dist/mobile/mobile-types.js +5 -5
- package/server/dist/mobile/mobile-webrtc-peer.js +129 -0
- package/server/dist/mobile/mobile-webrtc.js +117 -0
- package/client/dist/assets/index-NlH5BbXJ.css +0 -2
- package/client/dist/assets/settings-yMubjqYw.js +0 -1
- package/server/dist/mobile/mobile-mdns.js +0 -81
- 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
|
+
}
|