@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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,122 @@ All notable changes to this package are documented here. Released sections are
|
|
|
4
4
|
frozen: once a version is published its section is never edited again; new text
|
|
5
5
|
goes into the next version's section.
|
|
6
6
|
|
|
7
|
+
## 0.3.0 (2026-10-15)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- The default catalog seed (CN-009): `sema-connector seed --config-dir
|
|
12
|
+
<absolute path>`, the launcher's second command, and `seed({ configDir })`
|
|
13
|
+
mount every installed connector of the default catalog into
|
|
14
|
+
`config.d/mcp.json`. The catalog is `DEFAULT_CONNECTORS` (today `gitea` and
|
|
15
|
+
`github`, CN-044),
|
|
16
|
+
the one list of default connectors; `SEED_VERSION` is the newest version
|
|
17
|
+
that added a name. Each connector is resolved with the launcher's own rule
|
|
18
|
+
first (one that is not installed gets no entry) and mounted only through
|
|
19
|
+
`mount`, so another entry under its name is never overwritten.
|
|
20
|
+
`config.d/.sema-connector-seed.json` records what each name came to
|
|
21
|
+
(mounted, present, not installed): a name recorded as mounted or present is
|
|
22
|
+
never mounted again, so an entry a person removed stays removed; a conflict
|
|
23
|
+
or a refusal is not recorded and is tried again. Seeds on one directory
|
|
24
|
+
exclude each other with `config.d/.sema-connector-seed.json.lock` (a second
|
|
25
|
+
seed refuses as `locked`). One line per connector on stdout; exit 0 when
|
|
26
|
+
every connector is mounted, present or not installed, 1 otherwise. A marker
|
|
27
|
+
the seed cannot read refuses it before any mount; a marker write that fails
|
|
28
|
+
leaves the old marker as it was and the mounted entries in place.
|
|
29
|
+
- `pinnedUpstream({ connector, upstream, lockFile, handles })` (CN-044): what
|
|
30
|
+
a connector that runs a third-party MCP server takes from the shared layer,
|
|
31
|
+
so that no connector package carries its own copy. `install(configDir)` is
|
|
32
|
+
the install subcommand: this platform's release archive from the source the
|
|
33
|
+
package's `binaries.lock.json` gives (`https`, redirects followed only to
|
|
34
|
+
`https`, or `file` for a local copy of the releases), its sha256 compared
|
|
35
|
+
with the lock before anything is written, then the archive and its
|
|
36
|
+
`upstream` entry kept in `<configuration directory>/connectors/<name>/bin/`
|
|
37
|
+
(directory 0700, program 0755), each file written whole before it replaces
|
|
38
|
+
the old one; nothing changes when the pinned build is there. `start(env)`
|
|
39
|
+
checks `CONNECTOR_DIR`, one `<NAME>_FILE` per handle naming a regular file
|
|
40
|
+
(not a symbolic link) inside it, and the pinned build in the bin directory
|
|
41
|
+
(the same check as install's), and refuses with a sentence naming what to
|
|
42
|
+
run; its `run(args, extra)` starts the program as the connector's child on
|
|
43
|
+
the same standard input, output and error, with the connector's environment
|
|
44
|
+
minus `CONNECTOR_DIR` and the handle paths plus `extra`, passes `SIGTERM`,
|
|
45
|
+
`SIGINT` and `SIGHUP` on, and ends as the program ends. The Gitea connector
|
|
46
|
+
is its first user (moved from its own package) and the GitHub connector its
|
|
47
|
+
second.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- A launcher command line that starts with neither `run` nor `seed` is
|
|
52
|
+
refused with `expected the command "run" or "seed"` (exit 2, as before).
|
|
53
|
+
- `readStdinJson` with one key names it as "the key" in its refusals
|
|
54
|
+
(`the object must have exactly the key token`); with two or more keys the
|
|
55
|
+
wording is as before.
|
|
56
|
+
|
|
57
|
+
## 0.2.0 (2026-10-15)
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- Handles that are not secrets (CN-038). `<handle>.meta.json` may say
|
|
62
|
+
`"secret": false`; such a handle is delivered to the connector like every
|
|
63
|
+
handle (a per-launch 0600 copy named by `<NAME>_FILE`), but the launcher
|
|
64
|
+
does not withhold its value and the status does not count it. Every other
|
|
65
|
+
handle is a secret, as before. `openFileSecrets().put(name, value,
|
|
66
|
+
{ secret: false })` writes it, `SecretHandle.secret` reports it, and a
|
|
67
|
+
later `put` without it makes the handle a secret again.
|
|
68
|
+
- `CONNECTOR_DIR` (CN-038): the launcher gives its connector the absolute path
|
|
69
|
+
of the connector's directory, `<configuration directory>/connectors/<name>`
|
|
70
|
+
(standalone; it may not exist), so a connector no longer infers it from the
|
|
71
|
+
path of a handle copy. A hosted connector, which has no configuration
|
|
72
|
+
directory and no handles in this release, gets a fresh path of its own that
|
|
73
|
+
does not exist. A `CONNECTOR_DIR` set for the launcher never reaches the
|
|
74
|
+
connector.
|
|
75
|
+
- The lock of a connector's directory (CN-038), `.auth.lock`: every write of
|
|
76
|
+
the secrets backend and every launch's listing and copy of the handles hold
|
|
77
|
+
it, so a launch copies a group of writes before or after the group, never in
|
|
78
|
+
between. `openFileSecrets().withLock(fn)` holds it across several writes. A
|
|
79
|
+
taker waits up to 5 seconds for another holder, then refuses (the launcher
|
|
80
|
+
with exit 2 and `connector.internal`); only the holder removes the lock, and
|
|
81
|
+
one left by a process that died is removed by hand. `FileSecrets.directory`
|
|
82
|
+
names the connector's directory.
|
|
83
|
+
- The connector CLI kit (CN-039): `connectorCli` runs a connector package's
|
|
84
|
+
bin (no arguments: the connector's server; `install`, `auth`, `mount`,
|
|
85
|
+
`status` without arguments on the configuration directory of
|
|
86
|
+
`resolveConfigDir`), `cliOutput`, `readStdinLine`, `readStdinJson` (one line
|
|
87
|
+
holding a JSON object with exactly the given keys, each named once, every
|
|
88
|
+
value a string), `mountCommand`, `statusCommand` (the status resource read
|
|
89
|
+
through the launcher within 10 seconds) and `isLoopbackHttpOrHttps`, the
|
|
90
|
+
one rule for an address a connector sends requests to (the rule of a key set
|
|
91
|
+
URL).
|
|
92
|
+
- `writeAll(fd, data)` (CN-042): the one whole write. It writes every byte of
|
|
93
|
+
`data` into the empty file open at `fd` (a write that writes nothing or fails
|
|
94
|
+
ends it with an error), syncs the file and checks that its size is the number
|
|
95
|
+
of bytes, or throws. Every file this package writes goes through it, and a
|
|
96
|
+
connector package writes its own files with it.
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
|
|
100
|
+
- The status counts secret handles only: a connector whose only handles are
|
|
101
|
+
not secrets is `unauthorized`, and the expiry of a handle that is not a
|
|
102
|
+
secret changes nothing (CN-038).
|
|
103
|
+
- `<handle>.meta.json` must be a JSON object whose only keys are `expiresAt`
|
|
104
|
+
and `secret`, each named once; another key, a key named twice or a `secret`
|
|
105
|
+
that is not `true` or `false` makes the directory refused (another key used
|
|
106
|
+
to be ignored, and an object without `expiresAt` refused) (CN-038).
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- A write cut short is no longer taken as done (CN-042). A write can stop short
|
|
111
|
+
without an error (a file size limit, a full disk); `mount` used to rename the
|
|
112
|
+
cut-short text over `mcp.json` and report it written, losing the other
|
|
113
|
+
entries, and `put` stored a cut-short value and reported success. Now nothing
|
|
114
|
+
is put in place before it is written whole: `mount` refuses with the new
|
|
115
|
+
reason `write_failed` and `put` throws, the temporary file is removed and the
|
|
116
|
+
old file (or its absence) stays as it was. The lock files and the per-launch
|
|
117
|
+
copies of the handles are written the same way.
|
|
118
|
+
- The scope judge never grants the empty tool name (CN-036): a `tools/call`
|
|
119
|
+
whose name is `""` is refused with `connector.forbidden_scope` also under
|
|
120
|
+
`tool:*` (or a bare `tool:`), and a listed tool whose name is `""` is not
|
|
121
|
+
listed.
|
|
122
|
+
|
|
7
123
|
## 0.1.0 (2026-10-14)
|
|
8
124
|
|
|
9
125
|
### Added
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Env } from './config-dir.js';
|
|
2
|
+
export interface CliOutput {
|
|
3
|
+
readonly program: string;
|
|
4
|
+
say(line: string): void;
|
|
5
|
+
complain(line: string): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function cliOutput(name: string): CliOutput;
|
|
8
|
+
export interface ConnectorCommands {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly serve: (env: Env) => void | number | Promise<number>;
|
|
11
|
+
readonly install: (configDir: string) => number | Promise<number>;
|
|
12
|
+
readonly auth: (configDir: string) => number | Promise<number>;
|
|
13
|
+
readonly authReads: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function connectorCli(commands: ConnectorCommands, argv: readonly string[], env: Env): Promise<number | undefined>;
|
|
16
|
+
export type StdinLine = {
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
readonly line: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ok: false;
|
|
21
|
+
readonly problem: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function readStdinLine(limit: number): Promise<StdinLine>;
|
|
24
|
+
export type StdinJson = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
readonly values: Readonly<Record<string, string>>;
|
|
27
|
+
} | {
|
|
28
|
+
readonly ok: false;
|
|
29
|
+
readonly problem: string;
|
|
30
|
+
};
|
|
31
|
+
export declare const listed: (keys: readonly string[]) => string;
|
|
32
|
+
export declare function readStdinJson(limit: number, keys: readonly string[]): Promise<StdinJson>;
|
|
33
|
+
export declare function mountCommand(name: string, configDir: string): number;
|
|
34
|
+
export declare function statusCommand(name: string, configDir: string): Promise<number>;
|
package/dist/cli-kit.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { resolveConfigDir } from './config-dir.js';
|
|
6
|
+
import { CONNECTOR_NAME } from './connector-name.js';
|
|
7
|
+
import { hasRepeatedKey } from './json-text.js';
|
|
8
|
+
import { mount } from './mount.js';
|
|
9
|
+
export function cliOutput(name) {
|
|
10
|
+
if (typeof name !== 'string' || !CONNECTOR_NAME.test(name))
|
|
11
|
+
throw new Error('a connector name is lowercase letters and digits in hyphen-separated words');
|
|
12
|
+
const program = `sema-connector-${name}`;
|
|
13
|
+
return {
|
|
14
|
+
program,
|
|
15
|
+
say: (line) => void process.stdout.write(`${line}\n`),
|
|
16
|
+
complain: (line) => void process.stderr.write(`${program}: ${line}\n`),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const SUBCOMMANDS = ['install', 'auth', 'mount', 'status'];
|
|
20
|
+
export async function connectorCli(commands, argv, env) {
|
|
21
|
+
const { complain, program } = cliOutput(commands.name);
|
|
22
|
+
const [command, ...rest] = argv;
|
|
23
|
+
if (command === undefined) {
|
|
24
|
+
const code = await commands.serve(env);
|
|
25
|
+
return typeof code === 'number' ? code : undefined;
|
|
26
|
+
}
|
|
27
|
+
const usage = `usage: ${program} [${SUBCOMMANDS.join(' | ')}] (no argument: the connector's MCP stdio server; auth reads ${commands.authReads} from standard input)`;
|
|
28
|
+
if (!SUBCOMMANDS.includes(command)) {
|
|
29
|
+
complain(`unknown subcommand\n${usage}`);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
if (rest.length > 0) {
|
|
33
|
+
complain(`${command} takes no arguments${command === 'auth' ? ` (it reads ${commands.authReads} from standard input, never from an argument)` : ''}\n${usage}`);
|
|
34
|
+
return 2;
|
|
35
|
+
}
|
|
36
|
+
const config = resolveConfigDir(env);
|
|
37
|
+
if (!config.ok) {
|
|
38
|
+
complain(config.reason);
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
if (command === 'install')
|
|
42
|
+
return commands.install(config.dir);
|
|
43
|
+
if (command === 'auth')
|
|
44
|
+
return commands.auth(config.dir);
|
|
45
|
+
if (command === 'mount')
|
|
46
|
+
return mountCommand(commands.name, config.dir);
|
|
47
|
+
return statusCommand(commands.name, config.dir);
|
|
48
|
+
}
|
|
49
|
+
export async function readStdinLine(limit) {
|
|
50
|
+
if (process.stdin.isTTY)
|
|
51
|
+
return { ok: false, problem: 'standard input is a terminal here; pipe the input in instead (a token typed on a terminal would be shown)' };
|
|
52
|
+
const parts = [];
|
|
53
|
+
let size = 0;
|
|
54
|
+
for await (const chunk of process.stdin) {
|
|
55
|
+
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
56
|
+
size += b.length;
|
|
57
|
+
if (size > limit)
|
|
58
|
+
return { ok: false, problem: `standard input holds more than ${limit} bytes; one line is read` };
|
|
59
|
+
parts.push(b);
|
|
60
|
+
}
|
|
61
|
+
const line = Buffer.concat(parts).toString('utf8').replace(/\r?\n$/, '');
|
|
62
|
+
if (/[\r\n]/.test(line))
|
|
63
|
+
return { ok: false, problem: 'standard input must hold exactly one line' };
|
|
64
|
+
return { ok: true, line };
|
|
65
|
+
}
|
|
66
|
+
export const listed = (keys) => (keys.length < 2 ? keys.join('') : `${keys.slice(0, -1).join(', ')} and ${keys[keys.length - 1]}`);
|
|
67
|
+
export async function readStdinJson(limit, keys) {
|
|
68
|
+
const read = await readStdinLine(limit);
|
|
69
|
+
if (!read.ok)
|
|
70
|
+
return read;
|
|
71
|
+
const exactly = keys.length === 1 ? `exactly the key ${listed(keys)}` : `exactly the keys ${listed(keys)}`;
|
|
72
|
+
const shape = `standard input must be one JSON object with ${exactly}, ${keys.length === 1 ? 'a string' : 'each a string'}, on one line`;
|
|
73
|
+
let value;
|
|
74
|
+
try {
|
|
75
|
+
value = JSON.parse(read.line);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return { ok: false, problem: shape };
|
|
79
|
+
}
|
|
80
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
81
|
+
return { ok: false, problem: shape };
|
|
82
|
+
if (hasRepeatedKey(read.line))
|
|
83
|
+
return { ok: false, problem: 'the object names a key more than once' };
|
|
84
|
+
const object = value;
|
|
85
|
+
const own = Object.keys(object);
|
|
86
|
+
if (own.length !== keys.length || !keys.every((k) => own.includes(k)))
|
|
87
|
+
return { ok: false, problem: `the object must have ${exactly}` };
|
|
88
|
+
if (!keys.every((k) => typeof object[k] === 'string'))
|
|
89
|
+
return { ok: false, problem: shape };
|
|
90
|
+
return { ok: true, values: Object.fromEntries(keys.map((k) => [k, object[k]])) };
|
|
91
|
+
}
|
|
92
|
+
export function mountCommand(name, configDir) {
|
|
93
|
+
const { say, complain } = cliOutput(name);
|
|
94
|
+
let r;
|
|
95
|
+
try {
|
|
96
|
+
r = mount({ name, configDir });
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
complain(`mount failed: ${e instanceof Error ? e.message : 'unknown error'}`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
if (!r.ok) {
|
|
103
|
+
complain(`mount refused (${r.reason}): ${r.message}`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
say(r.changed ? `mounted connector ${name} in ${r.file}; restart the engine to apply` : `connector ${name} is already mounted in ${r.file}`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const PROTOCOL_VERSION = '2025-11-25';
|
|
110
|
+
const STATUS_TIMEOUT_MS = 10_000;
|
|
111
|
+
const KILL_GRACE_MS = 2_000;
|
|
112
|
+
const SHOWN_LINES = 5;
|
|
113
|
+
const RECEIPT_PREFIX = 'sema-connector: receipt ';
|
|
114
|
+
function ownManifest() {
|
|
115
|
+
const url = new URL('../package.json', import.meta.url);
|
|
116
|
+
const manifest = JSON.parse(readFileSync(url, 'utf8'));
|
|
117
|
+
return { version: manifest.version, launcher: join(dirname(fileURLToPath(url)), manifest.bin['sema-connector'] ?? '') };
|
|
118
|
+
}
|
|
119
|
+
function lineReader(stream) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
const waiters = [];
|
|
122
|
+
let rest = '';
|
|
123
|
+
let ended = false;
|
|
124
|
+
stream.setEncoding('utf8');
|
|
125
|
+
stream.on('data', (chunk) => {
|
|
126
|
+
rest += chunk;
|
|
127
|
+
let at;
|
|
128
|
+
while ((at = rest.indexOf('\n')) !== -1) {
|
|
129
|
+
const line = rest.slice(0, at);
|
|
130
|
+
rest = rest.slice(at + 1);
|
|
131
|
+
const waiter = waiters.shift();
|
|
132
|
+
if (waiter)
|
|
133
|
+
waiter(line);
|
|
134
|
+
else
|
|
135
|
+
lines.push(line);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
stream.on('close', () => {
|
|
139
|
+
ended = true;
|
|
140
|
+
for (const waiter of waiters.splice(0))
|
|
141
|
+
waiter(null);
|
|
142
|
+
});
|
|
143
|
+
return () => {
|
|
144
|
+
const line = lines.shift();
|
|
145
|
+
if (line !== undefined)
|
|
146
|
+
return Promise.resolve(line);
|
|
147
|
+
if (ended)
|
|
148
|
+
return Promise.resolve(null);
|
|
149
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export async function statusCommand(name, configDir) {
|
|
153
|
+
const { program, say, complain } = cliOutput(name);
|
|
154
|
+
const own = ownManifest();
|
|
155
|
+
const child = spawn(process.execPath, [own.launcher, 'run', name, '--mode', 'standalone', '--transport', 'stdio', '--config-dir', configDir], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
156
|
+
const exited = new Promise((resolve) => child.on('exit', (code) => resolve(code)));
|
|
157
|
+
const errors = [];
|
|
158
|
+
child.stderr.on('data', (d) => errors.push(d));
|
|
159
|
+
child.stdin.on('error', () => undefined);
|
|
160
|
+
const nextLine = lineReader(child.stdout);
|
|
161
|
+
let timedOut = false;
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
timedOut = true;
|
|
164
|
+
child.stdout.destroy();
|
|
165
|
+
child.stderr.destroy();
|
|
166
|
+
child.kill('SIGTERM');
|
|
167
|
+
setTimeout(() => child.kill('SIGKILL'), KILL_GRACE_MS).unref();
|
|
168
|
+
}, STATUS_TIMEOUT_MS);
|
|
169
|
+
const write = (message) => void child.stdin.write(JSON.stringify({ jsonrpc: '2.0', ...message }) + '\n');
|
|
170
|
+
const reply = async (id, method, params) => {
|
|
171
|
+
write({ id, method, ...(params === undefined ? {} : { params }) });
|
|
172
|
+
for (;;) {
|
|
173
|
+
const line = await nextLine();
|
|
174
|
+
if (line === null)
|
|
175
|
+
return null;
|
|
176
|
+
try {
|
|
177
|
+
const m = JSON.parse(line);
|
|
178
|
+
if (m?.id === id)
|
|
179
|
+
return m;
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
let found = null;
|
|
185
|
+
let problem = null;
|
|
186
|
+
try {
|
|
187
|
+
const init = await reply(1, 'initialize', { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: program, version: own.version } });
|
|
188
|
+
if (init?.result === undefined)
|
|
189
|
+
problem = 'the launcher did not complete initialize';
|
|
190
|
+
else {
|
|
191
|
+
write({ method: 'notifications/initialized' });
|
|
192
|
+
const list = await reply(2, 'resources/list');
|
|
193
|
+
const entries = list?.result?.resources;
|
|
194
|
+
const uri = Array.isArray(entries) && entries.length === 1 ? entries[0]?.uri : undefined;
|
|
195
|
+
if (typeof uri !== 'string')
|
|
196
|
+
problem = 'the launcher did not list exactly one resource, its status';
|
|
197
|
+
else {
|
|
198
|
+
const read = await reply(3, 'resources/read', { uri });
|
|
199
|
+
const text = read?.result?.contents?.[0]?.text;
|
|
200
|
+
if (typeof text !== 'string')
|
|
201
|
+
problem = `the status could not be read${typeof read?.error?.message === 'string' ? `: ${read.error.message}` : ''}`;
|
|
202
|
+
else
|
|
203
|
+
found = JSON.parse(text);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
problem = 'the launcher answered with something that is not JSON';
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
child.stdin.end();
|
|
212
|
+
}
|
|
213
|
+
const code = await exited;
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
if (found !== null && !timedOut) {
|
|
216
|
+
say(JSON.stringify(found));
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
complain(timedOut ? `the launcher did not answer within ${STATUS_TIMEOUT_MS} ms` : (problem ?? `the launcher exited with code ${String(code)}`));
|
|
220
|
+
const said = Buffer.concat(errors).toString('utf8').split('\n').filter((l) => l.trim() !== '' && !l.startsWith(RECEIPT_PREFIX));
|
|
221
|
+
for (const line of said.slice(-SHOWN_LINES))
|
|
222
|
+
complain(`the launcher said: ${line}`);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,11 @@ export { ASSERTION_HEADER, hostedResolver, standaloneResolver, verifyAssertion }
|
|
|
6
6
|
export type { AnonymousPrincipal, AssertionPrincipal, AssertionStep, Principal, PrincipalResolver, PrincipalResult, RequestContext, VerifyOptions, VerifyResult, } from './principal.js';
|
|
7
7
|
export { mount } from './mount.js';
|
|
8
8
|
export type { MountOptions, MountRefusal, MountResult } from './mount.js';
|
|
9
|
+
export { DEFAULT_CONNECTORS, SEED_VERSION, seed } from './seed.js';
|
|
10
|
+
export type { DefaultConnector, SeedOptions, SeedReading, SeedRefusal, SeedResult } from './seed.js';
|
|
11
|
+
export { cliOutput, connectorCli, mountCommand, readStdinJson, readStdinLine, statusCommand } from './cli-kit.js';
|
|
12
|
+
export type { CliOutput, ConnectorCommands, StdinJson, StdinLine } from './cli-kit.js';
|
|
13
|
+
export { isLoopbackHttpOrHttps } from './jwks.js';
|
|
14
|
+
export { pinnedUpstream } from './pinned-upstream.js';
|
|
15
|
+
export type { PinnedUpstream, PinnedUpstreamOptions, UpstreamStart } from './pinned-upstream.js';
|
|
16
|
+
export { writeAll } from './write-all.js';
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,8 @@ export { resolveConfigDir } from './config-dir.js';
|
|
|
2
2
|
export { openFileSecrets } from './secrets.js';
|
|
3
3
|
export { ASSERTION_HEADER, hostedResolver, standaloneResolver, verifyAssertion } from './principal.js';
|
|
4
4
|
export { mount } from './mount.js';
|
|
5
|
+
export { DEFAULT_CONNECTORS, SEED_VERSION, seed } from './seed.js';
|
|
6
|
+
export { cliOutput, connectorCli, mountCommand, readStdinJson, readStdinLine, statusCommand } from './cli-kit.js';
|
|
7
|
+
export { isLoopbackHttpOrHttps } from './jwks.js';
|
|
8
|
+
export { pinnedUpstream } from './pinned-upstream.js';
|
|
9
|
+
export { writeAll } from './write-all.js';
|
package/dist/jwks.d.ts
CHANGED
|
@@ -14,5 +14,6 @@ export type KeySet = {
|
|
|
14
14
|
readonly ok: false;
|
|
15
15
|
readonly problem: string;
|
|
16
16
|
};
|
|
17
|
+
export declare function isLoopbackHttpOrHttps(url: URL): boolean;
|
|
17
18
|
export declare function keySetSource(env: Env, issuer: string): KeySetSource | string;
|
|
18
19
|
export declare function obtainKeySet(source: KeySetSource): Promise<KeySet>;
|
package/dist/jwks.js
CHANGED
|
@@ -5,6 +5,9 @@ const INLINE = 'CONNECTOR_ASSERTION_JWKS';
|
|
|
5
5
|
const OVERRIDE = 'CONNECTOR_ASSERTION_JWKS_URL';
|
|
6
6
|
const WELL_KNOWN = '/.well-known/jwks.json';
|
|
7
7
|
const LOOPBACK_HOSTS = ['127.0.0.1', 'localhost'];
|
|
8
|
+
export function isLoopbackHttpOrHttps(url) {
|
|
9
|
+
return url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK_HOSTS.includes(url.hostname));
|
|
10
|
+
}
|
|
8
11
|
const FETCH_LIMIT_BYTES = 1024 * 1024;
|
|
9
12
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
10
13
|
export function keySetSource(env, issuer) {
|
|
@@ -24,7 +27,7 @@ export function keySetSource(env, issuer) {
|
|
|
24
27
|
}
|
|
25
28
|
if (url.username !== '' || url.password !== '')
|
|
26
29
|
return `${from} carries credentials; a key set URL never does`;
|
|
27
|
-
if (
|
|
30
|
+
if (isLoopbackHttpOrHttps(url))
|
|
28
31
|
return { kind: 'url', from, url };
|
|
29
32
|
return `${from} must be an https URL, or an http URL on 127.0.0.1 or localhost`;
|
|
30
33
|
}
|