@nextblock-cms/cortex 0.22.0 → 0.22.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.
@@ -1 +1 @@
1
- function e(e){let t=e.match(/^::ffff:(.+)$/i);if(!t)return null;let n=t[1];if(/^\d{1,3}(\.\d{1,3}){3}$/.test(n))return n;let r=n.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);if(!r)return null;let i=Number.parseInt(r[1],16),a=Number.parseInt(r[2],16);return[i>>8&255,i&255,a>>8&255,a&255].join(`.`)}function t(n){let r=n.trim().toLowerCase().replace(/\.$/,``).replace(/^\[|\]$/g,``);if(!r||r===`localhost`||r.endsWith(`.localhost`)||r.endsWith(`.local`)||r.endsWith(`.internal`)||r===`metadata.google.internal`||r===`0.0.0.0`||r===`::`||r===`::1`||r.startsWith(`fe80:`)||r.startsWith(`fc`)||r.startsWith(`fd`))return!0;let i=e(r);if(i)return t(i);let a=r.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);if(a){let e=Number(a[1]),t=Number(a[2]);if(e===10||e===127||e===0||e===192&&t===168||e===169&&t===254||e===172&&t>=16&&t<=31)return!0}return!1}exports.isBlockedFetchHost=t;
1
+ function e(e){let t=e.match(/^::ffff:(.+)$/i);if(!t)return null;let n=t[1];if(/^\d{1,3}(\.\d{1,3}){3}$/.test(n))return n;let r=n.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);if(!r)return null;let i=Number.parseInt(r[1],16),a=Number.parseInt(r[2],16);return[i>>8&255,i&255,a>>8&255,a&255].join(`.`)}async function t(e){let t=(e||``).trim().toLowerCase().replace(/\.$/,``).replace(/^\[|\]$/g,``);if(!t)return!0;if(/^\d{1,3}(\.\d{1,3}){3}$/.test(t)||t.includes(`:`))return n(t);try{let e=await(await import(`node:dns`)).promises.lookup(t,{all:!0});return!e||e.length===0||e.some(e=>n(e.address))}catch{return!0}}function n(t){let r=t.trim().toLowerCase().replace(/\.$/,``).replace(/^\[|\]$/g,``);if(!r||r===`localhost`||r.endsWith(`.localhost`)||r.endsWith(`.local`)||r.endsWith(`.internal`)||r===`metadata.google.internal`||r===`0.0.0.0`||r===`::`||r===`::1`||r.startsWith(`fe80:`)||r.startsWith(`fc`)||r.startsWith(`fd`))return!0;let i=e(r);if(i)return n(i);let a=r.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);if(a){let e=Number(a[1]),t=Number(a[2]);if(e===10||e===127||e===0||e===192&&t===168||e===169&&t===254||e===172&&t>=16&&t<=31)return!0}return!1}exports.isBlockedFetchHost=n,exports.resolvedHostIsBlocked=t;
@@ -1,2 +1,15 @@
1
+ /**
2
+ * DNS-rebinding guard. `isBlockedFetchHost` only inspects the hostname STRING, so a public name
3
+ * that resolves to a private/loopback/link-local/metadata address (attacker-controlled DNS) sails
4
+ * through it and the connection still lands inside the perimeter. This resolves the name and
5
+ * reports blocked if ANY resolved address is internal. Call it in addition to isBlockedFetchHost,
6
+ * before the fetch and again on the post-redirect host.
7
+ *
8
+ * Residual: there is a TOCTOU window between this lookup and the socket connect (global fetch does
9
+ * not expose IP pinning); a dispatcher that pins the vetted IP is the complete fix and is tracked
10
+ * as a follow-up. This check closes the static "public name -> private IP" rebind, which is what
11
+ * the assessment reproduced.
12
+ */
13
+ export declare function resolvedHostIsBlocked(hostname: string): Promise<boolean>;
1
14
  /** Exported for the SSRF regression tests in ai-global-agent-ssrf.test.ts. */
2
15
  export declare function isBlockedFetchHost(hostname: string): boolean;
@@ -14,11 +14,22 @@ function e(e) {
14
14
  a & 255
15
15
  ].join(".");
16
16
  }
17
- function t(n) {
18
- let r = n.trim().toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
17
+ async function t(e) {
18
+ let t = (e || "").trim().toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
19
+ if (!t) return !0;
20
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(t) || t.includes(":")) return n(t);
21
+ try {
22
+ let e = await (await import("node:dns")).promises.lookup(t, { all: !0 });
23
+ return !e || e.length === 0 || e.some((e) => n(e.address));
24
+ } catch {
25
+ return !0;
26
+ }
27
+ }
28
+ function n(t) {
29
+ let r = t.trim().toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
19
30
  if (!r || r === "localhost" || r.endsWith(".localhost") || r.endsWith(".local") || r.endsWith(".internal") || r === "metadata.google.internal" || r === "0.0.0.0" || r === "::" || r === "::1" || r.startsWith("fe80:") || r.startsWith("fc") || r.startsWith("fd")) return !0;
20
31
  let i = e(r);
21
- if (i) return t(i);
32
+ if (i) return n(i);
22
33
  let a = r.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
23
34
  if (a) {
24
35
  let e = Number(a[1]), t = Number(a[2]);
@@ -27,4 +38,4 @@ function t(n) {
27
38
  return !1;
28
39
  }
29
40
  //#endregion
30
- export { t as isBlockedFetchHost };
41
+ export { n as isBlockedFetchHost, t as resolvedHostIsBlocked };
@@ -1 +1 @@
1
- let e=require("node:crypto");var t=`Cortex AI MCP token utilities can only be imported from server-side code.`;function n(){if(typeof window<`u`)throw Error(t)}var r=`cortex_ai_mcp_settings`,i=`mcp_access_tokens`,a=`nbmcp_`,o=32,s=[`read`,`write`],c={allowLocalhostWithoutToken:!0,enabled:!1};function l(e){let t=e&&typeof e==`object`&&!Array.isArray(e)?e:{};return{allowLocalhostWithoutToken:typeof t.allowLocalhostWithoutToken==`boolean`?t.allowLocalhostWithoutToken:c.allowLocalhostWithoutToken,enabled:typeof t.enabled==`boolean`?t.enabled:c.enabled}}async function u(e){if(e)try{let{data:t}=await e.from(`site_settings`).select(`value`).eq(`key`,r).maybeSingle();if(t?.value)return l(t.value)}catch{}return{...c}}function d(t){return n(),(0,e.createHash)(`sha256`).update(t.trim(),`utf8`).digest(`hex`)}function f(){n();let t=`${a}${(0,e.randomBytes)(o).toString(`base64url`)}`;return{token:t,tokenHash:d(t),tokenPrefix:t.slice(0,14)}}function p(e){let t=e?.trim();if(!t)return null;let n=/^Bearer\s+(.+)$/i.exec(t);return(n?n[1]:t).trim()||null}function m(e){let t=(Array.isArray(e)?e:[]).filter(e=>s.includes(e));return t.length>0?Array.from(new Set(t)):[`read`]}async function h(t,r,a=new Date){n();let o=r?.trim();if(!o||o.length<16)return{reason:`malformed`,valid:!1};let s=d(o),{data:c,error:l}=await t.from(i).select(`id, name, scopes, token_prefix, created_at, created_by, expires_at, last_used_at, revoked_at`).eq(`token_hash`,s).maybeSingle();if(l||!c)return{reason:`unknown`,valid:!1};let u=Buffer.from(s,`utf8`);if(!(0,e.timingSafeEqual)(u,Buffer.from(d(o),`utf8`)))return{reason:`unknown`,valid:!1};let f=c;return f.revoked_at?{reason:`revoked`,valid:!1}:f.expires_at&&new Date(f.expires_at).getTime()<=a.getTime()?{reason:`expired`,valid:!1}:{scopes:m(f.scopes),token:f,valid:!0}}async function g(e,t,n=new Date){try{await e.from(i).update({last_used_at:n.toISOString()}).eq(`id`,t)}catch{}}var _=new Set([`localhost`,`127.0.0.1`,`[::1]`,`::1`,`0.0.0.0`]);function v(e){let t=e?.trim().toLowerCase();if(!t)return!1;let n=t.startsWith(`[`)?t.replace(/\]:\d+$/,`]`):t.replace(/:\d+$/,``);return _.has(n)||n.endsWith(`.localhost`)}function y(e){return!e.settings.allowLocalhostWithoutToken||process.env.NODE_ENV===`production`?!1:v(e.hostHeader)}var b=`MCP_BEARER_TOKEN`,x=32;function S(e=process.env){let t=e[b]?.trim();return!t||t.length<32?null:t}function C(t,r){n();let i=t?.trim(),a=r?.trim();return!i||!a||a.length<32?!1:(0,e.timingSafeEqual)(Buffer.from(d(i),`utf8`),Buffer.from(d(a),`utf8`))}exports.CORTEX_AI_MCP_ENV_TOKEN_MIN_LENGTH=x,exports.CORTEX_AI_MCP_ENV_TOKEN_VAR=b,exports.CORTEX_AI_MCP_SCOPES=s,exports.CORTEX_AI_MCP_SETTINGS_DEFAULTS=c,exports.CORTEX_AI_MCP_SETTINGS_KEY=r,exports.CORTEX_AI_MCP_TOKENS_TABLE=i,exports.CORTEX_AI_MCP_TOKEN_PREFIX=a,exports.hashCortexAiMcpToken=d,exports.isLocalhostHost=v,exports.matchesCortexAiMcpEnvToken=C,exports.mintCortexAiMcpToken=f,exports.normalizeCortexAiMcpSettings=l,exports.parseBearerToken=p,exports.readCortexAiMcpEnvToken=S,exports.resolveCortexAiMcpSettings=u,exports.shouldTrustLocalMcpRequest=y,exports.touchCortexAiMcpToken=g,exports.verifyCortexAiMcpToken=h;
1
+ let e=require("node:crypto");var t=`Cortex AI MCP token utilities can only be imported from server-side code.`;function n(){if(typeof window<`u`)throw Error(t)}var r=`cortex_ai_mcp_settings`,i=`mcp_access_tokens`,a=`nbmcp_`,o=32,s=[`read`,`write`],c={allowLocalhostWithoutToken:!1,enabled:!1};function l(e){let t=e&&typeof e==`object`&&!Array.isArray(e)?e:{};return{allowLocalhostWithoutToken:typeof t.allowLocalhostWithoutToken==`boolean`?t.allowLocalhostWithoutToken:c.allowLocalhostWithoutToken,enabled:typeof t.enabled==`boolean`?t.enabled:c.enabled}}async function u(e){if(e)try{let{data:t}=await e.from(`site_settings`).select(`value`).eq(`key`,r).maybeSingle();if(t?.value)return l(t.value)}catch{}return{...c}}function d(t){return n(),(0,e.createHash)(`sha256`).update(t.trim(),`utf8`).digest(`hex`)}function f(){n();let t=`${a}${(0,e.randomBytes)(o).toString(`base64url`)}`;return{token:t,tokenHash:d(t),tokenPrefix:t.slice(0,14)}}function p(e){let t=e?.trim();if(!t)return null;let n=/^Bearer\s+(.+)$/i.exec(t);return(n?n[1]:t).trim()||null}function m(e){let t=(Array.isArray(e)?e:[]).filter(e=>s.includes(e));return t.length>0?Array.from(new Set(t)):[`read`]}async function h(t,r,a=new Date){n();let o=r?.trim();if(!o||o.length<16)return{reason:`malformed`,valid:!1};let s=d(o),{data:c,error:l}=await t.from(i).select(`id, name, scopes, token_prefix, created_at, created_by, expires_at, last_used_at, revoked_at`).eq(`token_hash`,s).maybeSingle();if(l||!c)return{reason:`unknown`,valid:!1};let u=Buffer.from(s,`utf8`);if(!(0,e.timingSafeEqual)(u,Buffer.from(d(o),`utf8`)))return{reason:`unknown`,valid:!1};let f=c;return f.revoked_at?{reason:`revoked`,valid:!1}:f.expires_at&&new Date(f.expires_at).getTime()<=a.getTime()?{reason:`expired`,valid:!1}:{scopes:m(f.scopes),token:f,valid:!0}}async function g(e,t,n=new Date){try{await e.from(i).update({last_used_at:n.toISOString()}).eq(`id`,t)}catch{}}var _=new Set([`localhost`,`127.0.0.1`,`[::1]`,`::1`,`0.0.0.0`]);function v(e){let t=e?.trim().toLowerCase();if(!t)return!1;let n=t.startsWith(`[`)?t.replace(/\]:\d+$/,`]`):t.replace(/:\d+$/,``);return _.has(n)||n.endsWith(`.localhost`)}function y(e){return!e.settings.allowLocalhostWithoutToken||process.env.NODE_ENV===`production`?!1:v(e.hostHeader)}var b=`MCP_BEARER_TOKEN`,x=32;function S(e=process.env){let t=e[b]?.trim();return!t||t.length<32?null:t}function C(t,r){n();let i=t?.trim(),a=r?.trim();return!i||!a||a.length<32?!1:(0,e.timingSafeEqual)(Buffer.from(d(i),`utf8`),Buffer.from(d(a),`utf8`))}exports.CORTEX_AI_MCP_ENV_TOKEN_MIN_LENGTH=x,exports.CORTEX_AI_MCP_ENV_TOKEN_VAR=b,exports.CORTEX_AI_MCP_SCOPES=s,exports.CORTEX_AI_MCP_SETTINGS_DEFAULTS=c,exports.CORTEX_AI_MCP_SETTINGS_KEY=r,exports.CORTEX_AI_MCP_TOKENS_TABLE=i,exports.CORTEX_AI_MCP_TOKEN_PREFIX=a,exports.hashCortexAiMcpToken=d,exports.isLocalhostHost=v,exports.matchesCortexAiMcpEnvToken=C,exports.mintCortexAiMcpToken=f,exports.normalizeCortexAiMcpSettings=l,exports.parseBearerToken=p,exports.readCortexAiMcpEnvToken=S,exports.resolveCortexAiMcpSettings=u,exports.shouldTrustLocalMcpRequest=y,exports.touchCortexAiMcpToken=g,exports.verifyCortexAiMcpToken=h;
@@ -12,10 +12,16 @@ export type CortexAiMcpSettings = {
12
12
  */
13
13
  enabled: boolean;
14
14
  /**
15
- * Trust loopback callers without a token. Convenient for `npx nx serve nextblock`
16
- * plus a local Claude Code, and safe because the request must already originate on
17
- * the machine running the CMS. Ignored entirely in production (see
18
- * `shouldTrustLocalMcpRequest`).
15
+ * Trust loopback callers without a token — the dashboard half of a two-switch opt-in.
16
+ *
17
+ * Off by default: a tokenless loopback session is a full read+write surface onto the
18
+ * CMS, and `Host` is attacker-controllable behind a reverse proxy, so this is never
19
+ * enabled silently. Turning it on is NOT sufficient on its own — the endpoint also
20
+ * requires the `NEXTBLOCK_MCP_TRUST_LOCALHOST=true` environment opt-in and a
21
+ * non-production `NODE_ENV` before any tokenless request is honoured (see
22
+ * `shouldTrustLocalMcpRequest`, and `lib/cortex-ai/mcp-loopback-trust.ts` in the app
23
+ * for the forwarded-peer check layered on top). With the variable unset, localhost
24
+ * still needs a bearer token like everywhere else.
19
25
  */
20
26
  allowLocalhostWithoutToken: boolean;
21
27
  };
@@ -82,11 +88,16 @@ export declare function isLocalhostHost(value: string | null | undefined): boole
82
88
  /**
83
89
  * Decide whether a tokenless request may be trusted as local.
84
90
  *
85
- * Requires all three of: the setting on, a loopback Host header, and a non-production
86
- * `NODE_ENV`. The last condition is the important one — behind a reverse proxy the
87
- * Host header is attacker-controllable, so localhost trust is a development
88
- * affordance only and must never be the thing standing between the public internet
89
- * and a write-capable CMS endpoint.
91
+ * Requires all three of: the dashboard setting on, a loopback `Host` header, and a
92
+ * non-production `NODE_ENV`. Because the `Host` header is attacker-controllable behind
93
+ * a reverse proxy, this function is deliberately NOT the whole gate: the endpoint
94
+ * (`app/api/mcp/route.ts`) additionally requires the `NEXTBLOCK_MCP_TRUST_LOCALHOST=true`
95
+ * environment opt-in and a forwarded-peer-is-loopback check
96
+ * (`lib/cortex-ai/mcp-loopback-trust.ts`) before honouring a tokenless request. The
97
+ * environment variable is the real gate — without it the tokenless path does not exist,
98
+ * whatever the dashboard toggle says; this check is one of several AND-ed conditions.
99
+ * Localhost trust is a development affordance only and must never be the single thing
100
+ * standing between the public internet and a write-capable CMS endpoint.
90
101
  */
91
102
  export declare function shouldTrustLocalMcpRequest(params: {
92
103
  hostHeader: string | null | undefined;
@@ -5,7 +5,7 @@ function i() {
5
5
  if (typeof window < "u") throw Error(r);
6
6
  }
7
7
  var a = "cortex_ai_mcp_settings", o = "mcp_access_tokens", s = "nbmcp_", c = 32, l = ["read", "write"], u = {
8
- allowLocalhostWithoutToken: !0,
8
+ allowLocalhostWithoutToken: !1,
9
9
  enabled: !1
10
10
  };
11
11
  function d(e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/cortex",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "license": "AGPL-3.0-or-later",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -29,9 +29,9 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@ai-sdk/openai-compatible": "^3.0.53",
32
- "@nextblock-cms/db": "^0.22.0",
33
- "@nextblock-cms/ecommerce": "npm:@nextblock-cms/ecom@^0.22.0",
34
- "@nextblock-cms/utils": "^0.22.0",
32
+ "@nextblock-cms/db": "^0.22.1",
33
+ "@nextblock-cms/ecommerce": "npm:@nextblock-cms/ecom@^0.22.1",
34
+ "@nextblock-cms/utils": "^0.22.1",
35
35
  "ai": "^7.0.107",
36
36
  "zod": "^4.6.5"
37
37
  },