@sema-agent/connectors 0.4.0 → 0.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/CHANGELOG.md +85 -0
- package/INTEGRATION.md +21 -1
- package/dist/cli-kit.d.ts +1 -1
- package/dist/cli-kit.js +18 -12
- package/dist/host-url.d.ts +8 -0
- package/dist/host-url.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pinned-upstream.d.ts +2 -0
- package/dist/pinned-upstream.js +346 -85
- package/dist/release-archive.d.ts +22 -0
- package/dist/release-archive.js +462 -10
- package/dist/secrets.d.ts +1 -0
- package/dist/secrets.js +34 -0
- package/dist/session.js +1 -1
- package/dist/write-all.d.ts +2 -0
- package/dist/write-all.js +36 -6
- package/package.json +1 -1
package/dist/release-archive.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { chmodSync, closeSync, createReadStream, fchmodSync, fstatSync, mkdirSync, openSync, readSync, symlinkSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { Transform } from 'node:stream';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { createInflateRaw, gunzipSync, inflateRawSync } from 'node:zlib';
|
|
6
|
+
import { writeAll, writeAllStream } from './write-all.js';
|
|
2
7
|
const BLOCK = 512;
|
|
3
|
-
const UNPACKED_LIMIT =
|
|
8
|
+
export const UNPACKED_LIMIT = 1024 * 1024 * 1024;
|
|
9
|
+
const LINK_LIMIT = 4096;
|
|
10
|
+
const FILE_MODE = 0o644;
|
|
11
|
+
const EXEC_MODE = 0o755;
|
|
12
|
+
const DIR_MODE = 0o755;
|
|
13
|
+
export const formatOf = (file) => (/\.(?:tar\.gz|tgz)$/.test(file) ? 'tar.gz' : /\.zip$/.test(file) ? 'zip' : null);
|
|
4
14
|
function text(h, at, length) {
|
|
5
15
|
const raw = h.subarray(at, at + length);
|
|
6
16
|
const end = raw.indexOf(0);
|
|
@@ -25,6 +35,8 @@ function pax(body) {
|
|
|
25
35
|
const value = record.slice(eq + 1);
|
|
26
36
|
if (key === 'path')
|
|
27
37
|
out.path = value;
|
|
38
|
+
if (key === 'linkpath')
|
|
39
|
+
out.link = value;
|
|
28
40
|
if (key === 'size') {
|
|
29
41
|
if (!/^[0-9]+$/.test(value))
|
|
30
42
|
return null;
|
|
@@ -34,7 +46,7 @@ function pax(body) {
|
|
|
34
46
|
}
|
|
35
47
|
return out;
|
|
36
48
|
}
|
|
37
|
-
|
|
49
|
+
function tarEntries(archive) {
|
|
38
50
|
const refuse = (problem) => ({ ok: false, problem });
|
|
39
51
|
let tar;
|
|
40
52
|
try {
|
|
@@ -43,7 +55,7 @@ export function entryOf(archive, wanted) {
|
|
|
43
55
|
catch {
|
|
44
56
|
return refuse('is not a gzip stream within the size limit');
|
|
45
57
|
}
|
|
46
|
-
const
|
|
58
|
+
const entries = [];
|
|
47
59
|
let next = {};
|
|
48
60
|
for (let at = 0;;) {
|
|
49
61
|
if (at === tar.length)
|
|
@@ -59,7 +71,7 @@ export function entryOf(archive, wanted) {
|
|
|
59
71
|
if (octal(h, 148, 8) !== sum)
|
|
60
72
|
return refuse('has a damaged header');
|
|
61
73
|
const type = h[156] === 0 ? '0' : String.fromCharCode(h[156] ?? 0);
|
|
62
|
-
const meta = type === 'x' || type === 'g' || type === 'L';
|
|
74
|
+
const meta = type === 'x' || type === 'g' || type === 'L' || type === 'K';
|
|
63
75
|
const own = octal(h, 124, 12);
|
|
64
76
|
const size = meta ? own : (next.size ?? own);
|
|
65
77
|
if (size === null)
|
|
@@ -80,17 +92,457 @@ export function entryOf(archive, wanted) {
|
|
|
80
92
|
next = { ...next, path: body.toString('utf8').replace(/\0+$/, '') };
|
|
81
93
|
continue;
|
|
82
94
|
}
|
|
95
|
+
if (type === 'K') {
|
|
96
|
+
next = { ...next, link: body.toString('utf8').replace(/\0+$/, '') };
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
83
99
|
if (type === 'g')
|
|
84
100
|
continue;
|
|
85
101
|
const prefix = text(h, 257, 6).startsWith('ustar') ? text(h, 345, 155) : '';
|
|
86
102
|
const name = next.path ?? (prefix === '' ? text(h, 0, 100) : `${prefix}/${text(h, 0, 100)}`);
|
|
103
|
+
const link = next.link ?? text(h, 157, 100);
|
|
87
104
|
next = {};
|
|
88
|
-
|
|
89
|
-
found.push(Buffer.from(body));
|
|
105
|
+
entries.push({ name, type: type === '7' ? '0' : type, mode: octal(h, 100, 8) ?? 0, body, link });
|
|
90
106
|
}
|
|
107
|
+
return { ok: true, entries };
|
|
108
|
+
}
|
|
109
|
+
export function entryOf(archive, wanted) {
|
|
110
|
+
const read = tarEntries(archive);
|
|
111
|
+
if (!read.ok)
|
|
112
|
+
return read;
|
|
113
|
+
const found = read.entries.filter((e) => e.type === '0' && e.name.replace(/^\.\//, '') === wanted);
|
|
91
114
|
if (found.length === 0)
|
|
92
|
-
return
|
|
115
|
+
return { ok: false, problem: `holds no ${wanted}` };
|
|
93
116
|
if (found.length > 1)
|
|
94
|
-
return
|
|
95
|
-
return { ok: true, bytes: found[0] };
|
|
117
|
+
return { ok: false, problem: `holds ${wanted} more than once` };
|
|
118
|
+
return { ok: true, bytes: Buffer.from(found[0].body) };
|
|
119
|
+
}
|
|
120
|
+
const EOCD = 0x06054b50;
|
|
121
|
+
const CENTRAL = 0x02014b50;
|
|
122
|
+
const LOCAL = 0x04034b50;
|
|
123
|
+
const ZIP64_LOCATOR = 0x07064b50;
|
|
124
|
+
const UNIX = 3;
|
|
125
|
+
const S_IFMT = 0o170000;
|
|
126
|
+
function readAt(fd, position, length) {
|
|
127
|
+
const b = Buffer.alloc(length);
|
|
128
|
+
let got = 0;
|
|
129
|
+
while (got < length) {
|
|
130
|
+
const n = readSync(fd, b, got, length - got, position + got);
|
|
131
|
+
if (n === 0)
|
|
132
|
+
break;
|
|
133
|
+
got += n;
|
|
134
|
+
}
|
|
135
|
+
return got === length ? b : b.subarray(0, got);
|
|
136
|
+
}
|
|
137
|
+
function zipEntries(fd) {
|
|
138
|
+
const refuse = (problem) => ({ ok: false, problem });
|
|
139
|
+
const size = fstatSync(fd).size;
|
|
140
|
+
if (size < 22)
|
|
141
|
+
return refuse('is not a zip archive');
|
|
142
|
+
const tailStart = Math.max(0, size - 22 - 65535);
|
|
143
|
+
const tail = readAt(fd, tailStart, size - tailStart);
|
|
144
|
+
const ends = [];
|
|
145
|
+
for (let i = tail.length - 22; i >= 0; i--)
|
|
146
|
+
if (tail.readUInt32LE(i) === EOCD && i + 22 + tail.readUInt16LE(i + 20) === tail.length)
|
|
147
|
+
ends.push(i);
|
|
148
|
+
if (ends.length !== 1)
|
|
149
|
+
return refuse(ends.length === 0 ? 'is not a zip archive' : 'has more than one end record');
|
|
150
|
+
const e = ends[0];
|
|
151
|
+
const eocdAt = tailStart + e;
|
|
152
|
+
if (tail.readUInt16LE(e + 4) !== 0 || tail.readUInt16LE(e + 6) !== 0)
|
|
153
|
+
return refuse('is a split archive');
|
|
154
|
+
const count = tail.readUInt16LE(e + 10);
|
|
155
|
+
const cdSize = tail.readUInt32LE(e + 12);
|
|
156
|
+
const cdAt = tail.readUInt32LE(e + 16);
|
|
157
|
+
if (tail.readUInt16LE(e + 8) !== count || count === 0xffff || cdSize === 0xffffffff || cdAt === 0xffffffff)
|
|
158
|
+
return refuse('is a ZIP64 archive, which is not taken');
|
|
159
|
+
if (eocdAt >= 20 && readAt(fd, eocdAt - 20, 4).readUInt32LE(0) === ZIP64_LOCATOR)
|
|
160
|
+
return refuse('is a ZIP64 archive, which is not taken');
|
|
161
|
+
if (cdAt + cdSize !== eocdAt)
|
|
162
|
+
return refuse('has a central directory that does not end where its end record starts');
|
|
163
|
+
const cd = readAt(fd, cdAt, cdSize);
|
|
164
|
+
const entries = [];
|
|
165
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true });
|
|
166
|
+
let total = 0;
|
|
167
|
+
let at = 0;
|
|
168
|
+
for (let i = 0; i < count; i++) {
|
|
169
|
+
if (at + 46 > cd.length || cd.readUInt32LE(at) !== CENTRAL)
|
|
170
|
+
return refuse('has a damaged central directory');
|
|
171
|
+
const host = cd.readUInt8(at + 5);
|
|
172
|
+
const flags = cd.readUInt16LE(at + 8);
|
|
173
|
+
const method = cd.readUInt16LE(at + 10);
|
|
174
|
+
const crc = cd.readUInt32LE(at + 16);
|
|
175
|
+
const compressed = cd.readUInt32LE(at + 20);
|
|
176
|
+
const unpacked = cd.readUInt32LE(at + 24);
|
|
177
|
+
const nameLength = cd.readUInt16LE(at + 28);
|
|
178
|
+
const extraLength = cd.readUInt16LE(at + 30);
|
|
179
|
+
const commentLength = cd.readUInt16LE(at + 32);
|
|
180
|
+
const disk = cd.readUInt16LE(at + 34);
|
|
181
|
+
const external = cd.readUInt32LE(at + 38);
|
|
182
|
+
const localAt = cd.readUInt32LE(at + 42);
|
|
183
|
+
const raw = cd.subarray(at + 46, at + 46 + nameLength);
|
|
184
|
+
at += 46 + nameLength + extraLength + commentLength;
|
|
185
|
+
if (at > cd.length)
|
|
186
|
+
return refuse('has a damaged central directory');
|
|
187
|
+
if (compressed === 0xffffffff || unpacked === 0xffffffff || localAt === 0xffffffff || disk !== 0)
|
|
188
|
+
return refuse('is a ZIP64 archive, which is not taken');
|
|
189
|
+
if ((flags & 0x0001) !== 0 || (flags & 0x0040) !== 0 || (flags & 0x2000) !== 0)
|
|
190
|
+
return refuse('holds an encrypted entry');
|
|
191
|
+
if (method !== 0 && method !== 8)
|
|
192
|
+
return refuse('holds an entry compressed with a method other than stored or deflate');
|
|
193
|
+
if (method === 0 && compressed !== unpacked)
|
|
194
|
+
return refuse('holds a stored entry whose sizes disagree');
|
|
195
|
+
if ((flags & 0x0800) === 0 && raw.some((b) => b >= 0x80))
|
|
196
|
+
return refuse('holds a name that is not flagged as UTF-8');
|
|
197
|
+
let name;
|
|
198
|
+
try {
|
|
199
|
+
name = utf8.decode(raw);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return refuse('holds a name that is not UTF-8');
|
|
203
|
+
}
|
|
204
|
+
const unixMode = host === UNIX ? external >>> 16 : 0;
|
|
205
|
+
const type = unixMode & S_IFMT;
|
|
206
|
+
const slash = name.endsWith('/');
|
|
207
|
+
let kind;
|
|
208
|
+
if (type === 0o120000)
|
|
209
|
+
kind = 'link';
|
|
210
|
+
else if (type === 0o040000 || (type === 0 && slash))
|
|
211
|
+
kind = 'directory';
|
|
212
|
+
else if (type === 0o100000 || type === 0)
|
|
213
|
+
kind = 'file';
|
|
214
|
+
else
|
|
215
|
+
return refuse('holds an entry of a kind that is not taken (a device, a pipe or a socket)');
|
|
216
|
+
if ((kind === 'directory') !== slash)
|
|
217
|
+
return refuse('holds an entry whose kind and name disagree');
|
|
218
|
+
if (kind === 'link' && unpacked > LINK_LIMIT)
|
|
219
|
+
return refuse('holds a symbolic link with a target that is too long');
|
|
220
|
+
total += unpacked;
|
|
221
|
+
if (total > UNPACKED_LIMIT)
|
|
222
|
+
return refuse(`unpacks to more than ${UNPACKED_LIMIT} bytes`);
|
|
223
|
+
const local = readAt(fd, localAt, 30);
|
|
224
|
+
if (local.length !== 30 || local.readUInt32LE(0) !== LOCAL)
|
|
225
|
+
return refuse('has a damaged local header');
|
|
226
|
+
const localName = readAt(fd, localAt + 30, local.readUInt16LE(26));
|
|
227
|
+
if (!localName.equals(raw) || local.readUInt16LE(8) !== method || (local.readUInt16LE(6) & 0x0001) !== 0)
|
|
228
|
+
return refuse('has a local header that disagrees with its central directory');
|
|
229
|
+
const data = localAt + 30 + local.readUInt16LE(26) + local.readUInt16LE(28);
|
|
230
|
+
entries.push({ name, kind, mode: unixMode & 0o7777, method, crc, compressed, size: unpacked, local: localAt, data });
|
|
231
|
+
}
|
|
232
|
+
if (at !== cd.length)
|
|
233
|
+
return refuse('has a damaged central directory');
|
|
234
|
+
const byPlace = [...entries].sort((a, b) => a.local - b.local);
|
|
235
|
+
for (let i = 0; i < byPlace.length; i++) {
|
|
236
|
+
const x = byPlace[i];
|
|
237
|
+
const limit = i + 1 < byPlace.length ? byPlace[i + 1].local : cdAt;
|
|
238
|
+
if (x.local === limit || x.data + x.compressed > limit)
|
|
239
|
+
return refuse('holds entries whose data overlap');
|
|
240
|
+
}
|
|
241
|
+
return { ok: true, entries };
|
|
242
|
+
}
|
|
243
|
+
const CRC_TABLE = (() => {
|
|
244
|
+
const t = new Int32Array(256);
|
|
245
|
+
for (let n = 0; n < 256; n++) {
|
|
246
|
+
let c = n;
|
|
247
|
+
for (let k = 0; k < 8; k++)
|
|
248
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
249
|
+
t[n] = c;
|
|
250
|
+
}
|
|
251
|
+
return t;
|
|
252
|
+
})();
|
|
253
|
+
function crc32(crc, bytes) {
|
|
254
|
+
let c = ~crc;
|
|
255
|
+
for (let i = 0; i < bytes.length; i++)
|
|
256
|
+
c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
257
|
+
return ~c >>> 0;
|
|
258
|
+
}
|
|
259
|
+
function zipBytes(fd, e) {
|
|
260
|
+
const packed = readAt(fd, e.data, e.compressed);
|
|
261
|
+
if (packed.length !== e.compressed)
|
|
262
|
+
return null;
|
|
263
|
+
let bytes;
|
|
264
|
+
try {
|
|
265
|
+
bytes = e.method === 8 ? inflateRawSync(packed, { maxOutputLength: Math.max(e.size, 1) }) : packed;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return bytes.length === e.size && crc32(0, bytes) === e.crc ? bytes : null;
|
|
271
|
+
}
|
|
272
|
+
function withFile(path, fn) {
|
|
273
|
+
let fd;
|
|
274
|
+
try {
|
|
275
|
+
fd = openSync(path, 'r');
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
return { value: fn(fd) };
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
closeSync(fd);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const wholeFile = (path) => withFile(path, (fd) => readAt(fd, 0, fstatSync(fd).size))?.value ?? null;
|
|
288
|
+
export function archiveEntry(path, format, wanted) {
|
|
289
|
+
if (format === 'tar.gz') {
|
|
290
|
+
const bytes = wholeFile(path);
|
|
291
|
+
return bytes === null ? { ok: false, problem: 'cannot be read' } : entryOf(bytes, wanted);
|
|
292
|
+
}
|
|
293
|
+
const r = withFile(path, (fd) => {
|
|
294
|
+
const read = zipEntries(fd);
|
|
295
|
+
if (!read.ok)
|
|
296
|
+
return read;
|
|
297
|
+
const found = read.entries.filter((e) => e.kind === 'file' && e.name === wanted);
|
|
298
|
+
if (found.length === 0)
|
|
299
|
+
return { ok: false, problem: `holds no ${wanted}` };
|
|
300
|
+
if (found.length > 1)
|
|
301
|
+
return { ok: false, problem: `holds ${wanted} more than once` };
|
|
302
|
+
const bytes = zipBytes(fd, found[0]);
|
|
303
|
+
return bytes === null ? { ok: false, problem: `holds a damaged ${wanted}` } : { ok: true, bytes };
|
|
304
|
+
});
|
|
305
|
+
return r?.value ?? { ok: false, problem: 'cannot be read' };
|
|
306
|
+
}
|
|
307
|
+
const SEGMENT_BAD = /[\x00-\x1f\x7f\\]/;
|
|
308
|
+
const segmentProblem = (s) => s === '' || s === '.' || SEGMENT_BAD.test(s);
|
|
309
|
+
function below(name, top) {
|
|
310
|
+
const bare = name.replace(/^\.\//, '');
|
|
311
|
+
if (bare.startsWith('/'))
|
|
312
|
+
return { problem: 'holds an entry with an absolute path' };
|
|
313
|
+
if (bare === top || bare === `${top}/`)
|
|
314
|
+
return { segments: [] };
|
|
315
|
+
if (!bare.startsWith(`${top}/`))
|
|
316
|
+
return { problem: `holds an entry outside ${top}/` };
|
|
317
|
+
const rest = bare.slice(top.length + 1).replace(/\/$/, '');
|
|
318
|
+
const segments = rest.split('/');
|
|
319
|
+
if (segments.some((s) => s === '..'))
|
|
320
|
+
return { problem: 'holds an entry whose path climbs out of its directory (..)' };
|
|
321
|
+
if (segments.some(segmentProblem))
|
|
322
|
+
return { problem: 'holds an entry whose path has an empty or "." segment, a backslash or a control character' };
|
|
323
|
+
return { segments };
|
|
324
|
+
}
|
|
325
|
+
const linkTargetTaken = (target) => target !== '' && !target.startsWith('/') && target.split('/').every((s) => s !== '..' && !segmentProblem(s));
|
|
326
|
+
function plannedProblem(planned) {
|
|
327
|
+
const kinds = new Map();
|
|
328
|
+
for (const p of planned) {
|
|
329
|
+
const key = p.segments.join('/');
|
|
330
|
+
if (kinds.has(key))
|
|
331
|
+
return 'holds a name more than once';
|
|
332
|
+
kinds.set(key, p.kind);
|
|
333
|
+
}
|
|
334
|
+
for (const p of planned) {
|
|
335
|
+
for (let i = 1; i < p.segments.length; i++) {
|
|
336
|
+
const parent = kinds.get(p.segments.slice(0, i).join('/'));
|
|
337
|
+
if (parent !== undefined && parent !== 'directory')
|
|
338
|
+
return `holds an entry below a ${parent === 'link' ? 'symbolic link' : 'file'} of the archive`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
async function writePlanned(planned, dest) {
|
|
344
|
+
const pathOf = (p) => join(dest, ...p.segments);
|
|
345
|
+
const directory = (path) => {
|
|
346
|
+
mkdirSync(path, { recursive: true, mode: DIR_MODE });
|
|
347
|
+
chmodSync(path, DIR_MODE);
|
|
348
|
+
};
|
|
349
|
+
for (const p of planned)
|
|
350
|
+
if (p.kind === 'directory')
|
|
351
|
+
directory(pathOf(p));
|
|
352
|
+
for (const p of planned) {
|
|
353
|
+
if (p.kind !== 'file')
|
|
354
|
+
continue;
|
|
355
|
+
mkdirSync(dirname(pathOf(p)), { recursive: true, mode: DIR_MODE });
|
|
356
|
+
const problem = await p.write(pathOf(p));
|
|
357
|
+
if (problem !== null)
|
|
358
|
+
return problem;
|
|
359
|
+
}
|
|
360
|
+
for (const p of planned) {
|
|
361
|
+
if (p.kind !== 'link')
|
|
362
|
+
continue;
|
|
363
|
+
mkdirSync(dirname(pathOf(p)), { recursive: true, mode: DIR_MODE });
|
|
364
|
+
symlinkSync(p.link, pathOf(p));
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const modeOf = (mode) => ((mode & 0o111) !== 0 ? EXEC_MODE : FILE_MODE);
|
|
369
|
+
function writeBytes(target, bytes, mode) {
|
|
370
|
+
const fd = openSync(target, 'wx', mode);
|
|
371
|
+
try {
|
|
372
|
+
fchmodSync(fd, mode);
|
|
373
|
+
writeAll(fd, bytes);
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
closeSync(fd);
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
function planTar(archive, top) {
|
|
381
|
+
const read = tarEntries(archive);
|
|
382
|
+
if (!read.ok)
|
|
383
|
+
return { problem: read.problem };
|
|
384
|
+
const planned = [];
|
|
385
|
+
for (const e of read.entries) {
|
|
386
|
+
const where = below(e.name, top);
|
|
387
|
+
if ('problem' in where)
|
|
388
|
+
return where;
|
|
389
|
+
if (where.segments.length === 0)
|
|
390
|
+
continue;
|
|
391
|
+
if (e.type === '0') {
|
|
392
|
+
const mode = modeOf(e.mode);
|
|
393
|
+
planned.push({ segments: where.segments, kind: 'file', mode, size: e.body.length, write: (target) => writeBytes(target, e.body, mode) });
|
|
394
|
+
}
|
|
395
|
+
else if (e.type === '5')
|
|
396
|
+
planned.push({ segments: where.segments, kind: 'directory', mode: DIR_MODE, size: 0, write: () => null });
|
|
397
|
+
else if (e.type === '2') {
|
|
398
|
+
if (!linkTargetTaken(e.link))
|
|
399
|
+
return { problem: 'holds a symbolic link whose target is not a relative path below its own directory' };
|
|
400
|
+
planned.push({ segments: where.segments, kind: 'link', mode: 0, size: 0, link: e.link, write: () => null });
|
|
401
|
+
}
|
|
402
|
+
else if (e.type === '1')
|
|
403
|
+
return { problem: 'holds a hard link, which is not taken' };
|
|
404
|
+
else
|
|
405
|
+
return { problem: 'holds an entry of a kind that is not taken (a device, a pipe or a socket)' };
|
|
406
|
+
}
|
|
407
|
+
return { planned };
|
|
408
|
+
}
|
|
409
|
+
class Checked extends Transform {
|
|
410
|
+
limit;
|
|
411
|
+
size = 0;
|
|
412
|
+
crc = 0;
|
|
413
|
+
constructor(limit) {
|
|
414
|
+
super();
|
|
415
|
+
this.limit = limit;
|
|
416
|
+
}
|
|
417
|
+
_transform(chunk, _encoding, done) {
|
|
418
|
+
this.size += chunk.length;
|
|
419
|
+
if (this.size > this.limit)
|
|
420
|
+
return done(new Error('the entry is larger than the archive says'));
|
|
421
|
+
this.crc = crc32(this.crc, chunk);
|
|
422
|
+
done(null, chunk);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function planZip(fd, path, top) {
|
|
426
|
+
const read = zipEntries(fd);
|
|
427
|
+
if (!read.ok)
|
|
428
|
+
return { problem: read.problem };
|
|
429
|
+
const planned = [];
|
|
430
|
+
for (const e of read.entries) {
|
|
431
|
+
const where = below(e.name, top);
|
|
432
|
+
if ('problem' in where)
|
|
433
|
+
return where;
|
|
434
|
+
if (where.segments.length === 0)
|
|
435
|
+
continue;
|
|
436
|
+
if (e.kind === 'directory')
|
|
437
|
+
planned.push({ segments: where.segments, kind: 'directory', mode: DIR_MODE, size: 0, write: () => null });
|
|
438
|
+
else if (e.kind === 'link') {
|
|
439
|
+
const target = zipBytes(fd, e);
|
|
440
|
+
if (target === null)
|
|
441
|
+
return { problem: 'holds a damaged symbolic link' };
|
|
442
|
+
const link = target.toString('utf8');
|
|
443
|
+
if (!linkTargetTaken(link))
|
|
444
|
+
return { problem: 'holds a symbolic link whose target is not a relative path below its own directory' };
|
|
445
|
+
planned.push({ segments: where.segments, kind: 'link', mode: 0, size: 0, link, write: () => null });
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const mode = modeOf(e.mode);
|
|
449
|
+
planned.push({
|
|
450
|
+
segments: where.segments,
|
|
451
|
+
kind: 'file',
|
|
452
|
+
mode,
|
|
453
|
+
size: e.size,
|
|
454
|
+
write: async (target) => {
|
|
455
|
+
if (e.compressed === 0)
|
|
456
|
+
return e.size === 0 && e.crc === 0 ? writeBytes(target, Buffer.alloc(0), mode) : 'holds a damaged entry';
|
|
457
|
+
const checked = new Checked(e.size);
|
|
458
|
+
const source = createReadStream(path, { start: e.data, end: e.data + e.compressed - 1 });
|
|
459
|
+
let fd = null;
|
|
460
|
+
try {
|
|
461
|
+
fd = openSync(target, 'wx', mode);
|
|
462
|
+
fchmodSync(fd, mode);
|
|
463
|
+
if (e.method === 8)
|
|
464
|
+
await pipeline(source, createInflateRaw(), checked, writeAllStream(fd));
|
|
465
|
+
else
|
|
466
|
+
await pipeline(source, checked, writeAllStream(fd));
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
source.destroy();
|
|
470
|
+
return 'holds a damaged entry';
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
if (fd !== null)
|
|
474
|
+
closeSync(fd);
|
|
475
|
+
}
|
|
476
|
+
return checked.size === e.size && checked.crc === e.crc ? null : 'holds an entry whose size or CRC-32 does not match the archive';
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return { planned };
|
|
482
|
+
}
|
|
483
|
+
export async function unpackTree(path, format, top, dest) {
|
|
484
|
+
if (format === 'tar.gz') {
|
|
485
|
+
const bytes = wholeFile(path);
|
|
486
|
+
if (bytes === null)
|
|
487
|
+
return 'cannot be read';
|
|
488
|
+
const plan = planTar(bytes, top);
|
|
489
|
+
if ('problem' in plan)
|
|
490
|
+
return plan.problem;
|
|
491
|
+
return plannedProblem(plan.planned) ?? (await writePlanned(plan.planned, dest));
|
|
492
|
+
}
|
|
493
|
+
let fd;
|
|
494
|
+
try {
|
|
495
|
+
fd = openSync(path, 'r');
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return 'cannot be read';
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const plan = planZip(fd, path, top);
|
|
502
|
+
if ('problem' in plan)
|
|
503
|
+
return plan.problem;
|
|
504
|
+
return plannedProblem(plan.planned) ?? (await writePlanned(plan.planned, dest));
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
closeSync(fd);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
export function treeProblem(path, format, top) {
|
|
511
|
+
if (format === 'tar.gz') {
|
|
512
|
+
const bytes = wholeFile(path);
|
|
513
|
+
if (bytes === null)
|
|
514
|
+
return 'cannot be read';
|
|
515
|
+
const plan = planTar(bytes, top);
|
|
516
|
+
return 'problem' in plan ? plan.problem : plannedProblem(plan.planned);
|
|
517
|
+
}
|
|
518
|
+
const r = withFile(path, (fd) => {
|
|
519
|
+
const plan = planZip(fd, path, top);
|
|
520
|
+
return 'problem' in plan ? plan.problem : plannedProblem(plan.planned);
|
|
521
|
+
});
|
|
522
|
+
return r === null ? 'cannot be read' : r.value;
|
|
523
|
+
}
|
|
524
|
+
export function treeListing(path, format, top) {
|
|
525
|
+
const listed = (plan) => {
|
|
526
|
+
if ('problem' in plan)
|
|
527
|
+
return { ok: false, problem: plan.problem };
|
|
528
|
+
const problem = plannedProblem(plan.planned);
|
|
529
|
+
if (problem !== null)
|
|
530
|
+
return { ok: false, problem };
|
|
531
|
+
const paths = new Map();
|
|
532
|
+
for (const p of plan.planned) {
|
|
533
|
+
for (let i = 1; i < p.segments.length; i++) {
|
|
534
|
+
const parent = p.segments.slice(0, i).join('/');
|
|
535
|
+
if (!paths.has(parent))
|
|
536
|
+
paths.set(parent, { kind: 'directory', mode: DIR_MODE, size: 0 });
|
|
537
|
+
}
|
|
538
|
+
paths.set(p.segments.join('/'), { kind: p.kind, mode: p.kind === 'link' ? 0 : p.mode, size: p.size, ...(p.link !== undefined ? { link: p.link } : {}) });
|
|
539
|
+
}
|
|
540
|
+
return { ok: true, paths };
|
|
541
|
+
};
|
|
542
|
+
if (format === 'tar.gz') {
|
|
543
|
+
const bytes = wholeFile(path);
|
|
544
|
+
return bytes === null ? { ok: false, problem: 'cannot be read' } : listed(planTar(bytes, top));
|
|
545
|
+
}
|
|
546
|
+
const r = withFile(path, (fd) => listed(planZip(fd, path, top)));
|
|
547
|
+
return r === null ? { ok: false, problem: 'cannot be read' } : r.value;
|
|
96
548
|
}
|
package/dist/secrets.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface FileSecrets {
|
|
|
20
20
|
readonly directory: string;
|
|
21
21
|
list(): SecretListing;
|
|
22
22
|
put(name: string, value: string, options?: PutOptions): SecretHandle;
|
|
23
|
+
remove(name: string): boolean;
|
|
23
24
|
withLock<T>(fn: () => T): T;
|
|
24
25
|
}
|
|
25
26
|
export declare const isSecret: (handle: SecretHandle) => boolean;
|
package/dist/secrets.js
CHANGED
|
@@ -232,6 +232,8 @@ function putHandle(dir, connector, name, value, options) {
|
|
|
232
232
|
throw new Error('a handle name is upper-case letters, digits and underscores, starting with a letter');
|
|
233
233
|
if (typeof value !== 'string')
|
|
234
234
|
throw new Error('a secret value is a string');
|
|
235
|
+
if (value === '')
|
|
236
|
+
throw new TypeError('a secret value is never the empty string (use remove to take the handle away)');
|
|
235
237
|
if (options.expiresAt !== undefined && !validExpiry(options.expiresAt))
|
|
236
238
|
throw new Error('expiresAt must be an RFC 3339 UTC time ending in Z');
|
|
237
239
|
if (options.secret !== undefined && typeof options.secret !== 'boolean')
|
|
@@ -242,6 +244,37 @@ function putHandle(dir, connector, name, value, options) {
|
|
|
242
244
|
throw new Error(r.problem);
|
|
243
245
|
return r.value;
|
|
244
246
|
}
|
|
247
|
+
function removeHandle(dir, connector, name) {
|
|
248
|
+
if (!HANDLE_NAME.test(name))
|
|
249
|
+
throw new Error('a handle name is upper-case letters, digits and underscores, starting with a letter');
|
|
250
|
+
try {
|
|
251
|
+
lstatSync(dir);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
if (e.code === 'ENOENT')
|
|
255
|
+
return false;
|
|
256
|
+
throw new Error(`connector ${connector}: the secrets directory cannot be read`);
|
|
257
|
+
}
|
|
258
|
+
const dirProblem = entryProblem(dir, 'directory', 'the secrets directory');
|
|
259
|
+
if (dirProblem !== null)
|
|
260
|
+
throw new Error(`connector ${connector}: ${dirProblem}`);
|
|
261
|
+
const r = locked(dir, connector, () => {
|
|
262
|
+
let existed = true;
|
|
263
|
+
try {
|
|
264
|
+
rmSync(join(dir, name));
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
if (e.code !== 'ENOENT')
|
|
268
|
+
throw e;
|
|
269
|
+
existed = false;
|
|
270
|
+
}
|
|
271
|
+
rmSync(join(dir, name + META_SUFFIX), { force: true });
|
|
272
|
+
return existed;
|
|
273
|
+
});
|
|
274
|
+
if (!r.ok)
|
|
275
|
+
throw new Error(r.problem);
|
|
276
|
+
return r.value;
|
|
277
|
+
}
|
|
245
278
|
export function openFileSecrets(options) {
|
|
246
279
|
if (typeof options?.configDir !== 'string' || !isAbsolute(options.configDir))
|
|
247
280
|
throw new Error('configDir must be an absolute path');
|
|
@@ -253,6 +286,7 @@ export function openFileSecrets(options) {
|
|
|
253
286
|
directory: dir,
|
|
254
287
|
list: () => listHandles(dir, connector),
|
|
255
288
|
put: (name, value, putOptions = {}) => putHandle(dir, connector, name, value, putOptions),
|
|
289
|
+
remove: (name) => removeHandle(dir, connector, name),
|
|
256
290
|
withLock: (fn) => {
|
|
257
291
|
ensureDirectory(dir, connector);
|
|
258
292
|
const r = locked(dir, connector, fn);
|
package/dist/session.js
CHANGED
|
@@ -118,7 +118,7 @@ export class Session {
|
|
|
118
118
|
switch (m.method) {
|
|
119
119
|
case 'tools/call': {
|
|
120
120
|
if (typeof name !== 'string' || !isGranted(grant, name))
|
|
121
|
-
return [up(errorLine(id, INVALID_PARAMS, 'the requested tool is not in the granted scope', FORBIDDEN_SCOPE)), ...this.settle(call, 'refused', FORBIDDEN_SCOPE)];
|
|
121
|
+
return id === null ? [] : [up(errorLine(id, INVALID_PARAMS, 'the requested tool is not in the granted scope', FORBIDDEN_SCOPE)), ...this.settle(call, 'refused', FORBIDDEN_SCOPE)];
|
|
122
122
|
if (this.options.parked === true)
|
|
123
123
|
return id === null ? [] : [up(errorLine(id, INTERNAL_ERROR, noCredentials(this.options.connector), AUTH_REQUIRED)), ...this.settle(call, 'refused', AUTH_REQUIRED)];
|
|
124
124
|
return this.forward(raw, id, grant, undefined, undefined, call);
|
package/dist/write-all.d.ts
CHANGED
package/dist/write-all.js
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
import { fstatSync, fsyncSync, writeSync } from 'node:fs';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
function writeAllAt(fd, bytes, position) {
|
|
4
4
|
let written = 0;
|
|
5
5
|
while (written < bytes.length) {
|
|
6
|
-
const n = writeSync(fd, bytes, written, bytes.length - written, written);
|
|
6
|
+
const n = writeSync(fd, bytes, written, bytes.length - written, position + written);
|
|
7
7
|
if (n <= 0)
|
|
8
8
|
throw new Error(`a write stopped after ${written} of ${bytes.length} bytes`);
|
|
9
9
|
written += n;
|
|
10
10
|
}
|
|
11
|
+
return position + written;
|
|
12
|
+
}
|
|
13
|
+
function finished(fd, size) {
|
|
11
14
|
fsyncSync(fd);
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
14
|
-
throw new Error(`the file holds ${
|
|
15
|
+
const held = fstatSync(fd).size;
|
|
16
|
+
if (held !== size)
|
|
17
|
+
throw new Error(`the file holds ${held} bytes after a write of ${size}`);
|
|
18
|
+
}
|
|
19
|
+
export function writeAll(fd, data) {
|
|
20
|
+
const bytes = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
21
|
+
finished(fd, writeAllAt(fd, bytes, 0));
|
|
22
|
+
}
|
|
23
|
+
export function writeAllStream(fd) {
|
|
24
|
+
let position = 0;
|
|
25
|
+
return new Writable({
|
|
26
|
+
write(piece, _encoding, done) {
|
|
27
|
+
try {
|
|
28
|
+
position = writeAllAt(fd, piece, position);
|
|
29
|
+
done();
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
done(e);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
final(done) {
|
|
36
|
+
try {
|
|
37
|
+
finished(fd, position);
|
|
38
|
+
done();
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
done(e);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
15
45
|
}
|
|
16
46
|
export function writeFailure(e) {
|
|
17
47
|
const code = e?.code;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/connectors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Shared connector layer and the sema-connector launcher (MCP stdio proxy with principal, scope, normalize, status and trace stages).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|