@sema-agent/connectors 0.1.0 → 0.3.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 +116 -0
- package/dist/cli-kit.d.ts +34 -0
- package/dist/cli-kit.js +224 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/jwks.d.ts +1 -0
- package/dist/jwks.js +4 -1
- package/dist/launcher.js +73 -58
- package/dist/lock-file.d.ts +1 -0
- package/dist/lock-file.js +34 -0
- package/dist/mount.d.ts +5 -1
- package/dist/mount.js +23 -36
- package/dist/pinned-upstream.d.ts +21 -0
- package/dist/pinned-upstream.js +347 -0
- package/dist/release-archive.d.ts +8 -0
- package/dist/release-archive.js +96 -0
- package/dist/resolve-connector.d.ts +8 -0
- package/dist/resolve-connector.js +28 -0
- package/dist/scope.js +2 -0
- package/dist/secrets.d.ts +16 -1
- package/dist/secrets.js +139 -38
- package/dist/seed.d.ts +35 -0
- package/dist/seed.js +134 -0
- package/dist/status.d.ts +1 -1
- package/dist/status.js +3 -1
- package/dist/write-all.d.ts +2 -0
- package/dist/write-all.js +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { chmodSync, closeSync, constants, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
4
|
+
import { get as httpsGet } from 'node:https';
|
|
5
|
+
import { constants as osConstants } from 'node:os';
|
|
6
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { cliOutput, listed } from './cli-kit.js';
|
|
9
|
+
import { given } from './config-dir.js';
|
|
10
|
+
import { CONNECTOR_NAME } from './connector-name.js';
|
|
11
|
+
import { entryOf } from './release-archive.js';
|
|
12
|
+
import { HANDLE_NAME, openFileSecrets } from './secrets.js';
|
|
13
|
+
import { writeAll } from './write-all.js';
|
|
14
|
+
const CONNECTOR_DIR = 'CONNECTOR_DIR';
|
|
15
|
+
const BIN_DIR = 'bin';
|
|
16
|
+
const DIRECTORY_MODE = 0o700;
|
|
17
|
+
const BINARY_MODE = 0o755;
|
|
18
|
+
const ARCHIVE_MODE = 0o644;
|
|
19
|
+
const DOWNLOAD_LIMIT = 256 * 1024 * 1024;
|
|
20
|
+
const DOWNLOAD_IDLE_MS = 120_000;
|
|
21
|
+
const REDIRECT_LIMIT = 5;
|
|
22
|
+
const SIGNALS = ['SIGTERM', 'SIGINT', 'SIGHUP'];
|
|
23
|
+
const VERSION = /^[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
24
|
+
const PLATFORM = /^[a-z0-9]+-[a-z0-9]+$/;
|
|
25
|
+
const FILE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
26
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
27
|
+
const platformKey = () => `${process.platform}-${process.arch}`;
|
|
28
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
29
|
+
const keysAre = (v, keys) => Object.keys(v).sort().join('\0') === [...keys].sort().join('\0');
|
|
30
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
31
|
+
const below = (dir, file) => {
|
|
32
|
+
if (!isAbsolute(file))
|
|
33
|
+
return false;
|
|
34
|
+
const r = relative(dir, file);
|
|
35
|
+
return r !== '' && !isAbsolute(r) && r.split(sep)[0] !== '..';
|
|
36
|
+
};
|
|
37
|
+
const isRegularFile = (path) => {
|
|
38
|
+
try {
|
|
39
|
+
return lstatSync(path).isFile();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function regularFile(path) {
|
|
46
|
+
let fd;
|
|
47
|
+
try {
|
|
48
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return fstatSync(fd).isFile() ? readFileSync(fd) : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
closeSync(fd);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function fetchHttps(url, redirects) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
let settled = false;
|
|
66
|
+
const done = (r) => {
|
|
67
|
+
if (settled)
|
|
68
|
+
return;
|
|
69
|
+
settled = true;
|
|
70
|
+
resolve(r);
|
|
71
|
+
};
|
|
72
|
+
const req = httpsGet(url, (res) => {
|
|
73
|
+
const status = res.statusCode ?? 0;
|
|
74
|
+
const location = res.headers.location;
|
|
75
|
+
if (status >= 300 && status < 400 && location !== undefined) {
|
|
76
|
+
res.resume();
|
|
77
|
+
let next;
|
|
78
|
+
try {
|
|
79
|
+
next = new URL(location, url);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return done({ ok: false, problem: 'the download was redirected to something that is not a URL' });
|
|
83
|
+
}
|
|
84
|
+
if (redirects === 0)
|
|
85
|
+
return done({ ok: false, problem: `the download was redirected more than ${REDIRECT_LIMIT} times` });
|
|
86
|
+
if (next.protocol !== 'https:' || next.username !== '' || next.password !== '')
|
|
87
|
+
return done({ ok: false, problem: 'the download was redirected away from https' });
|
|
88
|
+
return void fetchHttps(next, redirects - 1).then(done);
|
|
89
|
+
}
|
|
90
|
+
if (status !== 200) {
|
|
91
|
+
res.resume();
|
|
92
|
+
return done({ ok: false, problem: `the download answered HTTP ${status}` });
|
|
93
|
+
}
|
|
94
|
+
const parts = [];
|
|
95
|
+
let size = 0;
|
|
96
|
+
res.on('data', (d) => {
|
|
97
|
+
size += d.length;
|
|
98
|
+
if (size > DOWNLOAD_LIMIT) {
|
|
99
|
+
req.destroy();
|
|
100
|
+
done({ ok: false, problem: `the archive is larger than ${DOWNLOAD_LIMIT} bytes` });
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
parts.push(d);
|
|
104
|
+
});
|
|
105
|
+
res.on('end', () => done({ ok: true, bytes: Buffer.concat(parts) }));
|
|
106
|
+
res.on('error', () => done({ ok: false, problem: 'the download broke off' }));
|
|
107
|
+
});
|
|
108
|
+
req.setTimeout(DOWNLOAD_IDLE_MS, () => req.destroy(new Error(`no data for ${DOWNLOAD_IDLE_MS / 1000} s`)));
|
|
109
|
+
req.on('error', (e) => done({ ok: false, problem: `the download failed (${e.message})` }));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async function fetchArchive(url) {
|
|
113
|
+
if (url.protocol === 'https:')
|
|
114
|
+
return fetchHttps(url, REDIRECT_LIMIT);
|
|
115
|
+
const path = fileURLToPath(url);
|
|
116
|
+
try {
|
|
117
|
+
const st = statSync(path);
|
|
118
|
+
if (!st.isFile())
|
|
119
|
+
return { ok: false, problem: `${path} is not a file` };
|
|
120
|
+
if (st.size > DOWNLOAD_LIMIT)
|
|
121
|
+
return { ok: false, problem: `the archive is larger than ${DOWNLOAD_LIMIT} bytes` };
|
|
122
|
+
return { ok: true, bytes: readFileSync(path) };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { ok: false, problem: `${path} cannot be read` };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function pinnedUpstream(options) {
|
|
129
|
+
const { connector, upstream, lockFile } = options ?? {};
|
|
130
|
+
if (typeof connector !== 'string' || !CONNECTOR_NAME.test(connector))
|
|
131
|
+
throw new Error('connector must be lowercase letters and digits in hyphen-separated words');
|
|
132
|
+
if (typeof upstream !== 'string' || !FILE.test(upstream))
|
|
133
|
+
throw new Error('upstream must be a file name: letters, digits, dots, underscores and hyphens, starting with a letter or digit');
|
|
134
|
+
if (!(lockFile instanceof URL) || lockFile.protocol !== 'file:')
|
|
135
|
+
throw new Error('lockFile must be a file URL');
|
|
136
|
+
const handles = options.handles;
|
|
137
|
+
if (!Array.isArray(handles) || !handles.every((h) => typeof h === 'string' && HANDLE_NAME.test(h)) || new Set(handles).size !== handles.length)
|
|
138
|
+
throw new Error('handles must list handle names, each once');
|
|
139
|
+
const { program, say, complain } = cliOutput(connector);
|
|
140
|
+
const fileVars = handles.map((h) => `${h}_FILE`);
|
|
141
|
+
const binaryOf = (connectorDir) => join(connectorDir, BIN_DIR, upstream);
|
|
142
|
+
function pinnedBuild() {
|
|
143
|
+
const refuse = (problem) => ({ ok: false, problem: `binaries.lock.json ${problem}` });
|
|
144
|
+
const isBuild = (v) => isObject(v) && keysAre(v, ['file', 'sha256']) && typeof v.file === 'string' && FILE.test(v.file) && v.file !== upstream && typeof v.sha256 === 'string' && SHA256.test(v.sha256);
|
|
145
|
+
let doc;
|
|
146
|
+
try {
|
|
147
|
+
doc = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return refuse('cannot be read as JSON');
|
|
151
|
+
}
|
|
152
|
+
if (!isObject(doc) || !keysAre(doc, [upstream]))
|
|
153
|
+
return refuse(`must hold exactly one entry, "${upstream}"`);
|
|
154
|
+
const entry = doc[upstream];
|
|
155
|
+
if (!isObject(entry) || !keysAre(entry, ['version', 'source', 'platforms']))
|
|
156
|
+
return refuse('must give exactly version, source and platforms');
|
|
157
|
+
const { version, source, platforms } = entry;
|
|
158
|
+
if (typeof version !== 'string' || !VERSION.test(version))
|
|
159
|
+
return refuse('must give the version as <major>.<minor>.<patch>');
|
|
160
|
+
if (typeof source !== 'string' || !source.includes('{file}'))
|
|
161
|
+
return refuse('must give the source as a URL template naming {file}');
|
|
162
|
+
if (!isObject(platforms) || !Object.entries(platforms).every(([k, v]) => PLATFORM.test(k) && isBuild(v)))
|
|
163
|
+
return refuse('must map each platform to {"file": <archive name>, "sha256": <64 hex digits>}');
|
|
164
|
+
const key = platformKey();
|
|
165
|
+
const build = Object.hasOwn(platforms, key) ? platforms[key] : undefined;
|
|
166
|
+
if (!isBuild(build))
|
|
167
|
+
return refuse(`pins no ${upstream} build for ${key}`);
|
|
168
|
+
return { ok: true, lock: { version, source, platforms: platforms }, build };
|
|
169
|
+
}
|
|
170
|
+
function sourceOf(lock, build) {
|
|
171
|
+
let url;
|
|
172
|
+
try {
|
|
173
|
+
url = new URL(lock.source.replaceAll('{version}', lock.version).replaceAll('{file}', build.file));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return { ok: false, problem: 'the source in binaries.lock.json is not a URL' };
|
|
177
|
+
}
|
|
178
|
+
if (url.protocol === 'https:' && url.username === '' && url.password === '')
|
|
179
|
+
return { ok: true, url };
|
|
180
|
+
if (url.protocol === 'file:' && (url.host === '' || url.host === 'localhost'))
|
|
181
|
+
return { ok: true, url };
|
|
182
|
+
return { ok: false, problem: 'the source in binaries.lock.json must be an https or file URL (an https URL without credentials)' };
|
|
183
|
+
}
|
|
184
|
+
function installedProblem(connectorDir, build) {
|
|
185
|
+
const dir = join(connectorDir, BIN_DIR);
|
|
186
|
+
let st;
|
|
187
|
+
try {
|
|
188
|
+
st = lstatSync(dir);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return `${upstream} is not installed`;
|
|
192
|
+
}
|
|
193
|
+
if (!st.isDirectory() || (st.mode & 0o777) !== DIRECTORY_MODE)
|
|
194
|
+
return 'the bin directory of the connector is not a 0700 directory';
|
|
195
|
+
const archive = regularFile(join(dir, build.file));
|
|
196
|
+
if (archive === null)
|
|
197
|
+
return `${upstream} is not installed (the pinned archive is not there)`;
|
|
198
|
+
if (sha256(archive) !== build.sha256)
|
|
199
|
+
return 'the archive there is not the one binaries.lock.json pins';
|
|
200
|
+
const entry = entryOf(archive, upstream);
|
|
201
|
+
if (!entry.ok)
|
|
202
|
+
return `the pinned archive ${entry.problem}`;
|
|
203
|
+
const binary = binaryOf(connectorDir);
|
|
204
|
+
const bytes = regularFile(binary);
|
|
205
|
+
if (bytes === null || !bytes.equals(entry.bytes))
|
|
206
|
+
return `${upstream} there is not the entry of the pinned archive`;
|
|
207
|
+
if ((lstatSync(binary).mode & 0o777) !== BINARY_MODE)
|
|
208
|
+
return `${upstream} there is not mode 0755`;
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function writeAtomically(path, bytes, mode) {
|
|
212
|
+
const tmp = join(dirname(path), `.${upstream}.${randomBytes(6).toString('hex')}.tmp`);
|
|
213
|
+
try {
|
|
214
|
+
const fd = openSync(tmp, 'wx', mode);
|
|
215
|
+
try {
|
|
216
|
+
fchmodSync(fd, mode);
|
|
217
|
+
writeAll(fd, bytes);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
closeSync(fd);
|
|
221
|
+
}
|
|
222
|
+
renameSync(tmp, path);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
rmSync(tmp, { force: true });
|
|
226
|
+
throw e;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function install(configDir) {
|
|
230
|
+
const pinned = pinnedBuild();
|
|
231
|
+
if (!pinned.ok) {
|
|
232
|
+
complain(`${pinned.problem}; nothing was installed`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
const { lock, build } = pinned;
|
|
236
|
+
const secrets = openFileSecrets({ configDir, connector });
|
|
237
|
+
const before = secrets.list();
|
|
238
|
+
if (!before.ok) {
|
|
239
|
+
complain(`${before.problem}; nothing was installed`);
|
|
240
|
+
return 1;
|
|
241
|
+
}
|
|
242
|
+
const connectorDir = secrets.directory;
|
|
243
|
+
const binDir = join(connectorDir, BIN_DIR);
|
|
244
|
+
if (installedProblem(connectorDir, build) === null) {
|
|
245
|
+
say(`${upstream} ${lock.version} is already installed in ${binDir}; nothing changed`);
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
const source = sourceOf(lock, build);
|
|
249
|
+
if (!source.ok) {
|
|
250
|
+
complain(`${source.problem}; nothing was installed`);
|
|
251
|
+
return 1;
|
|
252
|
+
}
|
|
253
|
+
const fetched = await fetchArchive(source.url);
|
|
254
|
+
if (!fetched.ok) {
|
|
255
|
+
complain(`${build.file} could not be fetched: ${fetched.problem}; nothing was installed`);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
const actual = sha256(fetched.bytes);
|
|
259
|
+
if (actual !== build.sha256) {
|
|
260
|
+
complain(`the fetched ${build.file} does not match binaries.lock.json (sha256 ${actual}, pinned ${build.sha256}); nothing was installed`);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
const entry = entryOf(fetched.bytes, upstream);
|
|
264
|
+
if (!entry.ok) {
|
|
265
|
+
complain(`the pinned archive ${build.file} ${entry.problem}; nothing was installed`);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
mkdirSync(binDir, { recursive: true, mode: DIRECTORY_MODE });
|
|
270
|
+
const after = secrets.list();
|
|
271
|
+
if (!after.ok)
|
|
272
|
+
throw new Error(after.problem);
|
|
273
|
+
const st = lstatSync(binDir);
|
|
274
|
+
if (!st.isDirectory())
|
|
275
|
+
throw new Error('the bin directory of the connector is not a directory');
|
|
276
|
+
chmodSync(binDir, DIRECTORY_MODE);
|
|
277
|
+
writeAtomically(join(binDir, build.file), fetched.bytes, ARCHIVE_MODE);
|
|
278
|
+
writeAtomically(binaryOf(connectorDir), entry.bytes, BINARY_MODE);
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
complain(`${upstream} was not installed: ${e instanceof Error ? e.message : 'unknown error'}`);
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
const problem = installedProblem(connectorDir, build);
|
|
285
|
+
if (problem !== null) {
|
|
286
|
+
complain(`${upstream} was written but does not read back as the pinned build: ${problem}`);
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
say(`installed ${upstream} ${lock.version} (${build.file}, sha256 ${build.sha256}) in ${binDir}`);
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
function run(binary, args, env) {
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
const child = spawn(binary, [...args], { stdio: 'inherit', env });
|
|
295
|
+
const forwarders = SIGNALS.map((signal) => [signal, () => void child.kill(signal)]);
|
|
296
|
+
for (const [signal, forward] of forwarders)
|
|
297
|
+
process.on(signal, forward);
|
|
298
|
+
const stopForwarding = () => {
|
|
299
|
+
for (const [signal, forward] of forwarders)
|
|
300
|
+
process.off(signal, forward);
|
|
301
|
+
};
|
|
302
|
+
child.on('error', (e) => {
|
|
303
|
+
stopForwarding();
|
|
304
|
+
complain(`${upstream} could not be started (${e.code ?? e.message})`);
|
|
305
|
+
resolve(1);
|
|
306
|
+
});
|
|
307
|
+
child.on('exit', (code, signal) => {
|
|
308
|
+
stopForwarding();
|
|
309
|
+
if (signal !== null) {
|
|
310
|
+
process.kill(process.pid, signal);
|
|
311
|
+
setTimeout(() => resolve(128 + (osConstants.signals[signal] ?? 0)), 1000);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
resolve(code ?? 1);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function start(env) {
|
|
319
|
+
const refuse = (problem) => ({ ok: false, problem });
|
|
320
|
+
const connectorDir = env[CONNECTOR_DIR];
|
|
321
|
+
if (!given(connectorDir) || !isAbsolute(connectorDir))
|
|
322
|
+
return refuse(`${CONNECTOR_DIR} does not name the connector's directory: start the connector with sema-connector run ${connector}`);
|
|
323
|
+
const paths = fileVars.map((v) => env[v]);
|
|
324
|
+
if (!paths.every(given))
|
|
325
|
+
return refuse(`connector ${connector} has no ${listed(handles)} ${handles.length === 1 ? 'handle' : 'handles'} to start ${upstream} with: store ${handles.length === 1 ? 'it' : 'them'} with ${program} auth`);
|
|
326
|
+
if (!paths.every((p) => below(connectorDir, p) && isRegularFile(p)))
|
|
327
|
+
return refuse(`${listed(fileVars)} must name files in the connector's directory (${CONNECTOR_DIR}), as the launcher hands ${handles.length === 1 ? 'it' : 'them'}: start the connector with sema-connector run ${connector}`);
|
|
328
|
+
const pinned = pinnedBuild();
|
|
329
|
+
if (!pinned.ok)
|
|
330
|
+
return refuse(pinned.problem);
|
|
331
|
+
const problem = installedProblem(connectorDir, pinned.build);
|
|
332
|
+
if (problem !== null)
|
|
333
|
+
return refuse(`${problem}: run ${program} install`);
|
|
334
|
+
const own = {};
|
|
335
|
+
for (const [k, v] of Object.entries(env))
|
|
336
|
+
if (v !== undefined && k !== CONNECTOR_DIR && !fileVars.includes(k))
|
|
337
|
+
own[k] = v;
|
|
338
|
+
const binary = binaryOf(connectorDir);
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
connectorDir,
|
|
342
|
+
files: Object.fromEntries(handles.map((h, i) => [h, paths[i]])),
|
|
343
|
+
run: (args, extra) => run(binary, args, { ...own, ...extra }),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return { install, start };
|
|
347
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { gunzipSync } from 'node:zlib';
|
|
2
|
+
const BLOCK = 512;
|
|
3
|
+
const UNPACKED_LIMIT = 512 * 1024 * 1024;
|
|
4
|
+
function text(h, at, length) {
|
|
5
|
+
const raw = h.subarray(at, at + length);
|
|
6
|
+
const end = raw.indexOf(0);
|
|
7
|
+
return raw.subarray(0, end === -1 ? length : end).toString('utf8');
|
|
8
|
+
}
|
|
9
|
+
function octal(h, at, length) {
|
|
10
|
+
const t = text(h, at, length).trim();
|
|
11
|
+
return /^[0-7]+$/.test(t) ? parseInt(t, 8) : null;
|
|
12
|
+
}
|
|
13
|
+
function pax(body) {
|
|
14
|
+
const out = {};
|
|
15
|
+
for (let at = 0; at < body.length;) {
|
|
16
|
+
const space = body.indexOf(0x20, at);
|
|
17
|
+
const length = space === -1 ? NaN : Number(body.subarray(at, space).toString('latin1'));
|
|
18
|
+
if (!Number.isInteger(length) || length <= space - at || at + length > body.length)
|
|
19
|
+
return null;
|
|
20
|
+
const record = body.subarray(space + 1, at + length - 1).toString('utf8');
|
|
21
|
+
const eq = record.indexOf('=');
|
|
22
|
+
if (eq === -1)
|
|
23
|
+
return null;
|
|
24
|
+
const key = record.slice(0, eq);
|
|
25
|
+
const value = record.slice(eq + 1);
|
|
26
|
+
if (key === 'path')
|
|
27
|
+
out.path = value;
|
|
28
|
+
if (key === 'size') {
|
|
29
|
+
if (!/^[0-9]+$/.test(value))
|
|
30
|
+
return null;
|
|
31
|
+
out.size = Number(value);
|
|
32
|
+
}
|
|
33
|
+
at += length;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export function entryOf(archive, wanted) {
|
|
38
|
+
const refuse = (problem) => ({ ok: false, problem });
|
|
39
|
+
let tar;
|
|
40
|
+
try {
|
|
41
|
+
tar = gunzipSync(archive, { maxOutputLength: UNPACKED_LIMIT });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return refuse('is not a gzip stream within the size limit');
|
|
45
|
+
}
|
|
46
|
+
const found = [];
|
|
47
|
+
let next = {};
|
|
48
|
+
for (let at = 0;;) {
|
|
49
|
+
if (at === tar.length)
|
|
50
|
+
break;
|
|
51
|
+
if (at + BLOCK > tar.length)
|
|
52
|
+
return refuse('ends inside a header');
|
|
53
|
+
const h = tar.subarray(at, at + BLOCK);
|
|
54
|
+
if (h.every((b) => b === 0))
|
|
55
|
+
break;
|
|
56
|
+
let sum = 0;
|
|
57
|
+
for (let i = 0; i < BLOCK; i++)
|
|
58
|
+
sum += i >= 148 && i < 156 ? 0x20 : (h[i] ?? 0);
|
|
59
|
+
if (octal(h, 148, 8) !== sum)
|
|
60
|
+
return refuse('has a damaged header');
|
|
61
|
+
const type = h[156] === 0 ? '0' : String.fromCharCode(h[156] ?? 0);
|
|
62
|
+
const meta = type === 'x' || type === 'g' || type === 'L';
|
|
63
|
+
const own = octal(h, 124, 12);
|
|
64
|
+
const size = meta ? own : (next.size ?? own);
|
|
65
|
+
if (size === null)
|
|
66
|
+
return refuse('has a damaged header');
|
|
67
|
+
const start = at + BLOCK;
|
|
68
|
+
if (start + size > tar.length)
|
|
69
|
+
return refuse('ends inside an entry');
|
|
70
|
+
const body = tar.subarray(start, start + size);
|
|
71
|
+
at = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
72
|
+
if (type === 'x') {
|
|
73
|
+
const records = pax(body);
|
|
74
|
+
if (records === null)
|
|
75
|
+
return refuse('has a damaged extended header');
|
|
76
|
+
next = { ...next, ...records };
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (type === 'L') {
|
|
80
|
+
next = { ...next, path: body.toString('utf8').replace(/\0+$/, '') };
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (type === 'g')
|
|
84
|
+
continue;
|
|
85
|
+
const prefix = text(h, 257, 6).startsWith('ustar') ? text(h, 345, 155) : '';
|
|
86
|
+
const name = next.path ?? (prefix === '' ? text(h, 0, 100) : `${prefix}/${text(h, 0, 100)}`);
|
|
87
|
+
next = {};
|
|
88
|
+
if ((type === '0' || type === '7') && name.replace(/^\.\//, '') === wanted)
|
|
89
|
+
found.push(Buffer.from(body));
|
|
90
|
+
}
|
|
91
|
+
if (found.length === 0)
|
|
92
|
+
return refuse(`holds no ${wanted}`);
|
|
93
|
+
if (found.length > 1)
|
|
94
|
+
return refuse(`holds ${wanted} more than once`);
|
|
95
|
+
return { ok: true, bytes: found[0] };
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export function resolveConnector(name) {
|
|
5
|
+
const packageName = `@sema-agent/connector-${name}`;
|
|
6
|
+
let manifestPath;
|
|
7
|
+
try {
|
|
8
|
+
manifestPath = createRequire(new URL('../package.json', import.meta.url)).resolve(`${packageName}/package.json`);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return { packageName, reason: 'is not installed where the launcher can resolve it' };
|
|
12
|
+
}
|
|
13
|
+
let bin;
|
|
14
|
+
try {
|
|
15
|
+
bin = JSON.parse(readFileSync(manifestPath, 'utf8')).bin;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { packageName, reason: 'has a package.json that cannot be read' };
|
|
19
|
+
}
|
|
20
|
+
let rel;
|
|
21
|
+
if (typeof bin === 'string')
|
|
22
|
+
rel = bin;
|
|
23
|
+
else if (bin !== null && typeof bin === 'object' && !Array.isArray(bin) && Object.keys(bin).length === 1)
|
|
24
|
+
rel = Object.values(bin)[0];
|
|
25
|
+
if (typeof rel !== 'string' || rel === '')
|
|
26
|
+
return { packageName, reason: 'does not declare exactly one bin' };
|
|
27
|
+
return { packageName, binPath: join(dirname(manifestPath), rel) };
|
|
28
|
+
}
|
package/dist/scope.js
CHANGED
package/dist/secrets.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface SecretHandle {
|
|
|
2
2
|
readonly connector: string;
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly source: 'file';
|
|
5
|
+
readonly secret: boolean;
|
|
5
6
|
readonly expiresAt?: string;
|
|
6
7
|
}
|
|
7
8
|
export type SecretListing = {
|
|
@@ -13,21 +14,35 @@ export type SecretListing = {
|
|
|
13
14
|
};
|
|
14
15
|
export interface PutOptions {
|
|
15
16
|
readonly expiresAt?: string;
|
|
17
|
+
readonly secret?: boolean;
|
|
16
18
|
}
|
|
17
19
|
export interface FileSecrets {
|
|
20
|
+
readonly directory: string;
|
|
18
21
|
list(): SecretListing;
|
|
19
22
|
put(name: string, value: string, options?: PutOptions): SecretHandle;
|
|
23
|
+
withLock<T>(fn: () => T): T;
|
|
20
24
|
}
|
|
25
|
+
export declare const isSecret: (handle: SecretHandle) => boolean;
|
|
21
26
|
export declare const HANDLE_NAME: RegExp;
|
|
27
|
+
export declare const LOCK_FILE = ".auth.lock";
|
|
28
|
+
export declare const LOCK_WAIT_MS = 5000;
|
|
22
29
|
export declare function handleDirectory(configDir: string, connector: string): string;
|
|
23
30
|
export declare function handleFilePath(configDir: string, handle: SecretHandle): string;
|
|
24
31
|
export declare function readSecretValue(configDir: string, handle: SecretHandle): Buffer;
|
|
25
32
|
export interface LaunchSecrets {
|
|
33
|
+
readonly handles: readonly SecretHandle[];
|
|
26
34
|
readonly files: ReadonlyMap<string, string>;
|
|
27
35
|
readonly values: readonly string[];
|
|
28
36
|
remove(): void;
|
|
29
37
|
}
|
|
30
|
-
export
|
|
38
|
+
export type LaunchCopy = ({
|
|
39
|
+
readonly ok: true;
|
|
40
|
+
} & LaunchSecrets) | {
|
|
41
|
+
readonly ok: false;
|
|
42
|
+
readonly problem: string;
|
|
43
|
+
};
|
|
44
|
+
export declare const NO_HANDLES: LaunchSecrets;
|
|
45
|
+
export declare function copyForLaunch(configDir: string, connector: string): LaunchCopy;
|
|
31
46
|
export declare function openFileSecrets(options: {
|
|
32
47
|
readonly configDir: string;
|
|
33
48
|
readonly connector: string;
|