brainllm 12.4.0 → 12.4.1

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/README.md CHANGED
@@ -10,7 +10,7 @@ A persistent, graph-structured second brain for Claude and any MCP client — bu
10
10
 
11
11
  [**brainllm site**](https://miisodev.github.io/BrainLLM/) · [How it works](https://miisodev.github.io/BrainLLM/how-it-works.html) · [Use cases](https://miisodev.github.io/BrainLLM/use-cases.html) · [Docs](https://miisodev.github.io/BrainLLM/docs.html)
12
12
 
13
- [![Version](https://img.shields.io/badge/version-12.4.0-f59e0b?style=flat-square)](https://github.com/miisodev/BrainLLM/releases)
13
+ [![Version](https://img.shields.io/badge/version-12.4.1-f59e0b?style=flat-square)](https://github.com/miisodev/BrainLLM/releases)
14
14
  [![CI](https://img.shields.io/github/actions/workflow/status/miisodev/BrainLLM/ci.yml?branch=main&style=flat-square&label=CI&color=f59e0b)](https://github.com/miisodev/BrainLLM/actions/workflows/ci.yml)
15
15
  [![License: MIT](https://img.shields.io/badge/license-MIT-a1a1aa?style=flat-square)](./LICENSE)
16
16
  [![Runtime: Bun](https://img.shields.io/badge/runtime-Bun%20%E2%89%A5%201.0-a1a1aa?style=flat-square&logo=bun&logoColor=white)](https://bun.sh)
package/dist/index.js CHANGED
@@ -27788,6 +27788,7 @@ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync, wri
27788
27788
  import { lookup } from "dns/promises";
27789
27789
  import { isIP } from "net";
27790
27790
  import { request as httpsRequest } from "https";
27791
+ var CIMD_USER_AGENT = "BrainLLM-CIMD/1.0 (+https://github.com/miisodev/BrainLLM)";
27791
27792
  function isNonPublicAddress(address) {
27792
27793
  const value = address.trim().toLowerCase().replace(/^\[|\]$/g, "").split("%")[0];
27793
27794
  const version = isIP(value);
@@ -27937,7 +27938,13 @@ async function fetchPinned(target, maxBytes, timeoutMs) {
27937
27938
  const hostname = target.url.hostname.replace(/^\[|\]$/g, "");
27938
27939
  const request = httpsRequest(target.url, {
27939
27940
  method: "GET",
27940
- headers: { Accept: "application/json", Host: target.url.host, Connection: "close" },
27941
+ headers: {
27942
+ Accept: "application/json",
27943
+ "Accept-Encoding": "identity",
27944
+ "User-Agent": CIMD_USER_AGENT,
27945
+ Host: target.url.host,
27946
+ Connection: "close"
27947
+ },
27941
27948
  agent: false,
27942
27949
  servername: isIP(hostname) ? undefined : hostname,
27943
27950
  lookup: (_hostname, options, callback) => {
@@ -27989,6 +27996,7 @@ var SCOPE = "brain";
27989
27996
  var AUTH_CODE_TTL_MS = 10 * 60 * 1000;
27990
27997
  var ACCESS_TOKEN_TTL_S = 60 * 60;
27991
27998
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1000;
27999
+ var CONSENT_TRANSACTION_TTL_S = 5 * 60;
27992
28000
  var CIMD_FETCH_TIMEOUT_MS = 5000;
27993
28001
  var MAX_OAUTH_BODY_BYTES = 64 * 1024;
27994
28002
  var MAX_CIMD_DOCUMENT_BYTES = 64 * 1024;
@@ -28044,13 +28052,16 @@ var bodyLimitResponse = (format = "json") => {
28044
28052
  };
28045
28053
  var b64url = (buf) => Buffer.from(buf).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
28046
28054
  var b64urlDecode = (s) => Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
28047
- function signJwt(claims, secret) {
28055
+ function signSignedClaims(claims, secret) {
28048
28056
  const header = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
28049
28057
  const payload = b64url(JSON.stringify(claims));
28050
28058
  const sig = b64url(createHmac("sha256", secret).update(`${header}.${payload}`).digest());
28051
28059
  return `${header}.${payload}.${sig}`;
28052
28060
  }
28053
- function verifyJwt(token, secret) {
28061
+ function signJwt(claims, secret) {
28062
+ return signSignedClaims(claims, secret);
28063
+ }
28064
+ function verifySignedClaims(token, secret) {
28054
28065
  const parts = token.split(".");
28055
28066
  if (parts.length !== 3)
28056
28067
  return null;
@@ -28069,6 +28080,51 @@ function verifyJwt(token, secret) {
28069
28080
  return null;
28070
28081
  }
28071
28082
  }
28083
+ function verifyJwt(token, secret) {
28084
+ const claims = verifySignedClaims(token, secret);
28085
+ if (!claims || typeof claims.iss !== "string" || typeof claims.aud !== "string" || typeof claims.sub !== "string" || typeof claims.scope !== "string" || typeof claims.iat !== "number")
28086
+ return null;
28087
+ return claims;
28088
+ }
28089
+ function issueConsentTransaction(request, base) {
28090
+ const now = Math.floor(Date.now() / 1000);
28091
+ const transaction = {
28092
+ kind: "authorization-consent",
28093
+ iss: base,
28094
+ iat: now,
28095
+ exp: now + CONSENT_TRANSACTION_TTL_S,
28096
+ ...request
28097
+ };
28098
+ return signSignedClaims(transaction, signingSecret());
28099
+ }
28100
+ function verifyConsentTransaction(ticket, base) {
28101
+ const claims = verifySignedClaims(ticket, signingSecret());
28102
+ if (!claims || claims.kind !== "authorization-consent" || claims.iss !== base || typeof claims.clientId !== "string" || claims.clientName !== undefined && typeof claims.clientName !== "string" || typeof claims.redirectUri !== "string" || typeof claims.responseType !== "string" || typeof claims.codeChallenge !== "string" || typeof claims.challengeMethod !== "string" || typeof claims.state !== "string" || typeof claims.resource !== "string" || typeof claims.scope !== "string")
28103
+ return null;
28104
+ let redirect;
28105
+ try {
28106
+ redirect = new URL(claims.redirectUri);
28107
+ } catch {
28108
+ return null;
28109
+ }
28110
+ if (redirect.protocol !== "http:" && redirect.protocol !== "https:")
28111
+ return null;
28112
+ if (claims.responseType !== "code" || claims.challengeMethod !== "S256" || !claims.codeChallenge)
28113
+ return null;
28114
+ if (claims.resource.replace(/\/+$/, "") !== resourceUri(base).replace(/\/+$/, ""))
28115
+ return null;
28116
+ return {
28117
+ clientId: claims.clientId,
28118
+ ...typeof claims.clientName === "string" ? { clientName: claims.clientName } : {},
28119
+ redirectUri: claims.redirectUri,
28120
+ responseType: claims.responseType,
28121
+ codeChallenge: claims.codeChallenge,
28122
+ challengeMethod: claims.challengeMethod,
28123
+ state: claims.state,
28124
+ resource: claims.resource,
28125
+ scope: claims.scope
28126
+ };
28127
+ }
28072
28128
  var MAX_REGISTERED_CLIENTS = 200;
28073
28129
  function storePath() {
28074
28130
  return configFilePath().replace(/\.json$/, "") + ".oauth.json";
@@ -28196,6 +28252,12 @@ function wwwAuthenticate(base, error) {
28196
28252
  ];
28197
28253
  return parts.join(", ");
28198
28254
  }
28255
+ var PRE_REGISTERED_CLIENTS = {
28256
+ "https://claude.ai/oauth/client-metadata": {
28257
+ client_id: "https://claude.ai/oauth/client-metadata",
28258
+ redirect_uris: ["https://claude.ai/api/mcp/auth_callback"]
28259
+ }
28260
+ };
28199
28261
  function isLoopback(u) {
28200
28262
  return (u.protocol === "http:" || u.protocol === "https:") && (u.hostname === "127.0.0.1" || u.hostname === "::1" || u.hostname === "localhost");
28201
28263
  }
@@ -28270,6 +28332,10 @@ async function resolveClient(clientId) {
28270
28332
  }
28271
28333
  };
28272
28334
  }
28335
+ const preRegistered = PRE_REGISTERED_CLIENTS[clientId];
28336
+ if (preRegistered) {
28337
+ return { client: { ...preRegistered, redirect_uris: [...preRegistered.redirect_uris] } };
28338
+ }
28273
28339
  let url;
28274
28340
  try {
28275
28341
  url = new URL(clientId);
@@ -28451,6 +28517,26 @@ function landingPage(base, oauthOn, sseOn) {
28451
28517
  <p class="foot">Health: <span class="host">/health</span> \xB7 Everything else is MCP.</p>
28452
28518
  </div>`);
28453
28519
  }
28520
+ var PAGE_SECURITY_DIRECTIVES = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; img-src 'self' data:";
28521
+ function pageContentSecurityPolicy(redirectUri) {
28522
+ let formAction = "'self'";
28523
+ if (redirectUri) {
28524
+ const origin = new URL(redirectUri).origin;
28525
+ if (origin !== "null")
28526
+ formAction += ` ${origin}`;
28527
+ }
28528
+ return `${PAGE_SECURITY_DIRECTIVES}; form-action ${formAction}; style-src 'unsafe-inline'`;
28529
+ }
28530
+ function consentResponse(transaction, redirectUri, clientHost, verifiedHost, error, status = 200) {
28531
+ return new Response(consentPage({ transaction }, clientHost, error, verifiedHost), {
28532
+ status,
28533
+ headers: {
28534
+ "Content-Type": "text/html; charset=utf-8",
28535
+ "Cache-Control": "no-store",
28536
+ "Content-Security-Policy": pageContentSecurityPolicy(redirectUri)
28537
+ }
28538
+ });
28539
+ }
28454
28540
  function consentPage(params, clientHost, error, verifiedHost = true) {
28455
28541
  const hidden = Object.entries(params).map(([k, v]) => `<input type="hidden" name="${esc2(k)}" value="${esc2(v)}">`).join("");
28456
28542
  const foot = verifiedHost ? "Only the host above is shown, and it is the one DNS and TLS vouch for." : "The name above is self-reported by the application \u2014 your password is what authorizes access.";
@@ -28481,7 +28567,13 @@ var htmlError = (message, status = 400) => new Response(shell("Authorization err
28481
28567
  <h1>Authorization error</h1>
28482
28568
  <div class="err">${esc2(message)}</div>
28483
28569
  <p class="foot">Nothing was authorized. Close this window and start again from your client.</p>
28484
- </div>`), { status, headers: { "Content-Type": "text/html; charset=utf-8" } });
28570
+ </div>`), {
28571
+ status,
28572
+ headers: {
28573
+ "Content-Type": "text/html; charset=utf-8",
28574
+ "Content-Security-Policy": pageContentSecurityPolicy()
28575
+ }
28576
+ });
28485
28577
  async function handleAuthorize(req, base) {
28486
28578
  const url = new URL(req.url);
28487
28579
  let form = null;
@@ -28494,63 +28586,73 @@ async function handleAuthorize(req, base) {
28494
28586
  return htmlError("The authorization form could not be read.", 400);
28495
28587
  }
28496
28588
  }
28497
- const get = (k) => (form ? form.get(k) : url.searchParams.get(k)) ?? "";
28498
- const clientId = get("client_id");
28499
- const redirectUri = get("redirect_uri");
28500
- const responseType = get("response_type");
28501
- const codeChallenge = get("code_challenge");
28502
- const challengeMethod = get("code_challenge_method");
28503
- const state = get("state");
28504
- const resource = get("resource");
28505
- const scope = get("scope") || SCOPE;
28506
- if (!clientId || !redirectUri)
28507
- return htmlError("Missing client_id or redirect_uri.");
28508
- const resolved = await resolveClient(clientId);
28509
- if ("error" in resolved)
28510
- return htmlError(resolved.error);
28511
- if (!redirectUriAllowed(redirectUri, resolved.client.redirect_uris)) {
28512
- return htmlError("redirect_uri is not listed in the client's metadata document.");
28589
+ let request;
28590
+ let transaction;
28591
+ if (form) {
28592
+ transaction = form.get("transaction") ?? "";
28593
+ const verified = verifyConsentTransaction(transaction, base);
28594
+ if (!verified) {
28595
+ return htmlError("This authorization request expired or was altered. Start again from your client.");
28596
+ }
28597
+ request = verified;
28598
+ } else {
28599
+ const get = (k) => url.searchParams.get(k) ?? "";
28600
+ const clientId = get("client_id");
28601
+ const redirectUri = get("redirect_uri");
28602
+ const responseType = get("response_type");
28603
+ const codeChallenge = get("code_challenge");
28604
+ const challengeMethod = get("code_challenge_method");
28605
+ const state = get("state");
28606
+ const resource = get("resource");
28607
+ const scope = get("scope") || SCOPE;
28608
+ if (!clientId || !redirectUri)
28609
+ return htmlError("Missing client_id or redirect_uri.");
28610
+ const resolved = await resolveClient(clientId);
28611
+ if ("error" in resolved)
28612
+ return htmlError(resolved.error);
28613
+ if (!redirectUriAllowed(redirectUri, resolved.client.redirect_uris)) {
28614
+ return htmlError("redirect_uri is not listed in the client's metadata document.");
28615
+ }
28616
+ const fail = (error, description) => redirectWith(redirectUri, { error, error_description: description, iss: base, ...state ? { state } : {} });
28617
+ if (responseType !== "code")
28618
+ return fail("unsupported_response_type", "Only response_type=code is supported.");
28619
+ if (!codeChallenge || challengeMethod !== "S256") {
28620
+ return fail("invalid_request", "PKCE with code_challenge_method=S256 is required.");
28621
+ }
28622
+ if (resource && resource.replace(/\/+$/, "") !== resourceUri(base).replace(/\/+$/, "")) {
28623
+ return fail("invalid_target", `This server only issues tokens for ${resourceUri(base)}.`);
28624
+ }
28625
+ request = {
28626
+ clientId,
28627
+ ...resolved.client.client_name ? { clientName: resolved.client.client_name } : {},
28628
+ redirectUri,
28629
+ responseType,
28630
+ codeChallenge,
28631
+ challengeMethod,
28632
+ state,
28633
+ resource: resource || resourceUri(base),
28634
+ scope
28635
+ };
28636
+ transaction = issueConsentTransaction(request, base);
28513
28637
  }
28638
+ const { clientId, redirectUri, codeChallenge, state, resource, scope } = request;
28514
28639
  const fail = (error, description) => redirectWith(redirectUri, { error, error_description: description, iss: base, ...state ? { state } : {} });
28515
- if (responseType !== "code")
28516
- return fail("unsupported_response_type", "Only response_type=code is supported.");
28517
- if (!codeChallenge || challengeMethod !== "S256") {
28518
- return fail("invalid_request", "PKCE with code_challenge_method=S256 is required.");
28519
- }
28520
- if (resource && resource.replace(/\/+$/, "") !== resourceUri(base).replace(/\/+$/, "")) {
28521
- return fail("invalid_target", `This server only issues tokens for ${resourceUri(base)}.`);
28522
- }
28523
28640
  let clientHost;
28524
28641
  let verifiedHost = true;
28525
28642
  try {
28526
28643
  clientHost = new URL(clientId).host;
28527
28644
  } catch {
28528
28645
  verifiedHost = false;
28529
- clientHost = resolved.client.client_name ?? "a dynamically registered client";
28646
+ clientHost = request.clientName ?? "a dynamically registered client";
28530
28647
  }
28531
- const carried = {
28532
- client_id: clientId,
28533
- redirect_uri: redirectUri,
28534
- response_type: responseType,
28535
- code_challenge: codeChallenge,
28536
- code_challenge_method: challengeMethod,
28537
- scope,
28538
- ...state ? { state } : {},
28539
- ...resource ? { resource } : {}
28540
- };
28541
28648
  if (!form) {
28542
- return new Response(consentPage(carried, clientHost, undefined, verifiedHost), {
28543
- headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" }
28544
- });
28649
+ return consentResponse(transaction, redirectUri, clientHost, verifiedHost);
28545
28650
  }
28546
- if (get("decision") !== "approve") {
28651
+ if (form.get("decision") !== "approve") {
28547
28652
  return fail("access_denied", "The owner denied the request.");
28548
28653
  }
28549
- if (!ownerPasswordMatches(get("password"))) {
28550
- return new Response(consentPage(carried, clientHost, "Incorrect password.", verifiedHost), {
28551
- status: 401,
28552
- headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" }
28553
- });
28654
+ if (!ownerPasswordMatches(form.get("password") ?? "")) {
28655
+ return consentResponse(transaction, redirectUri, clientHost, verifiedHost, "Incorrect password.", 401);
28554
28656
  }
28555
28657
  const store = loadStore();
28556
28658
  const code = randomBytes(32).toString("base64url");
@@ -28559,7 +28661,7 @@ async function handleAuthorize(req, base) {
28559
28661
  redirectUri,
28560
28662
  codeChallenge,
28561
28663
  scope,
28562
- resource: resource || resourceUri(base),
28664
+ resource,
28563
28665
  expiresAt: Date.now() + AUTH_CODE_TTL_MS
28564
28666
  };
28565
28667
  saveStore();
@@ -28572,7 +28674,7 @@ var tokenError = (error, description, status = 400) => new Response(JSON.stringi
28572
28674
  function issueTokens(base, clientId, resource, scope) {
28573
28675
  const store = loadStore();
28574
28676
  const now = Math.floor(Date.now() / 1000);
28575
- const accessToken = signJwt({ iss: base, aud: resource, sub: "owner", scope, iat: now, exp: now + ACCESS_TOKEN_TTL_S }, store.secret);
28677
+ const accessToken = signJwt({ iss: base, aud: resource, sub: "owner", scope, iat: now, exp: now + ACCESS_TOKEN_TTL_S }, signingSecret());
28576
28678
  const refreshToken = randomBytes(32).toString("base64url");
28577
28679
  store.refresh[refreshToken] = { clientId, resource, scope, expiresAt: Date.now() + REFRESH_TOKEN_TTL_MS };
28578
28680
  saveStore();
@@ -28722,7 +28824,7 @@ function createServer(origin = null) {
28722
28824
  const s = new McpServer({
28723
28825
  name: "BrainLLM",
28724
28826
  title: "BrainLLM",
28725
- version: "12.4.0",
28827
+ version: "12.4.1",
28726
28828
  icons: brandingIcons(origin)
28727
28829
  });
28728
28830
  registerTools(s, trilium, brainRef);
@@ -28761,8 +28863,11 @@ if (port) {
28761
28863
  "Strict-Transport-Security": "max-age=31536000; includeSubDomains"
28762
28864
  };
28763
28865
  const withCors = (res) => {
28764
- for (const [k, v] of Object.entries({ ...CORS_HEADERS, ...SECURITY_HEADERS }))
28866
+ for (const [k, v] of Object.entries({ ...CORS_HEADERS, ...SECURITY_HEADERS })) {
28867
+ if (k === "Content-Security-Policy" && res.headers.has(k))
28868
+ continue;
28765
28869
  res.headers.set(k, v);
28870
+ }
28766
28871
  return res;
28767
28872
  };
28768
28873
  const rateLimiters = {
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "brainllm",
4
4
  "display_name": "BrainLLM",
5
- "version": "12.4.0",
5
+ "version": "12.4.1",
6
6
  "description": "Turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
7
7
  "long_description": "BrainLLM is an MCP server that organises memory into five purpose-built areas (Master, LLM, Memory, Knowledge, Insights). The model supplies content while the server owns placement, format, lifecycle, dates and backups. Includes a full raw ETAPI surface for advanced use. Requires a running TriliumNext instance and the Bun runtime.",
8
8
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainllm",
3
- "version": "12.4.0",
3
+ "version": "12.4.1",
4
4
  "mcpName": "io.github.miisodev/brainllm",
5
5
  "description": "MCP server that turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
6
6
  "author": "Kevin Miiso Novo <miisodev@gmail.com>",
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.miisodev/brainllm",
4
4
  "description": "Persistent graph-structured memory for LLMs in TriliumNext Notes — readable and editable by humans.",
5
- "version": "12.4.0",
5
+ "version": "12.4.1",
6
6
  "websiteUrl": "https://github.com/miisodev/BrainLLM#readme",
7
7
  "repository": {
8
8
  "url": "https://github.com/miisodev/BrainLLM",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "brainllm",
15
- "version": "12.4.0",
15
+ "version": "12.4.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },