@sema-agent/connectors 0.0.0-stage → 0.1.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/dist/jwks.js ADDED
@@ -0,0 +1,78 @@
1
+ import { get as httpGet } from 'node:http';
2
+ import { get as httpsGet } from 'node:https';
3
+ import { given } from './config-dir.js';
4
+ const INLINE = 'CONNECTOR_ASSERTION_JWKS';
5
+ const OVERRIDE = 'CONNECTOR_ASSERTION_JWKS_URL';
6
+ const WELL_KNOWN = '/.well-known/jwks.json';
7
+ const LOOPBACK_HOSTS = ['127.0.0.1', 'localhost'];
8
+ const FETCH_LIMIT_BYTES = 1024 * 1024;
9
+ const FETCH_TIMEOUT_MS = 10_000;
10
+ export function keySetSource(env, issuer) {
11
+ const inline = env[INLINE];
12
+ const override = env[OVERRIDE];
13
+ if (given(override) && given(inline))
14
+ return `both ${OVERRIDE} and ${INLINE} are set; the key set must have one source`;
15
+ if (given(inline))
16
+ return { kind: 'inline', text: inline };
17
+ const from = given(override) ? OVERRIDE : `the default key set location (CONNECTOR_ASSERTION_ISSUER followed by ${WELL_KNOWN})`;
18
+ let url;
19
+ try {
20
+ url = new URL(given(override) ? override : issuer.replace(/\/+$/, '') + WELL_KNOWN);
21
+ }
22
+ catch {
23
+ return `${from} is not a URL`;
24
+ }
25
+ if (url.username !== '' || url.password !== '')
26
+ return `${from} carries credentials; a key set URL never does`;
27
+ if (url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK_HOSTS.includes(url.hostname)))
28
+ return { kind: 'url', from, url };
29
+ return `${from} must be an https URL, or an http URL on 127.0.0.1 or localhost`;
30
+ }
31
+ const isKeySet = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && Array.isArray(v.keys);
32
+ export async function obtainKeySet(source) {
33
+ const what = source.kind === 'inline' ? INLINE : `the key set from ${source.from}`;
34
+ let text;
35
+ if (source.kind === 'inline')
36
+ text = source.text;
37
+ else {
38
+ const fetched = await fetchOnce(source.url);
39
+ if (!fetched.ok)
40
+ return { ok: false, problem: `${what} could not be fetched: ${fetched.why}` };
41
+ text = fetched.text;
42
+ }
43
+ let value;
44
+ try {
45
+ value = JSON.parse(text);
46
+ }
47
+ catch {
48
+ return { ok: false, problem: `${what} is not JSON` };
49
+ }
50
+ return isKeySet(value) ? { ok: true, keys: value } : { ok: false, problem: `${what} is not a JSON Web Key Set (an object with a keys list)` };
51
+ }
52
+ function fetchOnce(url) {
53
+ return new Promise((resolve) => {
54
+ const get = url.protocol === 'https:' ? httpsGet : httpGet;
55
+ const req = get(url, { agent: false, headers: { accept: 'application/json' } }, (res) => {
56
+ if (res.statusCode !== 200) {
57
+ res.resume();
58
+ resolve({ ok: false, why: `it answered HTTP ${String(res.statusCode)}` });
59
+ return;
60
+ }
61
+ const parts = [];
62
+ let size = 0;
63
+ res.on('data', (chunk) => {
64
+ size += chunk.length;
65
+ if (size > FETCH_LIMIT_BYTES) {
66
+ req.destroy();
67
+ resolve({ ok: false, why: `its answer is over ${FETCH_LIMIT_BYTES / (1024 * 1024)} MiB` });
68
+ }
69
+ else
70
+ parts.push(chunk);
71
+ });
72
+ res.on('end', () => resolve({ ok: true, text: Buffer.concat(parts).toString('utf8') }));
73
+ res.on('error', () => resolve({ ok: false, why: 'its answer broke off' }));
74
+ });
75
+ req.setTimeout(FETCH_TIMEOUT_MS, () => req.destroy(Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' })));
76
+ req.on('error', (e) => resolve({ ok: false, why: `the request failed (${e.code ?? 'no answer'})` }));
77
+ });
78
+ }
@@ -0,0 +1,2 @@
1
+ import { type Env } from './config-dir.js';
2
+ export declare function main(argv: readonly string[], env: Env): Promise<number>;
@@ -0,0 +1,585 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { isIPv4, isIPv6 } from 'node:net';
6
+ import { dirname, join } from 'node:path';
7
+ import { absoluteDir, given } from './config-dir.js';
8
+ import { CONNECTOR_NAME } from './connector-name.js';
9
+ import { serveHttp } from './http.js';
10
+ import { keySetSource, obtainKeySet } from './jwks.js';
11
+ import { hostedResolver, standaloneResolver } from './principal.js';
12
+ import { Scrubber, secretMatcher } from './redact.js';
13
+ import { copyForLaunch, openFileSecrets } from './secrets.js';
14
+ import { INSPECT_LIMIT_BYTES, Session } from './session.js';
15
+ import { mintStatus } from './status.js';
16
+ const EXIT = { ok: 0, guard: 2, notInstalled: 4, connectorFailed: 5 };
17
+ const NOT_INSTALLED = 'connector.not_installed';
18
+ const INTERNAL = 'connector.internal';
19
+ const STOP_SIGNALS = ['SIGTERM', 'SIGINT', 'SIGHUP'];
20
+ const TEARDOWN_STEP_MS = 1000;
21
+ const STALL_STEPS = 10;
22
+ const STALL_MS = STALL_STEPS * TEARDOWN_STEP_MS;
23
+ const PIECE_BYTES = 1024;
24
+ const USAGE = 'usage: sema-connector run <name> --mode standalone|hosted --transport stdio|http [--listen <ip>:<port>] [--config-dir <absolute path>]';
25
+ const FLAGS = ['--mode', '--transport', '--listen', '--config-dir'];
26
+ const CHILD_ENV = ['HOME', 'LOGNAME', 'USER', 'PATH', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'TZ'];
27
+ function guard(argv, env) {
28
+ if (argv[0] !== 'run')
29
+ return 'expected the command "run"';
30
+ const name = argv[1];
31
+ if (name === undefined || name.startsWith('-'))
32
+ return 'missing the connector <name> after "run"';
33
+ if (!CONNECTOR_NAME.test(name))
34
+ return '<name> must be lowercase letters and digits in hyphen-separated words (it names the package @sema-agent/connector-<name>)';
35
+ const values = new Map();
36
+ for (let i = 2; i < argv.length; i += 2) {
37
+ const flag = FLAGS.find((f) => f === argv[i]);
38
+ if (flag === undefined)
39
+ return `unknown argument at position ${i + 1}`;
40
+ if (values.has(flag))
41
+ return `${flag} is given more than once`;
42
+ const value = argv[i + 1];
43
+ if (value === undefined)
44
+ return `${flag} needs a value`;
45
+ values.set(flag, value);
46
+ }
47
+ const mode = values.get('--mode');
48
+ const transport = values.get('--transport');
49
+ const listen = values.get('--listen');
50
+ if (mode === undefined)
51
+ return '--mode is required (standalone or hosted); there is no default';
52
+ if (mode !== 'standalone' && mode !== 'hosted')
53
+ return '--mode must be standalone or hosted';
54
+ if (transport === undefined)
55
+ return '--transport is required (stdio or http); there is no default';
56
+ if (transport !== 'stdio' && transport !== 'http')
57
+ return '--transport must be stdio or http';
58
+ if (mode === 'standalone') {
59
+ if (transport !== 'stdio')
60
+ return 'standalone mode requires --transport stdio';
61
+ if (listen !== undefined)
62
+ return '--listen is only valid with --transport http';
63
+ const configDirArgument = values.get('--config-dir');
64
+ if (configDirArgument === undefined)
65
+ return 'standalone mode requires --config-dir <absolute path>, the configuration directory its secrets are read from (there is no default)';
66
+ const configDir = absoluteDir(configDirArgument);
67
+ if (configDir === null)
68
+ return '--config-dir must be an absolute path';
69
+ return { name, mode, configDir };
70
+ }
71
+ if (transport !== 'http')
72
+ return 'hosted mode requires --transport http';
73
+ if (values.has('--config-dir'))
74
+ return '--config-dir is only valid in standalone mode (hosted configuration comes from the deployment declaration)';
75
+ if (listen === undefined)
76
+ return 'hosted mode requires --listen <ip>:<port>';
77
+ const address = listenAddress(listen);
78
+ if (address === null)
79
+ return '--listen must be <IPv4 or [IPv6]>:<port> with a port from 1 to 65535';
80
+ const issuer = env.CONNECTOR_ASSERTION_ISSUER;
81
+ if (!given(issuer))
82
+ return 'hosted mode requires CONNECTOR_ASSERTION_ISSUER (an empty value counts as missing)';
83
+ const audience = env.CONNECTOR_ASSERTION_AUDIENCE;
84
+ if (!given(audience))
85
+ return 'hosted mode requires CONNECTOR_ASSERTION_AUDIENCE, the audience every assertion must name (an empty value counts as missing)';
86
+ const keySet = keySetSource(env, issuer);
87
+ if (typeof keySet === 'string')
88
+ return keySet;
89
+ return { name, mode, listen: address, issuer, audience, keySet };
90
+ }
91
+ function listenAddress(value) {
92
+ const m = /^(?:\[([^\]]+)\]|([^:[\]]+)):([1-9][0-9]{0,4})$/.exec(value);
93
+ if (m === null)
94
+ return null;
95
+ const host = m[1] ?? m[2] ?? '';
96
+ const hostOk = m[1] !== undefined ? isIPv6(host) : isIPv4(host);
97
+ const port = Number(m[3]);
98
+ return hostOk && port <= 65535 ? { host, port } : null;
99
+ }
100
+ function resolveConnector(name) {
101
+ const packageName = `@sema-agent/connector-${name}`;
102
+ let manifestPath;
103
+ try {
104
+ manifestPath = createRequire(new URL('../package.json', import.meta.url)).resolve(`${packageName}/package.json`);
105
+ }
106
+ catch {
107
+ return { packageName, reason: 'is not installed where the launcher can resolve it' };
108
+ }
109
+ let bin;
110
+ try {
111
+ bin = JSON.parse(readFileSync(manifestPath, 'utf8')).bin;
112
+ }
113
+ catch {
114
+ return { packageName, reason: 'has a package.json that cannot be read' };
115
+ }
116
+ let rel;
117
+ if (typeof bin === 'string')
118
+ rel = bin;
119
+ else if (bin !== null && typeof bin === 'object' && !Array.isArray(bin) && Object.keys(bin).length === 1)
120
+ rel = Object.values(bin)[0];
121
+ if (typeof rel !== 'string' || rel === '')
122
+ return { packageName, reason: 'does not declare exactly one bin' };
123
+ return { packageName, binPath: join(dirname(manifestPath), rel) };
124
+ }
125
+ class Out {
126
+ stream;
127
+ held = [];
128
+ heldBytes = 0;
129
+ writing = false;
130
+ ending = false;
131
+ needDrain = false;
132
+ gone = false;
133
+ settled = [];
134
+ stall = null;
135
+ constructor(stream) {
136
+ this.stream = stream;
137
+ const lost = () => {
138
+ if (this.gone)
139
+ return;
140
+ this.gone = true;
141
+ this.held.length = 0;
142
+ this.heldBytes = 0;
143
+ this.idle();
144
+ };
145
+ stream.on('error', lost);
146
+ stream.once('close', lost);
147
+ }
148
+ behind() {
149
+ return !this.gone && this.needDrain;
150
+ }
151
+ whenCaughtUp(go) {
152
+ void this.flushed().then(go);
153
+ }
154
+ write(chunk) {
155
+ if (this.gone || this.ending)
156
+ return;
157
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
158
+ for (let at = 0; at < bytes.length; at += PIECE_BYTES)
159
+ this.held.push(bytes.subarray(at, at + PIECE_BYTES));
160
+ this.heldBytes += bytes.length;
161
+ if (this.heldBytes >= this.stream.writableHighWaterMark)
162
+ this.needDrain = true;
163
+ this.next();
164
+ }
165
+ end() {
166
+ this.ending = true;
167
+ this.next();
168
+ }
169
+ flushed() {
170
+ if (this.gone || (!this.writing && this.held.length === 0))
171
+ return Promise.resolve();
172
+ return new Promise((resolve) => this.settled.push(resolve));
173
+ }
174
+ watchStall(ms, stalled) {
175
+ this.stall = { ms, stalled };
176
+ }
177
+ next() {
178
+ if (this.writing || this.gone)
179
+ return;
180
+ const piece = this.held.shift();
181
+ if (piece === undefined) {
182
+ if (this.ending && !this.stream.writableEnded)
183
+ this.stream.end();
184
+ this.idle();
185
+ return;
186
+ }
187
+ this.writing = true;
188
+ const stall = this.stall;
189
+ if (stall !== null) {
190
+ stall.timer ??= setTimeout(() => {
191
+ this.stall = null;
192
+ stall.stalled();
193
+ }, stall.ms).unref();
194
+ stall.timer.refresh();
195
+ }
196
+ this.stream.write(piece, () => {
197
+ this.writing = false;
198
+ this.heldBytes -= piece.length;
199
+ this.next();
200
+ });
201
+ }
202
+ idle() {
203
+ if (this.stall?.timer !== undefined) {
204
+ clearTimeout(this.stall.timer);
205
+ delete this.stall.timer;
206
+ }
207
+ this.needDrain = false;
208
+ const settled = this.settled;
209
+ this.settled = [];
210
+ for (const resolve of settled)
211
+ resolve();
212
+ }
213
+ }
214
+ const LINE_FEED = 0x0a;
215
+ const OWN_PREFIX = Buffer.from('sema-connector:');
216
+ const MARK = Buffer.from('connector: ');
217
+ const RECEIPT_PREFIX = 'sema-connector: receipt ';
218
+ const LINE_BREAK = /\n|\r|\x0b|\x0c|[\x1c-\x1e]|\xc2\x85|\xe2\x80[\xa8\xa9]/g;
219
+ const LONGEST_BREAK = 3;
220
+ const lineStarts = (text) => [...text.matchAll(LINE_BREAK)].map((m) => m.index + m[0].length);
221
+ const asciiJson = (value) => JSON.stringify(value).replace(/[\u007f-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
222
+ class Log {
223
+ out;
224
+ tail = Buffer.from([LINE_FEED]);
225
+ constructor(out) {
226
+ this.out = out;
227
+ }
228
+ line(text) {
229
+ if (this.tail[this.tail.length - 1] !== LINE_FEED)
230
+ this.write(Buffer.from('\n'));
231
+ this.write(Buffer.from(text, 'utf8'));
232
+ }
233
+ connector(scrubber) {
234
+ let held = Buffer.alloc(0);
235
+ const pass = (bytes, final) => {
236
+ const data = held.length > 0 ? Buffer.concat([held, bytes]) : bytes;
237
+ held = Buffer.alloc(0);
238
+ const before = this.tail.length;
239
+ const starts = lineStarts(this.tail.toString('latin1')).includes(before) ? [0] : [];
240
+ for (const end of lineStarts(Buffer.concat([this.tail, data]).toString('latin1')))
241
+ if (end > before)
242
+ starts.push(end - before);
243
+ const parts = [];
244
+ let at = 0;
245
+ for (const start of starts) {
246
+ const rest = data.length - start;
247
+ if (rest < OWN_PREFIX.length && !final && data.subarray(start).equals(OWN_PREFIX.subarray(0, rest))) {
248
+ held = Buffer.from(data.subarray(start));
249
+ break;
250
+ }
251
+ if (rest >= OWN_PREFIX.length && data.subarray(start, start + OWN_PREFIX.length).equals(OWN_PREFIX)) {
252
+ parts.push(data.subarray(at, start), MARK);
253
+ at = start;
254
+ }
255
+ }
256
+ parts.push(data.subarray(at, data.length - held.length));
257
+ this.write(Buffer.concat(parts));
258
+ };
259
+ return {
260
+ chunk: (bytes) => pass(scrubber.feed(bytes), false),
261
+ end: () => {
262
+ pass(scrubber.flush(), true);
263
+ if (this.tail[this.tail.length - 1] !== LINE_FEED)
264
+ this.write(Buffer.from('\n'));
265
+ },
266
+ };
267
+ }
268
+ write(bytes) {
269
+ if (bytes.length === 0)
270
+ return;
271
+ this.out.write(bytes);
272
+ const both = Buffer.concat([this.tail, bytes]);
273
+ this.tail = Buffer.from(both.subarray(Math.max(0, both.length - LONGEST_BREAK)));
274
+ }
275
+ }
276
+ const onAbandon = new Set();
277
+ let isAbandoned = false;
278
+ let abandon = () => undefined;
279
+ const abandoned = new Promise((resolve) => {
280
+ abandon = () => {
281
+ isAbandoned = true;
282
+ for (const release of onAbandon)
283
+ release();
284
+ onAbandon.clear();
285
+ resolve();
286
+ };
287
+ });
288
+ function pump(src, dst, framing, also = [], passing, giveUp) {
289
+ return new Promise((resolve) => {
290
+ let gone = false;
291
+ let parts = [];
292
+ let size = 0;
293
+ let dropping = false;
294
+ const release = () => {
295
+ if (gone)
296
+ return;
297
+ gone = true;
298
+ parts = [];
299
+ src.resume();
300
+ };
301
+ dst?.stream.once('error', release);
302
+ dst?.stream.once('close', release);
303
+ if (isAbandoned)
304
+ release();
305
+ else
306
+ onAbandon.add(release);
307
+ void giveUp?.then(release);
308
+ const append = (piece) => {
309
+ if (piece.length === 0 || framing === undefined || dropping)
310
+ return;
311
+ if (size + piece.length > INSPECT_LIMIT_BYTES) {
312
+ dropping = true;
313
+ parts = [];
314
+ size = 0;
315
+ framing.oversized();
316
+ return;
317
+ }
318
+ parts.push(Buffer.from(piece));
319
+ size += piece.length;
320
+ };
321
+ const endLine = () => {
322
+ if (!dropping)
323
+ framing?.line(Buffer.concat(parts));
324
+ parts = [];
325
+ size = 0;
326
+ dropping = false;
327
+ };
328
+ src.on('data', (chunk) => {
329
+ if (gone)
330
+ return;
331
+ if (framing === undefined)
332
+ passing?.chunk(chunk);
333
+ else {
334
+ let start = 0;
335
+ let at;
336
+ while ((at = chunk.indexOf(LINE_FEED, start)) !== -1) {
337
+ append(chunk.subarray(start, at));
338
+ endLine();
339
+ start = at + 1;
340
+ }
341
+ if (start < chunk.length)
342
+ append(chunk.subarray(start));
343
+ }
344
+ const slow = gone ? undefined : [...(dst === null ? [] : [dst]), ...also].find((o) => o.behind());
345
+ if (slow !== undefined) {
346
+ src.pause();
347
+ slow.whenCaughtUp(() => src.resume());
348
+ }
349
+ });
350
+ let finished = false;
351
+ const finish = () => {
352
+ if (finished)
353
+ return;
354
+ finished = true;
355
+ if (!gone) {
356
+ if (framing === undefined)
357
+ passing?.end();
358
+ else if (size > 0)
359
+ endLine();
360
+ }
361
+ parts = [];
362
+ dst?.stream.off('error', release);
363
+ dst?.stream.off('close', release);
364
+ onAbandon.delete(release);
365
+ resolve();
366
+ };
367
+ src.on('end', finish);
368
+ src.on('error', finish);
369
+ src.on('close', finish);
370
+ });
371
+ }
372
+ function childEnvironment(env, secrets) {
373
+ const out = {};
374
+ for (const name of CHILD_ENV) {
375
+ const value = env[name];
376
+ if (value !== undefined)
377
+ out[name] = value;
378
+ }
379
+ for (const [name, path] of secrets.files)
380
+ out[`${name}_FILE`] = path;
381
+ return out;
382
+ }
383
+ const describe = (o) => `connector process ${o.stalled ? `stopped reading its input (stalled input: none of it taken for ${STALL_MS / 1000} s) and ` : ''}${o.error ? `could not be started (${o.error.message})` : o.signal !== null ? `was terminated by signal ${o.signal}` : `exited with code ${String(o.code)}`}`;
384
+ const abnormal = (o) => o.code !== 0 || o.stalled === true;
385
+ function startConnector(binPath, childEnv, session, scrubber, toUpstream, upstreamOut, log, stderrAfterExitMs) {
386
+ const child = spawn(process.execPath, [binPath], { stdio: ['pipe', 'pipe', 'pipe'], detached: true, env: childEnv });
387
+ const toGroup = (signal) => {
388
+ if (child.pid === undefined)
389
+ return false;
390
+ try {
391
+ process.kill(-child.pid, signal);
392
+ return true;
393
+ }
394
+ catch {
395
+ return false;
396
+ }
397
+ };
398
+ const toChild = new Out(child.stdin);
399
+ const perform = (actions) => {
400
+ for (const a of actions) {
401
+ if (a.to === 'receipt')
402
+ log.line(`${RECEIPT_PREFIX}${asciiJson(a.receipt)}\n`);
403
+ else if (a.to === 'upstream')
404
+ toUpstream(a.data);
405
+ else
406
+ toChild.write(a.data);
407
+ }
408
+ };
409
+ const judged = (judge, unreadable) => {
410
+ let actions;
411
+ try {
412
+ actions = judge();
413
+ }
414
+ catch {
415
+ actions = unreadable();
416
+ }
417
+ perform(actions);
418
+ };
419
+ let rung = 0;
420
+ let stopped = false;
421
+ const climb = (to, signal = 'SIGTERM') => {
422
+ if (to <= rung)
423
+ return;
424
+ rung = to;
425
+ if (to === 1)
426
+ toChild.end();
427
+ else
428
+ toGroup(to === 2 ? signal : 'SIGKILL');
429
+ if (to < 3)
430
+ setTimeout(() => climb(to + 1), TEARDOWN_STEP_MS).unref();
431
+ else if (stopped)
432
+ abandon();
433
+ };
434
+ let stalled = false;
435
+ toChild.watchStall(STALL_MS, () => {
436
+ if (rung >= 2)
437
+ return;
438
+ stalled = true;
439
+ climb(2);
440
+ });
441
+ const fromChild = pump(child.stdout, upstreamOut, {
442
+ line: (line) => judged(() => session.fromConnector(line), () => session.connectorOversized()),
443
+ oversized: () => perform(session.connectorOversized()),
444
+ });
445
+ const exited = new Promise((resolve) => {
446
+ child.on('error', (error) => resolve({ code: null, signal: null, error }));
447
+ child.on('exit', (code, signal) => resolve({ code, signal }));
448
+ });
449
+ const stderrGiveUp = stderrAfterExitMs === undefined ? undefined : exited.then(() => new Promise((resolve) => setTimeout(resolve, stderrAfterExitMs).unref()));
450
+ const fromChildErr = pump(child.stderr, log.out, undefined, [], log.connector(scrubber), stderrGiveUp);
451
+ const outcome = (async () => {
452
+ const exit = await exited;
453
+ climb(2);
454
+ await Promise.all([fromChild, fromChildErr]);
455
+ const ended = stalled ? { ...exit, stalled } : exit;
456
+ perform(session.connectorGone(describe(ended), abnormal(ended)));
457
+ while (rung < 3 && toGroup(0))
458
+ await new Promise((resolve) => setTimeout(resolve, 25));
459
+ return ended;
460
+ })();
461
+ return {
462
+ toChild,
463
+ perform,
464
+ fromUpstream: (line, principal) => judged(() => session.fromUpstream(line, principal), () => session.upstreamOversized()),
465
+ endInput: () => climb(1),
466
+ stop: (signal) => {
467
+ stopped = true;
468
+ if (rung >= 3)
469
+ abandon();
470
+ else
471
+ climb(2, signal);
472
+ },
473
+ outcome,
474
+ };
475
+ }
476
+ async function runStdio(packageName, binPath, childEnv, session, scrubber, stdout, log) {
477
+ const resolution = standaloneResolver().resolve({ headers: {} });
478
+ if (!resolution.ok)
479
+ throw new Error('the standalone principal cannot be refused');
480
+ const anonymous = resolution.principal;
481
+ const run = startConnector(binPath, childEnv, session, scrubber, (data) => stdout.write(data), stdout, log);
482
+ const upstream = pump(process.stdin, run.toChild, { line: (line) => run.fromUpstream(line, anonymous), oversized: () => run.perform(session.upstreamOversized()) }, [stdout]);
483
+ void upstream.then(() => run.endInput());
484
+ for (const s of STOP_SIGNALS)
485
+ process.on(s, (signal) => run.stop(signal));
486
+ const outcome = await run.outcome;
487
+ process.stdin.destroy();
488
+ if (!abnormal(outcome))
489
+ return EXIT.ok;
490
+ log.line(`sema-connector: ${INTERNAL}: ${packageName}: ${describe(outcome)}\n`);
491
+ return EXIT.connectorFailed;
492
+ }
493
+ async function runHosted(plan, keys, packageName, binPath, env, log) {
494
+ const childEnv = childEnvironment(env, NO_SECRETS);
495
+ const start = (deliver) => {
496
+ const session = new Session({
497
+ connector: plan.name,
498
+ status: (tools) => mintStatus({ handles: [], nowMs: Date.now(), tools }),
499
+ ownIdPrefix: `sema-connector/${randomUUID()}`,
500
+ withheld: secretMatcher(NO_SECRETS.values),
501
+ });
502
+ const run = startConnector(binPath, childEnv, session, new Scrubber(NO_SECRETS.values), (data) => deliver(String(data)), null, log, TEARDOWN_STEP_MS);
503
+ return {
504
+ send: (raw, principal) => run.fromUpstream(raw, principal),
505
+ busy: () => run.toChild.behind(),
506
+ readStatus: (id, principal) => run.perform(session.statusFor(id, principal)),
507
+ end: (signal) => (signal === undefined ? run.endInput() : run.stop(signal)),
508
+ gone: run.outcome.then((o) => {
509
+ if (abnormal(o))
510
+ log.line(`sema-connector: ${INTERNAL}: ${packageName}: ${describe(o)}\n`);
511
+ }),
512
+ };
513
+ };
514
+ const face = await serveHttp({ host: plan.listen.host, port: plan.listen.port, resolver: hostedResolver({ issuer: plan.issuer, audience: plan.audience, keys }), start });
515
+ if (!face.ok) {
516
+ log.line(`sema-connector: cannot listen on the address given with --listen (${face.code})\n`);
517
+ return EXIT.guard;
518
+ }
519
+ const signal = await new Promise((resolve) => {
520
+ for (const s of STOP_SIGNALS)
521
+ process.on(s, resolve);
522
+ });
523
+ await face.close(signal);
524
+ setTimeout(abandon, TEARDOWN_STEP_MS);
525
+ return EXIT.ok;
526
+ }
527
+ const NO_SECRETS = { files: new Map(), values: [], remove: () => undefined };
528
+ export async function main(argv, env) {
529
+ const stdout = new Out(process.stdout);
530
+ const log = new Log(new Out(process.stderr));
531
+ const finish = async (code) => {
532
+ await Promise.race([Promise.all([stdout.flushed(), log.out.flushed()]), abandoned]);
533
+ return code;
534
+ };
535
+ const plan = guard(argv, env);
536
+ if (typeof plan === 'string') {
537
+ log.line(`sema-connector: ${plan}\n${USAGE}\n`);
538
+ return finish(EXIT.guard);
539
+ }
540
+ if (plan.mode === 'hosted') {
541
+ const keys = await obtainKeySet(plan.keySet);
542
+ if (!keys.ok) {
543
+ log.line(`sema-connector: ${keys.problem}\n`);
544
+ return finish(EXIT.guard);
545
+ }
546
+ const resolved = resolveConnector(plan.name);
547
+ if ('reason' in resolved) {
548
+ log.line(`sema-connector: ${NOT_INSTALLED}: package ${resolved.packageName} ${resolved.reason}\n`);
549
+ return finish(EXIT.notInstalled);
550
+ }
551
+ return finish(await runHosted(plan, keys.keys, resolved.packageName, resolved.binPath, env, log));
552
+ }
553
+ const listing = openFileSecrets({ configDir: plan.configDir, connector: plan.name }).list();
554
+ if (!listing.ok) {
555
+ log.line(`sema-connector: ${INTERNAL}: ${listing.problem}\n`);
556
+ return finish(EXIT.guard);
557
+ }
558
+ const { handles } = listing;
559
+ let secrets;
560
+ try {
561
+ secrets = copyForLaunch(plan.configDir, plan.name, handles);
562
+ }
563
+ catch {
564
+ log.line(`sema-connector: ${INTERNAL}: connector ${plan.name}: its secrets cannot be read for this launch\n`);
565
+ return finish(EXIT.guard);
566
+ }
567
+ const resolved = resolveConnector(plan.name);
568
+ if ('reason' in resolved) {
569
+ secrets.remove();
570
+ log.line(`sema-connector: ${NOT_INSTALLED}: package ${resolved.packageName} ${resolved.reason}\n`);
571
+ return finish(EXIT.notInstalled);
572
+ }
573
+ const session = new Session({
574
+ connector: plan.name,
575
+ status: (tools) => mintStatus({ handles, nowMs: Date.now(), tools }),
576
+ ownIdPrefix: `sema-connector/${randomUUID()}`,
577
+ withheld: secretMatcher(secrets.values),
578
+ });
579
+ try {
580
+ return finish(await runStdio(resolved.packageName, resolved.binPath, childEnvironment(env, secrets), session, new Scrubber(secrets.values), stdout, log));
581
+ }
582
+ finally {
583
+ secrets.remove();
584
+ }
585
+ }
@@ -0,0 +1,23 @@
1
+ export interface MountOptions {
2
+ readonly name: string;
3
+ readonly configDir: string;
4
+ }
5
+ export type MountRefusal = 'config_dir' | 'invalid_name' | 'reserved_name' | 'unreadable' | 'unexpected_shape' | 'conflict' | 'locked' | 'changed_during_write';
6
+ export type MountResult = {
7
+ readonly ok: true;
8
+ readonly changed: boolean;
9
+ readonly file: string;
10
+ } | {
11
+ readonly ok: false;
12
+ readonly reason: MountRefusal;
13
+ readonly message: string;
14
+ };
15
+ export declare function standaloneEntry(name: string, configDir: string): {
16
+ name: string;
17
+ transport: {
18
+ kind: 'stdio';
19
+ command: string;
20
+ args: string[];
21
+ };
22
+ };
23
+ export declare function mount(options: MountOptions): MountResult;