@nextblock-cms/cortex 0.22.0 → 0.22.2
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/lib/ai-global-agent-db-tools.cjs +1 -1
- package/lib/ai-global-agent-db-tools.es.js +148 -122
- package/lib/ai-global-agent-tools.cjs +1 -1
- package/lib/ai-global-agent-tools.es.js +801 -800
- package/lib/fetch-host-guard.cjs +1 -1
- package/lib/fetch-host-guard.d.ts +13 -0
- package/lib/fetch-host-guard.es.js +15 -4
- package/lib/mcp-tokens.cjs +1 -1
- package/lib/mcp-tokens.d.ts +20 -9
- package/lib/mcp-tokens.es.js +1 -1
- package/package.json +4 -4
package/lib/fetch-host-guard.cjs
CHANGED
|
@@ -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
|
|
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(
|
|
18
|
-
let
|
|
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
|
|
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 {
|
|
41
|
+
export { n as isBlockedFetchHost, t as resolvedHostIsBlocked };
|
package/lib/mcp-tokens.cjs
CHANGED
|
@@ -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:!
|
|
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;
|
package/lib/mcp-tokens.d.ts
CHANGED
|
@@ -12,10 +12,16 @@ export type CortexAiMcpSettings = {
|
|
|
12
12
|
*/
|
|
13
13
|
enabled: boolean;
|
|
14
14
|
/**
|
|
15
|
-
* Trust loopback callers without a token
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
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
|
|
86
|
-
* `NODE_ENV`.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* and a
|
|
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;
|
package/lib/mcp-tokens.es.js
CHANGED
|
@@ -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: !
|
|
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.
|
|
3
|
+
"version": "0.22.2",
|
|
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.
|
|
33
|
-
"@nextblock-cms/ecommerce": "npm:@nextblock-cms/ecom@^0.22.
|
|
34
|
-
"@nextblock-cms/utils": "^0.22.
|
|
32
|
+
"@nextblock-cms/db": "^0.22.2",
|
|
33
|
+
"@nextblock-cms/ecommerce": "npm:@nextblock-cms/ecom@^0.22.2",
|
|
34
|
+
"@nextblock-cms/utils": "^0.22.2",
|
|
35
35
|
"ai": "^7.0.107",
|
|
36
36
|
"zod": "^4.6.5"
|
|
37
37
|
},
|