@bluenotelogic/harness 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/index.js ADDED
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bnl-harness — point this machine at Blue Note Memory in one command.
4
+ *
5
+ * npx @bluenotelogic/harness init
6
+ *
7
+ * THE PROBLEM THIS SOLVES: configuring a machine used to mean pasting a bearer token by
8
+ * hand into `.mcp.json`, `~/.claude.json`, `~/.codex/config.toml` and a VS Code roaming
9
+ * profile — and rotating anything meant editing every one of them again.
10
+ *
11
+ * THE IMPORTANT PART — the `.mcp.json` this writes carries NO TOKEN:
12
+ *
13
+ * { "mcpServers": { "mem": { "type": "http", "url": "https://mem.bnlgit.com/mcp" } } }
14
+ *
15
+ * OAuth-capable hosts (Claude Code, VS Code) discover auth from the endpoint's 401 +
16
+ * WWW-Authenticate, run the browser flow against Blue Note ID, and store their own
17
+ * credentials. Nothing secret is written into a repo file, so the file is safe to commit
18
+ * and there is nothing to rotate.
19
+ *
20
+ * Codex and CI cannot do that yet, so `--mint-token` exists for them — and it writes the
21
+ * token to `~/.codex/.env`, never into a repo. Minting is OPT-IN precisely so the default
22
+ * path creates no credential and no database row.
23
+ */
24
+ import { homedir } from "node:os";
25
+ import { join, resolve } from "node:path";
26
+ import { discover, registerClient, authorizeViaBrowser, authorizeViaDevice, } from "./oauth.js";
27
+ import { exchangeForTenantToken, fetchBundle, SubscriptionRequiredError, } from "./bundle.js";
28
+ import { planMcpJson, planSettingsLocal, planCodexConfig, planEnvVar, planRoutingFile, assertNoSecret, readIfExists, isNoop, apply, } from "./writers.js";
29
+ const DEFAULT_BASE_URL = "https://mem.bnlgit.com";
30
+ const DEFAULT_CLIENT_ID = "blue-note-mem-mcp";
31
+ /**
32
+ * `blue_note_identity`, NOT `email`. The two are easy to confuse and the failure is silent
33
+ * until the very last step:
34
+ *
35
+ * The `blue-note-mem-mcp` provider has exactly four scope mappings attached — openid, profile,
36
+ * offline_access and blue_note_identity. The stock `email` mapping is deliberately NOT among
37
+ * them, and Authentik ignores a scope it has no mapping for rather than refusing it. So asking
38
+ * for `email` succeeded all the way through consent and the code exchange, then produced a
39
+ * userinfo document with no `email_verified`, which `/api/oidc_exchange.php` rejects as
40
+ * "A verified email is required" — surfaced to the user as a 401 that reads like a bad token.
41
+ *
42
+ * `blue_note_identity` ("Blue Note verified identity") is the mapping that supplies email and
43
+ * email_verified. This matches what the browser login has always requested — see
44
+ * BlueNoteOidc.php:201 in caveau-mem, which is the same identity resolution this trades into.
45
+ */
46
+ const DEFAULT_SCOPE = "openid profile blue_note_identity offline_access";
47
+ const TOKEN_ENV_VAR = "BNL_MEMORY_MCP_TOKEN";
48
+ /**
49
+ * `--timeout`, in seconds. Refused rather than coerced: a typo'd `--timeout abc` silently
50
+ * becoming NaN — and therefore an instant timeout — would be a second, quieter version of the
51
+ * exact failure this flag exists to fix.
52
+ */
53
+ function parseTimeout(raw) {
54
+ const sec = Number(raw);
55
+ if (!Number.isInteger(sec) || sec < 30 || sec > 3600) {
56
+ fail(`--timeout wants whole seconds between 30 and 3600, got: ${raw}`);
57
+ }
58
+ return sec * 1000;
59
+ }
60
+ function parseArgs(argv) {
61
+ const opts = {
62
+ baseUrl: DEFAULT_BASE_URL,
63
+ dir: process.cwd(),
64
+ dryRun: false,
65
+ device: false,
66
+ token: null,
67
+ mintToken: false,
68
+ harnessHome: null,
69
+ serverName: null,
70
+ clientId: DEFAULT_CLIENT_ID,
71
+ openBrowser: true,
72
+ codex: false,
73
+ timeoutMs: null,
74
+ };
75
+ let command = "init";
76
+ const rest = [...argv];
77
+ if (rest[0] && !rest[0].startsWith("-"))
78
+ command = rest.shift();
79
+ while (rest.length) {
80
+ const arg = rest.shift();
81
+ const next = () => {
82
+ const v = rest.shift();
83
+ if (v === undefined)
84
+ fail(`${arg} needs a value`);
85
+ return v;
86
+ };
87
+ switch (arg) {
88
+ case "--url":
89
+ opts.baseUrl = next().replace(/\/$/, "");
90
+ break;
91
+ case "--dir":
92
+ opts.dir = resolve(next());
93
+ break;
94
+ case "--dry-run":
95
+ opts.dryRun = true;
96
+ break;
97
+ case "--device":
98
+ opts.device = true;
99
+ break;
100
+ case "--token":
101
+ opts.token = next();
102
+ break;
103
+ case "--mint-token":
104
+ opts.mintToken = true;
105
+ break;
106
+ case "--codex":
107
+ opts.codex = true;
108
+ opts.mintToken = true;
109
+ break;
110
+ case "--harness-home":
111
+ opts.harnessHome = resolve(next());
112
+ break;
113
+ case "--server-name":
114
+ opts.serverName = next();
115
+ break;
116
+ case "--client-id":
117
+ opts.clientId = next();
118
+ break;
119
+ case "--no-browser":
120
+ opts.openBrowser = false;
121
+ break;
122
+ case "--timeout":
123
+ opts.timeoutMs = parseTimeout(next());
124
+ break;
125
+ case "-h":
126
+ case "--help":
127
+ command = "help";
128
+ break;
129
+ case "-v":
130
+ case "--version":
131
+ command = "version";
132
+ break;
133
+ default: fail(`unknown argument: ${arg}\nRun \`bnl-harness --help\`.`);
134
+ }
135
+ }
136
+ return { command, opts };
137
+ }
138
+ const HELP = `
139
+ bnl-harness — configure this machine for Blue Note Memory
140
+
141
+ npx @bluenotelogic/harness init [options]
142
+
143
+ Options
144
+ --url <base> Server base URL (default ${DEFAULT_BASE_URL})
145
+ --dir <path> Repo to configure (default: current directory)
146
+ --dry-run Show every file that would change; write nothing
147
+ --device Device-code login, for headless boxes and SSH sessions
148
+ --token <token> Use an existing cmem_ token instead of logging in (CI)
149
+ --mint-token Also mint a token for hosts that cannot do OAuth
150
+ --codex Configure Codex too (implies --mint-token)
151
+ --harness-home <dir> Also materialize hosted routing into <dir>/state/routing/
152
+ --server-name <name> Override the MCP server key written into .mcp.json
153
+ --client-id <id> OAuth client_id (default ${DEFAULT_CLIENT_ID})
154
+ --no-browser Print the login URL instead of opening a browser
155
+ --timeout <seconds> How long to wait for you to sign in (default 300, max 3600).
156
+ With --device the server's own device-code lifetime still wins.
157
+ -h, --help This text
158
+ -v, --version Version
159
+
160
+ By default nothing secret is written: Claude Code and VS Code authenticate over OAuth and
161
+ keep their own credentials, so the .mcp.json this produces is safe to commit.
162
+ `;
163
+ async function main() {
164
+ const { command, opts } = parseArgs(process.argv.slice(2));
165
+ if (command === "help") {
166
+ console.log(HELP.trim());
167
+ return;
168
+ }
169
+ if (command === "version") {
170
+ console.log(await version());
171
+ return;
172
+ }
173
+ if (command !== "init")
174
+ fail(`unknown command: ${command}\nRun \`bnl-harness --help\`.`);
175
+ const bundle = await getBundle(opts);
176
+ const changes = planAll(bundle, opts);
177
+ report(changes, opts, bundle);
178
+ for (const c of changes)
179
+ assertNoSecret2(c, opts);
180
+ apply(changes, opts.dryRun);
181
+ if (!opts.dryRun)
182
+ printNextSteps(bundle, opts, changes);
183
+ }
184
+ /** Authenticate (unless given a token) and fetch the bundle. */
185
+ async function getBundle(opts) {
186
+ if (opts.token) {
187
+ log(`Using the token supplied on the command line (skipping login).`);
188
+ return fetchBundle(opts.baseUrl, opts.token, { mintToken: opts.mintToken });
189
+ }
190
+ log(`Discovering ${opts.baseUrl}…`);
191
+ const meta = await discover(opts.baseUrl);
192
+ const clientId = await registerClient(meta, opts.clientId);
193
+ const tokens = opts.device
194
+ ? await authorizeViaDevice({
195
+ meta, clientId, scope: DEFAULT_SCOPE, resource: `${opts.baseUrl}/mcp`,
196
+ timeoutMs: opts.timeoutMs ?? undefined, log,
197
+ })
198
+ : await authorizeViaBrowser({
199
+ meta, clientId, scope: DEFAULT_SCOPE, resource: `${opts.baseUrl}/mcp`,
200
+ timeoutMs: opts.timeoutMs ?? undefined, openBrowser: opts.openBrowser, log,
201
+ });
202
+ log("Signed in. Resolving your memory tenant…");
203
+ let tenantToken;
204
+ try {
205
+ tenantToken = await exchangeForTenantToken(opts.baseUrl, tokens.access_token);
206
+ }
207
+ catch (e) {
208
+ if (e instanceof SubscriptionRequiredError) {
209
+ // Authenticated fine — they just do not have the product. Say that, and say where to
210
+ // go, rather than leaving them re-trying a login that already worked.
211
+ fail(`${e.message}\n\n` +
212
+ `Your Blue Note Account signed in successfully — it just has no Blue Note Memory\n` +
213
+ `subscription attached. Start one at ${opts.baseUrl}/pricing.php`);
214
+ }
215
+ throw e;
216
+ }
217
+ return fetchBundle(opts.baseUrl, tenantToken, { mintToken: opts.mintToken });
218
+ }
219
+ /** Everything this run would write, as data. Nothing here touches the filesystem twice. */
220
+ function planAll(bundle, opts) {
221
+ const changes = [];
222
+ // --- The tokenless repo config: the whole point of the tool. --------------------------
223
+ let servers = bundle.mcp_json.mcpServers;
224
+ if (opts.serverName) {
225
+ const [firstKey] = Object.keys(servers);
226
+ if (firstKey)
227
+ servers = { [opts.serverName]: servers[firstKey] };
228
+ }
229
+ const mcpPath = join(opts.dir, ".mcp.json");
230
+ changes.push(planMcpJson(mcpPath, readIfExists(mcpPath), servers));
231
+ // --- Optional: a real credential, for hosts that cannot do OAuth. ---------------------
232
+ const minted = bundle.token?.value;
233
+ if (minted) {
234
+ const codexHome = process.env["CODEX_HOME"] ?? join(homedir(), ".codex");
235
+ const serverName = opts.serverName ?? Object.keys(servers)[0] ?? "mem";
236
+ if (opts.codex) {
237
+ const cfg = join(codexHome, "config.toml");
238
+ changes.push(planCodexConfig(cfg, readIfExists(cfg), serverName, bundle.endpoint.mcp_url, TOKEN_ENV_VAR));
239
+ }
240
+ const envPath = join(codexHome, ".env");
241
+ changes.push(planEnvVar(envPath, readIfExists(envPath), TOKEN_ENV_VAR, minted));
242
+ // The VS Code extension launches `claude` without a shell, so an env block in
243
+ // settings.local.json is what makes a token-based setup work from the editor button.
244
+ const settings = join(opts.dir, ".claude", "settings.local.json");
245
+ changes.push(planSettingsLocal(settings, readIfExists(settings), { [TOKEN_ENV_VAR]: minted }));
246
+ }
247
+ // --- Optional: materialize the hosted routing the console edits. -----------------------
248
+ if (opts.harnessHome && bundle.routing) {
249
+ const dir = join(opts.harnessHome, "state", "routing");
250
+ if (bundle.routing.providers_yaml) {
251
+ const p = join(dir, "providers.server.yaml");
252
+ changes.push(planRoutingFile(p, readIfExists(p), bundle.routing.providers_yaml));
253
+ }
254
+ if (bundle.routing.profiles_yaml) {
255
+ const p = join(dir, "profiles.server.yaml");
256
+ changes.push(planRoutingFile(p, readIfExists(p), bundle.routing.profiles_yaml));
257
+ }
258
+ }
259
+ return changes;
260
+ }
261
+ /**
262
+ * The secret guard applies to REPO files only. `~/.codex/.env` and
263
+ * `.claude/settings.local.json` are where a minted token is supposed to live — asserting
264
+ * against them would forbid the very thing `--mint-token` is for.
265
+ */
266
+ function assertNoSecret2(c, opts) {
267
+ const credentialStores = [
268
+ join(opts.dir, ".claude", "settings.local.json"),
269
+ join(process.env["CODEX_HOME"] ?? join(homedir(), ".codex"), ".env"),
270
+ ];
271
+ if (credentialStores.includes(c.path))
272
+ return;
273
+ assertNoSecret(c);
274
+ }
275
+ function report(changes, opts, bundle) {
276
+ console.log("");
277
+ console.log(` tenant ${bundle.tenant.slug || bundle.tenant.id}`);
278
+ console.log(` endpoint ${bundle.endpoint.mcp_url}`);
279
+ if (bundle.routing?.source)
280
+ console.log(` routing ${bundle.routing.source}`);
281
+ console.log("");
282
+ for (const c of changes) {
283
+ if (c.skipped) {
284
+ console.log(` SKIP ${c.path}\n ${c.skipped.replace(/\n/g, "\n ")}`);
285
+ continue;
286
+ }
287
+ if (isNoop(c)) {
288
+ console.log(` same ${c.path}`);
289
+ continue;
290
+ }
291
+ console.log(` ${c.before === null ? "create" : "update"} ${c.path}`);
292
+ if (opts.dryRun && c.after) {
293
+ const body = c.after.split("\n").map((l) => ` │ ${l}`).join("\n");
294
+ console.log(body.replace(/\n\s+│\s*$/, ""));
295
+ }
296
+ }
297
+ console.log("");
298
+ if (opts.dryRun)
299
+ console.log(" --dry-run: nothing was written.\n");
300
+ }
301
+ function printNextSteps(bundle, opts, changes) {
302
+ const oauth = !bundle.token;
303
+ console.log("Done.\n");
304
+ if (oauth) {
305
+ console.log(" Your .mcp.json carries no token — Claude Code and VS Code will authenticate");
306
+ console.log(" over OAuth on first use and keep their own credentials. It is safe to commit.\n");
307
+ }
308
+ console.log(` 1. Open ${opts.dir} in Claude Code or VS Code`);
309
+ console.log(` 2. The '${Object.keys(bundle.mcp_json.mcpServers)[0]}' server appears automatically`);
310
+ if (bundle.token) {
311
+ console.log(` 3. A token was minted into ${TOKEN_ENV_VAR} — it is in your Codex env file, not this repo`);
312
+ }
313
+ const notes = changes.filter((c) => c.note && !isNoop(c));
314
+ if (notes.length) {
315
+ console.log("");
316
+ for (const c of notes)
317
+ console.log(` NOTE ${c.path}: ${c.note}`);
318
+ }
319
+ console.log("");
320
+ }
321
+ async function version() {
322
+ try {
323
+ const pkg = await import("node:fs").then((fs) => JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")));
324
+ return String(pkg.version ?? "0.0.0");
325
+ }
326
+ catch {
327
+ return "0.0.0";
328
+ }
329
+ }
330
+ function log(msg) {
331
+ console.log(msg);
332
+ }
333
+ function fail(msg) {
334
+ console.error(`\nbnl-harness: ${msg}\n`);
335
+ process.exit(1);
336
+ }
337
+ main().catch((e) => {
338
+ fail(e instanceof Error ? e.message : String(e));
339
+ });
340
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * OAuth 2.1 client for Blue Note Memory — authorization-code + PKCE over a loopback
3
+ * redirect, with a device-code fallback for headless machines.
4
+ *
5
+ * WHY LOOPBACK AND NOT A FIXED PORT: this is a public native client, so RFC 8252 §7.3
6
+ * applies — bind an ephemeral port and register a regex redirect. The `blue-note-mem-mcp`
7
+ * client on Blue Note ID already has `http://(127\.0\.0\.1|localhost):[0-9]{1,5}/.*`
8
+ * registered for exactly this. A fixed port would collide with whatever else is running.
9
+ *
10
+ * WHY WE STILL CALL /register: Authentik has no RFC 7591 dynamic client registration, so
11
+ * mem-mcp serves a shim that hands back the pre-registered public client_id. Calling it is
12
+ * the spec-correct discovery path and costs one request; `--client-id` skips it.
13
+ *
14
+ * NO CLIENT SECRET ANYWHERE. A public client cannot keep one, which is the whole point:
15
+ * PKCE is what binds the authorization code to this process.
16
+ */
17
+ /** Endpoints as advertised by the resource + authorization-server metadata documents. */
18
+ export interface AsMetadata {
19
+ issuer: string;
20
+ authorization_endpoint: string;
21
+ token_endpoint: string;
22
+ device_authorization_endpoint?: string;
23
+ registration_endpoint?: string;
24
+ code_challenge_methods_supported?: string[];
25
+ }
26
+ export interface PkcePair {
27
+ verifier: string;
28
+ challenge: string;
29
+ }
30
+ /**
31
+ * PKCE S256. The verifier is 32 random bytes base64url-encoded (43 chars — comfortably
32
+ * inside RFC 7636's 43..128 range); the challenge is its SHA-256, also base64url.
33
+ */
34
+ export declare function createPkce(): PkcePair;
35
+ export declare function base64url(buf: Buffer): string;
36
+ /**
37
+ * Constant-time compare for the `state` parameter. Not strictly required — state is an
38
+ * integrity check, not a secret — but a length-independent comparison keeps the callback
39
+ * handler free of an early-exit oracle, and it costs nothing.
40
+ */
41
+ export declare function safeEqual(a: string, b: string): boolean;
42
+ /**
43
+ * Every discovered endpoint must be https and live on the same host as the issuer, and
44
+ * must not smuggle credentials in the userinfo part. Same rule the PHP side applies in
45
+ * lib/Auth/BlueNoteOidc.php — a metadata document is attacker-influencable input, so a
46
+ * redirected authorization_endpoint is a credential-phishing primitive if left unchecked.
47
+ *
48
+ * Loopback (http://127.0.0.1) is exempt because that is our OWN redirect target, never a
49
+ * discovered endpoint.
50
+ */
51
+ export declare function assertSafeEndpoint(url: string, issuer: string, what: string): void;
52
+ /**
53
+ * RFC 9728 protected-resource metadata -> RFC 8414 authorization-server metadata.
54
+ *
55
+ * The resource document is what a spec-compliant MCP client finds from the endpoint's
56
+ * `401 + WWW-Authenticate: Bearer resource_metadata="..."`. We follow the same chain
57
+ * rather than hardcoding paths, so a self-hosted deployment works with only `--url`.
58
+ */
59
+ export declare function discover(baseUrl: string): Promise<AsMetadata>;
60
+ /** The DCR shim. Any failure falls back to the caller's default client_id. */
61
+ export declare function registerClient(meta: AsMetadata, fallbackClientId: string): Promise<string>;
62
+ export interface TokenResponse {
63
+ access_token: string;
64
+ token_type?: string;
65
+ expires_in?: number;
66
+ refresh_token?: string;
67
+ }
68
+ /**
69
+ * Browser flow. Resolves with an access token, or rejects on timeout / user denial.
70
+ *
71
+ * The listener binds 127.0.0.1 EXPLICITLY (not 0.0.0.0) so nothing off-box can reach the
72
+ * callback, and it shuts down the moment it has an answer — a lingering listener holding a
73
+ * valid `state` is a needless window.
74
+ */
75
+ export declare function authorizeViaBrowser(opts: {
76
+ meta: AsMetadata;
77
+ clientId: string;
78
+ scope: string;
79
+ resource: string;
80
+ timeoutMs?: number;
81
+ openBrowser?: boolean;
82
+ log?: (msg: string) => void;
83
+ }): Promise<TokenResponse>;
84
+ /**
85
+ * Device-code flow — for a headless box, an SSH session, or any machine where opening a
86
+ * browser is not possible. Authentik supports this grant natively and `blue-note-mem-mcp`
87
+ * already has it enabled, so there is no server-side work behind this path.
88
+ */
89
+ export declare function authorizeViaDevice(opts: {
90
+ meta: AsMetadata;
91
+ clientId: string;
92
+ scope: string;
93
+ resource: string;
94
+ timeoutMs?: number;
95
+ log?: (msg: string) => void;
96
+ }): Promise<TokenResponse>;