@sema-agent/connectors 0.5.0 → 0.7.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 +75 -0
- package/INTEGRATION.md +2 -1
- package/dist/cli-kit.d.ts +1 -1
- package/dist/cli-kit.js +18 -10
- 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/launcher.js +3 -3
- package/dist/pinned-upstream.d.ts +2 -0
- package/dist/pinned-upstream.js +346 -85
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +5 -2
- package/dist/release-archive.d.ts +22 -0
- package/dist/release-archive.js +462 -10
- package/dist/secrets.d.ts +9 -0
- package/dist/secrets.js +66 -4
- package/dist/write-all.d.ts +2 -0
- package/dist/write-all.js +36 -6
- package/package.json +1 -1
package/dist/pinned-upstream.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
-
import { chmodSync, closeSync, constants, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync
|
|
3
|
+
import { chmodSync, closeSync, constants, createReadStream, fchmodSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, renameSync, rmSync } from 'node:fs';
|
|
4
4
|
import { get as httpsGet } from 'node:https';
|
|
5
5
|
import { constants as osConstants } from 'node:os';
|
|
6
6
|
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
7
|
+
import { Transform } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
9
|
import { fileURLToPath } from 'node:url';
|
|
8
10
|
import { cliOutput, listed } from './cli-kit.js';
|
|
9
11
|
import { given } from './config-dir.js';
|
|
10
12
|
import { CONNECTOR_NAME } from './connector-name.js';
|
|
11
|
-
import {
|
|
13
|
+
import { archiveEntry, formatOf, treeListing, treeProblem, unpackTree } from './release-archive.js';
|
|
12
14
|
import { HANDLE_NAME, openFileSecrets } from './secrets.js';
|
|
13
|
-
import { writeAll } from './write-all.js';
|
|
15
|
+
import { writeAll, writeAllStream } from './write-all.js';
|
|
14
16
|
const CONNECTOR_DIR = 'CONNECTOR_DIR';
|
|
15
17
|
const BIN_DIR = 'bin';
|
|
16
18
|
const DIRECTORY_MODE = 0o700;
|
|
@@ -22,12 +24,16 @@ const REDIRECT_LIMIT = 5;
|
|
|
22
24
|
const SIGNALS = ['SIGTERM', 'SIGINT', 'SIGHUP'];
|
|
23
25
|
const VERSION = /^[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
24
26
|
const PLATFORM = /^[a-z0-9]+-[a-z0-9]+$/;
|
|
27
|
+
const EVERY_PLATFORM = '*';
|
|
25
28
|
const FILE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
29
|
+
const SEGMENT = /^[A-Za-z0-9@][A-Za-z0-9@._-]*$/;
|
|
26
30
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
27
31
|
const platformKey = () => `${process.platform}-${process.arch}`;
|
|
28
32
|
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
29
33
|
const keysAre = (v, keys) => Object.keys(v).sort().join('\0') === [...keys].sort().join('\0');
|
|
30
|
-
const
|
|
34
|
+
const isPath = (v) => typeof v === 'string' && v.split('/').every((s) => SEGMENT.test(s));
|
|
35
|
+
const isUnpack = (v) => isObject(v) && ((keysAre(v, ['entry']) && typeof v.entry === 'string' && FILE.test(v.entry)) || (keysAre(v, ['directory', 'to']) && typeof v.directory === 'string' && FILE.test(v.directory) && isPath(v.to)));
|
|
36
|
+
const topOf = (u) => ('entry' in u ? u.entry : u.to.split('/')[0]);
|
|
31
37
|
const below = (dir, file) => {
|
|
32
38
|
if (!isAbsolute(file))
|
|
33
39
|
return false;
|
|
@@ -60,7 +66,88 @@ function regularFile(path) {
|
|
|
60
66
|
closeSync(fd);
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
|
-
function
|
|
69
|
+
function sha256Of(path) {
|
|
70
|
+
let fd;
|
|
71
|
+
try {
|
|
72
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
if (!fstatSync(fd).isFile())
|
|
79
|
+
return null;
|
|
80
|
+
const hash = createHash('sha256');
|
|
81
|
+
const piece = Buffer.alloc(1024 * 1024);
|
|
82
|
+
for (let n; (n = readSync(fd, piece, 0, piece.length, null)) > 0;)
|
|
83
|
+
hash.update(piece.subarray(0, n));
|
|
84
|
+
return hash.digest('hex');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
closeSync(fd);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function treeDiffers(root, listed) {
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
const visit = (rel) => {
|
|
96
|
+
let names;
|
|
97
|
+
try {
|
|
98
|
+
names = readdirSync(rel === '' ? root : join(root, rel));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return rel === '' ? '.' : rel;
|
|
102
|
+
}
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
const path = rel === '' ? name : `${rel}/${name}`;
|
|
105
|
+
const want = listed.get(path);
|
|
106
|
+
if (want === undefined)
|
|
107
|
+
return path;
|
|
108
|
+
seen.add(path);
|
|
109
|
+
let st;
|
|
110
|
+
try {
|
|
111
|
+
st = lstatSync(join(root, path));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return path;
|
|
115
|
+
}
|
|
116
|
+
const kind = st.isSymbolicLink() ? 'link' : st.isDirectory() ? 'directory' : st.isFile() ? 'file' : 'other';
|
|
117
|
+
if (kind !== want.kind)
|
|
118
|
+
return path;
|
|
119
|
+
if (kind === 'file' && (st.size !== want.size || (st.mode & 0o777) !== want.mode))
|
|
120
|
+
return path;
|
|
121
|
+
if (kind === 'link' && readlinkSync(join(root, path)) !== want.link)
|
|
122
|
+
return path;
|
|
123
|
+
if (kind === 'directory') {
|
|
124
|
+
const deeper = visit(path);
|
|
125
|
+
if (deeper !== null)
|
|
126
|
+
return deeper;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
};
|
|
131
|
+
const first = visit('');
|
|
132
|
+
if (first !== null)
|
|
133
|
+
return first;
|
|
134
|
+
for (const path of listed.keys())
|
|
135
|
+
if (!seen.has(path))
|
|
136
|
+
return path;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
class Hashed extends Transform {
|
|
140
|
+
hash = createHash('sha256');
|
|
141
|
+
size = 0;
|
|
142
|
+
_transform(chunk, _encoding, done) {
|
|
143
|
+
this.size += chunk.length;
|
|
144
|
+
if (this.size > DOWNLOAD_LIMIT)
|
|
145
|
+
return done(new Error(`the archive is larger than ${DOWNLOAD_LIMIT} bytes`));
|
|
146
|
+
this.hash.update(chunk);
|
|
147
|
+
done(null, chunk);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function fetchHttps(url, redirects, dest) {
|
|
64
151
|
return new Promise((resolve) => {
|
|
65
152
|
let settled = false;
|
|
66
153
|
const done = (r) => {
|
|
@@ -85,45 +172,67 @@ function fetchHttps(url, redirects) {
|
|
|
85
172
|
return done({ ok: false, problem: `the download was redirected more than ${REDIRECT_LIMIT} times` });
|
|
86
173
|
if (next.protocol !== 'https:' || next.username !== '' || next.password !== '')
|
|
87
174
|
return done({ ok: false, problem: 'the download was redirected away from https' });
|
|
88
|
-
return void fetchHttps(next, redirects - 1).then(done);
|
|
175
|
+
return void fetchHttps(next, redirects - 1, dest).then(done);
|
|
89
176
|
}
|
|
90
177
|
if (status !== 200) {
|
|
91
178
|
res.resume();
|
|
92
179
|
return done({ ok: false, problem: `the download answered HTTP ${status}` });
|
|
93
180
|
}
|
|
94
|
-
const
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
181
|
+
const hashed = new Hashed();
|
|
182
|
+
let out;
|
|
183
|
+
try {
|
|
184
|
+
out = openSync(dest, 'wx', 0o600);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
res.resume();
|
|
188
|
+
return done({ ok: false, problem: `the download could not be written (${e.code ?? 'unknown error'})` });
|
|
189
|
+
}
|
|
190
|
+
pipeline(res, hashed, writeAllStream(out)).then(() => {
|
|
191
|
+
closeSync(out);
|
|
192
|
+
done({ ok: true, sha256: hashed.hash.digest('hex') });
|
|
193
|
+
}, (e) => {
|
|
194
|
+
closeSync(out);
|
|
195
|
+
req.destroy();
|
|
196
|
+
done({ ok: false, problem: hashed.size > DOWNLOAD_LIMIT ? `the archive is larger than ${DOWNLOAD_LIMIT} bytes` : `the download broke off (${e.message})` });
|
|
104
197
|
});
|
|
105
|
-
res.on('end', () => done({ ok: true, bytes: Buffer.concat(parts) }));
|
|
106
|
-
res.on('error', () => done({ ok: false, problem: 'the download broke off' }));
|
|
107
198
|
});
|
|
108
199
|
req.setTimeout(DOWNLOAD_IDLE_MS, () => req.destroy(new Error(`no data for ${DOWNLOAD_IDLE_MS / 1000} s`)));
|
|
109
200
|
req.on('error', (e) => done({ ok: false, problem: `the download failed (${e.message})` }));
|
|
110
201
|
});
|
|
111
202
|
}
|
|
112
|
-
async function fetchArchive(url) {
|
|
203
|
+
async function fetchArchive(url, dest) {
|
|
113
204
|
if (url.protocol === 'https:')
|
|
114
|
-
return fetchHttps(url, REDIRECT_LIMIT);
|
|
205
|
+
return fetchHttps(url, REDIRECT_LIMIT, dest);
|
|
115
206
|
const path = fileURLToPath(url);
|
|
207
|
+
let fd;
|
|
116
208
|
try {
|
|
117
|
-
|
|
209
|
+
fd = openSync(path, 'r');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return { ok: false, problem: `${path} cannot be read` };
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const st = fstatSync(fd);
|
|
118
216
|
if (!st.isFile())
|
|
119
217
|
return { ok: false, problem: `${path} is not a file` };
|
|
120
218
|
if (st.size > DOWNLOAD_LIMIT)
|
|
121
219
|
return { ok: false, problem: `the archive is larger than ${DOWNLOAD_LIMIT} bytes` };
|
|
122
|
-
|
|
220
|
+
const hashed = new Hashed();
|
|
221
|
+
const out = openSync(dest, 'wx', 0o600);
|
|
222
|
+
try {
|
|
223
|
+
await pipeline(createReadStream('', { fd, autoClose: false }), hashed, writeAllStream(out));
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
closeSync(out);
|
|
227
|
+
}
|
|
228
|
+
return { ok: true, sha256: hashed.hash.digest('hex') };
|
|
123
229
|
}
|
|
124
230
|
catch {
|
|
125
231
|
return { ok: false, problem: `${path} cannot be read` };
|
|
126
232
|
}
|
|
233
|
+
finally {
|
|
234
|
+
closeSync(fd);
|
|
235
|
+
}
|
|
127
236
|
}
|
|
128
237
|
export function pinnedUpstream(options) {
|
|
129
238
|
const { connector, upstream, lockFile } = options ?? {};
|
|
@@ -136,12 +245,16 @@ export function pinnedUpstream(options) {
|
|
|
136
245
|
const handles = options.handles;
|
|
137
246
|
if (!Array.isArray(handles) || !handles.every((h) => typeof h === 'string' && HANDLE_NAME.test(h)) || new Set(handles).size !== handles.length)
|
|
138
247
|
throw new Error('handles must list handle names, each once');
|
|
248
|
+
const optionalHandles = options.optionalHandles ?? [];
|
|
249
|
+
if (!Array.isArray(optionalHandles) || !optionalHandles.every((h) => typeof h === 'string' && HANDLE_NAME.test(h)))
|
|
250
|
+
throw new Error('optionalHandles must list handle names');
|
|
251
|
+
const allHandles = [...handles, ...optionalHandles];
|
|
252
|
+
if (new Set(allHandles).size !== allHandles.length)
|
|
253
|
+
throw new Error('handles and optionalHandles must list handle names, each once');
|
|
139
254
|
const { program, say, complain } = cliOutput(connector);
|
|
140
|
-
const fileVars =
|
|
141
|
-
const binaryOf = (connectorDir) => join(connectorDir, BIN_DIR, upstream);
|
|
255
|
+
const fileVars = allHandles.map((h) => `${h}_FILE`);
|
|
142
256
|
function pinnedBuild() {
|
|
143
257
|
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
258
|
let doc;
|
|
146
259
|
try {
|
|
147
260
|
doc = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
@@ -152,25 +265,48 @@ export function pinnedUpstream(options) {
|
|
|
152
265
|
if (!isObject(doc) || !keysAre(doc, [upstream]))
|
|
153
266
|
return refuse(`must hold exactly one entry, "${upstream}"`);
|
|
154
267
|
const entry = doc[upstream];
|
|
155
|
-
if (!isObject(entry) || !keysAre(entry, ['version', '
|
|
156
|
-
return refuse('must give exactly version,
|
|
157
|
-
const { version,
|
|
268
|
+
if (!isObject(entry) || !keysAre(entry, ['version', 'archives', 'program']))
|
|
269
|
+
return refuse('must give exactly version, archives and program');
|
|
270
|
+
const { version, archives, program: prog } = entry;
|
|
158
271
|
if (typeof version !== 'string' || !VERSION.test(version))
|
|
159
272
|
return refuse('must give the version as <major>.<minor>.<patch>');
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
273
|
+
const isArchive = (v) => isObject(v) &&
|
|
274
|
+
keysAre(v, ['platform', 'file', 'source', 'sha256', 'unpack']) &&
|
|
275
|
+
typeof v.platform === 'string' &&
|
|
276
|
+
(v.platform === EVERY_PLATFORM || PLATFORM.test(v.platform)) &&
|
|
277
|
+
typeof v.file === 'string' &&
|
|
278
|
+
FILE.test(v.file) &&
|
|
279
|
+
formatOf(v.file) !== null &&
|
|
280
|
+
typeof v.source === 'string' &&
|
|
281
|
+
typeof v.sha256 === 'string' &&
|
|
282
|
+
SHA256.test(v.sha256) &&
|
|
283
|
+
isUnpack(v.unpack);
|
|
284
|
+
if (!Array.isArray(archives) || archives.length === 0 || !archives.every(isArchive))
|
|
285
|
+
return refuse('must list archives, each {"platform": <platform> or "*", "file": <name ending in .tar.gz, .tgz or .zip>, "source": <URL template>, "sha256": <64 hex digits>, "unpack": {"entry": <name>} or {"directory": <name>, "to": <path>}}');
|
|
286
|
+
if (!isObject(prog) || !((keysAre(prog, ['binary']) && isPath(prog.binary)) || (keysAre(prog, ['node']) && isPath(prog.node))))
|
|
287
|
+
return refuse('must give the program as {"binary": <path>} or {"node": <path>}');
|
|
164
288
|
const key = platformKey();
|
|
165
|
-
const
|
|
166
|
-
if (!
|
|
289
|
+
const all = archives.map((a) => ({ ...a, format: formatOf(a.file) }));
|
|
290
|
+
if (!all.some((a) => a.platform === key))
|
|
167
291
|
return refuse(`pins no ${upstream} build for ${key}`);
|
|
168
|
-
|
|
292
|
+
const mine = all.filter((a) => a.platform === key || a.platform === EVERY_PLATFORM);
|
|
293
|
+
const tops = mine.map((a) => topOf(a.unpack));
|
|
294
|
+
const places = [...mine.map((a) => a.file), ...mine.filter((a) => 'entry' in a.unpack).map((a) => a.unpack.entry)];
|
|
295
|
+
const trees = mine.flatMap((a) => ('to' in a.unpack ? [a.unpack.to] : []));
|
|
296
|
+
const overlap = trees.some((t, i) => trees.some((u, j) => i !== j && (t === u || t.startsWith(`${u}/`)))) || places.some((p, i) => places.indexOf(p) !== i) || mine.some((a) => trees.some((t) => t.split('/')[0] === a.file)) || mine.some((a) => 'entry' in a.unpack && trees.some((t) => t.split('/')[0] === topOf(a.unpack)));
|
|
297
|
+
if (overlap || tops.length !== mine.length)
|
|
298
|
+
return refuse(`gives two archives for ${key} the same place in the bin directory`);
|
|
299
|
+
const kind = 'binary' in prog ? 'binary' : 'node';
|
|
300
|
+
const path = (kind === 'binary' ? prog.binary : prog.node);
|
|
301
|
+
const provides = mine.filter((a) => ('entry' in a.unpack ? a.unpack.entry === path : path.startsWith(`${a.unpack.to}/`)));
|
|
302
|
+
if (provides.length !== 1)
|
|
303
|
+
return refuse(`does not unpack the program ${path} from exactly one archive for ${key}`);
|
|
304
|
+
return { ok: true, build: { version, archives: mine, program: { kind, path } } };
|
|
169
305
|
}
|
|
170
|
-
function sourceOf(
|
|
306
|
+
function sourceOf(build, archive) {
|
|
171
307
|
let url;
|
|
172
308
|
try {
|
|
173
|
-
url = new URL(
|
|
309
|
+
url = new URL(archive.source.replaceAll('{version}', build.version).replaceAll('{file}', archive.file));
|
|
174
310
|
}
|
|
175
311
|
catch {
|
|
176
312
|
return { ok: false, problem: 'the source in binaries.lock.json is not a URL' };
|
|
@@ -181,6 +317,11 @@ export function pinnedUpstream(options) {
|
|
|
181
317
|
return { ok: true, url };
|
|
182
318
|
return { ok: false, problem: 'the source in binaries.lock.json must be an https or file URL (an https URL without credentials)' };
|
|
183
319
|
}
|
|
320
|
+
function programSource(build) {
|
|
321
|
+
const { path } = build.program;
|
|
322
|
+
const archive = build.archives.find((a) => ('entry' in a.unpack ? a.unpack.entry === path : path.startsWith(`${a.unpack.to}/`)));
|
|
323
|
+
return { archive, name: 'entry' in archive.unpack ? archive.unpack.entry : `${archive.unpack.directory}/${path.slice(archive.unpack.to.length + 1)}` };
|
|
324
|
+
}
|
|
184
325
|
function installedProblem(connectorDir, build) {
|
|
185
326
|
const dir = join(connectorDir, BIN_DIR);
|
|
186
327
|
let st;
|
|
@@ -192,21 +333,52 @@ export function pinnedUpstream(options) {
|
|
|
192
333
|
}
|
|
193
334
|
if (!st.isDirectory() || (st.mode & 0o777) !== DIRECTORY_MODE)
|
|
194
335
|
return 'the bin directory of the connector is not a 0700 directory';
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
336
|
+
const sameAs = (archive, name, file, executable) => {
|
|
337
|
+
const entry = archiveEntry(join(dir, archive.file), archive.format, name);
|
|
338
|
+
if (!entry.ok)
|
|
339
|
+
return `the pinned archive ${entry.problem}`;
|
|
340
|
+
const path = join(dir, file);
|
|
341
|
+
const bytes = regularFile(path);
|
|
342
|
+
if (bytes === null || !bytes.equals(entry.bytes))
|
|
343
|
+
return `${file} there is not the entry of the pinned archive`;
|
|
344
|
+
if (executable && (lstatSync(path).mode & 0o777) !== BINARY_MODE)
|
|
345
|
+
return `${file} there is not mode 0755`;
|
|
346
|
+
return null;
|
|
347
|
+
};
|
|
348
|
+
for (const archive of build.archives) {
|
|
349
|
+
const hash = sha256Of(join(dir, archive.file));
|
|
350
|
+
if (hash === null)
|
|
351
|
+
return `${upstream} is not installed (the pinned archive is not there)`;
|
|
352
|
+
if (hash !== archive.sha256)
|
|
353
|
+
return 'the archive there is not the one binaries.lock.json pins';
|
|
354
|
+
if ('entry' in archive.unpack) {
|
|
355
|
+
const problem = sameAs(archive, archive.unpack.entry, archive.unpack.entry, true);
|
|
356
|
+
if (problem !== null)
|
|
357
|
+
return problem;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
let tree;
|
|
361
|
+
try {
|
|
362
|
+
tree = lstatSync(join(dir, archive.unpack.to));
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
tree = null;
|
|
366
|
+
}
|
|
367
|
+
if (tree === null || !tree.isDirectory())
|
|
368
|
+
return `${upstream} is not installed (${archive.unpack.to} is not there)`;
|
|
369
|
+
const listing = treeListing(join(dir, archive.file), archive.format, archive.unpack.directory);
|
|
370
|
+
if (!listing.ok)
|
|
371
|
+
return `the pinned archive ${listing.problem}`;
|
|
372
|
+
const differs = treeDiffers(join(dir, archive.unpack.to), listing.paths);
|
|
373
|
+
if (differs !== null)
|
|
374
|
+
return `${archive.unpack.to}/${differs} there is not as the pinned archive gives it`;
|
|
375
|
+
}
|
|
376
|
+
const { archive, name } = programSource(build);
|
|
377
|
+
return 'entry' in archive.unpack ? null : sameAs(archive, name, build.program.path, build.program.kind === 'binary');
|
|
378
|
+
}
|
|
379
|
+
function commit(tmp, path, mode) {
|
|
380
|
+
chmodSync(tmp, mode);
|
|
381
|
+
renameSync(tmp, path);
|
|
210
382
|
}
|
|
211
383
|
function writeAtomically(path, bytes, mode) {
|
|
212
384
|
const tmp = join(dirname(path), `.${upstream}.${randomBytes(6).toString('hex')}.tmp`);
|
|
@@ -226,13 +398,52 @@ export function pinnedUpstream(options) {
|
|
|
226
398
|
throw e;
|
|
227
399
|
}
|
|
228
400
|
}
|
|
401
|
+
async function placeTree(binDir, archive, from) {
|
|
402
|
+
const target = join(binDir, archive.unpack.to);
|
|
403
|
+
const parent = dirname(target);
|
|
404
|
+
mkdirSync(parent, { recursive: true, mode: 0o755 });
|
|
405
|
+
for (let p = parent; p !== binDir; p = dirname(p))
|
|
406
|
+
if (!lstatSync(p).isDirectory())
|
|
407
|
+
throw new Error(`${relative(binDir, p)} in the bin directory is not a directory`);
|
|
408
|
+
const tmp = mkdtempSync(join(binDir, `.${upstream}.unpack-`));
|
|
409
|
+
const old = join(binDir, `.${upstream}.old-${randomBytes(6).toString('hex')}`);
|
|
410
|
+
try {
|
|
411
|
+
const problem = await unpackTree(from, archive.format, archive.unpack.directory, tmp);
|
|
412
|
+
if (problem !== null)
|
|
413
|
+
throw new Error(`the pinned archive ${archive.file} ${problem}`);
|
|
414
|
+
chmodSync(tmp, 0o755);
|
|
415
|
+
let had = true;
|
|
416
|
+
try {
|
|
417
|
+
renameSync(target, old);
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
if (e.code !== 'ENOENT')
|
|
421
|
+
throw e;
|
|
422
|
+
had = false;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
renameSync(tmp, target);
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
if (had)
|
|
429
|
+
renameSync(old, target);
|
|
430
|
+
throw e;
|
|
431
|
+
}
|
|
432
|
+
if (had)
|
|
433
|
+
rmSync(old, { recursive: true, force: true });
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
437
|
+
throw e;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
229
440
|
async function install(configDir) {
|
|
230
441
|
const pinned = pinnedBuild();
|
|
231
442
|
if (!pinned.ok) {
|
|
232
443
|
complain(`${pinned.problem}; nothing was installed`);
|
|
233
444
|
return 1;
|
|
234
445
|
}
|
|
235
|
-
const {
|
|
446
|
+
const { build } = pinned;
|
|
236
447
|
const secrets = openFileSecrets({ configDir, connector });
|
|
237
448
|
const before = secrets.list();
|
|
238
449
|
if (!before.ok) {
|
|
@@ -242,29 +453,23 @@ export function pinnedUpstream(options) {
|
|
|
242
453
|
const connectorDir = secrets.directory;
|
|
243
454
|
const binDir = join(connectorDir, BIN_DIR);
|
|
244
455
|
if (installedProblem(connectorDir, build) === null) {
|
|
245
|
-
say(`${upstream} ${
|
|
456
|
+
say(`${upstream} ${build.version} is already installed in ${binDir}; nothing changed`);
|
|
246
457
|
return 0;
|
|
247
458
|
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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;
|
|
459
|
+
const sources = [];
|
|
460
|
+
for (const archive of build.archives) {
|
|
461
|
+
const source = sourceOf(build, archive);
|
|
462
|
+
if (!source.ok) {
|
|
463
|
+
complain(`${source.problem}; nothing was installed`);
|
|
464
|
+
return 1;
|
|
465
|
+
}
|
|
466
|
+
sources.push(source.url);
|
|
267
467
|
}
|
|
468
|
+
const temps = [];
|
|
469
|
+
const dropTemps = () => {
|
|
470
|
+
for (const t of temps)
|
|
471
|
+
rmSync(t, { force: true });
|
|
472
|
+
};
|
|
268
473
|
try {
|
|
269
474
|
mkdirSync(binDir, { recursive: true, mode: DIRECTORY_MODE });
|
|
270
475
|
const after = secrets.list();
|
|
@@ -274,24 +479,74 @@ export function pinnedUpstream(options) {
|
|
|
274
479
|
if (!st.isDirectory())
|
|
275
480
|
throw new Error('the bin directory of the connector is not a directory');
|
|
276
481
|
chmodSync(binDir, DIRECTORY_MODE);
|
|
277
|
-
writeAtomically(join(binDir, build.file), fetched.bytes, ARCHIVE_MODE);
|
|
278
|
-
writeAtomically(binaryOf(connectorDir), entry.bytes, BINARY_MODE);
|
|
279
482
|
}
|
|
280
483
|
catch (e) {
|
|
281
484
|
complain(`${upstream} was not installed: ${e instanceof Error ? e.message : 'unknown error'}`);
|
|
282
485
|
return 1;
|
|
283
486
|
}
|
|
487
|
+
for (const [i, archive] of build.archives.entries()) {
|
|
488
|
+
const tmp = join(binDir, `.${upstream}.${randomBytes(6).toString('hex')}.download`);
|
|
489
|
+
temps.push(tmp);
|
|
490
|
+
const fetched = await fetchArchive(sources[i], tmp);
|
|
491
|
+
if (!fetched.ok) {
|
|
492
|
+
dropTemps();
|
|
493
|
+
complain(`${archive.file} could not be fetched: ${fetched.problem}; nothing was installed`);
|
|
494
|
+
return 1;
|
|
495
|
+
}
|
|
496
|
+
if (fetched.sha256 !== archive.sha256) {
|
|
497
|
+
dropTemps();
|
|
498
|
+
complain(`the fetched ${archive.file} does not match binaries.lock.json (sha256 ${fetched.sha256}, pinned ${archive.sha256}); nothing was installed`);
|
|
499
|
+
return 1;
|
|
500
|
+
}
|
|
501
|
+
const problem = 'entry' in archive.unpack ? (() => {
|
|
502
|
+
const e = archiveEntry(tmp, archive.format, archive.unpack.entry);
|
|
503
|
+
return e.ok ? null : e.problem;
|
|
504
|
+
})() : treeProblem(tmp, archive.format, archive.unpack.directory);
|
|
505
|
+
if (problem !== null) {
|
|
506
|
+
dropTemps();
|
|
507
|
+
complain(`the pinned archive ${archive.file} ${problem}; nothing was installed`);
|
|
508
|
+
return 1;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
{
|
|
512
|
+
const { archive, name } = programSource(build);
|
|
513
|
+
const entry = archiveEntry(temps[build.archives.indexOf(archive)], archive.format, name);
|
|
514
|
+
if (!entry.ok) {
|
|
515
|
+
dropTemps();
|
|
516
|
+
complain(`the pinned archive ${archive.file} ${entry.problem}; nothing was installed`);
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
for (const [i, archive] of build.archives.entries()) {
|
|
522
|
+
const tmp = temps[i];
|
|
523
|
+
if ('entry' in archive.unpack) {
|
|
524
|
+
const e = archiveEntry(tmp, archive.format, archive.unpack.entry);
|
|
525
|
+
if (!e.ok)
|
|
526
|
+
throw new Error(`the pinned archive ${archive.file} ${e.problem}`);
|
|
527
|
+
writeAtomically(join(binDir, archive.unpack.entry), e.bytes, BINARY_MODE);
|
|
528
|
+
}
|
|
529
|
+
else
|
|
530
|
+
await placeTree(binDir, archive, tmp);
|
|
531
|
+
commit(tmp, join(binDir, archive.file), ARCHIVE_MODE);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (e) {
|
|
535
|
+
dropTemps();
|
|
536
|
+
complain(`${upstream} was not installed: ${e instanceof Error ? e.message : 'unknown error'}`);
|
|
537
|
+
return 1;
|
|
538
|
+
}
|
|
284
539
|
const problem = installedProblem(connectorDir, build);
|
|
285
540
|
if (problem !== null) {
|
|
286
541
|
complain(`${upstream} was written but does not read back as the pinned build: ${problem}`);
|
|
287
542
|
return 1;
|
|
288
543
|
}
|
|
289
|
-
say(`installed ${upstream} ${
|
|
544
|
+
say(`installed ${upstream} ${build.version} (${build.archives.map((a) => `${a.file}, sha256 ${a.sha256}`).join('; ')}) in ${binDir}`);
|
|
290
545
|
return 0;
|
|
291
546
|
}
|
|
292
|
-
function run(
|
|
547
|
+
function run(command, args, env) {
|
|
293
548
|
return new Promise((resolve) => {
|
|
294
|
-
const child = spawn(
|
|
549
|
+
const child = spawn(command, [...args], { stdio: 'inherit', env });
|
|
295
550
|
const forwarders = SIGNALS.map((signal) => [signal, () => void child.kill(signal)]);
|
|
296
551
|
for (const [signal, forward] of forwarders)
|
|
297
552
|
process.on(signal, forward);
|
|
@@ -320,11 +575,14 @@ export function pinnedUpstream(options) {
|
|
|
320
575
|
const connectorDir = env[CONNECTOR_DIR];
|
|
321
576
|
if (!given(connectorDir) || !isAbsolute(connectorDir))
|
|
322
577
|
return refuse(`${CONNECTOR_DIR} does not name the connector's directory: start the connector with sema-connector run ${connector}`);
|
|
323
|
-
const
|
|
324
|
-
if (!
|
|
578
|
+
const requiredPaths = handles.map((h) => env[`${h}_FILE`]);
|
|
579
|
+
if (!requiredPaths.every(given))
|
|
325
580
|
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
|
-
|
|
327
|
-
|
|
581
|
+
const presentHandles = [...handles, ...optionalHandles.filter((h) => given(env[`${h}_FILE`]))];
|
|
582
|
+
const presentVars = presentHandles.map((h) => `${h}_FILE`);
|
|
583
|
+
const presentPaths = presentVars.map((v) => env[v]);
|
|
584
|
+
if (!presentPaths.every((p) => given(p) && below(connectorDir, p) && isRegularFile(p)))
|
|
585
|
+
return refuse(`${listed(presentVars)} must name files in the connector's directory (${CONNECTOR_DIR}), as the launcher hands ${presentHandles.length === 1 ? 'it' : 'them'}: start the connector with sema-connector run ${connector}`);
|
|
328
586
|
const pinned = pinnedBuild();
|
|
329
587
|
if (!pinned.ok)
|
|
330
588
|
return refuse(pinned.problem);
|
|
@@ -335,12 +593,15 @@ export function pinnedUpstream(options) {
|
|
|
335
593
|
for (const [k, v] of Object.entries(env))
|
|
336
594
|
if (v !== undefined && k !== CONNECTOR_DIR && !fileVars.includes(k))
|
|
337
595
|
own[k] = v;
|
|
338
|
-
const
|
|
596
|
+
const binDir = join(connectorDir, BIN_DIR);
|
|
597
|
+
const path = join(binDir, pinned.build.program.path);
|
|
598
|
+
const [command, first] = pinned.build.program.kind === 'node' ? [process.execPath, [path]] : [path, []];
|
|
339
599
|
return {
|
|
340
600
|
ok: true,
|
|
341
601
|
connectorDir,
|
|
342
|
-
|
|
343
|
-
|
|
602
|
+
binDir,
|
|
603
|
+
files: Object.fromEntries(presentHandles.map((h, i) => [h, presentPaths[i]])),
|
|
604
|
+
run: (args, extra) => run(command, [...first, ...args], { ...own, ...extra }),
|
|
344
605
|
};
|
|
345
606
|
}
|
|
346
607
|
return { install, start };
|
package/dist/principal.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type PrincipalResult = {
|
|
|
41
41
|
export interface PrincipalResolver {
|
|
42
42
|
resolve(request: RequestContext): PrincipalResult;
|
|
43
43
|
}
|
|
44
|
-
export declare function standaloneResolver(): PrincipalResolver;
|
|
44
|
+
export declare function standaloneResolver(policy?: readonly string[] | null): PrincipalResolver;
|
|
45
45
|
export declare function hostedResolver(options: {
|
|
46
46
|
readonly issuer: string;
|
|
47
47
|
readonly audience: string;
|
package/dist/principal.js
CHANGED
|
@@ -95,8 +95,11 @@ export function verifyAssertion(compactJws, options) {
|
|
|
95
95
|
return { ok: true, principal: { kind: 'assertion', sub, space, scope: scope.split(' ') } };
|
|
96
96
|
}
|
|
97
97
|
const ANONYMOUS = Object.freeze({ kind: 'anonymous', scope: Object.freeze(['tool:*']) });
|
|
98
|
-
export function standaloneResolver() {
|
|
99
|
-
|
|
98
|
+
export function standaloneResolver(policy) {
|
|
99
|
+
if (policy === null || policy === undefined)
|
|
100
|
+
return { resolve: () => ({ ok: true, principal: ANONYMOUS }) };
|
|
101
|
+
const principal = Object.freeze({ kind: 'anonymous', scope: Object.freeze(policy.map((name) => `tool:${name}`)) });
|
|
102
|
+
return { resolve: () => ({ ok: true, principal }) };
|
|
100
103
|
}
|
|
101
104
|
export function hostedResolver(options) {
|
|
102
105
|
if (typeof options?.issuer !== 'string' || options.issuer === '' || typeof options.audience !== 'string' || options.audience === '') {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare const UNPACKED_LIMIT: number;
|
|
2
|
+
export type ArchiveFormat = 'tar.gz' | 'zip';
|
|
1
3
|
export type Entry = {
|
|
2
4
|
readonly ok: true;
|
|
3
5
|
readonly bytes: Buffer;
|
|
@@ -5,4 +7,24 @@ export type Entry = {
|
|
|
5
7
|
readonly ok: false;
|
|
6
8
|
readonly problem: string;
|
|
7
9
|
};
|
|
10
|
+
type Problem = string | null;
|
|
11
|
+
export declare const formatOf: (file: string) => ArchiveFormat | null;
|
|
8
12
|
export declare function entryOf(archive: Buffer, wanted: string): Entry;
|
|
13
|
+
export declare function archiveEntry(path: string, format: ArchiveFormat, wanted: string): Entry;
|
|
14
|
+
export declare function unpackTree(path: string, format: ArchiveFormat, top: string, dest: string): Promise<Problem>;
|
|
15
|
+
export declare function treeProblem(path: string, format: ArchiveFormat, top: string): Problem;
|
|
16
|
+
export interface Listed {
|
|
17
|
+
readonly kind: 'file' | 'directory' | 'link';
|
|
18
|
+
readonly mode: number;
|
|
19
|
+
readonly size: number;
|
|
20
|
+
readonly link?: string;
|
|
21
|
+
}
|
|
22
|
+
export type Listing = {
|
|
23
|
+
readonly ok: true;
|
|
24
|
+
readonly paths: ReadonlyMap<string, Listed>;
|
|
25
|
+
} | {
|
|
26
|
+
readonly ok: false;
|
|
27
|
+
readonly problem: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function treeListing(path: string, format: ArchiveFormat, top: string): Listing;
|
|
30
|
+
export {};
|