termdock 1.4.235 → 1.4.237
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.
|
@@ -41,25 +41,43 @@ function mapContext(context, map) {
|
|
|
41
41
|
* No cookies, browser device keys, or direct HTTP business requests. */
|
|
42
42
|
export class CollaborationRpc {
|
|
43
43
|
channel;
|
|
44
|
+
receive;
|
|
44
45
|
pending = new Map();
|
|
45
46
|
closed = false;
|
|
46
|
-
constructor(channel) {
|
|
47
|
+
constructor(channel, receive, externalReader = false) {
|
|
47
48
|
this.channel = channel;
|
|
48
|
-
|
|
49
|
+
this.receive = receive;
|
|
50
|
+
if (!externalReader)
|
|
51
|
+
void this.read();
|
|
49
52
|
void channel.done.catch(error => this.close(error));
|
|
50
53
|
}
|
|
54
|
+
accept(packet) {
|
|
55
|
+
if (packet.type !== 'result' && packet.type !== 'error')
|
|
56
|
+
return false;
|
|
57
|
+
const request = this.pending.get(packet.id);
|
|
58
|
+
if (!request)
|
|
59
|
+
return false;
|
|
60
|
+
clearTimeout(request.timer);
|
|
61
|
+
this.pending.delete(packet.id);
|
|
62
|
+
if (packet.type === 'error')
|
|
63
|
+
request.reject(new Error(String(packet.error)));
|
|
64
|
+
else
|
|
65
|
+
request.resolve(packet);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
51
68
|
async read() {
|
|
52
69
|
try {
|
|
53
70
|
for await (const packet of this.channel.read()) {
|
|
54
|
-
|
|
55
|
-
if (!request)
|
|
71
|
+
if (this.accept(packet) || packet.type === 'result' || packet.type === 'error')
|
|
56
72
|
continue;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
try {
|
|
74
|
+
if (!this.receive || !['collaboration-service', 'collaboration-exchange'].includes(packet.type))
|
|
75
|
+
throw new Error('COLLABORATION_OPERATION_UNSUPPORTED');
|
|
76
|
+
this.channel.send({ ...this.receive(packet), type: 'result', id: packet.id });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
this.channel.send({ type: 'error', id: packet.id, error: error instanceof Error ? error.message : 'REQUEST_FAILED' });
|
|
80
|
+
}
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
catch (error) {
|
|
@@ -98,7 +116,7 @@ export class CollaborationRpc {
|
|
|
98
116
|
this.pending.clear();
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
|
-
export async function connectCollaborationRpc(identity, peer, endpoint) {
|
|
119
|
+
export async function connectCollaborationRpc(identity, peer, endpoint, receive) {
|
|
102
120
|
validateCollaborationNode(peer);
|
|
103
121
|
const ca = peer.caFingerprint256 ? await readPinnedCertificateAuthority(peer.origin, peer.caFingerprint256) : undefined;
|
|
104
122
|
const url = endpoint ?? `${peer.origin.replace(/^https:/, 'wss:')}/api/federation/secure`;
|
|
@@ -114,7 +132,21 @@ export async function connectCollaborationRpc(identity, peer, endpoint) {
|
|
|
114
132
|
if (relay)
|
|
115
133
|
await relay.ready;
|
|
116
134
|
const secured = await secureConnection({ identity, duplex, initiator: true, targetPinnedPeerId: peer.serviceId, signal: AbortSignal.timeout(5000) });
|
|
117
|
-
|
|
135
|
+
const rpc = new CollaborationRpc(new PacketChannel(secured.duplex), receive);
|
|
136
|
+
if (receive) {
|
|
137
|
+
try {
|
|
138
|
+
await rpc.request({ type: 'collaboration-connect' });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Older servers may lack duplex support; ordinary requests remain gated.
|
|
142
|
+
// Missing peer authorization must reconnect after enrollment completes.
|
|
143
|
+
if (!(error instanceof Error) || !['UNKNOWN_PACKET', 'COLLABORATION_UPGRADE_REQUIRED'].includes(error.message)) {
|
|
144
|
+
rpc.close();
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return rpc;
|
|
118
150
|
}
|
|
119
151
|
catch (error) {
|
|
120
152
|
socket.terminate();
|
|
@@ -201,10 +233,10 @@ export class CollaborationPeerTransport {
|
|
|
201
233
|
}
|
|
202
234
|
catch { /* First run has no server authorizations. */ }
|
|
203
235
|
}
|
|
204
|
-
notify() {
|
|
236
|
+
notify(serviceId) {
|
|
205
237
|
for (const b of this.bindings) {
|
|
206
238
|
const key = `${b.groupId}:${b.peer.serviceId}`;
|
|
207
|
-
if (!this.options.store.federationSnapshot().messages.some(m => m.groupId === b.groupId && m.status === 'pending'
|
|
239
|
+
if (b.peer.serviceId === serviceId || !this.options.store.federationSnapshot().messages.some(m => m.groupId === b.groupId && m.status === 'pending'
|
|
208
240
|
&& this.options.store.diagnostic(m.id)?.last_error))
|
|
209
241
|
this.nextAttempt.delete(key);
|
|
210
242
|
}
|
|
@@ -333,6 +365,9 @@ export class CollaborationPeerTransport {
|
|
|
333
365
|
}
|
|
334
366
|
}
|
|
335
367
|
async client(b) {
|
|
368
|
+
const reverse = this.options.reverse?.(b.peer.serviceId);
|
|
369
|
+
if (reverse && !reverse.closed)
|
|
370
|
+
return reverse;
|
|
336
371
|
const key = b.peer.serviceId;
|
|
337
372
|
let pending = this.clients.get(key);
|
|
338
373
|
if (pending) {
|
|
@@ -108,7 +108,7 @@ export class CollaborationService {
|
|
|
108
108
|
throw new Error('PAIRING_EXPIRED_OR_INVALID');
|
|
109
109
|
validateCollaborationNode(invitation.node);
|
|
110
110
|
this.setOrigin(origin);
|
|
111
|
-
const rpc = await this.options.connect(invitation.node);
|
|
111
|
+
const rpc = await (this.options.pairConnect ?? this.options.connect)(invitation.node);
|
|
112
112
|
try {
|
|
113
113
|
await rpc.request({ type: 'collaboration-service', action: 'pair', code: invitation.code, node: this.node() });
|
|
114
114
|
this.remember(invitation.node);
|
|
@@ -139,6 +139,8 @@ export class CollaborationService {
|
|
|
139
139
|
const peer = this.document.peers.find(node => node.serviceId === subject);
|
|
140
140
|
if (!peer)
|
|
141
141
|
throw new Error('COLLABORATION_PAIRING_REQUIRED');
|
|
142
|
+
if (packet.action === 'duplex')
|
|
143
|
+
return { ok: true };
|
|
142
144
|
if (packet.action === 'directory')
|
|
143
145
|
return this.snapshot(peer.origin);
|
|
144
146
|
if (packet.action === 'group') {
|
|
@@ -207,6 +209,9 @@ export class CollaborationService {
|
|
|
207
209
|
close() { this.stopped = true; clearInterval(this.timer); for (const rpc of this.clients.values())
|
|
208
210
|
void rpc.then(client => client.close()).catch(() => { }); this.clients.clear(); }
|
|
209
211
|
async client(peer) {
|
|
212
|
+
const reverse = this.options.reverse?.(peer.serviceId);
|
|
213
|
+
if (reverse && !reverse.closed)
|
|
214
|
+
return reverse;
|
|
210
215
|
let pending = this.clients.get(peer.serviceId);
|
|
211
216
|
if (pending && (await pending).closed) {
|
|
212
217
|
this.clients.delete(peer.serviceId);
|
package/dist/server/entry.js
CHANGED
|
@@ -408,6 +408,8 @@ export function startServer(options = {}) {
|
|
|
408
408
|
runtimeMonitor,
|
|
409
409
|
});
|
|
410
410
|
const { server, scheme } = createServerForApp(app, options);
|
|
411
|
+
const collaborationChannels = new Map();
|
|
412
|
+
const reverseCollaboration = (id) => [...(collaborationChannels.get(id) ?? [])].find(rpc => !rpc.closed);
|
|
411
413
|
let collaborationService;
|
|
412
414
|
let collaborationTransport;
|
|
413
415
|
let entrySubjectAllowed = (_subjectId) => false;
|
|
@@ -447,6 +449,15 @@ export function startServer(options = {}) {
|
|
|
447
449
|
const federation = createFederationRuntime(app, path.join(homedir(), '.termdock', 'federation'), {
|
|
448
450
|
terminal: handleTerminalWebSocket, control: handleControlWebSocket,
|
|
449
451
|
}, {
|
|
452
|
+
collaborationConnected: (subjectId, rpc) => {
|
|
453
|
+
const channels = collaborationChannels.get(subjectId) ?? new Set();
|
|
454
|
+
channels.add(rpc);
|
|
455
|
+
collaborationChannels.set(subjectId, channels);
|
|
456
|
+
collaborationTransport?.notify(subjectId);
|
|
457
|
+
setImmediate(() => { void collaborationService?.refresh(); });
|
|
458
|
+
return () => { channels.delete(rpc); if (!channels.size)
|
|
459
|
+
collaborationChannels.delete(subjectId); };
|
|
460
|
+
},
|
|
450
461
|
collaborationDescriptor: () => {
|
|
451
462
|
if (!collaborationService)
|
|
452
463
|
throw new Error('COLLABORATION_UNAVAILABLE');
|
|
@@ -499,12 +510,25 @@ export function startServer(options = {}) {
|
|
|
499
510
|
setPushTargetPeerId(runtime.serviceId);
|
|
500
511
|
refreshDirectTargets();
|
|
501
512
|
app.locals.passwordRuntime = runtime;
|
|
513
|
+
const receiveCollaboration = (subjectId, packet) => {
|
|
514
|
+
if (packet.type === 'collaboration-service' && collaborationService)
|
|
515
|
+
return collaborationService.receive(subjectId, packet);
|
|
516
|
+
if (packet.type === 'collaboration-exchange' && collaborationTransport)
|
|
517
|
+
return collaborationTransport.receive(subjectId, packet);
|
|
518
|
+
throw new Error('COLLABORATION_UNAVAILABLE');
|
|
519
|
+
};
|
|
520
|
+
const connectPeer = (peer) => {
|
|
521
|
+
if (['localhost', '127.0.0.1', '[::1]'].includes(new URL(peer.origin).hostname))
|
|
522
|
+
throw new Error('WAITING_FOR_PEER_CONNECTION');
|
|
523
|
+
return connectCollaborationRpc(runtime.identity, peer, undefined, packet => receiveCollaboration(peer.serviceId, packet));
|
|
524
|
+
};
|
|
502
525
|
collaborationTransport = new CollaborationPeerTransport({
|
|
526
|
+
reverse: reverseCollaboration,
|
|
503
527
|
file: path.join(homedir(), '.termdock', 'federation', 'collaboration-peers.json'), serviceId: runtime.serviceId,
|
|
504
528
|
store: collaborationStore, deliver: deliverPeerCollaboration, activity: collaborationLocalActivity,
|
|
505
529
|
connect: async (peer, via) => {
|
|
506
530
|
if (!via)
|
|
507
|
-
return
|
|
531
|
+
return connectPeer(peer);
|
|
508
532
|
const entry = await connectCollaborationRpc(runtime.identity, via);
|
|
509
533
|
try {
|
|
510
534
|
const ticket = await entry.request({ type: 'route-ticket', serviceId: peer.serviceId });
|
|
@@ -513,7 +537,7 @@ export function startServer(options = {}) {
|
|
|
513
537
|
const url = new URL('/api/federation/relay', via.origin.replace(/^https:/, 'wss:'));
|
|
514
538
|
url.searchParams.set('routeToken', ticket.routeToken);
|
|
515
539
|
// TLS authenticates the entry; Noise pins the final destination.
|
|
516
|
-
return await connectCollaborationRpc(runtime.identity, { ...via, serviceId: peer.serviceId }, url.href);
|
|
540
|
+
return await connectCollaborationRpc(runtime.identity, { ...via, serviceId: peer.serviceId }, url.href, packet => receiveCollaboration(peer.serviceId, packet));
|
|
517
541
|
}
|
|
518
542
|
finally {
|
|
519
543
|
entry.close();
|
|
@@ -530,7 +554,8 @@ export function startServer(options = {}) {
|
|
|
530
554
|
app.locals.collaborationNode = { serviceId: runtime.serviceId, ...(caFingerprint256 ? { caFingerprint256 } : {}) };
|
|
531
555
|
collaborationService = new CollaborationService({ file: path.join(homedir(), '.termdock', 'federation', 'collaboration-services.json'),
|
|
532
556
|
store: collaborationStore, transport: collaborationTransport, node: () => app.locals.collaborationNode,
|
|
533
|
-
sessions: collaborationDirectorySessions,
|
|
557
|
+
sessions: collaborationDirectorySessions, reverse: reverseCollaboration, connect: connectPeer,
|
|
558
|
+
pairConnect: peer => connectCollaborationRpc(runtime.identity, peer) });
|
|
534
559
|
app.locals.collaborationService = collaborationService;
|
|
535
560
|
collaborationService.start();
|
|
536
561
|
server.once('close', () => collaborationService?.close());
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CollaborationRpc } from '../agent/collaborationPeerTransport.js';
|
|
1
2
|
import { PasswordBootstrapServer } from './passwordBootstrap.js';
|
|
2
3
|
import { DeviceProfiles } from './deviceProfiles.js';
|
|
3
4
|
import { DeviceNames } from './deviceNames.js';
|
|
@@ -122,6 +123,8 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
122
123
|
return Array.isArray(inventory.clientSessions) ? inventory.clientSessions : [];
|
|
123
124
|
}
|
|
124
125
|
async function accept(socket, context = {}) {
|
|
126
|
+
let collaborationRpc;
|
|
127
|
+
let unregisterCollaboration;
|
|
125
128
|
const open = (action) => openAccessAllowed(context.allowOpenAccess === true, isAuthEnabled(), action);
|
|
126
129
|
const allowed = (subjectId, action, sessionId) => open(action) || store.authorize({ subjectId, serviceId, action, sessionId }).allowed;
|
|
127
130
|
const full = (subjectId) => open() || store.hasFullServiceAccess({ subjectId, serviceId });
|
|
@@ -251,7 +254,19 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
251
254
|
try {
|
|
252
255
|
if (packet.id.length > 128)
|
|
253
256
|
throw new Error('INVALID_ID');
|
|
254
|
-
if (packet
|
|
257
|
+
if (collaborationRpc?.accept(packet))
|
|
258
|
+
continue;
|
|
259
|
+
if (packet.type === 'collaboration-connect') {
|
|
260
|
+
if (!options.collaborationService || !options.collaborationConnected)
|
|
261
|
+
throw new Error('COLLABORATION_UPGRADE_REQUIRED');
|
|
262
|
+
options.collaborationService(subjectId, { type: 'collaboration-service', id: packet.id, action: 'duplex' });
|
|
263
|
+
if (!collaborationRpc) {
|
|
264
|
+
collaborationRpc = new CollaborationRpc(channel, undefined, true);
|
|
265
|
+
unregisterCollaboration = options.collaborationConnected(subjectId, collaborationRpc);
|
|
266
|
+
}
|
|
267
|
+
send({ type: 'result', id: packet.id, ok: true });
|
|
268
|
+
}
|
|
269
|
+
else if (packet.type === 'collaboration-descriptor') {
|
|
255
270
|
if (!options.collaborationDescriptor)
|
|
256
271
|
throw new Error('COLLABORATION_UPGRADE_REQUIRED');
|
|
257
272
|
send({ type: 'result', id: packet.id, node: options.collaborationDescriptor() });
|
|
@@ -542,6 +557,8 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
542
557
|
socket.close(4003, 'Secure channel closed');
|
|
543
558
|
}
|
|
544
559
|
finally {
|
|
560
|
+
unregisterCollaboration?.();
|
|
561
|
+
collaborationRpc?.close();
|
|
545
562
|
clearInterval(revokeTimer);
|
|
546
563
|
for (const operation of operations.values())
|
|
547
564
|
cancelHttp(operation);
|
package/package.json
CHANGED
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.237",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-K4KyYJuVoQDpZsBM8ZX1Rw3MddOxAzVdp/cPm0iGZz0=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-nRIwkH5+bg0aR08alnrQVcN5Q10duk4wtzWPNtdJAFU=",
|
|
10
10
|
"clientBundleHash": "sha256-trS+ETMzb9TMz5LAjLxWuftR8zROhre6jdKGsnMbg60=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|