@myagentroam/node 0.9.69 → 0.9.70
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/dist/capabilities.js
CHANGED
|
@@ -11,10 +11,11 @@ const runtimeCredentialCapability = () => ({
|
|
|
11
11
|
httpVersions: ['1.1', '2']
|
|
12
12
|
});
|
|
13
13
|
const workspaceDirectTransferCapability = () => ({
|
|
14
|
-
apiVersion:
|
|
15
|
-
|
|
14
|
+
apiVersion: 2,
|
|
15
|
+
transport: true,
|
|
16
16
|
upload: true,
|
|
17
17
|
download: true,
|
|
18
|
+
range: true,
|
|
18
19
|
gitCommitBlob: true,
|
|
19
20
|
maxChunkBytes: 256 * 1024
|
|
20
21
|
});
|
package/dist/connector.js
CHANGED
|
@@ -1395,7 +1395,7 @@ export class NodeConnector {
|
|
|
1395
1395
|
send: (type, payload) => this.send(type, payload),
|
|
1396
1396
|
udpPortRange: () => directTransferUdpPortRange(this.config),
|
|
1397
1397
|
openUpload: async (descriptor) => {
|
|
1398
|
-
if (descriptor.resource.kind !== 'WORKSPACE_FILE')
|
|
1398
|
+
if (descriptor.direction !== 'UPLOAD' || descriptor.resource.kind !== 'WORKSPACE_FILE')
|
|
1399
1399
|
throw new Error('DIRECT_RESOURCE_UNSUPPORTED');
|
|
1400
1400
|
const workspace = this.workspaceCoordinator.require(descriptor.resource.workspaceId);
|
|
1401
1401
|
const upload = await this.workspaceUploadService.createDirect(workspace, {
|
|
@@ -2,14 +2,16 @@ import { directDataControlFrameSchema, directNodeCommandSchema } from '@myagentr
|
|
|
2
2
|
import { IceUdpMuxListener as NativeIceUdpMuxListener } from 'node-datachannel';
|
|
3
3
|
import { RTCPeerConnection } from 'node-datachannel/polyfill';
|
|
4
4
|
const MAX_HOST_CANDIDATES = 32;
|
|
5
|
-
const
|
|
5
|
+
const MAX_ACTIVE_TRANSPORTS = 128;
|
|
6
|
+
const MAX_ACTIVE_TRANSFERS = 128;
|
|
6
7
|
const DATA_CHANNEL_HIGH_WATER_BYTES = 4 * 1024 * 1024;
|
|
7
8
|
const DATA_CHANNEL_LOW_WATER_BYTES = 1024 * 1024;
|
|
8
9
|
const LEASE_GRACE_MS = 30_000;
|
|
9
10
|
const LEASE_CHECK_MS = 5_000;
|
|
10
11
|
export class DirectTransferService {
|
|
11
12
|
options;
|
|
12
|
-
|
|
13
|
+
transports = new Map();
|
|
14
|
+
transfers = new Map();
|
|
13
15
|
now;
|
|
14
16
|
createPeerConnection;
|
|
15
17
|
iceUdpMuxListener;
|
|
@@ -53,8 +55,10 @@ export class DirectTransferService {
|
|
|
53
55
|
return true;
|
|
54
56
|
}
|
|
55
57
|
close() {
|
|
56
|
-
for (const
|
|
57
|
-
void this.
|
|
58
|
+
for (const transfer of [...this.transfers.values()])
|
|
59
|
+
void this.removeTransfer(transfer, 'DIRECT_NODE_STOPPED');
|
|
60
|
+
for (const transport of [...this.transports.values()])
|
|
61
|
+
this.removeTransport(transport, 'DIRECT_NODE_STOPPED');
|
|
58
62
|
}
|
|
59
63
|
dispose() {
|
|
60
64
|
if (this.disposed)
|
|
@@ -65,51 +69,71 @@ export class DirectTransferService {
|
|
|
65
69
|
}
|
|
66
70
|
async dispatch(command) {
|
|
67
71
|
if (command.type === 'direct.prepare') {
|
|
68
|
-
|
|
72
|
+
if (command.purpose === 'TRANSPORT')
|
|
73
|
+
this.prepareTransport(command);
|
|
74
|
+
else
|
|
75
|
+
this.prepareTransfer(command);
|
|
69
76
|
return;
|
|
70
77
|
}
|
|
71
|
-
const session = this.sessions.get(command.directSessionId);
|
|
72
|
-
if (session === undefined)
|
|
73
|
-
throw new Error('DIRECT_SESSION_INVALID');
|
|
74
78
|
if (command.type === 'direct.signal') {
|
|
75
|
-
|
|
79
|
+
const transport = this.transports.get(command.directSessionId);
|
|
80
|
+
if (transport === undefined)
|
|
81
|
+
throw new Error('DIRECT_TRANSFER_SESSION_NOT_FOUND');
|
|
82
|
+
await this.signal(transport, command.signal);
|
|
76
83
|
return;
|
|
77
84
|
}
|
|
78
85
|
if (command.type === 'direct.grant') {
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
const transfer = this.transfers.get(command.directSessionId);
|
|
87
|
+
const transport = this.transports.get(command.transportSessionId);
|
|
88
|
+
if (transfer === undefined ||
|
|
89
|
+
transport === undefined ||
|
|
90
|
+
transfer.command.transportSessionId !== command.transportSessionId ||
|
|
91
|
+
command.browserFingerprint !== fingerprint(transport.peer.remoteDescription?.sdp) ||
|
|
92
|
+
command.nodeFingerprint !== fingerprint(transport.peer.localDescription?.sdp) ||
|
|
81
93
|
command.openExpiresAt <= this.now() ||
|
|
82
|
-
command.expiresAt >
|
|
83
|
-
throw new Error('
|
|
84
|
-
|
|
94
|
+
command.expiresAt > transfer.command.expiresAt)
|
|
95
|
+
throw new Error('DIRECT_TRANSFER_GRANT_INVALID');
|
|
96
|
+
transfer.grant = command;
|
|
85
97
|
return;
|
|
86
98
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
const transport = this.transports.get(command.directSessionId);
|
|
100
|
+
if (transport !== undefined) {
|
|
101
|
+
if (command.type === 'direct.heartbeat') {
|
|
102
|
+
transport.lastLeaseAliveAt = this.now();
|
|
103
|
+
transport.leaseExpiresAt = command.expiresAt;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.removeTransport(transport, command.code);
|
|
92
107
|
return;
|
|
93
108
|
}
|
|
94
|
-
|
|
109
|
+
const transfer = this.transfers.get(command.directSessionId);
|
|
110
|
+
if (transfer === undefined)
|
|
111
|
+
throw new Error('DIRECT_TRANSFER_SESSION_NOT_FOUND');
|
|
112
|
+
if (command.type === 'direct.heartbeat') {
|
|
113
|
+
transfer.lastLeaseAliveAt = this.now();
|
|
114
|
+
transfer.leaseExpiresAt = command.expiresAt;
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
await this.removeTransfer(transfer, command.code);
|
|
95
118
|
}
|
|
96
|
-
|
|
119
|
+
prepareTransport(command) {
|
|
97
120
|
if (command.nodeGeneration !== this.options.nodeGeneration())
|
|
98
121
|
throw new Error('DIRECT_NODE_GENERATION_MISMATCH');
|
|
99
|
-
if (this.
|
|
100
|
-
throw new Error('
|
|
101
|
-
if (this.
|
|
122
|
+
if (this.transports.has(command.directSessionId) || this.transfers.has(command.directSessionId))
|
|
123
|
+
throw new Error('DIRECT_TRANSFER_SESSION_CONFLICT');
|
|
124
|
+
if (this.transports.size >= MAX_ACTIVE_TRANSPORTS)
|
|
102
125
|
throw new Error('DIRECT_SESSION_LIMIT_REACHED');
|
|
103
126
|
const peer = this.createPeerConnection(this.peerConfiguration);
|
|
104
|
-
const
|
|
127
|
+
const transport = {
|
|
105
128
|
command,
|
|
106
129
|
peer,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
sentCandidateKeys: new Set(),
|
|
131
|
+
lastLeaseAliveAt: this.now(),
|
|
132
|
+
leaseExpiresAt: command.expiresAt,
|
|
133
|
+
leaseTimer: undefined
|
|
111
134
|
};
|
|
112
|
-
this.
|
|
135
|
+
transport.leaseTimer = this.startLeaseWatch(() => transport.leaseExpiresAt, () => transport.lastLeaseAliveAt, () => this.removeTransport(transport, 'DIRECT_TRANSFER_CONTROL_UNAVAILABLE'));
|
|
136
|
+
this.transports.set(command.directSessionId, transport);
|
|
113
137
|
peer.onicecandidate = (event) => {
|
|
114
138
|
const candidate = event.candidate;
|
|
115
139
|
if (candidate === null) {
|
|
@@ -122,7 +146,7 @@ export class DirectTransferService {
|
|
|
122
146
|
}
|
|
123
147
|
const json = candidate.toJSON();
|
|
124
148
|
if (typeof json.candidate !== 'string' ||
|
|
125
|
-
!acceptHostCandidate(json.candidate,
|
|
149
|
+
!acceptHostCandidate(json.candidate, transport.sentCandidateKeys))
|
|
126
150
|
return;
|
|
127
151
|
this.emit(command.directSessionId, {
|
|
128
152
|
type: 'direct.signal',
|
|
@@ -137,178 +161,198 @@ export class DirectTransferService {
|
|
|
137
161
|
}
|
|
138
162
|
});
|
|
139
163
|
};
|
|
140
|
-
peer.ondatachannel = (event) => this.
|
|
164
|
+
peer.ondatachannel = (event) => this.attachPendingChannel(transport, event.channel);
|
|
141
165
|
peer.onconnectionstatechange = () => {
|
|
142
166
|
if (peer.connectionState === 'connected')
|
|
143
|
-
this.emit(
|
|
167
|
+
this.emit(command.directSessionId, {
|
|
144
168
|
type: 'direct.connected',
|
|
145
|
-
directSessionId:
|
|
169
|
+
directSessionId: command.directSessionId
|
|
146
170
|
});
|
|
147
171
|
if (peer.connectionState === 'failed' || peer.connectionState === 'closed')
|
|
148
|
-
|
|
172
|
+
this.removeTransport(transport, 'DIRECT_CONNECTION_FAILED');
|
|
149
173
|
};
|
|
150
174
|
}
|
|
151
|
-
|
|
175
|
+
prepareTransfer(command) {
|
|
176
|
+
if (command.nodeGeneration !== this.options.nodeGeneration())
|
|
177
|
+
throw new Error('DIRECT_NODE_GENERATION_MISMATCH');
|
|
178
|
+
if (this.transfers.has(command.directSessionId) || this.transports.has(command.directSessionId))
|
|
179
|
+
throw new Error('DIRECT_TRANSFER_SESSION_CONFLICT');
|
|
180
|
+
const transport = this.transports.get(command.transportSessionId);
|
|
181
|
+
if (transport === undefined ||
|
|
182
|
+
transport.command.workbenchConnectionId !== command.workbenchConnectionId ||
|
|
183
|
+
transport.command.nodeGeneration !== command.nodeGeneration ||
|
|
184
|
+
transport.peer.connectionState !== 'connected')
|
|
185
|
+
throw new Error('DIRECT_TRANSFER_UNAVAILABLE');
|
|
186
|
+
if (this.transfers.size >= MAX_ACTIVE_TRANSFERS)
|
|
187
|
+
throw new Error('DIRECT_SESSION_LIMIT_REACHED');
|
|
188
|
+
const transfer = {
|
|
189
|
+
command,
|
|
190
|
+
uploadCommitted: false,
|
|
191
|
+
opened: false,
|
|
192
|
+
processing: Promise.resolve(),
|
|
193
|
+
lastLeaseAliveAt: this.now(),
|
|
194
|
+
leaseExpiresAt: command.expiresAt,
|
|
195
|
+
leaseTimer: undefined
|
|
196
|
+
};
|
|
197
|
+
transfer.leaseTimer = this.startLeaseWatch(() => transfer.leaseExpiresAt, () => transfer.lastLeaseAliveAt, () => void this.removeTransfer(transfer, 'DIRECT_TRANSFER_CONTROL_UNAVAILABLE'));
|
|
198
|
+
this.transfers.set(command.directSessionId, transfer);
|
|
199
|
+
}
|
|
200
|
+
async signal(transport, signal) {
|
|
152
201
|
if (signal.kind === 'CANDIDATE') {
|
|
153
202
|
if (!safeHostCandidate(signal.candidate.candidate))
|
|
154
|
-
throw new Error('
|
|
155
|
-
await
|
|
203
|
+
throw new Error('DIRECT_TRANSFER_CANDIDATE_REJECTED');
|
|
204
|
+
await transport.peer.addIceCandidate(signal.candidate);
|
|
156
205
|
return;
|
|
157
206
|
}
|
|
158
207
|
if (signal.kind === 'END_OF_CANDIDATES') {
|
|
159
|
-
await
|
|
208
|
+
await transport.peer.addIceCandidate(null);
|
|
160
209
|
return;
|
|
161
210
|
}
|
|
162
211
|
if (!hostOnlySdp(signal.sdp))
|
|
163
|
-
throw new Error('
|
|
164
|
-
await
|
|
212
|
+
throw new Error('DIRECT_TRANSFER_CANDIDATE_REJECTED');
|
|
213
|
+
await transport.peer.setRemoteDescription({
|
|
165
214
|
type: signal.kind === 'OFFER' ? 'offer' : 'answer',
|
|
166
215
|
sdp: signal.sdp
|
|
167
216
|
});
|
|
168
217
|
if (signal.kind !== 'OFFER')
|
|
169
218
|
return;
|
|
170
|
-
const answer = await
|
|
171
|
-
await
|
|
172
|
-
const localSdp =
|
|
173
|
-
const
|
|
174
|
-
const sdp = filtered.sdp;
|
|
219
|
+
const answer = await transport.peer.createAnswer();
|
|
220
|
+
await transport.peer.setLocalDescription(answer);
|
|
221
|
+
const localSdp = transport.peer.localDescription?.sdp;
|
|
222
|
+
const sdp = filterIceCandidates(localSdp, transport.sentCandidateKeys).sdp;
|
|
175
223
|
const localFingerprint = fingerprint(localSdp);
|
|
176
224
|
if (sdp === undefined || localFingerprint === undefined)
|
|
177
225
|
throw new Error('DIRECT_FINGERPRINT_MISSING');
|
|
178
|
-
this.emit(
|
|
226
|
+
this.emit(transport.command.directSessionId, {
|
|
179
227
|
type: 'direct.signal',
|
|
180
|
-
directSessionId:
|
|
228
|
+
directSessionId: transport.command.directSessionId,
|
|
181
229
|
signal: { kind: 'ANSWER', sdp, fingerprint: localFingerprint }
|
|
182
230
|
});
|
|
183
231
|
}
|
|
184
|
-
|
|
185
|
-
session.channel = channel;
|
|
232
|
+
attachPendingChannel(transport, channel) {
|
|
186
233
|
channel.binaryType = 'arraybuffer';
|
|
234
|
+
let attached = false;
|
|
235
|
+
channel.onmessage = (event) => {
|
|
236
|
+
if (attached)
|
|
237
|
+
return;
|
|
238
|
+
attached = true;
|
|
239
|
+
void this.openChannel(transport, channel, event.data).catch((error) => {
|
|
240
|
+
channel.close();
|
|
241
|
+
const id = openFrameSessionId(event.data);
|
|
242
|
+
if (id !== undefined)
|
|
243
|
+
this.fail(id, errorCode(error), errorMessage(error));
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async openChannel(transport, channel, data) {
|
|
248
|
+
if (typeof data !== 'string')
|
|
249
|
+
throw new Error('DIRECT_OPEN_REQUIRED');
|
|
250
|
+
const parsed = directDataControlFrameSchema.parse(JSON.parse(data));
|
|
251
|
+
if (parsed.type !== 'OPEN')
|
|
252
|
+
throw new Error('DIRECT_OPEN_REQUIRED');
|
|
253
|
+
const transfer = this.transfers.get(parsed.directSessionId);
|
|
254
|
+
const grant = transfer?.grant;
|
|
255
|
+
if (transfer === undefined ||
|
|
256
|
+
grant === undefined ||
|
|
257
|
+
transfer.command.transportSessionId !== transport.command.directSessionId ||
|
|
258
|
+
parsed.grant !== grant.grant ||
|
|
259
|
+
grant.openExpiresAt <= this.now() ||
|
|
260
|
+
grant.expiresAt <= this.now())
|
|
261
|
+
throw new Error('DIRECT_TRANSFER_GRANT_INVALID');
|
|
262
|
+
transfer.channel = channel;
|
|
263
|
+
transfer.opened = true;
|
|
187
264
|
channel.onmessage = (event) => {
|
|
188
|
-
|
|
189
|
-
.then(() => this.channelMessage(
|
|
190
|
-
.catch((error) => this.fail(
|
|
265
|
+
transfer.processing = transfer.processing
|
|
266
|
+
.then(() => this.channelMessage(transfer, event.data))
|
|
267
|
+
.catch((error) => this.fail(transfer.command.directSessionId, errorCode(error), errorMessage(error)));
|
|
191
268
|
};
|
|
192
269
|
channel.onclose = () => {
|
|
193
|
-
if (this.
|
|
194
|
-
void this.
|
|
270
|
+
if (this.transfers.get(transfer.command.directSessionId) === transfer)
|
|
271
|
+
void this.removeTransfer(transfer, 'DIRECT_CHANNEL_CLOSED');
|
|
195
272
|
};
|
|
273
|
+
await this.open(transfer);
|
|
196
274
|
}
|
|
197
|
-
async channelMessage(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (parsed.type !== 'OPEN')
|
|
203
|
-
throw new Error('DIRECT_OPEN_REQUIRED');
|
|
204
|
-
const grant = session.grant;
|
|
205
|
-
if (grant === undefined ||
|
|
206
|
-
parsed.directSessionId !== session.command.directSessionId ||
|
|
207
|
-
parsed.grant !== grant.grant ||
|
|
208
|
-
grant.openExpiresAt <= this.now() ||
|
|
209
|
-
grant.expiresAt <= this.now())
|
|
210
|
-
throw new Error('DIRECT_GRANT_INVALID');
|
|
211
|
-
session.opened = true;
|
|
212
|
-
if (session.command.purpose === 'TRANSFER')
|
|
213
|
-
this.startLeaseWatch(session);
|
|
214
|
-
await this.open(session);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (session.command.purpose !== 'TRANSFER' || session.command.transfer === undefined)
|
|
218
|
-
throw new Error('DIRECT_FRAME_INVALID');
|
|
219
|
-
if (session.command.transfer.direction === 'DOWNLOAD') {
|
|
275
|
+
async channelMessage(transfer, data) {
|
|
276
|
+
const descriptor = transfer.command.transfer;
|
|
277
|
+
const expectedSize = transferredSize(descriptor);
|
|
278
|
+
const expectedSha256 = transferredSha256(descriptor);
|
|
279
|
+
if (descriptor.direction === 'DOWNLOAD') {
|
|
220
280
|
if (typeof data !== 'string')
|
|
221
281
|
throw new Error('DIRECT_FRAME_INVALID');
|
|
222
282
|
const control = directDataControlFrameSchema.parse(JSON.parse(data));
|
|
223
|
-
if (control.type !== 'ACK' || control.offset !==
|
|
283
|
+
if (control.type !== 'ACK' || control.offset !== expectedSize)
|
|
224
284
|
throw new Error('DIRECT_FRAME_INVALID');
|
|
225
|
-
this.emit(
|
|
285
|
+
this.emit(transfer.command.directSessionId, {
|
|
226
286
|
type: 'direct.complete',
|
|
227
|
-
directSessionId:
|
|
228
|
-
size:
|
|
229
|
-
sha256:
|
|
287
|
+
directSessionId: transfer.command.directSessionId,
|
|
288
|
+
size: expectedSize,
|
|
289
|
+
sha256: expectedSha256
|
|
230
290
|
});
|
|
231
|
-
await this.
|
|
291
|
+
await this.removeTransfer(transfer);
|
|
232
292
|
return;
|
|
233
293
|
}
|
|
234
294
|
if (typeof data === 'string') {
|
|
235
295
|
const control = directDataControlFrameSchema.parse(JSON.parse(data));
|
|
236
|
-
if (control.type !== 'COMPLETE' ||
|
|
296
|
+
if (control.type !== 'COMPLETE' || transfer.upload === undefined)
|
|
237
297
|
throw new Error('DIRECT_FRAME_INVALID');
|
|
238
|
-
if (control.size !==
|
|
239
|
-
control.sha256 !== session.command.transfer.sha256)
|
|
298
|
+
if (control.size !== descriptor.size || control.sha256 !== descriptor.sha256)
|
|
240
299
|
throw new Error('DIRECT_INTEGRITY_FAILED');
|
|
241
|
-
await
|
|
242
|
-
|
|
243
|
-
this.emit(
|
|
300
|
+
await transfer.upload.complete(control.size, control.sha256);
|
|
301
|
+
transfer.uploadCommitted = true;
|
|
302
|
+
this.emit(transfer.command.directSessionId, {
|
|
244
303
|
type: 'direct.complete',
|
|
245
|
-
directSessionId:
|
|
304
|
+
directSessionId: transfer.command.directSessionId,
|
|
246
305
|
size: control.size,
|
|
247
306
|
sha256: control.sha256
|
|
248
307
|
});
|
|
249
|
-
await this.
|
|
308
|
+
await this.removeTransfer(transfer);
|
|
250
309
|
return;
|
|
251
310
|
}
|
|
252
311
|
const bytes = toBytes(data);
|
|
253
|
-
if (bytes.byteLength === 0 || bytes.byteLength > 256 * 1024 ||
|
|
312
|
+
if (bytes.byteLength === 0 || bytes.byteLength > 256 * 1024 || transfer.upload === undefined)
|
|
254
313
|
throw new Error('DIRECT_CHUNK_INVALID');
|
|
255
|
-
const offset = await
|
|
256
|
-
|
|
314
|
+
const offset = await transfer.upload.write(bytes);
|
|
315
|
+
transfer.channel?.send(JSON.stringify({ type: 'ACK', offset }));
|
|
257
316
|
}
|
|
258
|
-
async open(
|
|
259
|
-
|
|
260
|
-
session.channel?.send(JSON.stringify({ type: 'ACK', offset: 0 }));
|
|
261
|
-
this.emit(session.command.directSessionId, {
|
|
262
|
-
type: 'direct.probe-result',
|
|
263
|
-
directSessionId: session.command.directSessionId,
|
|
264
|
-
result: 'DIRECT_AVAILABLE'
|
|
265
|
-
});
|
|
266
|
-
await this.remove(session);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
const descriptor = session.command.transfer;
|
|
270
|
-
if (descriptor === undefined)
|
|
271
|
-
throw new Error('DIRECT_TRANSFER_INVALID');
|
|
317
|
+
async open(transfer) {
|
|
318
|
+
const descriptor = transfer.command.transfer;
|
|
272
319
|
if (descriptor.direction === 'UPLOAD') {
|
|
273
320
|
if (this.options.openUpload === undefined)
|
|
274
321
|
throw new Error('DIRECT_UPLOAD_UNAVAILABLE');
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.emit(
|
|
322
|
+
transfer.upload = await this.options.openUpload(descriptor);
|
|
323
|
+
transfer.channel?.send(JSON.stringify({ type: 'ACK', offset: transfer.upload.offset }));
|
|
324
|
+
this.emit(transfer.command.directSessionId, {
|
|
278
325
|
type: 'direct.opened',
|
|
279
|
-
directSessionId:
|
|
280
|
-
offset:
|
|
326
|
+
directSessionId: transfer.command.directSessionId,
|
|
327
|
+
offset: transfer.upload.offset
|
|
281
328
|
});
|
|
282
329
|
return;
|
|
283
330
|
}
|
|
284
331
|
if (this.options.openDownload === undefined)
|
|
285
332
|
throw new Error('DIRECT_DOWNLOAD_UNAVAILABLE');
|
|
286
333
|
const source = await this.options.openDownload(descriptor);
|
|
287
|
-
|
|
288
|
-
|
|
334
|
+
const expectedSize = transferredSize(descriptor);
|
|
335
|
+
const expectedSha256 = transferredSha256(descriptor);
|
|
336
|
+
transfer.channel?.send(JSON.stringify({ type: 'ACK', offset: source.offset }));
|
|
337
|
+
this.emit(transfer.command.directSessionId, {
|
|
289
338
|
type: 'direct.opened',
|
|
290
|
-
directSessionId:
|
|
339
|
+
directSessionId: transfer.command.directSessionId,
|
|
291
340
|
offset: source.offset
|
|
292
341
|
});
|
|
293
|
-
for await (const chunk of source.chunks)
|
|
342
|
+
for await (const chunk of source.chunks)
|
|
294
343
|
for (let offset = 0; offset < chunk.byteLength; offset += 256 * 1024) {
|
|
295
|
-
if (this.
|
|
344
|
+
if (this.transfers.get(transfer.command.directSessionId) !== transfer)
|
|
296
345
|
return;
|
|
297
|
-
const channel =
|
|
346
|
+
const channel = transfer.channel;
|
|
298
347
|
if (channel === undefined)
|
|
299
348
|
throw new Error('DIRECT_CHANNEL_CLOSED');
|
|
300
|
-
await this.waitForSendCapacity(
|
|
349
|
+
await this.waitForSendCapacity(transfer, channel);
|
|
301
350
|
const end = Math.min(chunk.byteLength, offset + 256 * 1024);
|
|
302
351
|
channel.send(chunk.buffer.slice(chunk.byteOffset + offset, chunk.byteOffset + end));
|
|
303
352
|
}
|
|
304
|
-
}
|
|
305
|
-
session.channel?.send(JSON.stringify({
|
|
306
|
-
type: 'COMPLETE',
|
|
307
|
-
size: descriptor.size,
|
|
308
|
-
sha256: descriptor.sha256
|
|
309
|
-
}));
|
|
353
|
+
transfer.channel?.send(JSON.stringify({ type: 'COMPLETE', size: expectedSize, sha256: expectedSha256 }));
|
|
310
354
|
}
|
|
311
|
-
async waitForSendCapacity(
|
|
355
|
+
async waitForSendCapacity(transfer, channel) {
|
|
312
356
|
if (channel.readyState !== 'open')
|
|
313
357
|
throw new Error('DIRECT_CHANNEL_CLOSED');
|
|
314
358
|
if (channel.bufferedAmount <= DATA_CHANNEL_HIGH_WATER_BYTES)
|
|
@@ -318,62 +362,93 @@ export class DirectTransferService {
|
|
|
318
362
|
const complete = (error) => {
|
|
319
363
|
channel.removeEventListener('bufferedamountlow', available);
|
|
320
364
|
channel.removeEventListener('close', closed);
|
|
321
|
-
|
|
322
|
-
resolve();
|
|
323
|
-
else
|
|
324
|
-
reject(error);
|
|
365
|
+
error === undefined ? resolve() : reject(error);
|
|
325
366
|
};
|
|
326
367
|
const available = () => complete();
|
|
327
368
|
const closed = () => complete(new Error('DIRECT_CHANNEL_CLOSED'));
|
|
328
369
|
channel.addEventListener('bufferedamountlow', available, { once: true });
|
|
329
370
|
channel.addEventListener('close', closed, { once: true });
|
|
330
|
-
if (this.
|
|
371
|
+
if (this.transfers.get(transfer.command.directSessionId) !== transfer ||
|
|
331
372
|
channel.readyState !== 'open')
|
|
332
373
|
closed();
|
|
333
374
|
else if (channel.bufferedAmount <= DATA_CHANNEL_LOW_WATER_BYTES)
|
|
334
375
|
available();
|
|
335
376
|
});
|
|
336
377
|
}
|
|
337
|
-
startLeaseWatch(
|
|
338
|
-
|
|
339
|
-
session.leaseTimer = setInterval(() => {
|
|
378
|
+
startLeaseWatch(expiresAt, lastAlive, expire) {
|
|
379
|
+
const timer = setInterval(() => {
|
|
340
380
|
const now = this.now();
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (now - (session.lastLeaseAliveAt ?? now) > this.leaseGraceMs)
|
|
346
|
-
void this.remove(session, 'DIRECT_LEASE_EXPIRED');
|
|
381
|
+
if (expiresAt() <= now || now - lastAlive() > this.leaseGraceMs)
|
|
382
|
+
expire();
|
|
347
383
|
}, this.leaseCheckMs);
|
|
384
|
+
return timer;
|
|
348
385
|
}
|
|
349
386
|
emit(directSessionId, event) {
|
|
350
|
-
if (this.
|
|
387
|
+
if (this.transports.has(directSessionId) || this.transfers.has(directSessionId))
|
|
351
388
|
this.options.send(event.type, event);
|
|
352
389
|
}
|
|
353
390
|
fail(directSessionId, code, message) {
|
|
354
|
-
const session = this.sessions.get(directSessionId);
|
|
355
391
|
this.options.send('direct.error', { type: 'direct.error', directSessionId, code, message });
|
|
356
|
-
|
|
357
|
-
|
|
392
|
+
const transfer = this.transfers.get(directSessionId);
|
|
393
|
+
if (transfer !== undefined)
|
|
394
|
+
void this.removeTransfer(transfer);
|
|
395
|
+
const transport = this.transports.get(directSessionId);
|
|
396
|
+
if (transport !== undefined)
|
|
397
|
+
this.removeTransport(transport);
|
|
398
|
+
}
|
|
399
|
+
removeTransport(transport, code) {
|
|
400
|
+
if (!this.transports.delete(transport.command.directSessionId))
|
|
401
|
+
return;
|
|
402
|
+
clearInterval(transport.leaseTimer);
|
|
403
|
+
for (const transfer of [...this.transfers.values()])
|
|
404
|
+
if (transfer.command.transportSessionId === transport.command.directSessionId)
|
|
405
|
+
void this.removeTransfer(transfer, code ?? 'DIRECT_CONNECTION_FAILED');
|
|
406
|
+
transport.peer.close();
|
|
407
|
+
if (code !== undefined)
|
|
408
|
+
this.options.send('direct.error', {
|
|
409
|
+
type: 'direct.error',
|
|
410
|
+
directSessionId: transport.command.directSessionId,
|
|
411
|
+
code,
|
|
412
|
+
message: directErrorMessage(code)
|
|
413
|
+
});
|
|
358
414
|
}
|
|
359
|
-
async
|
|
360
|
-
if (!this.
|
|
415
|
+
async removeTransfer(transfer, code) {
|
|
416
|
+
if (!this.transfers.delete(transfer.command.directSessionId))
|
|
361
417
|
return;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
session.channel?.close();
|
|
367
|
-
session.peer.close();
|
|
418
|
+
clearInterval(transfer.leaseTimer);
|
|
419
|
+
if (!transfer.uploadCommitted)
|
|
420
|
+
await transfer.upload?.cancel().catch(() => undefined);
|
|
421
|
+
transfer.channel?.close();
|
|
368
422
|
if (code !== undefined)
|
|
369
423
|
this.options.send('direct.error', {
|
|
370
424
|
type: 'direct.error',
|
|
371
|
-
directSessionId:
|
|
425
|
+
directSessionId: transfer.command.directSessionId,
|
|
372
426
|
code,
|
|
373
427
|
message: directErrorMessage(code)
|
|
374
428
|
});
|
|
375
429
|
}
|
|
376
430
|
}
|
|
431
|
+
function transferredSize(descriptor) {
|
|
432
|
+
return descriptor.direction === 'DOWNLOAD' && descriptor.range !== undefined
|
|
433
|
+
? descriptor.range.length
|
|
434
|
+
: descriptor.size;
|
|
435
|
+
}
|
|
436
|
+
function transferredSha256(descriptor) {
|
|
437
|
+
return descriptor.direction === 'DOWNLOAD' && descriptor.range !== undefined
|
|
438
|
+
? descriptor.range.sha256
|
|
439
|
+
: descriptor.sha256;
|
|
440
|
+
}
|
|
441
|
+
function openFrameSessionId(data) {
|
|
442
|
+
if (typeof data !== 'string')
|
|
443
|
+
return undefined;
|
|
444
|
+
try {
|
|
445
|
+
const value = JSON.parse(data);
|
|
446
|
+
return typeof value.directSessionId === 'string' ? value.directSessionId : undefined;
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
377
452
|
function bindIceUdpMuxListener(range, create) {
|
|
378
453
|
let lastError;
|
|
379
454
|
for (let port = range.begin; port <= range.end; port += 1) {
|
|
@@ -63,37 +63,78 @@ export class WorkspaceFileService {
|
|
|
63
63
|
const service = this;
|
|
64
64
|
return (async function* () {
|
|
65
65
|
const hash = createHash('sha256');
|
|
66
|
-
let offset = 0;
|
|
67
|
-
|
|
66
|
+
let offset = input.range?.offset ?? 0;
|
|
67
|
+
const endOffset = input.range === undefined ? input.size : input.range.offset + input.range.length;
|
|
68
|
+
while (offset < endOffset) {
|
|
68
69
|
const range = (await service.readContent(workspace, {
|
|
69
70
|
path: input.path,
|
|
70
71
|
offset,
|
|
71
|
-
limit: 512 * 1024
|
|
72
|
+
limit: Math.min(512 * 1024, endOffset - offset)
|
|
72
73
|
}, workspacePath));
|
|
73
|
-
if (range.size !== input.size)
|
|
74
|
+
if (range.size !== input.size || range.sourceRevision !== input.revision)
|
|
74
75
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
75
76
|
const bytes = Buffer.from(range.contentBase64, 'base64');
|
|
76
77
|
hash.update(bytes);
|
|
77
78
|
offset += bytes.length;
|
|
78
79
|
if (bytes.length > 0)
|
|
79
80
|
yield bytes;
|
|
80
|
-
if (
|
|
81
|
+
if (offset === endOffset)
|
|
81
82
|
break;
|
|
82
83
|
if (range.nextOffset !== offset)
|
|
83
84
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
const expectedHash = input.range?.sha256 ?? input.sha256;
|
|
87
|
+
if (offset !== endOffset || hash.digest('hex') !== expectedHash)
|
|
86
88
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
87
89
|
})();
|
|
88
90
|
}
|
|
89
91
|
async directMetadata(workspace, input, workspacePath = workspace.path) {
|
|
92
|
+
if (input.offset !== undefined ||
|
|
93
|
+
input.length !== undefined ||
|
|
94
|
+
input.size !== undefined ||
|
|
95
|
+
input.sha256 !== undefined) {
|
|
96
|
+
if (!Number.isSafeInteger(input.offset) ||
|
|
97
|
+
input.offset < 0 ||
|
|
98
|
+
!Number.isSafeInteger(input.length) ||
|
|
99
|
+
input.length < 1 ||
|
|
100
|
+
!Number.isSafeInteger(input.size) ||
|
|
101
|
+
input.size < 0 ||
|
|
102
|
+
typeof input.sha256 !== 'string' ||
|
|
103
|
+
!/^[a-f0-9]{64}$/u.test(input.sha256) ||
|
|
104
|
+
typeof input.revision !== 'string' ||
|
|
105
|
+
!/^[a-f0-9]{64}$/u.test(input.revision))
|
|
106
|
+
throw new Error('FILE_RANGE_INVALID');
|
|
107
|
+
const offset = input.offset;
|
|
108
|
+
const length = input.length;
|
|
109
|
+
const expectedSize = input.size;
|
|
110
|
+
if (offset + length > expectedSize)
|
|
111
|
+
throw new Error('FILE_RANGE_INVALID');
|
|
112
|
+
const read = (await this.readContent(workspace, { ...input, offset, limit: length }, workspacePath));
|
|
113
|
+
const bytes = Buffer.from(read.contentBase64, 'base64');
|
|
114
|
+
if (read.size !== expectedSize ||
|
|
115
|
+
read.sourceRevision !== input.revision ||
|
|
116
|
+
bytes.length !== length)
|
|
117
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
118
|
+
return {
|
|
119
|
+
size: expectedSize,
|
|
120
|
+
sha256: input.sha256,
|
|
121
|
+
revision: input.revision,
|
|
122
|
+
range: {
|
|
123
|
+
offset,
|
|
124
|
+
length,
|
|
125
|
+
sha256: createHash('sha256').update(bytes).digest('hex')
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
90
129
|
const hash = createHash('sha256');
|
|
91
130
|
let offset = 0;
|
|
92
131
|
let size;
|
|
132
|
+
let revision;
|
|
93
133
|
while (true) {
|
|
94
134
|
const range = (await this.readContent(workspace, { ...input, offset, limit: 512 * 1024 }, workspacePath));
|
|
95
135
|
size ??= range.size;
|
|
96
|
-
|
|
136
|
+
revision ??= range.sourceRevision;
|
|
137
|
+
if (range.size !== size || range.sourceRevision !== revision)
|
|
97
138
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
98
139
|
const bytes = Buffer.from(range.contentBase64, 'base64');
|
|
99
140
|
hash.update(bytes);
|
|
@@ -103,9 +144,10 @@ export class WorkspaceFileService {
|
|
|
103
144
|
if (range.nextOffset !== offset)
|
|
104
145
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
105
146
|
}
|
|
106
|
-
if (size === undefined || offset !== size)
|
|
147
|
+
if (size === undefined || revision === undefined || offset !== size)
|
|
107
148
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
108
|
-
|
|
149
|
+
const sha256 = hash.digest('hex');
|
|
150
|
+
return { size, sha256, revision };
|
|
109
151
|
}
|
|
110
152
|
async write(workspace, input) {
|
|
111
153
|
if (typeof input.path !== 'string' ||
|
|
@@ -235,6 +235,8 @@ export class WorkspaceWorkbenchService {
|
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
async directDownload(descriptor) {
|
|
238
|
+
if (descriptor.direction !== 'DOWNLOAD')
|
|
239
|
+
throw new Error('DIRECT_RESOURCE_UNSUPPORTED');
|
|
238
240
|
const workspace = this.options.requireWorkspace(descriptor.resource.workspaceId);
|
|
239
241
|
if (descriptor.resource.kind === 'WORKSPACE_FILE') {
|
|
240
242
|
const input = {
|
|
@@ -245,41 +247,51 @@ export class WorkspaceWorkbenchService {
|
|
|
245
247
|
: { worktreePath: descriptor.resource.worktreePath })
|
|
246
248
|
};
|
|
247
249
|
return {
|
|
248
|
-
offset: descriptor.
|
|
250
|
+
offset: descriptor.range?.offset ?? 0,
|
|
249
251
|
chunks: this.options.files.readDirect(workspace, {
|
|
250
252
|
path: descriptor.resource.path,
|
|
251
253
|
size: descriptor.size,
|
|
252
|
-
sha256: descriptor.sha256
|
|
254
|
+
sha256: descriptor.sha256,
|
|
255
|
+
revision: descriptor.revision,
|
|
256
|
+
...(descriptor.range === undefined ? {} : { range: descriptor.range })
|
|
253
257
|
}, await this.fileRepository(workspace, input))
|
|
254
258
|
};
|
|
255
259
|
}
|
|
260
|
+
const resource = descriptor.resource;
|
|
256
261
|
const input = {
|
|
257
262
|
workspaceId: workspace.id,
|
|
258
|
-
path:
|
|
259
|
-
commit:
|
|
260
|
-
...(
|
|
261
|
-
|
|
262
|
-
: { repositoryPath: descriptor.resource.repositoryPath }),
|
|
263
|
-
...(descriptor.resource.worktreePath === undefined
|
|
264
|
-
? {}
|
|
265
|
-
: { worktreePath: descriptor.resource.worktreePath })
|
|
263
|
+
path: resource.path,
|
|
264
|
+
commit: resource.commit,
|
|
265
|
+
...(resource.repositoryPath === undefined ? {} : { repositoryPath: resource.repositoryPath }),
|
|
266
|
+
...(resource.worktreePath === undefined ? {} : { worktreePath: resource.worktreePath })
|
|
266
267
|
};
|
|
267
268
|
const repository = await this.gitRepository(input);
|
|
268
|
-
|
|
269
|
-
if (content.size !== descriptor.size)
|
|
269
|
+
if (descriptor.revision !== gitResourceRevision(resource.commit, resource.path))
|
|
270
270
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
271
|
+
const refs = this.refs(input);
|
|
271
272
|
return {
|
|
272
|
-
offset: descriptor.
|
|
273
|
+
offset: descriptor.range?.offset ?? 0,
|
|
273
274
|
chunks: (async function* () {
|
|
274
275
|
const hash = createHash('sha256');
|
|
275
|
-
let offset = 0;
|
|
276
|
-
|
|
276
|
+
let offset = descriptor.range?.offset ?? 0;
|
|
277
|
+
const rangeStart = descriptor.range?.offset ?? 0;
|
|
278
|
+
const rangeEnd = rangeStart + (descriptor.range?.length ?? descriptor.size);
|
|
279
|
+
while (offset < rangeEnd) {
|
|
280
|
+
const range = await readGitHistoryFileContentRange(repository, resource.commit, resource.path, offset, Math.min(512 * 1024, rangeEnd - offset), refs);
|
|
281
|
+
if (range.size !== descriptor.size)
|
|
282
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
283
|
+
const bytes = Buffer.from(range.contentBase64, 'base64');
|
|
277
284
|
hash.update(bytes);
|
|
278
285
|
offset += bytes.length;
|
|
279
286
|
if (bytes.length > 0)
|
|
280
287
|
yield bytes;
|
|
288
|
+
if (offset === rangeEnd)
|
|
289
|
+
break;
|
|
290
|
+
if (range.nextOffset !== offset)
|
|
291
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
281
292
|
}
|
|
282
|
-
|
|
293
|
+
const expectedHash = descriptor.range?.sha256 ?? descriptor.sha256;
|
|
294
|
+
if (offset !== rangeEnd || hash.digest('hex') !== expectedHash)
|
|
283
295
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
284
296
|
})()
|
|
285
297
|
};
|
|
@@ -288,6 +300,43 @@ export class WorkspaceWorkbenchService {
|
|
|
288
300
|
const repository = await this.gitRepository(input);
|
|
289
301
|
if (typeof input.commit !== 'string' || typeof input.path !== 'string')
|
|
290
302
|
throw new Error('GIT_HISTORY_COMMIT_INVALID');
|
|
303
|
+
const revision = gitResourceRevision(input.commit, input.path);
|
|
304
|
+
if (input.offset !== undefined ||
|
|
305
|
+
input.length !== undefined ||
|
|
306
|
+
input.size !== undefined ||
|
|
307
|
+
input.sha256 !== undefined ||
|
|
308
|
+
input.revision !== undefined) {
|
|
309
|
+
if (!Number.isSafeInteger(input.offset) ||
|
|
310
|
+
input.offset < 0 ||
|
|
311
|
+
!Number.isSafeInteger(input.length) ||
|
|
312
|
+
input.length < 1 ||
|
|
313
|
+
input.length > 512 * 1024 ||
|
|
314
|
+
!Number.isSafeInteger(input.size) ||
|
|
315
|
+
input.size < 0 ||
|
|
316
|
+
typeof input.sha256 !== 'string' ||
|
|
317
|
+
!/^[a-f0-9]{64}$/u.test(input.sha256) ||
|
|
318
|
+
input.revision !== revision)
|
|
319
|
+
throw new Error('FILE_RANGE_INVALID');
|
|
320
|
+
const offset = input.offset;
|
|
321
|
+
const length = input.length;
|
|
322
|
+
const size = input.size;
|
|
323
|
+
if (offset + length > size)
|
|
324
|
+
throw new Error('FILE_RANGE_INVALID');
|
|
325
|
+
const range = await readGitHistoryFileContentRange(repository, input.commit, input.path, offset, length, this.refs(input));
|
|
326
|
+
const bytes = Buffer.from(range.contentBase64, 'base64');
|
|
327
|
+
if (range.size !== size || bytes.length !== length)
|
|
328
|
+
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
329
|
+
return {
|
|
330
|
+
size,
|
|
331
|
+
sha256: input.sha256,
|
|
332
|
+
revision,
|
|
333
|
+
range: {
|
|
334
|
+
offset,
|
|
335
|
+
length,
|
|
336
|
+
sha256: createHash('sha256').update(bytes).digest('hex')
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
291
340
|
const content = await openGitHistoryFileContent(repository, input.commit, input.path, this.refs(input));
|
|
292
341
|
const hash = createHash('sha256');
|
|
293
342
|
let offset = 0;
|
|
@@ -297,7 +346,7 @@ export class WorkspaceWorkbenchService {
|
|
|
297
346
|
}
|
|
298
347
|
if (offset !== content.size)
|
|
299
348
|
throw new Error('DIRECT_FILE_REVISION_CHANGED');
|
|
300
|
-
return { size: content.size, sha256: hash.digest('hex') };
|
|
349
|
+
return { size: content.size, sha256: hash.digest('hex'), revision };
|
|
301
350
|
}
|
|
302
351
|
async diff(input) {
|
|
303
352
|
if (typeof input.workspaceId !== 'string' ||
|
|
@@ -314,6 +363,9 @@ export class WorkspaceWorkbenchService {
|
|
|
314
363
|
return workspace;
|
|
315
364
|
}
|
|
316
365
|
}
|
|
366
|
+
function gitResourceRevision(commit, path) {
|
|
367
|
+
return createHash('sha256').update(commit).update('\0').update(path).digest('hex');
|
|
368
|
+
}
|
|
317
369
|
function record(value) {
|
|
318
370
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
319
371
|
? value
|
package/dist/workspace.js
CHANGED
|
@@ -672,6 +672,7 @@ export async function readWorkspaceFileContentRange(workspaceRoot, requestedPath
|
|
|
672
672
|
return {
|
|
673
673
|
path: relative(root, file).split(sep).join('/'),
|
|
674
674
|
size: metadata.size,
|
|
675
|
+
sourceRevision: fileSourceRevision(metadata),
|
|
675
676
|
offset,
|
|
676
677
|
contentBase64: source.subarray(0, bytesRead).toString('base64'),
|
|
677
678
|
nextOffset: end < metadata.size ? end : null
|
|
@@ -701,6 +702,7 @@ export async function readAllowedFileContentRange(requestedPath, allowedRoots, o
|
|
|
701
702
|
return {
|
|
702
703
|
path: file,
|
|
703
704
|
size: metadata.size,
|
|
705
|
+
sourceRevision: fileSourceRevision(metadata),
|
|
704
706
|
offset,
|
|
705
707
|
contentBase64: source.subarray(0, bytesRead).toString('base64'),
|
|
706
708
|
nextOffset: end < metadata.size ? end : null
|
|
@@ -710,6 +712,11 @@ export async function readAllowedFileContentRange(requestedPath, allowedRoots, o
|
|
|
710
712
|
await handle.close();
|
|
711
713
|
}
|
|
712
714
|
}
|
|
715
|
+
function fileSourceRevision(metadata) {
|
|
716
|
+
return createHash('sha256')
|
|
717
|
+
.update([metadata.dev, metadata.ino, metadata.size, metadata.mtimeMs, metadata.ctimeMs].join(':'))
|
|
718
|
+
.digest('hex');
|
|
719
|
+
}
|
|
713
720
|
/** Reads one absolute file only when the resolved target remains within a preview root. */
|
|
714
721
|
export async function readAllowedTextFile(requestedPath, allowedRoots, offset = 0, limit = FILE_READ_LIMIT) {
|
|
715
722
|
const { file, metadata } = await resolveAllowedFile(requestedPath, allowedRoots);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.70",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"node-pty": "1.1.0",
|
|
29
29
|
"ws": "^8.21.3",
|
|
30
30
|
"zod": "4.4.3",
|
|
31
|
-
"@myagentroam/agent": "0.9.
|
|
32
|
-
"@myagentroam/protocol": "0.9.
|
|
31
|
+
"@myagentroam/agent": "0.9.70",
|
|
32
|
+
"@myagentroam/protocol": "0.9.70"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/ws": "^8.18.1"
|